packages feed

genai-lib-1.3: src/lib/GenAILib/HTTP.hs

{-# LANGUAGE DataKinds, TypeOperators #-}

module GenAILib.HTTP
  ( display
  , doChat
  , doCompletion
  )
  where

import Data.Aeson (FromJSON, ToJSON, Value (Object, String), decode, encode)
import Data.Aeson.KeyMap qualified as A
import Data.ByteString.Lazy qualified as BL
import Data.Maybe (fromMaybe)
import Data.Proxy (Proxy (..))
import Data.String.Conv (toS)
import Data.Text.IO qualified as TS
import Formatting ((%+), formatToString, string)
import Network.HTTP.Client (defaultManagerSettings, managerResponseTimeout,
  newManager, responseTimeoutMicro)
import Servant.API ((:<|>) (..), (:>), JSON, Post, ReqBody)
import Servant.Client (BaseUrl (..), Scheme (Http), ClientM,
  mkClientEnv, runClientM)
import Servant.Client.Core (clientIn)
import System.Exit (exitFailure)

import GenAILib.Common (RawOutput (..))
import GenAILib.Ollama (Host (..))
import GenAILib.System.Log (debugM, emergencyM, errorM, lname)


defaultHttpTimeout :: Int
defaultHttpTimeout = 1_200_000_000  -- In microseconds, this is 2 minutes


{- NOTE: The reason we've chosen Aeson's Value type for the response is we want
   to do one of these things with these responses:

   1. Display only the LLM response text (in the JSON Value's 'response' or 'messages.content' field)
   2. Pretty-print the entire JSON response ( | jq . )
   3. Dump the entire JSON response as-is ( | jq -r|--raw-output . )

   We don't need to model the entire Ollama response JSON document to achieve these goals.
-}
type API
  =    "api" :> "generate" :> ReqBody '[JSON] Value :> Post '[JSON] Value
  :<|> "api" :> "chat" :> ReqBody '[JSON] Value :> Post '[JSON] Value

-- A Proxy to our API
api :: Proxy API
api = Proxy

clientM :: Proxy ClientM
clientM = Proxy

completion :: Value -> ClientM Value
chat :: Value -> ClientM Value
(completion :<|> chat) = api `clientIn` clientM


doCompletion :: ToJSON j => Host -> j -> IO Value
doCompletion = doRequest completion

doChat :: ToJSON j => Host -> j -> IO Value
doChat = doRequest chat

doRequest :: (FromJSON f, ToJSON t) => (f -> ClientM Value) -> Host -> t -> IO Value
doRequest client' (Host hostName port) bodyData = do
  debugM lname . toS . encode $ bodyData
  let
    bodyDoc = fromMaybe (error "Something went wrong with JSON-ifying the POST data")
      . decode . encode $ bodyData
  manager' <- newManager $ defaultManagerSettings
    { managerResponseTimeout = responseTimeoutMicro defaultHttpTimeout }
  eres <- runClientM (client' bodyDoc)
    (mkClientEnv manager' (BaseUrl Http (toS hostName) port ""))
  case eres of
    Left err -> logAndExit $ show err
    Right v@(Object _) -> pure v
    Right somethingElse -> logAndExit $ show somethingElse


logAndExit :: String -> IO a
logAndExit msg = do
  errorM lname $ formatToString ("Can't continue:" %+ string) msg
  exitFailure


display :: RawOutput -> Value -> IO ()

display (RawOutput True) v = BL.putStr $ encode v <> "\n"

display (RawOutput False) (Object keyMap) = case A.lookup "response" keyMap of
  Just (String t) -> TS.putStrLn t
  Just somethingElse -> emergencyM lname $ show somethingElse
  Nothing -> emergencyM lname $ show keyMap

display _ somethingElse = emergencyM lname $ show somethingElse