packages feed

sensei-0.9.0: src/DeepSeek.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE NoImplicitPrelude #-}
module DeepSeek (
  apply
#ifdef TEST
, Patch(..)
, extractPatch
#endif
) where

import           Imports hiding (putStrLn, strip)

import           Data.Ord
import           System.Process
import           System.Environment.Blank
import           Data.ByteString.Lazy (toStrict)
import           Data.Aeson
import qualified Data.Yaml.Pretty as Yaml
import           Data.Text (Text)
import           Network.HTTP.Types
import           Network.HTTP.Client
import           Network.HTTP.Simple

import           GHC.Diagnostic (Diagnostic)
import qualified GHC.Diagnostic.Type as Diagnostic
import qualified Config.DeepSeek as Config
import           DeepSeek.Types

separatorLength :: Int
separatorLength = 40

separator :: String
separator = take separatorLength $ repeat '#'

section :: String -> String
section name = take separatorLength $ take 3 separator ++ ' ' : name ++ ' ' : separator

apply :: (String -> IO ()) -> Config.DeepSeek -> FilePath -> Diagnostic -> IO ()
apply putStrLn config dir diagnostic = createChatCompletion dir diagnostic >>= \ case
  Nothing -> pass
  Just request -> (.choices) <$> query putStrLn config request >>= \ case
    [] -> pass
    choice : _ -> applyChoice choice
  where
    applyChoice :: Choice -> IO ()
    applyChoice choice = do
      case extractPatch diagnostic choice.message.content of
        Nothing -> putStrLn $ section "no patch"
        Just patch -> applyPatch putStrLn dir patch
      putStrLn separator

applyPatch :: (String -> IO ()) -> String -> Patch -> IO ()
applyPatch putStrLn dir patch = do
  env <- (:) ("POSIXLY_CORRECT", "true") <$> getEnvironment
  (status, out, err) <- readCreateProcessWithExitCode (proc "patch" ["-p" <> show patch.strip]) {
    cwd = guard (not $ null dir) $> dir
  , env = Just env
  } patch.diff
  when (status /= ExitSuccess) $ do
    putStrLn $ section "patch"
    unless (null out) $ putStrLn out
    unless (null err) $ putStrLn err

query :: (String -> IO ()) -> Config.DeepSeek -> CreateChatCompletion -> IO ChatCompletion
query putStrLn config (RequestBodyLBS . encode -> requestBody) = do
  body <- responseBody <$> httpLBS "https://api.deepseek.com/chat/completions" {
    method = "POST"
  , requestHeaders = [
      (hAccept, "application/json")
    , (hContentType, "application/json")
    , (hAuthorization, "Bearer " <> config.auth.bearer)
    ]
  , requestBody
  }
  logYamlBody body
  throwDecode body
  where
    logYamlBody :: LazyByteString -> IO ()
    logYamlBody body = do
      putStrLn $ section "response"
      putStrLn . decodeUtf8 $ toPrettyYaml body

    toPrettyYaml :: LazyByteString -> ByteString
    toPrettyYaml input = case decode @Value input of
      Nothing -> toStrict input
      Just value -> Yaml.encodePretty conf value
      where
        conf :: Yaml.Config
        conf = Yaml.setConfCompare (comparing fieldOrder) Yaml.defConfig

        fieldOrder :: Text -> Int
        fieldOrder name = fromMaybe maxBound . lookup name $ flip zip [1..] [
            "id"
          , "choices"
          , "created"
          , "model"
          , "system_fingerprint"
          , "object"
          , "usage"

          , "finish_reason"
          , "index"
          , "message"
          , "logprobs"

          , "content"
          , "role"

          , "completion_tokens"
          , "prompt_tokens"
          , "prompt_cache_hit_tokens"
          , "prompt_cache_miss_tokens"
          , "total_tokens"
          , "prompt_tokens_details"
          , "completion_tokens_details"
          ]

createChatCompletion :: FilePath -> Diagnostic -> IO (Maybe CreateChatCompletion)
createChatCompletion dir diagnostic = sequence $ diagnostic.span <&> \ span -> do
  source <- readFile (dir </> span.file)
  let
    content :: String
    content = unlines [
        "Given the following GHC diagnostics message, please suggest a fix for the corresponding Haskell code."
      , "Produce your fix as a unified diff so that it can be applied with the `patch` program."
      , "Don't provide explanations."
      , ""
      , "Enclose you answer in:"
      , ""
      , "```diff"
      , "--- " <> span.file
      , "+++ " <> span.file
      , "..."
      , "```"
      , ""
      , "(where ... is the placeholder for your answer)"
      , ""
      , "GHC diagnostics message:"
      , ""
      , "```console"
      , Diagnostic.format diagnostic
      , "```"
      , ""
      , "Corresponding Haskell code:"
      , ""
      , "```haskell"
      , source
      , "```"
      ]
  return CreateChatCompletion {
    messages = [ Message { role = User , content } ]
  , model = "deepseek-chat"
  , temperature = Just 0
  }

data Patch = Patch {
  strip :: Int
, diff :: String
} deriving (Eq, Show)

extractPatch :: Diagnostic -> String -> Maybe Patch
extractPatch diagnostic input = do
  diff <- extractDiffCodeBlock input
  span <- diagnostic.span
  file <- takeWhile (not . isSpace) . dropWhile isSpace <$> stripPrefix "--- " diff
  let strip = (length $ splitDirectories file) - (length $ splitDirectories span.file)
  Just Patch { strip, diff }

extractDiffCodeBlock :: String -> Maybe String
extractDiffCodeBlock = lines >>> \ case
  "```diff" : code -> case reverse code of
    "```" : (reverse >>> unlines -> diff) -> Just diff
    _ -> Nothing
  _ -> Nothing