ollama-haskell 0.1.2.0 → 0.1.3.0
raw patch · 8 files changed
+344/−75 lines, 8 filesdep +containersPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: containers
API changes (from Hackage documentation)
- Data.Ollama.Embeddings: instance GHC.Generics.Generic Data.Ollama.Embeddings.EmbeddingResp
+ Data.Ollama.Chat: JsonFormat :: Format
+ Data.Ollama.Chat: SchemaFormat :: Value -> Format
+ Data.Ollama.Chat: [options] :: ChatOps -> Maybe Value
+ Data.Ollama.Chat: [tool_calls] :: Message -> Maybe [Value]
+ Data.Ollama.Chat: data Format
+ Data.Ollama.Chat: schemaFromType :: ToJSON a => a -> ByteString
+ Data.Ollama.Common.Types: JsonFormat :: Format
+ Data.Ollama.Common.Types: SchemaFormat :: Value -> Format
+ Data.Ollama.Common.Types: data Format
+ Data.Ollama.Common.Types: instance Data.Aeson.Types.ToJSON.ToJSON Data.Ollama.Common.Types.Format
+ Data.Ollama.Common.Types: instance GHC.Classes.Eq Data.Ollama.Common.Types.Format
+ Data.Ollama.Common.Types: instance GHC.Show.Show Data.Ollama.Common.Types.Format
+ Data.Ollama.Embeddings: EmbeddingOps :: Text -> Text -> Maybe Bool -> Maybe Text -> EmbeddingOps
+ Data.Ollama.Embeddings: EmbeddingResp :: Text -> [[Float]] -> EmbeddingResp
+ Data.Ollama.Embeddings: [$sel:embedding_:EmbeddingResp] :: EmbeddingResp -> [[Float]]
+ Data.Ollama.Embeddings: [$sel:input:EmbeddingOps] :: EmbeddingOps -> Text
+ Data.Ollama.Embeddings: [$sel:keepAlive:EmbeddingOps] :: EmbeddingOps -> Maybe Text
+ Data.Ollama.Embeddings: [$sel:model:EmbeddingOps] :: EmbeddingOps -> Text
+ Data.Ollama.Embeddings: [$sel:model:EmbeddingResp] :: EmbeddingResp -> Text
+ Data.Ollama.Embeddings: [$sel:truncate:EmbeddingOps] :: EmbeddingOps -> Maybe Bool
+ Data.Ollama.Embeddings: data EmbeddingOps
+ Data.Ollama.Embeddings: data EmbeddingResp
+ Data.Ollama.Generate: [$sel:options:GenerateOps] :: GenerateOps -> Maybe Value
+ Ollama: EmbeddingOps :: Text -> Text -> Maybe Bool -> Maybe Text -> EmbeddingOps
+ Ollama: EmbeddingResp :: Text -> [[Float]] -> EmbeddingResp
+ Ollama: JsonFormat :: Format
+ Ollama: SchemaFormat :: Value -> Format
+ Ollama: [$sel:embedding_:EmbeddingResp] :: EmbeddingResp -> [[Float]]
+ Ollama: [$sel:input:EmbeddingOps] :: EmbeddingOps -> Text
+ Ollama: [$sel:keepAlive:EmbeddingOps] :: EmbeddingOps -> Maybe Text
+ Ollama: [$sel:model:EmbeddingOps] :: EmbeddingOps -> Text
+ Ollama: [$sel:model:EmbeddingResp] :: EmbeddingResp -> Text
+ Ollama: [$sel:options:GenerateOps] :: GenerateOps -> Maybe Value
+ Ollama: [$sel:truncate:EmbeddingOps] :: EmbeddingOps -> Maybe Bool
+ Ollama: [options] :: ChatOps -> Maybe Value
+ Ollama: [tool_calls] :: Message -> Maybe [Value]
+ Ollama: data EmbeddingOps
+ Ollama: data EmbeddingResp
+ Ollama: data Format
- Data.Ollama.Chat: ChatOps :: Text -> NonEmpty Message -> Maybe Text -> Maybe Text -> Maybe (ChatResponse -> IO (), IO ()) -> Maybe Text -> Maybe Text -> Maybe Int -> ChatOps
+ Data.Ollama.Chat: ChatOps :: Text -> NonEmpty Message -> Maybe [Value] -> Maybe Format -> Maybe (ChatResponse -> IO (), IO ()) -> Maybe Text -> Maybe Text -> Maybe Int -> Maybe Value -> ChatOps
- Data.Ollama.Chat: Message :: Role -> Text -> Maybe [Text] -> Message
+ Data.Ollama.Chat: Message :: Role -> Text -> Maybe [Text] -> Maybe [Value] -> Message
- Data.Ollama.Chat: [format] :: ChatOps -> Maybe Text
+ Data.Ollama.Chat: [format] :: ChatOps -> Maybe Format
- Data.Ollama.Chat: [tools] :: ChatOps -> Maybe Text
+ Data.Ollama.Chat: [tools] :: ChatOps -> Maybe [Value]
- Data.Ollama.Embeddings: embedding :: Text -> Text -> IO (Maybe EmbeddingResp)
+ Data.Ollama.Embeddings: embedding :: Text -> Text -> IO (Either String EmbeddingResp)
- Data.Ollama.Embeddings: embeddingOps :: Text -> Text -> Maybe Bool -> Maybe Text -> IO (Maybe EmbeddingResp)
+ Data.Ollama.Embeddings: embeddingOps :: Text -> Text -> Maybe Bool -> Maybe Text -> IO (Either String EmbeddingResp)
- Data.Ollama.Generate: GenerateOps :: Text -> Text -> Maybe Text -> Maybe [Text] -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe (GenerateResponse -> IO (), IO ()) -> Maybe Bool -> Maybe Text -> Maybe Text -> Maybe Int -> GenerateOps
+ Data.Ollama.Generate: GenerateOps :: Text -> Text -> Maybe Text -> Maybe [Text] -> Maybe Format -> Maybe Text -> Maybe Text -> Maybe (GenerateResponse -> IO (), IO ()) -> Maybe Bool -> Maybe Text -> Maybe Text -> Maybe Int -> Maybe Value -> GenerateOps
- Data.Ollama.Generate: [$sel:format:GenerateOps] :: GenerateOps -> Maybe Text
+ Data.Ollama.Generate: [$sel:format:GenerateOps] :: GenerateOps -> Maybe Format
- Ollama: ChatOps :: Text -> NonEmpty Message -> Maybe Text -> Maybe Text -> Maybe (ChatResponse -> IO (), IO ()) -> Maybe Text -> Maybe Text -> Maybe Int -> ChatOps
+ Ollama: ChatOps :: Text -> NonEmpty Message -> Maybe [Value] -> Maybe Format -> Maybe (ChatResponse -> IO (), IO ()) -> Maybe Text -> Maybe Text -> Maybe Int -> Maybe Value -> ChatOps
- Ollama: GenerateOps :: Text -> Text -> Maybe Text -> Maybe [Text] -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe (GenerateResponse -> IO (), IO ()) -> Maybe Bool -> Maybe Text -> Maybe Text -> Maybe Int -> GenerateOps
+ Ollama: GenerateOps :: Text -> Text -> Maybe Text -> Maybe [Text] -> Maybe Format -> Maybe Text -> Maybe Text -> Maybe (GenerateResponse -> IO (), IO ()) -> Maybe Bool -> Maybe Text -> Maybe Text -> Maybe Int -> Maybe Value -> GenerateOps
- Ollama: Message :: Role -> Text -> Maybe [Text] -> Message
+ Ollama: Message :: Role -> Text -> Maybe [Text] -> Maybe [Value] -> Message
- Ollama: [$sel:format:GenerateOps] :: GenerateOps -> Maybe Text
+ Ollama: [$sel:format:GenerateOps] :: GenerateOps -> Maybe Format
- Ollama: [format] :: ChatOps -> Maybe Text
+ Ollama: [format] :: ChatOps -> Maybe Format
- Ollama: [tools] :: ChatOps -> Maybe Text
+ Ollama: [tools] :: ChatOps -> Maybe [Value]
- Ollama: embedding :: Text -> Text -> IO (Maybe EmbeddingResp)
+ Ollama: embedding :: Text -> Text -> IO (Either String EmbeddingResp)
- Ollama: embeddingOps :: Text -> Text -> Maybe Bool -> Maybe Text -> IO (Maybe EmbeddingResp)
+ Ollama: embeddingOps :: Text -> Text -> Maybe Bool -> Maybe Text -> IO (Either String EmbeddingResp)
Files
- CHANGELOG.md +6/−0
- ollama-haskell.cabal +4/−2
- src/Data/Ollama/Chat.hs +126/−41
- src/Data/Ollama/Common/Types.hs +29/−0
- src/Data/Ollama/Embeddings.hs +39/−25
- src/Data/Ollama/Generate.hs +42/−2
- src/Ollama.hs +5/−1
- test/Main.hs +93/−4
CHANGELOG.md view
@@ -1,5 +1,11 @@ # Revision history for ollama-haskell +## 0.1.3.0 -- 2025-03-25++* Added options, tools and tool_calls fields in chat and generate.+* Exported EmbeddingResponse.+* Added Format argument in chat and generate function for structured output.+ ## 0.1.2.0 -- 2024-11-20 * Added hostUrl and responseTimeOut options in generate function.
ollama-haskell.cabal view
@@ -5,9 +5,9 @@ -- see: https://github.com/sol/hpack name: ollama-haskell-version: 0.1.2.0+version: 0.1.3.0 synopsis: Haskell bindings for ollama.-description: Please see the README on GitHub at <https://github.com/tusharad/ollama-haskell#readme>+description: Ollama client for Haskell category: Web homepage: https://github.com/tusharad/ollama-haskell#readme bug-reports: https://github.com/tusharad/ollama-haskell/issues@@ -53,6 +53,7 @@ , base >=4.7 && <5 , base64-bytestring , bytestring+ , containers , directory , filepath , http-client@@ -76,6 +77,7 @@ , base >=4.7 && <5 , base64-bytestring , bytestring+ , containers , directory , filepath , http-client
src/Data/Ollama/Chat.hs view
@@ -12,22 +12,26 @@ , defaultChatOps , ChatOps (..) , ChatResponse (..)+ , Format (..)+ , schemaFromType ) where import Control.Exception (try) import Data.Aeson-import Data.ByteString.Char8 qualified as BS-import Data.ByteString.Lazy.Char8 qualified as BSL+import qualified Data.ByteString.Char8 as BS+import qualified Data.ByteString.Lazy.Char8 as BSL import Data.List.NonEmpty as NonEmpty import Data.Maybe (fromMaybe, isNothing) import Data.Ollama.Common.Utils as CU+import Data.Ollama.Common.Types (Format(..)) import Data.Text (Text)-import Data.Text qualified as T-import Data.Text.Encoding qualified as T+import qualified Data.Text as T+import qualified Data.Text.Encoding as T import Data.Time (UTCTime) import GHC.Generics import GHC.Int (Int64) import Network.HTTP.Client+import qualified Data.Aeson.KeyMap as HM -- | Enumerated roles that can participate in a chat. data Role = System | User | Assistant | Tool@@ -56,6 +60,9 @@ -- ^ The textual content of the message. , images :: Maybe [Text] -- ^ Optional list of base64 encoded images that accompany the message.+ , tool_calls :: Maybe [Value]+ -- ^ a list of tools in JSON that the model wants to use+ -- ^ Since 0.1.3.0 } deriving (Show, Eq, Generic, ToJSON, FromJSON) @@ -65,10 +72,11 @@ -- ^ The name of the chat model to be used. , messages :: NonEmpty Message -- ^ A non-empty list of messages forming the conversation context.- , tools :: Maybe Text+ , tools :: Maybe [Value] -- ^ Optional tools that may be used in the chat.- , format :: Maybe Text- -- ^ An optional format for the chat response.+ , format :: Maybe Format+ -- ^ An optional format for the chat response (json or JSON schema).+ -- ^ Since 0.1.3.0 , stream :: Maybe (ChatResponse -> IO (), IO ()) -- ^ Optional streaming functions where the first handles each chunk of the response, and the second flushes the stream. , keepAlive :: Maybe Text@@ -77,6 +85,9 @@ -- ^ Override default Ollama host url. Default url = "http://127.0.0.1:11434" , responseTimeOut :: Maybe Int -- ^ Override default response timeout in minutes. Default = 15 minutes+ , options :: Maybe Value+ -- ^ additional model parameters listed in the documentation for the Modelfile such as temperature+ -- ^ Since 0.1.3.0 } instance Show ChatOps where@@ -128,7 +139,7 @@ deriving (Show, Eq) instance ToJSON ChatOps where- toJSON (ChatOps model_ messages_ tools_ format_ stream_ keepAlive_ _ _) =+ toJSON (ChatOps model_ messages_ tools_ format_ stream_ keepAlive_ _ _ options) = object [ "model" .= model_ , "messages" .= messages_@@ -136,6 +147,7 @@ , "format" .= format_ , "stream" .= if isNothing stream_ then Just False else Just True , "keep_alive" .= keepAlive_+ , "options" .= options ] instance FromJSON ChatResponse where@@ -165,13 +177,14 @@ defaultChatOps = ChatOps { chatModelName = "llama3.2"- , messages = Message User "What is 2+2?" Nothing :| []+ , messages = Message User "What is 2+2?" Nothing Nothing :| [] , tools = Nothing , format = Nothing , stream = Nothing , keepAlive = Nothing , hostUrl = Nothing , responseTimeOut = Nothing+ , options = Nothing } {- |@@ -187,6 +200,17 @@ > case result of > Left errorMsg -> putStrLn ("Error: " ++ errorMsg) > Right response -> print response++To request a JSON format response:++> let ops = defaultChatOps { format = Just JsonFormat }+> result <- chat ops++To request a structured output with a JSON schema:++> import Data.Aeson (object, (.=))+> let ops = defaultChatOps { format = Just (SchemaFormat schema) }+> result <- chat ops -} chat :: ChatOps -> IO (Either String ChatResponse) chat cOps = do@@ -212,7 +236,8 @@ IO (Either HttpException (Either String ChatResponse)) case eRes of Left e -> return $ Left $ "HTTP error occured: " <> show e- Right r -> return r+ Right r -> do + return r handleRequest :: ChatOps -> Response BodyReader -> IO (Either String ChatResponse) handleRequest cOps response = do@@ -249,6 +274,10 @@ response in certain JSON format and serializes the response. This function will be helpful when you want to use the LLM to do something programmatic. + Note: This function predates the format parameter in the API. For new code, consider using+ the `format` parameter with a SchemaFormat instead, which leverages the model's native+ JSON output capabilities.+ For Example: > let expectedJsonStrucutre = Example { > sortedList = ["sorted List here"]@@ -283,34 +312,90 @@ Maybe Int -> IO (Either String jsonResult) chatJson cOps@ChatOps {..} jsonStructure mMaxRetries = do- let lastMessage = NonEmpty.last messages- jsonHelperPrompt =- "You are an AI that returns only JSON object. \n"- <> "* Your output should be a JSON object that matches the following schema: \n"- <> T.decodeUtf8 (BSL.toStrict $ encode jsonStructure)- <> content lastMessage- <> "\n"- <> "# How to treat the task:\n"- <> "* Stricly follow the schema for the output.\n"- <> "* Never return anything other than a JSON object.\n"- <> "* Do not talk to the user.\n"- chatResponse <-- chat- cOps- { messages =- NonEmpty.fromList $- lastMessage {content = jsonHelperPrompt} : NonEmpty.init messages- }- case chatResponse of- Left err -> return $ Left err- Right r -> do- let mMessage = message r- case mMessage of- Nothing -> return $ Left "Something went wrong"- Just res -> do- case decode (BSL.fromStrict . T.encodeUtf8 $ content res) of- Nothing -> do- case mMaxRetries of- Nothing -> return $ Left "Decoding Failed :("- Just n -> if n < 1 then return $ Left "Decoding Failed :(" else chatJson cOps jsonStructure (Just (n - 1))- Just resultInType -> return $ Right resultInType+ -- For models that support the format parameter, use that directly+ --let jsonSchema = encode jsonStructure+ let useNativeFormat = False -- Set to True to use the native format parameter when appropriate+ + if useNativeFormat+ then do+ let formattedOps = cOps { format = Just (SchemaFormat (Object $ HM.fromList [("schema", Object $ HM.fromList [("type", String "object")])])) }+ chatResponse <- chat formattedOps+ case chatResponse of+ Left err -> return $ Left err+ Right r -> do+ let mMessage = message r+ case mMessage of+ Nothing -> return $ Left "Something went wrong"+ Just res -> case decode (BSL.fromStrict . T.encodeUtf8 $ content res) of+ Nothing -> return $ Left "Decoding Failed :("+ Just resultInType -> return $ Right resultInType+ else do+ -- Fall back to the original implementation using prompts+ let lastMessage = NonEmpty.last messages+ jsonHelperPrompt =+ "You are an AI that returns only JSON object. \n"+ <> "* Your output should be a JSON object that matches the following schema: \n"+ <> T.decodeUtf8 (BSL.toStrict $ encode jsonStructure)+ <> content lastMessage+ <> "\n"+ <> "# How to treat the task:\n"+ <> "* Stricly follow the schema for the output.\n"+ <> "* Never return anything other than a JSON object.\n"+ <> "* Do not talk to the user.\n"+ chatResponse <-+ chat+ cOps+ { messages =+ NonEmpty.fromList $+ lastMessage {content = jsonHelperPrompt} : NonEmpty.init messages+ }+ case chatResponse of+ Left err -> return $ Left err+ Right r -> do+ let mMessage = message r+ case mMessage of+ Nothing -> return $ Left "Something went wrong"+ Just res -> do+ case decode (BSL.fromStrict . T.encodeUtf8 $ content res) of+ Nothing -> do+ case mMaxRetries of+ Nothing -> return $ Left "Decoding Failed :("+ Just n -> if n < 1 then return $ Left "Decoding Failed :(" else chatJson cOps jsonStructure (Just (n - 1))+ Just resultInType -> return $ Right resultInType++-- | Helper function to create a JSON schema from a Haskell type+schemaFromType :: ToJSON a => a -> BSL.ByteString+schemaFromType = encode -- This is a simplified version; a real implementation would generate a JSON Schema++{- |+ Example usage of 'Ollama.chat' with a JSON schema format and options field.++ The first example sends a message requesting a JSON response conforming to a given schema.+ The second example uses an alternative JSON format (here, @JsonFormat@).++ >>> import Data.Aeson (Value, object, (.=))+ >>> import Data.List.NonEmpty (NonEmpty(..))+ >>> import Ollama (defaultChatOps, Message(..), SchemaFormat, JsonFormat)+ >>> let x :: Value+ ... x = object [ "type" .= ("object" :: String)+ ... , "properties" .= object [ "age" .= object ["type" .= ("integer" :: String)] ]+ ... ]+ >>> let msg = Message User "Ollama is 22 years old and is busy saving the world. Respond using JSON" Nothing+ >>> let opts = object ["option" .= ("some value" :: String)]+ >>> res <- chat defaultChatOps+ ... { chatModelName = "llama3.2"+ ... , messages = msg :| []+ ... , format = Just (SchemaFormat x)+ ... , options = opts+ ... }+ >>> print (message res)+ Just (Message {role = Assistant, content = "{\n \"age\": 22\n}", images = Nothing})+ >>> res2 <- chat defaultChatOps+ ... { chatModelName = "llama3.2"+ ... , messages = msg :| []+ ... , format = Just JsonFormat+ ... , options = object ["option" .= ("other value" :: String)]+ ... }+ >>> print (message res2)+ Just (Message {role = Assistant, content = "{ \"Name\": \"Ollama\", \"Age\": 22, \"Occupation\": \"World Savior\", \"Goals\": [\"Save humanity from alien invasion\", \"Unite warring nations\", \"Protect the environment\"] }", images = Nothing})+-}
src/Data/Ollama/Common/Types.hs view
@@ -4,6 +4,7 @@ module Data.Ollama.Common.Types ( ModelDetails (..) , OllamaClient (..)+ , Format (..) ) where import Data.Aeson@@ -33,3 +34,31 @@ { host :: Text } deriving (Eq, Show)+++{-|+E.g SchemaFormat+{+ "type": "object",+ "properties": {+ "age": {+ "type": "integer"+ },+ "available": {+ "type": "boolean"+ }+ },+ "required": [+ "age",+ "available"+ ]+ }+|-}+-- | Format specification for the chat output+-- | Since 0.1.3.0+data Format = JsonFormat | SchemaFormat Value+ deriving (Show, Eq)++instance ToJSON Format where+ toJSON JsonFormat = String "json"+ toJSON (SchemaFormat schema) = schema
src/Data/Ollama/Embeddings.hs view
@@ -1,5 +1,3 @@-{-# LANGUAGE DeriveAnyClass #-}-{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE OverloadedStrings #-} @@ -7,14 +5,17 @@ ( -- * Embedding API embedding , embeddingOps+ , EmbeddingOps (..)+ , EmbeddingResp (..) ) where import Data.Aeson import Data.Ollama.Common.Utils as CU import Data.Text (Text) import Data.Text qualified as T-import GHC.Generics import Network.HTTP.Client+import Control.Exception (try)+import Data.ByteString.Lazy.Char8 (ByteString) -- TODO: Add Options parameter data EmbeddingOps = EmbeddingOps@@ -27,10 +28,15 @@ data EmbeddingResp = EmbeddingResp { model :: Text- , embedding' :: [[Float]]+ , embedding_ :: [[Float]] }- deriving (Show, Eq, Generic, FromJSON)+ deriving (Show, Eq) +instance FromJSON EmbeddingResp where+ parseJSON = withObject "EmbeddingResp" $ \v -> EmbeddingResp+ <$> v .: "model"+ <*> v .: "embeddings"+ instance ToJSON EmbeddingOps where toJSON (EmbeddingOps model_ input_ truncate' keepAlive_) = object@@ -52,28 +58,36 @@ Maybe Bool -> -- | Keep Alive Maybe Text ->- IO (Maybe EmbeddingResp)+ IO (Either String EmbeddingResp) embeddingOps modelName input_ mTruncate mKeepAlive = do let url = defaultOllamaUrl manager <- newManager defaultManagerSettings- initialRequest <- parseRequest $ T.unpack (url <> "/api/embed")- let reqBody =- EmbeddingOps- { model = modelName- , input = input_- , truncate = mTruncate- , keepAlive = mKeepAlive- }- request =- initialRequest- { method = "POST"- , requestBody = RequestBodyLBS $ encode reqBody- }- resp <- httpLbs request manager- let mRes = decode (responseBody resp) :: Maybe EmbeddingResp- case mRes of- Nothing -> pure Nothing- Just r -> pure $ Just r+ --einitialRequest <- parseRequest $ T.unpack (url <> "/api/embed")+ eInitialRequest <-+ try $ parseRequest $ T.unpack (url <> "/api/embed") :: IO (Either HttpException Request)+ case eInitialRequest of+ Left e -> do+ return $ Left $ show e+ Right initialRequest -> do+ let reqBody =+ EmbeddingOps+ { model = modelName+ , input = input_+ , truncate = mTruncate+ , keepAlive = mKeepAlive+ }+ request =+ initialRequest+ { method = "POST"+ , requestBody = RequestBodyLBS $ encode reqBody+ }+ eResp <- try $ httpLbs request manager :: IO (Either HttpException (Response ByteString))+ case eResp of+ Left err -> return $ Left (show err)+ Right resp -> + case decode (responseBody resp) of+ Nothing -> return $ Left $ "Couldn't decode response: " <> show (responseBody resp)+ Just r -> return $ Right r -- Higher level binding that only takes important params @@ -83,6 +97,6 @@ Text -> -- | Input Text ->- IO (Maybe EmbeddingResp)+ IO (Either String EmbeddingResp) embedding modelName input_ = embeddingOps modelName input_ Nothing Nothing
src/Data/Ollama/Generate.hs view
@@ -17,6 +17,7 @@ import Data.ByteString.Lazy.Char8 qualified as BSL import Data.Maybe import Data.Ollama.Common.Utils as CU+import Data.Ollama.Common.Types (Format(..)) import Data.Text (Text) import Data.Text qualified as T import Data.Text.Encoding qualified as T@@ -55,8 +56,9 @@ -- ^ An optional suffix to append to the generated text. , images :: Maybe [Text] -- ^ Optional list of base64 encoded images to include with the request.- , format :: Maybe Text+ , format :: Maybe Format -- ^ An optional format specifier for the response.+ -- ^ Since 0.1.3.0 , system :: Maybe Text -- ^ Optional system text that can be included in the generation context. , template :: Maybe Text@@ -71,6 +73,9 @@ -- ^ Override default Ollama host url. Default url = "http://127.0.0.1:11434" , responseTimeOut :: Maybe Int -- ^ Override default response timeout in minutes. Default = 15 minutes+ , options :: Maybe Value+ -- ^ additional model parameters listed in the documentation for the Modelfile such as temperature+ -- ^ Since 0.1.3.0 } instance Show GenerateOps where@@ -96,6 +101,8 @@ <> show raw <> ", keepAlive : " <> show keepAlive+ <> ", options : "+ <> show options instance Eq GenerateOps where (==) a b =@@ -108,6 +115,7 @@ && template a == template b && raw a == raw b && keepAlive a == keepAlive b+ && options a == options b -- TODO: Add Context Param @@ -153,6 +161,7 @@ keepAlive _ -- Host url _ -- Response timeout+ options ) = object [ "model" .= model@@ -165,6 +174,7 @@ , "stream" .= if isNothing stream then Just False else Just True , "raw" .= raw , "keep_alive" .= keepAlive+ , "options" .= options ] instance FromJSON GenerateResponse where@@ -206,6 +216,7 @@ , keepAlive = Nothing , hostUrl = Nothing , responseTimeOut = Nothing+ , options = Nothing } {- |@@ -288,7 +299,7 @@ Right r -> do _ <- sendChunk r _ <- flush- if done r then pure (Left "") else streamResponse sendChunk flush+ if done r then pure (Right r) else streamResponse sendChunk flush let genResponse op = do bs <- brRead $ responseBody response if bs == ""@@ -368,3 +379,32 @@ then return $ Left "Decoding failed :(" else generateJson genOps jsonStructure (Just (n - 1)) Just resultInType -> return $ Right resultInType+++{- |+ Example usage of 'Ollama.generate' with a JSON schema format and options field.++ In this example we pass a JSON schema that expects an object with an integer field @age@.+ The options field is supplied as a JSON value.++ >>> import Data.Aeson (Value, object, (.=))+ >>> import Ollama (GenerateOps, defaultGenerateOps, SchemaFormat)+ >>> let x :: Value+ ... x = object [ "type" .= ("object" :: String)+ ... , "properties" .= object [ "age" .= object ["type" .= ("integer" :: String)] ]+ ... ]+ >>> let opts :: Value+ ... opts = object ["option" .= ("some value" :: String)]+ >>> generate defaultGenerateOps+ ... { modelName = "llama3.2"+ ... , prompt = "Ollama is 22 years old and is busy saving the world. Respond using JSON"+ ... , format = Just (SchemaFormat x)+ ... , options = opts+ ... }+ Right (GenerateResponse {model = "llama3.2", createdAt = 2025-03-25 09:34:15.853417157 UTC,+ response_ = "{\n \"age\": 22\n}", done = True,+ totalDuration = Just 6625631744, loadDuration = Just 2578791966,+ promptEvalCount = Just 43, promptEvalDuration = Just 2983000000,+ evalCount = Just 10, evalDuration = Just 1061000000})+-}+
src/Ollama.hs view
@@ -24,6 +24,8 @@ -- ** Embeddings , embedding , embeddingOps+ , EmbeddingOps (..)+ , EmbeddingResp (..) -- * Ollama operations @@ -60,6 +62,7 @@ , RunningModels (..) , RunningModel (..) , Message (..)+ , Format(..) ) where @@ -75,7 +78,7 @@ import Data.Ollama.Copy (copyModel) import Data.Ollama.Create (createModel, createModelOps) import Data.Ollama.Delete (deleteModel)-import Data.Ollama.Embeddings (embedding, embeddingOps)+import Data.Ollama.Embeddings (embedding, embeddingOps, EmbeddingOps (..), EmbeddingResp (..)) import Data.Ollama.Generate ( GenerateOps (..) , GenerateResponse (..)@@ -88,3 +91,4 @@ import Data.Ollama.Pull (pull, pullOps) import Data.Ollama.Push (push, pushOps) import Data.Ollama.Show (ShowModelResponse (..), showModel, showModelOps)+import Data.Ollama.Common.Types (Format (..))
test/Main.hs view
@@ -12,6 +12,8 @@ import System.IO.Silently (capture) import Test.Tasty import Test.Tasty.HUnit+import qualified Data.Text as T +import Data.Aeson tests :: TestTree tests =@@ -21,6 +23,9 @@ , chatTest , psTest , showTest+ , embeddingTest+ , generateFormatTest+ , chatFormatTest ] generateTest :: TestTree@@ -59,13 +64,48 @@ assertBool "Expecting generation to fail with invalid model" (isLeft eRes) ] +generateFormatTest :: TestTree+generateFormatTest =+ testGroup "Generate tests with format and options"+ [ testCase "Generate with SchemaFormat and options" $ do+ let schema = object+ [ "type" .= ("object" :: String)+ , "properties" .= object [ "age" .= object ["type" .= ("integer" :: String)] ]+ ]+ eRes <-+ Ollama.generate+ defaultGenerateOps+ { modelName = "llama3.2"+ , prompt = "Ollama is 22 years old and is busy saving the world. Respond using JSON"+ , format = Just (Ollama.SchemaFormat schema)+ }+ case eRes of+ Right res ->+ assertBool "Response should contain JSON with key \"age\"" ( "age" `T.isInfixOf` Ollama.response_ res )+ Left err ->+ assertFailure $ "Generation failed with error: " ++ show err+ , testCase "Generate with JsonFormat and options" $ do+ eRes <-+ Ollama.generate+ defaultGenerateOps+ { modelName = "llama3.2"+ , prompt = "Provide a simple JSON response describing Ollama."+ , format = Just Ollama.JsonFormat+ }+ case eRes of+ Right res ->+ assertBool "Response should start with '{'" ( "{" `T.isPrefixOf` Ollama.response_ res )+ Left err ->+ assertFailure $ "Generation failed with error: " ++ show err+ ]+ chatTest :: TestTree chatTest = testGroup "Chat tests" [ testCase "chat stream" $ do- let msg = Ollama.Message User "What is 29 + 3?" Nothing- defaultMsg = Ollama.Message User "" Nothing+ let msg = Ollama.Message User "What is 29 + 3?" Nothing Nothing+ defaultMsg = Ollama.Message User "" Nothing Nothing output <- capture $ Ollama.chat@@ -76,7 +116,7 @@ } assertBool "Checking if chat function is printing anything" (length output > 0) , testCase "Chat non-stream" $ do- let msg = Ollama.Message User "What is 29 + 3?" Nothing+ let msg = Ollama.Message User "What is 29 + 3?" Nothing Nothing eRes <- Ollama.chat defaultChatOps@@ -85,7 +125,7 @@ } assertBool "Checking if chat function returns a valid value" (isRight eRes) , testCase "Chat invalid host url" $ do- let msg = Ollama.Message User "What is 29 + 3?" Nothing+ let msg = Ollama.Message User "What is 29 + 3?" Nothing Nothing eRes <- Ollama.chat defaultChatOps@@ -97,6 +137,46 @@ assertBool "It should return Left" (isLeft eRes) ] +chatFormatTest :: TestTree+chatFormatTest =+ testGroup "Chat tests with format and options"+ [ testCase "Chat with SchemaFormat and options" $ do+ let schema = object+ [ "type" .= ("object" :: String)+ , "properties" .= object [ "age" .= object ["type" .= ("integer" :: String)] ]+ ]+ msg = Ollama.Message User "Ollama is 22 years old and is busy saving the world. Respond using JSON" Nothing Nothing+ eRes <-+ Ollama.chat+ defaultChatOps+ { Chat.chatModelName = "llama3.2"+ , Chat.messages = msg :| []+ , Chat.format = Just (Ollama.SchemaFormat schema)+ , Chat.options = Just $ object ["penalize_newline" .= Bool True]+ }+ case eRes of+ Right res ->+ assertBool "Chat response should contain key \"age\"" $+ maybe False (\m -> "age" `T.isInfixOf` Chat.content m) (Chat.message res)+ Left err ->+ assertFailure $ "Chat failed with error: " ++ show err+ , testCase "Chat with JsonFormat and options" $ do+ let msg = Ollama.Message User "Tell me about Ollama in JSON format." Nothing Nothing+ eRes <-+ Ollama.chat+ defaultChatOps+ { Chat.chatModelName = "llama3.2"+ , Chat.messages = msg :| []+ , Chat.format = Just Ollama.JsonFormat+ }+ case eRes of+ Right res ->+ assertBool "Chat response should start with '{'" $+ maybe False (\m -> "{" `T.isPrefixOf` Chat.content m) (Chat.message res)+ Left err ->+ assertFailure $ "Chat failed with error: " ++ show err+ ]+ psTest :: TestTree psTest = testGroup@@ -113,6 +193,15 @@ [ testCase "check show" $ do mRes <- Ollama.showModel "llama3.2" assertBool "Check if model exists or not" (isJust mRes)+ ]++embeddingTest :: TestTree+embeddingTest =+ testGroup+ "Embedding test"+ [ testCase "check embedding" $ do+ eRes <- Ollama.embedding "llama3.2" "Why is sky blue?"+ assertBool "Check if embedding returns anything" (isRight eRes) ] main :: IO ()