genai-lib-2.0: src/lib/GenAILib/Ollama.hs
{-# LANGUAGE DuplicateRecordFields, OverloadedRecordDot #-}
module GenAILib.Ollama
( OllamaRequest (..)
, defaultUrl
, KeepAlive (..)
, keepalive
, OllamaResponse (..)
, getMessage
-- , stream'
)
where
import Data.Aeson (FromJSON, ToJSON, Value (Object),
genericToEncoding, genericToJSON, toEncoding, toJSON)
import Data.Aeson.Key (fromText)
import Data.Aeson.KeyMap (singleton, union)
import Data.Maybe (fromMaybe)
import Data.Sequence (Seq, (|>), empty)
import Data.Text qualified as TS
import Data.Text.Lazy qualified as TL
import Data.Time (UTCTime)
import GHC.Generics (Generic)
import Servant.Client (BaseUrl, parseBaseUrl)
import GenAILib (Content (..), Message (..), Model (..), Request (..),
RequestMod (..), Stream (..))
import GenAILib.JSON (customJSONOptions)
-- | Data for making Ollama requests
data OllamaRequest = OllamaRequest
{ model :: Model
, messages :: Seq Message
, options :: Maybe Value
, stream :: Stream
, keep_alive :: Maybe KeepAlive
}
deriving (Generic, Show)
instance ToJSON OllamaRequest where
toJSON = genericToJSON customJSONOptions
toEncoding = genericToEncoding customJSONOptions
instance Request OllamaRequest where
appendMessage :: Message -> OllamaRequest -> OllamaRequest
appendMessage newMessage oldReq = oldReq { messages = oldReq.messages |> newMessage }
mkRequest :: TL.Text -> RequestMod OllamaRequest -> OllamaRequest
mkRequest modelName (RequestMod optSetter) = optSetter
$ OllamaRequest (Model modelName) empty Nothing (Stream False) Nothing
setOpt :: TS.Text -> Value -> OllamaRequest -> OllamaRequest
setOpt keystr val req = req { options = Just . Object $ combinedKm }
where
newKm = singleton (fromText keystr) val
combinedKm = case options req of
(Just (Object existingKm)) -> existingKm `union` newKm
_ -> newKm
-- | How long an Ollama model is kept in memory before clean-up by the Ollama
-- daemon
--
-- This is specific to Ollama.
newtype KeepAlive = KeepAlive TL.Text
deriving (Generic, Show)
instance ToJSON KeepAlive
-- FIXME streaming does not work yet for Ollama via this library
-- Enable or disable streaming
-- stream' :: Bool -> OllamaRequest -> OllamaRequest
-- stream' b req = req { stream = Stream b }
-- | Set the keep_alive duration
--
-- This takes string duration values like '35m' or '1h'
keepalive :: TL.Text -> RequestMod OllamaRequest
keepalive ka = RequestMod (\req -> req { keep_alive = Just (KeepAlive ka) })
-- | Data returned for Ollama requests
data OllamaResponse = OllamaResponse
{ model :: Model
, created_at :: UTCTime
, message :: Message
, done :: Bool
-- Doubtful anyone really needs these fields
-- , total_duration :: Integer
-- , load_duration :: Integer
-- , prompt_eval_count :: Integer
-- , prompt_eval_duration :: Integer
-- , eval_count :: Integer
-- , eval_duration :: Integer
}
deriving (Generic, Show)
instance FromJSON OllamaResponse
-- | The default URL for making Ollama REST calls
defaultUrl :: BaseUrl
defaultUrl = fromMaybe (error "Unable to construct the default url for Ollama!")
$ parseBaseUrl "localhost:11434"
-- | Convenience function to extract the response text
getMessage :: OllamaResponse -> TL.Text
getMessage res = assistantResponse
where (Content assistantResponse) = content . message $ res