packages feed

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 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 ()