packages feed

ollama-haskell (empty) → 0.1.0.0

raw patch · 19 files changed

+1713/−0 lines, 19 filesdep +aesondep +basedep +bytestring

Dependencies added: aeson, base, bytestring, http-client, http-types, ollama-haskell, silently, tasty, tasty-hunit, text, time

Files

+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for ollama-haskell++## 0.1.0.0 -- YYYY-mm-dd++* First version. Released on an unsuspecting world.
+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2024 Tushar Adhatrao++Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions:++The above copyright notice and this permission notice shall be included+in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ ollama-haskell.cabal view
@@ -0,0 +1,71 @@+cabal-version:      3.4+name:               ollama-haskell+version:            0.1.0.0+synopsis:           Ollama Haskell library+-- description:+license:            MIT+license-file:       LICENSE+author:             Tushar Adhatrao+maintainer:         tusharadhatrao@gmail.com+description:        Haskell bindings for Ollama.+-- copyright:+category:           Data+build-type:         Simple+extra-doc-files:    CHANGELOG.md+-- extra-source-files:+Homepage:            https://github.com/tusharad/ollama-haskell+Bug-reports:         https://github.com/tusharad/ollama-haskell/issues+extra-source-files:  CHANGELOG.md++Source-repository head+  type:     git+  location: https://github.com/tusharad/ollama-haskell++common warnings+    ghc-options: -Wall++library+    import:           warnings+    exposed-modules:  Ollama+                    , Data.Ollama.Chat+                    , Data.Ollama.Copy+                    , Data.Ollama.Common.Types+                    , Data.Ollama.Common.Utils+                    , Data.Ollama.Create+                    , Data.Ollama.Delete+                    , Data.Ollama.Embeddings+                    , Data.Ollama.Generate+                    , Data.Ollama.List+                    , Data.Ollama.Ps+                    , Data.Ollama.Pull+                    , Data.Ollama.Push+                    , Data.Ollama.Show+                    , Lib+    -- other-modules:+    -- other-extensions:+    build-depends:    base ^>=4.18+                    , aeson >= 2.2.1 && < 2.3+                    , bytestring >= 0.11.5 && < 0.12+                    , text >= 2.0.2 && < 2.1+                    , time >= 1.12.2 && < 1.13+                    , http-client >= 0.7.17 && < 0.8+                    , http-types >= 0.12.4 && < 0.13++    hs-source-dirs:   src+    default-language: GHC2021++test-suite ollama-haskell-test+    import:           warnings+    default-language: GHC2021+    -- other-modules:+    -- other-extensions:+    type:             exitcode-stdio-1.0+    hs-source-dirs:   test+    main-is:          Main.hs+    build-depends:+        base ^>=4.18,+        tasty,+        tasty-hunit,+        text,+        silently,+        ollama-haskell
+ src/Data/Ollama/Chat.hs view
@@ -0,0 +1,212 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE OverloadedStrings #-}++module Data.Ollama.Chat+  ( -- * Chat APIs+    chat+  , Message (..)+  , Role (..)+  , defaultChatOps+  , ChatOps (..)+  , ChatResponse (..)+  ) where++import Data.Aeson+import Data.ByteString.Char8 qualified as BS+import Data.ByteString.Lazy.Char8 qualified as BSL+import Data.List.NonEmpty+import Data.Maybe (isNothing)+import Data.Ollama.Common.Utils as CU+import Data.Text (Text)+import Data.Text qualified as T+import Data.Time (UTCTime)+import GHC.Generics+import GHC.Int (Int64)+import Network.HTTP.Client++-- | Enumerated roles that can participate in a chat.+data Role = System | User | Assistant | Tool+  deriving (Show, Eq)++instance ToJSON Role where+  toJSON System = String "system"+  toJSON User = String "user"+  toJSON Assistant = String "assistant"+  toJSON Tool = String "tool"++instance FromJSON Role where+  parseJSON (String "system") = pure System+  parseJSON (String "user") = pure User+  parseJSON (String "assistant") = pure Assistant+  parseJSON (String "tool") = pure Tool+  parseJSON _ = fail "Invalid Role value"++-- TODO : Add tool_calls parameter+-- | Represents a message within a chat, including its role and content.+data Message = Message+  { role :: Role+  -- ^ The role of the entity sending the message (e.g., 'User', 'Assistant').+  , content :: Text+  -- ^ The textual content of the message.+  , images :: Maybe [Text]+  -- ^ Optional list of base64 encoded images that accompany the message.+  }+  deriving (Show, Eq, Generic, ToJSON, FromJSON)++-- TODO: Add Options parameter+data ChatOps = ChatOps+  { chatModelName :: Text+ -- ^ 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+  -- ^ Optional tools that may be used in the chat.+  , format :: Maybe Text+  -- ^ An optional format for the chat response.+  , 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+  -- ^ Optional text to specify keep-alive behavior.  +  }++instance Show ChatOps where+  show (ChatOps {chatModelName = m, messages = ms, tools = t, format = f, keepAlive = ka}) =+    let messagesStr = show (toList ms)+        toolsStr = show t+        formatStr = show f+        keepAliveStr = show ka+     in T.unpack m+          ++ "\nMessages:\n"+          ++ messagesStr+          ++ "\n"+          ++ toolsStr+          ++ "\n"+          ++ formatStr+          ++ "\n"+          ++ keepAliveStr++instance Eq ChatOps where+  (==) a b =+    chatModelName a == chatModelName b+      && messages a == messages b+      && tools a == tools b+      && format a == format b+      && keepAlive a == keepAlive b++data ChatResponse = ChatResponse+  { model :: Text+ -- ^ The name of the model that generated this response.+  , createdAt :: UTCTime+  -- ^ The timestamp when the response was created.+  , message :: Maybe Message+  -- ^ The message content of the response, if any.+  , done :: Bool+  -- ^ Indicates whether the chat process has completed.+  , totalDuration :: Maybe Int64+  -- ^ Optional total duration in milliseconds for the chat process.+  , loadDuration :: Maybe Int64+  -- ^ Optional load duration in milliseconds for loading the model.+  , promptEvalCount :: Maybe Int64+  -- ^ Optional count of prompt evaluations during the chat process.+  , promptEvalDuration :: Maybe Int64+  -- ^ Optional duration in milliseconds for evaluating the prompt.+  , evalCount :: Maybe Int64+  -- ^ Optional count of evaluations during the chat process.+  , evalDuration :: Maybe Int64+  -- ^ Optional duration in milliseconds for evaluations during the chat process.  +  }+  deriving (Show, Eq)++instance ToJSON ChatOps where+  toJSON (ChatOps model messages tools format stream keepAlive) =+    object+      [ "model" .= model+      , "messages" .= messages+      , "tools" .= tools+      , "format" .= format+      , "stream" .= if isNothing stream then Just False else Just True+      , "keep_alive" .= keepAlive+      ]++instance FromJSON ChatResponse where+  parseJSON = withObject "ChatResponse" $ \v ->+    ChatResponse+      <$> v .: "model"+      <*> v .: "created_at"+      <*> v .: "message"+      <*> v .: "done"+      <*> v .:? "total_duration"+      <*> v .:? "load_duration"+      <*> v .:? "prompt_eval_count"+      <*> v .:? "prompt_eval_duration"+      <*> v .:? "eval_count"+      <*> v .:? "eval_duration"++-- | +-- A default configuration for initiating a chat with a model. +-- This can be used as a starting point and modified as needed.+-- +-- Example:+-- +-- > let ops = defaultChatOps { chatModelName = "customModel" }+-- > chat ops+defaultChatOps :: ChatOps+defaultChatOps =+  ChatOps+    { chatModelName = "llama3.2"+    , messages = Message User "What is 2+2?" Nothing :| []+    , tools = Nothing+    , format = Nothing+    , stream = Nothing+    , keepAlive = Nothing+    }++-- | +-- Initiates a chat session with the specified 'ChatOps' configuration and returns either+-- a 'ChatResponse' or an error message.+--+-- This function sends a request to the Ollama chat API with the given options.+-- +-- Example:+--+-- > let ops = defaultChatOps+-- > result <- chat ops+-- > case result of+-- >   Left errorMsg -> putStrLn ("Error: " ++ errorMsg)+-- >   Right response -> print response+chat :: ChatOps -> IO (Either String ChatResponse)+chat cOps = do+  let url = CU.host defaultOllama+  manager <- newManager defaultManagerSettings+  initialRequest <- parseRequest $ T.unpack (url <> "/api/chat")+  let reqBody = cOps+      request =+        initialRequest+          { method = "POST"+          , requestBody = RequestBodyLBS $ encode reqBody+          }+  withResponse request manager $ \response -> do+    let streamResponse sendChunk flush = do+          bs <- brRead $ responseBody response+          if BS.null bs+            then putStrLn "" >> pure (Left "")+            else do+              let eRes = eitherDecode (BSL.fromStrict bs) :: Either String ChatResponse+              case eRes of+                Left e -> pure (Left e)+                Right r -> do+                  _ <- sendChunk r+                  _ <- flush+                  if done r then pure (Left "") else streamResponse sendChunk flush+    let genResponse op = do+          bs <- brRead $ responseBody response+          if BS.null bs+            then do+              let eRes = eitherDecode (BSL.fromStrict op) :: Either String ChatResponse+              case eRes of+                Left e -> pure (Left e)+                Right r -> pure (Right r)+            else genResponse (op <> bs)+    case stream cOps of+      Nothing -> genResponse ""+      Just (sendChunk, flush) -> streamResponse sendChunk flush
+ src/Data/Ollama/Common/Types.hs view
@@ -0,0 +1,32 @@+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE OverloadedStrings #-}++module Data.Ollama.Common.Types where++import Data.Aeson+import Data.Text (Text)++data ModelDetails = ModelDetails+  { parentModel :: Maybe Text+  , format :: Text+  , familiy :: Text+  , families :: [Text]+  , parameterSize :: Text+  , quantizationLevel :: Text+  }+  deriving (Eq, Show)++instance FromJSON ModelDetails where+  parseJSON = withObject "ModelDetails" $ \v ->+    ModelDetails+      <$> v .: "parent_model"+      <*> v .: "format"+      <*> v .: "family"+      <*> v .:? "families" .!= []+      <*> v .: "parameter_size"+      <*> v .: "quantization_level"++newtype OllamaClient = OllamaClient+  { host :: Text+  }+  deriving (Eq, Show)
+ src/Data/Ollama/Common/Utils.hs view
@@ -0,0 +1,8 @@+{-# LANGUAGE OverloadedStrings #-}++module Data.Ollama.Common.Utils (defaultOllama, OllamaClient (..)) where++import Data.Ollama.Common.Types++defaultOllama :: OllamaClient+defaultOllama = OllamaClient "http://127.0.0.1:11434"
+ src/Data/Ollama/Copy.hs view
@@ -0,0 +1,54 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE OverloadedStrings #-}++module Data.Ollama.Copy+  ( -- * Copy Model API+    copyModel+  ) where++import Control.Monad (when)+import Data.Aeson+import Data.Ollama.Common.Utils qualified as CU+import Data.Text (Text)+import Data.Text qualified as T+import GHC.Generics+import Network.HTTP.Client+import Network.HTTP.Types.Status (status404)++-- TODO: Add Options parameter+-- TODO: Add Context parameter+data CopyModelOps = CopyModelOps+  { source :: Text+  , destination :: Text+  }+  deriving (Show, Eq, Generic, ToJSON)++-- | Copy model from source to destination+copyModel ::+  -- | Source model+  Text ->+  -- | Destination model+  Text ->+  IO ()+copyModel+  source+  destination =+    do+      let url = CU.host CU.defaultOllama+      manager <- newManager defaultManagerSettings+      initialRequest <- parseRequest $ T.unpack (url <> "/api/copy")+      let reqBody =+            CopyModelOps+              { source = source+              , destination = destination+              }+          request =+            initialRequest+              { method = "POST"+              , requestBody = RequestBodyLBS $ encode reqBody+              }+      response <- httpLbs request manager+      when+        (responseStatus response == status404)+        (putStrLn "Source Model does not exist")
+ src/Data/Ollama/Create.hs view
@@ -0,0 +1,120 @@+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE OverloadedStrings #-}++module Data.Ollama.Create+  ( -- * Create Model API+    createModel+  , createModelOps+  ) where++import Control.Monad (unless)+import Data.Aeson+import Data.ByteString.Lazy.Char8 qualified as BSL+import Data.Ollama.Common.Utils as CU+import Data.Text (Text)+import Data.Text qualified as T+import Data.Text.IO qualified as T+import Network.HTTP.Client++-- TODO: Add Options parameter+-- TODO: Add Context parameter+data CreateModelOps = CreateModelOps+  { name :: Text+  , modelFile :: Maybe Text+  , stream :: Maybe Bool+  , path :: Maybe FilePath+  }+  deriving (Show, Eq)++-- TODO: Add Context Param+newtype CreateModelResp = CreateModelResp {status :: Text}+  deriving (Show, Eq)++instance ToJSON CreateModelOps where+  toJSON+    ( CreateModelOps+        name+        modelFile+        stream+        path+      ) =+      object+        [ "name" .= name+        , "modelfile" .= modelFile+        , "stream" .= stream+        , "path" .= path+        ]++instance FromJSON CreateModelResp where+  parseJSON = withObject "CreateModelResp" $ \v ->+    CreateModelResp+      <$> v .: "status"++{- | Create a new model either from ModelFile or Path+Please note, if you specify both ModelFile and Path, ModelFile will be used.+-}+createModelOps ::+  -- | Model Name+  Text ->+  -- | Model File+  Maybe Text ->+  -- | Stream+  Maybe Bool ->+  -- | Path+  Maybe FilePath ->+  IO ()+createModelOps+  modelName+  modelFile+  stream+  path =+    do+      let url = CU.host defaultOllama+      manager <- newManager defaultManagerSettings+      initialRequest <- parseRequest $ T.unpack (url <> "/api/create")+      let reqBody =+            CreateModelOps+              { name = modelName+              , modelFile = modelFile+              , stream = stream+              , path = path+              }+          request =+            initialRequest+              { method = "POST"+              , requestBody = RequestBodyLBS $ encode reqBody+              }+      withResponse request manager $ \response -> do+        let go = do+              bs <- brRead $ responseBody response+              let eRes =+                    eitherDecode (BSL.fromStrict bs) ::+                      Either String CreateModelResp+              case eRes of+                Left err -> do+                  putStrLn $ "Error: " <> err+                Right res -> do+                  unless+                    (status res /= "success")+                    ( do+                        T.putStr $ status res+                        go+                    )+        go++{- | Create a new model+| Please note, if you specify both ModelFile and Path, ModelFile will be used.+-}+createModel ::+  -- | Model Name+  Text ->+  -- | Model File+  Maybe Text ->+  -- | Path+  Maybe FilePath ->+  IO ()+createModel modelName modelFile =+  createModelOps+    modelName+    modelFile+    Nothing
+ src/Data/Ollama/Delete.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE OverloadedStrings #-}++module Data.Ollama.Delete+  ( -- * Delete downloaded Models+    deleteModel+  ) where++import Control.Monad (when)+import Data.Aeson+import Data.Ollama.Common.Utils qualified as CU+import Data.Text (Text)+import Data.Text qualified as T+import Network.HTTP.Client+import Network.HTTP.Types.Status (status404)++-- TODO: Add Options parameter+-- TODO: Add Context parameter+newtype DeleteModelReq = DeleteModelReq {name :: Text}+  deriving newtype (Show, Eq, ToJSON)++-- | Delete a model+deleteModel ::+  -- | Model name+  Text ->+  IO ()+deleteModel modelName =+  do+    let url = CU.host CU.defaultOllama+    manager <- newManager defaultManagerSettings+    initialRequest <- parseRequest $ T.unpack (url <> "/api/delete")+    let reqBody =+          DeleteModelReq {name = modelName}+        request =+          initialRequest+            { method = "DELETE"+            , requestBody = RequestBodyLBS $ encode reqBody+            }+    response <- httpLbs request manager+    when+      (responseStatus response == status404)+      (putStrLn "Model does not exist")
+ src/Data/Ollama/Embeddings.hs view
@@ -0,0 +1,87 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE OverloadedStrings #-}++module Data.Ollama.Embeddings+  ( -- * Embedding API+    embedding+  , embeddingOps+  ) 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++-- TODO: Add Options parameter+data EmbeddingOps = EmbeddingOps+  { model :: Text+  , input :: Text+  , truncate :: Maybe Bool+  , keepAlive :: Maybe Text+  }+  deriving (Show, Eq)++data EmbeddingResp = EmbeddingResp+  { model :: Text+  , embedding' :: [[Float]]+  }+  deriving (Show, Eq, Generic, FromJSON)++instance ToJSON EmbeddingOps where+  toJSON (EmbeddingOps model input truncate' keepAlive) =+    object+      [ "model" .= model+      , "input" .= input+      , "truncate" .= truncate'+      , "keep_alive" .= keepAlive+      ]++-- TODO: Add Options parameter++-- | Embedding API+embeddingOps ::+  -- | Model+  Text ->+  -- | Input+  Text ->+  -- | Truncate+  Maybe Bool ->+  -- | Keep Alive+  Maybe Text ->+  IO (Maybe EmbeddingResp)+embeddingOps modelName input mTruncate mKeepAlive = do+  let url = CU.host defaultOllama+  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++-- Higher level binding that only takes important params++-- | Embedding API+embedding ::+  -- | Model+  Text ->+  -- | Input+  Text ->+  IO (Maybe EmbeddingResp)+embedding modelName input =+  embeddingOps modelName input Nothing Nothing
+ src/Data/Ollama/Generate.hs view
@@ -0,0 +1,272 @@+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++module Data.Ollama.Generate+  ( -- * Generate Texts+    generate+  , defaultGenerateOps+  , GenerateOps (..)+  , GenerateResponse (..)+  ) where++import Data.Aeson+import Data.ByteString.Char8 qualified as BS+import Data.ByteString.Lazy.Char8 qualified as BSL+import Data.Maybe+import Data.Ollama.Common.Utils as CU+import Data.Text (Text)+import Data.Text qualified as T+import Data.Time (UTCTime)+import GHC.Int (Int64)+import Network.HTTP.Client++-- TODO: Add Options parameter+-- TODO: Add Context parameter++{- | +  Input type for generate functions. This data type represents all possible configurations +  that you can pass to the Ollama generate API.+  +  Example:+  +  > let ops = GenerateOps +  >         { modelName = "llama3.2"+  >         , prompt = "What is the meaning of life?"+  >         , suffix = Nothing+  >         , images = Nothing+  >         , format = Just "text"+  >         , system = Nothing+  >         , template = Nothing+  >         , stream = Nothing+  >         , raw = Just False+  >         , keepAlive = Just "yes"+  >         }+-}+data GenerateOps = GenerateOps+  { modelName :: Text+  -- ^ The name of the model to be used for generation.+  , prompt :: Text+  -- ^ The prompt text that will be provided to the model for generating a response.+  , suffix :: Maybe Text+  -- ^ 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+  -- ^ An optional format specifier for the response.+  , system :: Maybe Text+  -- ^ Optional system text that can be included in the generation context.+  , template :: Maybe Text+  -- ^ An optional template to format the response.+  , stream :: Maybe (GenerateResponse -> IO (), IO ())+  -- ^ An optional streaming function where the first function handles each chunk of response, and the second flushes the stream.+  , raw :: Maybe Bool+  -- ^ An optional flag to return the raw response.+  , keepAlive :: Maybe Text+  -- ^ Optional text to specify keep-alive behavior.+  }++instance Show GenerateOps where+  show GenerateOps {..} =+    "GenerateOps { "+      <> "model : "+      <> T.unpack modelName+      <> ", prompt : "+      <> T.unpack prompt+      <> ", suffix : "+      <> show suffix+      <> ", images : "+      <> show images+      <> ", format : "+      <> show format+      <> ", system : "+      <> show system+      <> ", template : "+      <> show template+      <> ", stream : "+      <> "Stream functions"+      <> ", raw : "+      <> show raw+      <> ", keepAlive : "+      <> show keepAlive++instance Eq GenerateOps where+    (==) a b = +        modelName a == modelName b &&+        prompt a == prompt b &&+        suffix a == suffix b &&+        images a == images b &&+        format a == format b &&+        system a == system b &&+        template a == template b &&+        raw a == raw b &&+        keepAlive a == keepAlive b++-- TODO: Add Context Param++-- | +-- Result type for generate function containing the model's response and meta-information.+data GenerateResponse = GenerateResponse+  { model :: Text+  -- ^ The name of the model that generated the response.+  , createdAt :: UTCTime+  -- ^ The timestamp when the response was created.+  , response_ :: Text+  -- ^ The generated response from the model.+  , done :: Bool+  -- ^ A flag indicating whether the generation process is complete.+  , totalDuration :: Maybe Int64+  -- ^ Optional total duration in milliseconds for the generation process.+  , loadDuration :: Maybe Int64+  -- ^ Optional load duration in milliseconds for loading the model.+  , promptEvalCount :: Maybe Int64+  -- ^ Optional count of prompt evaluations during the generation process.+  , promptEvalDuration :: Maybe Int64+  -- ^ Optional duration in milliseconds for evaluating the prompt.+  , evalCount :: Maybe Int64+  -- ^ Optional count of evaluations during the generation process.+  , evalDuration :: Maybe Int64+  -- ^ Optional duration in milliseconds for evaluations during the generation process.+  }+  deriving (Show, Eq)++instance ToJSON GenerateOps where+  toJSON+    ( GenerateOps+        model+        prompt+        suffix+        images+        format+        system+        template+        stream+        raw+        keepAlive+      ) =+      object+        [ "model" .= model+        , "prompt" .= prompt+        , "suffix" .= suffix+        , "images" .= images+        , "format" .= format+        , "system" .= system+        , "template" .= template+        , "stream" .= if isNothing stream then Just False else Just True+        , "raw" .= raw+        , "keep_alive" .= keepAlive+        ]++instance FromJSON GenerateResponse where+  parseJSON = withObject "GenerateResponse" $ \v ->+    GenerateResponse+      <$> v .: "model"+      <*> v .: "created_at"+      <*> v .: "response"+      <*> v .: "done"+      <*> v .:? "total_duration"+      <*> v .:? "load_duration"+      <*> v .:? "prompt_eval_count"+      <*> v .:? "prompt_eval_duration"+      <*> v .:? "eval_count"+      <*> v .:? "eval_duration"++-- | +-- A function to create a default 'GenerateOps' type with preset values.+-- +-- Example:+--+-- > let ops = defaultGenerateOps+-- > generate ops+-- +-- This will generate a response using the default configuration.+defaultGenerateOps :: GenerateOps+defaultGenerateOps =+  GenerateOps+    { modelName = "llama3.2"+    , prompt = "what is 2+2"+    , suffix = Nothing+    , images = Nothing+    , format = Nothing+    , system = Nothing+    , template = Nothing+    , stream = Nothing+    , raw = Nothing+    , keepAlive = Nothing+    }++-- | +-- Generate function that returns either a 'GenerateResponse' type or an error message.+-- It takes a 'GenerateOps' configuration and performs a request to the Ollama generate API.+-- +-- Examples:+--+-- Basic usage without streaming:+--+-- > let ops = GenerateOps +-- >         { modelName = "llama3.2"+-- >         , prompt = "Tell me a joke."+-- >         , suffix = Nothing+-- >         , images = Nothing+-- >         , format = Nothing+-- >         , system = Nothing+-- >         , template = Nothing+-- >         , stream = Nothing+-- >         , raw = Nothing+-- >         , keepAlive = Nothing+-- >         }+-- > result <- generate ops+-- > case result of+-- >   Left errorMsg -> putStrLn ("Error: " ++ errorMsg)+-- >   Right response -> print response+--+-- Usage with streaming to print responses to the console:+--+-- > void $+-- >   generate+-- >     defaultGenerateOps+-- >       { modelName = "llama3.2"+-- >       , prompt = "what is functional programming?"+-- >       , stream = Just (T.putStr . response_, pure ())+-- >       }+--+-- In this example, the first function in the 'stream' tuple processes each chunk of response by printing it,+-- and the second function is a simple no-op flush.generate :: GenerateOps -> IO (Either String GenerateResponse)+generate :: GenerateOps -> IO (Either String GenerateResponse)+generate genOps = do+  let url = CU.host defaultOllama+  manager <-+    newManager -- Setting response timeout to 5 minutes, since llm takes time+      defaultManagerSettings {managerResponseTimeout = responseTimeoutMicro (5 * 60 * 1000000)}+  initialRequest <- parseRequest $ T.unpack (url <> "/api/generate")+  let reqBody = genOps+      request =+        initialRequest+          { method = "POST"+          , requestBody = RequestBodyLBS $ encode reqBody+          }+  withResponse request manager $ \response -> do+    let streamResponse sendChunk flush = do+          bs <- brRead $ responseBody response+          if BS.null bs+            then putStrLn "" >> pure (Left "")+            else do+              let eRes = eitherDecode (BSL.fromStrict bs) :: Either String GenerateResponse+              case eRes of+                Left e -> pure (Left e)+                Right r -> do+                  _ <- sendChunk r+                  _ <- flush+                  if done r then pure (Left "") else streamResponse sendChunk flush+    let genResponse op = do+          bs <- brRead $ responseBody response+          if bs == ""+            then do+              let eRes0 = eitherDecode (BSL.fromStrict op) :: Either String GenerateResponse+              case eRes0 of+                Left e -> pure (Left e)+                Right r -> pure (Right r)+            else genResponse (op <> bs)+    case stream genOps of+      Nothing -> genResponse ""+      Just (sendChunk, flush) -> streamResponse sendChunk flush
+ src/Data/Ollama/List.hs view
@@ -0,0 +1,59 @@+{-# LANGUAGE OverloadedStrings #-}++module Data.Ollama.List+  ( -- * List Models API+    list+  , Models (..)+  , ModelInfo (..)+  )+where++import Data.Aeson+import Data.Ollama.Common.Types as CT+import Data.Ollama.Common.Utils as CU+import Data.Text (Text)+import Data.Text qualified as T+import Data.Time+import GHC.Int (Int64)+import Network.HTTP.Client+import Network.HTTP.Types.Status (statusCode)++newtype Models = Models [ModelInfo]+  deriving (Eq, Show)++data ModelInfo = ModelInfo+  { name :: Text+  , modifiedAt :: UTCTime+  , size :: Int64+  , digest :: Text+  , details :: ModelDetails+  }+  deriving (Eq, Show)++-- Instances+instance FromJSON Models where+  parseJSON = withObject "Models" $ \v -> Models <$> v .: "models"++instance FromJSON ModelInfo where+  parseJSON = withObject "ModelInfo" $ \v ->+    ModelInfo+      <$> v .: "name"+      <*> v .: "modified_at"+      <*> v .: "size"+      <*> v .: "digest"+      <*> v .: "details"++-- | List all models from local+list :: IO (Maybe Models)+list = do+  let url = CU.host defaultOllama+  manager <- newManager defaultManagerSettings+  request <- parseRequest $ T.unpack (url <> "/api/tags")+  response <- httpLbs request manager+  if statusCode (responseStatus response) /= 200+    then pure Nothing+    else do+      let res = decode (responseBody response) :: Maybe Models+      case res of+        Nothing -> pure Nothing+        Just l -> pure $ Just l
+ src/Data/Ollama/Ps.hs view
@@ -0,0 +1,61 @@+{-# LANGUAGE OverloadedStrings #-}++module Data.Ollama.Ps+  ( ps+  , RunningModels (..)+  , RunningModel (..)+  ) where++import Data.Aeson+import Data.Ollama.Common.Types as CT+import Data.Ollama.Common.Utils as CU+import Data.Text (Text)+import Data.Text qualified as T+import Data.Time+import GHC.Int (Int64)+import Network.HTTP.Client+import Network.HTTP.Types.Status (statusCode)++-- Types for Ps API+newtype RunningModels = RunningModels [RunningModel]+  deriving (Eq, Show)++data RunningModel = RunningModel+  { name_ :: Text+  , modelName :: Text+  , size_ :: Int64+  , modelDigest :: Text+  , modelDetails :: ModelDetails+  , expiresAt :: UTCTime+  , sizeVRam :: Int64+  }+  deriving (Eq, Show)++instance FromJSON RunningModels where+  parseJSON = withObject "Models" $ \v -> RunningModels <$> v .: "models"++instance FromJSON RunningModel where+  parseJSON = withObject "RunningModel" $ \v ->+    RunningModel+      <$> v .: "name"+      <*> v .: "model"+      <*> v .: "size"+      <*> v .: "digest"+      <*> v .: "details"+      <*> v .: "expires_at"+      <*> v .: "size_vram"++-- | List running models+ps :: IO (Maybe RunningModels)+ps = do+  let url = CU.host defaultOllama+  manager <- newManager defaultManagerSettings+  request <- parseRequest $ T.unpack (url <> "/api/ps")+  response <- httpLbs request manager+  if statusCode (responseStatus response) /= 200+    then pure Nothing+    else do+      let res = decode (responseBody response) :: Maybe RunningModels+      case res of+        Nothing -> pure Nothing+        Just l -> pure $ Just l
+ src/Data/Ollama/Pull.hs view
@@ -0,0 +1,113 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE OverloadedStrings #-}++module Data.Ollama.Pull+  ( -- * Downloaded Models+    pull+  , pullOps+  ) where++import Data.Aeson+import Data.ByteString.Lazy.Char8 qualified as BSL+import Data.Maybe (fromMaybe)+import Data.Ollama.Common.Utils as CU+import Data.Text (Text)+import Data.Text qualified as T+import GHC.Generics+import GHC.Int (Int64)+import Network.HTTP.Client++-- TODO: Add Options parameter++-- | Configuration options for pulling a model.+data PullOps = PullOps+  { name :: Text+  -- ^ The name of the model to pull.+  , insecure :: Maybe Bool+  -- ^ Option to allow insecure connections.+  -- If set to 'Just True', the pull operation will allow insecure connections.+  , stream :: Maybe Bool+  -- ^ Option to enable streaming of the download.+  -- If set to 'Just True', the download will be streamed.+  }+  deriving (Show, Eq, Generic, ToJSON)++-- | Response data from a pull operation.+data PullResp = PullResp+  { status :: Text+  -- ^ The status of the pull operation, e.g., "success" or "failure".+  , digest :: Maybe Text+  -- ^ The digest of the model, if available.+  , total :: Maybe Int64+  -- ^ The total size of the model in bytes, if available.+  , completed :: Maybe Int64+  -- ^ The number of bytes completed, if available.+  }+  deriving (Show, Eq, Generic, FromJSON)++{- |+Pull a model with additional options for insecure connections and streaming.+This function interacts directly with the Ollama API to download the specified model.++Example:++> pullOps "myModel" (Just True) (Just True)++This will attempt to pull "myModel" with insecure connections allowed and enable streaming.+-}+pullOps ::+  -- | Model Name+  Text ->+  -- | Insecure+  Maybe Bool ->+  -- | Stream+  Maybe Bool ->+  IO ()+pullOps modelName mInsecure mStream = do+  let url = CU.host defaultOllama+  manager <- newManager defaultManagerSettings+  initialRequest <- parseRequest $ T.unpack (url <> "/api/pull")+  let reqBody =+        PullOps+          { name = modelName+          , insecure = mInsecure+          , stream = mStream+          }+      request =+        initialRequest+          { method = "POST"+          , requestBody = RequestBodyLBS $ encode reqBody+          }+  withResponse request manager $ \response -> do+    let go = do+          bs <- brRead $ responseBody response+          let eRes = decode (BSL.fromStrict bs) :: Maybe PullResp+          case eRes of+            Nothing -> putStrLn "Something went wrong"+            Just res -> do+              if status res /= "success"+                then do+                  let completed' = fromMaybe 0 (completed res)+                  let total' = fromMaybe 0 (total res)+                  putStrLn $ "Remaining bytes: " <> show (total' - completed')+                  go+                else do+                  putStrLn "Completed"+    go++{- |+Pull a model using default options. This simplifies the pull operation by+not requiring additional options.++Example:++> pull "myModel"++This will pull "myModel" using default settings (no insecure connections and no streaming).+-}+pull ::+  -- | Model Name+  Text ->+  IO ()+pull modelName = pullOps modelName Nothing Nothing
+ src/Data/Ollama/Push.hs view
@@ -0,0 +1,84 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE OverloadedStrings #-}++module Data.Ollama.Push+  ( -- * Push API+    push+  , pushOps+  ) where++import Data.Aeson+import Data.ByteString.Lazy.Char8 qualified as BSL+import Data.Maybe (fromMaybe)+import Data.Ollama.Common.Utils as CU+import Data.Text (Text)+import Data.Text qualified as T+import GHC.Generics+import GHC.Int (Int64)+import Network.HTTP.Client++-- TODO: Add Options parameter+data PushOps = PushOps+  { name :: Text+  , insecure :: Maybe Bool+  , stream :: Maybe Bool+  }+  deriving (Show, Eq, Generic, ToJSON)++data PushResp = PushResp+  { status :: Text+  , digest :: Maybe Text+  , total :: Maybe Int64+  }+  deriving (Show, Eq, Generic, FromJSON)++-- | Push a model with options+pushOps ::+  -- | Model name+  Text ->+  -- | Insecure+  Maybe Bool ->+  -- | Stream+  Maybe Bool ->+  IO ()+pushOps modelName mInsecure mStream = do+  let url = CU.host defaultOllama+  manager <- newManager defaultManagerSettings+  initialRequest <- parseRequest $ T.unpack (url <> "/api/push")+  let reqBody =+        PushOps+          { name = modelName+          , insecure = mInsecure+          , stream = mStream+          }+      request =+        initialRequest+          { method = "POST"+          , requestBody = RequestBodyLBS $ encode reqBody+          }+  withResponse request manager $ \response -> do+    let go = do+          bs <- brRead $ responseBody response+          let eRes = decode (BSL.fromStrict bs) :: Maybe PushResp+          case eRes of+            Nothing -> putStrLn "Something went wrong"+            Just res -> do+              if status res /= "success"+                then do+                  let total' = fromMaybe 0 (total res)+                  putStrLn $ "Remaining bytes: " <> show total'+                  go+                else do+                  putStrLn "Completed"+    go++-- Higher level API for Pull+-- This API is untested. Will test soon!++-- | Push a model+push ::+  -- | Model name+  Text ->+  IO ()+push modelName = pushOps modelName Nothing Nothing
+ src/Data/Ollama/Show.hs view
@@ -0,0 +1,174 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE OverloadedStrings #-}++module Data.Ollama.Show+  ( -- * Show Model Info API+    showModel+  , showModelOps+  , ShowModelResponse (..)+  ) where++import Data.Aeson+import Data.Ollama.Common.Utils qualified as CU+import Data.Text (Text)+import Data.Text qualified as T+import GHC.Generics+import GHC.Int (Int64)+import Network.HTTP.Client++-- TODO: Add Options parameter+-- TODO: Add Context parameter++{- |+ #ShowModelOps#+ Input parameters for show model information.+-}+data ShowModelOps = ShowModelOps+  { name :: Text+  , verbose :: Maybe Bool+  }+  deriving (Show, Eq, Generic, ToJSON)++{- |+ #ShowModelResponse#++ Ouput structure for show model information.+-}+data ShowModelResponse = ShowModelResponse+  { modelFile :: Text+  , parameters :: Text+  , template :: Text+  , details :: ModelDetails+  , modelInfo :: ModelInfo+  }+  deriving (Show, Eq)++data ModelDetails = ModelDetails+  { parentModel :: Text+  , format :: Text+  , familiy :: Text+  , families :: [Text]+  , parameterSize :: Text+  , quantizationLevel :: Text+  }+  deriving (Show, Eq)++data ModelInfo = ModelInfo+  { generalArchitecture :: Maybe Text+  , generalFileType :: Maybe Int+  , generalParameterCount :: Maybe Int64+  , generalQuantizationVersion :: Maybe Int+  , llamaAttentionHeadCount :: Maybe Int+  , llamaAttentionHeadCountKV :: Maybe Int+  , llamaAttentionLayerNormRMSEpsilon :: Maybe Float+  , llamaBlockCount :: Maybe Int+  , llamaContextLength :: Maybe Int+  , llamaEmbeddingLength :: Maybe Int+  , llamaFeedForwardLength :: Maybe Int+  , llamaRopeDimensionCount :: Maybe Int+  , llamaRopeFreqBase :: Maybe Int64+  , llamaVocabSize :: Maybe Int64+  , tokenizerGgmlBosToken_id :: Maybe Int+  , tokenizerGgmlEosToken_id :: Maybe Int+  , tokenizerGgmlMerges :: Maybe [Text]+  , tokenizerGgmlMode :: Maybe Text+  , tokenizerGgmlPre :: Maybe Text+  , tokenizerGgmlTokenType :: Maybe [Text]+  , tokenizerGgmlTokens :: Maybe [Text]+  }+  deriving (Show, Eq)++-- FromJSON instances++-- | The instance for show model response+instance FromJSON ShowModelResponse where+  parseJSON = withObject "ShowModelResponse" $ \v ->+    ShowModelResponse+      <$> v .: "modelfile"+      <*> v .: "parameters"+      <*> v .: "template"+      <*> v .: "details"+      <*> v .: "model_info"++instance FromJSON ModelDetails where+  parseJSON = withObject "ModelDetails" $ \v ->+    ModelDetails+      <$> v .: "parent_model"+      <*> v .: "format"+      <*> v .: "family"+      <*> v .: "families"+      <*> v .: "parameter_size"+      <*> v .: "quantization_level"++instance FromJSON ModelInfo where+  parseJSON = withObject "ModelInfo" $ \v ->+    ModelInfo+      <$> v .:? "general.architecture"+      <*> v .:? "general.file_type"+      <*> v .:? "general.parameter_count"+      <*> v .:? "general.quantization_version"+      <*> v .:? "llama.attention.head_count"+      <*> v .:? "llama.attention.head_count_kv"+      <*> v .:? "llama.attention.layer_norm_rms_epsilon"+      <*> v .:? "llama.block_count"+      <*> v .:? "llama.context_length"+      <*> v .:? "llama.embedding_length"+      <*> v .:? "llama.feed_forward_length"+      <*> v .:? "llama.rope.dimension_count"+      <*> v .:? "llama.rope.freq_base"+      <*> v .:? "llama.vocab_size"+      <*> v .:? "tokenizer.ggml.bos_token_id"+      <*> v .:? "tokenizer.ggml.eos_token_id"+      <*> v .:? "tokenizer.ggml.merges"+      <*> v .:? "tokenizer.ggml.model"+      <*> v .:? "tokenizer.ggml.pre"+      <*> v .:? "tokenizer.ggml.token_type"+      <*> v .:? "tokenizer.ggml.tokens"++{- | Show given model's information with options.++@since 1.0.0.0+-}+showModelOps ::+  -- | model name+  Text ->+  -- | verbose+  Maybe Bool ->+  IO (Maybe ShowModelResponse)+showModelOps+  modelName+  verbose =+    do+      let url = CU.host CU.defaultOllama+      manager <- newManager defaultManagerSettings+      initialRequest <- parseRequest $ T.unpack (url <> "/api/show")+      let reqBody =+            ShowModelOps+              { name = modelName+              , verbose = verbose+              }+          request =+            initialRequest+              { method = "POST"+              , requestBody = RequestBodyLBS $ encode reqBody+              }+      response <- httpLbs request manager+      let eRes =+            eitherDecode (responseBody response) ::+              Either String ShowModelResponse+      case eRes of+        Left _ -> pure Nothing+        Right r -> pure $ Just r++{- | Show given model's information.++Higher level API for show.+@since 1.0.0.0+-}+showModel ::+  -- | model name+  Text ->+  IO (Maybe ShowModelResponse)+showModel modelName =+  showModelOps modelName Nothing
+ src/Lib.hs view
@@ -0,0 +1,138 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++module Lib where++import Control.Monad (void)+import Data.List.NonEmpty (NonEmpty((:|)))+import Data.Maybe (fromMaybe)+import Data.Ollama.Chat qualified as Chat+import Data.Text.IO qualified as T+import Ollama (GenerateOps(..), Role(..), chat, defaultChatOps, defaultGenerateOps, generate)+import Ollama qualified++main :: IO ()+main = do+  -- Example 1: Streamed Text Generation+  -- This example demonstrates how to generate text using a model and stream the output directly to the console.+  -- The `stream` option enables processing of each chunk of the response as it arrives.+  void $+    generate+      defaultGenerateOps+        { modelName = "llama3.2"+        , prompt = "what is functional programming?"+        , stream = Just (T.putStr . Ollama.response_, pure ())+        }++  -- Example 2: Non-streamed Text Generation+  -- This example shows how to generate text and handle the complete response.+  -- The result is either an error message or the generated text.+  eRes <-+    generate+      defaultGenerateOps+        { modelName = "llama3.2"+        , prompt = "What is 2+2?"+        }+  case eRes of+    Left e -> putStrLn e+    Right Ollama.GenerateResponse {..} -> T.putStrLn response_++  -- Example 3: Chat with Streaming+  -- This example demonstrates setting up a chat session with streaming enabled.+  -- As messages are received, they are printed to the console.+  let msg = Ollama.Message User "What is functional programming?" Nothing+      defaultMsg = Ollama.Message User "" Nothing+  void $+    chat+      defaultChatOps+        { Chat.chatModelName = "llama3.2"+        , Chat.messages = msg :| []+        , Chat.stream =+            Just (T.putStr . Chat.content . fromMaybe defaultMsg . Chat.message, pure ())+        }++  -- Example 4: Non-streamed Chat+  -- Here, we handle a complete chat response, checking for potential errors.+  eRes1 <-+    chat+      defaultChatOps+        { Chat.chatModelName = "llama3.2"+        , Chat.messages = msg :| []+        }+  case eRes1 of+    Left e -> putStrLn e+    Right r -> do+      let mMessage = Ollama.message r+      case mMessage of+        Nothing -> putStrLn "Something went wrong"+        Just res -> T.putStrLn $ Ollama.content res++  -- Example 5: Check Model Status (ps)+  -- This example checks the status of models using the `ps` function.+  -- It outputs the status or details of the available models.+  res <- Ollama.ps+  print res++  -- Example 6: Simple Embedding+  -- This demonstrates how to request embeddings for a given text using a specific model.+  void $ Ollama.embedding "llama3.1" "What is 5+2?"++  -- Example 7: Embedding with Options+  -- This example uses the `embeddingOps` function, allowing for additional configuration like options and streaming.+  void $ Ollama.embeddingOps "llama3.1" "What is 5+2?" Nothing Nothing++{-+Scotty example:+{-# LANGUAGE OverloadedStrings #-}++module Main where++import Web.Scotty+import Control.Monad.IO.Class (liftIO)+import Data.Aeson (FromJSON, ToJSON)+import Data.Text (Text)+import Data.Text qualified as T+import Database.SQLite.Simple+import Ollama (GenerateOps(..), defaultGenerateOps, generate)+import Data.Maybe (fromRight)++data PromptInput = PromptInput+  { conversation_id :: Int+  , prompt :: Text+  } deriving (Show, Generic)++instance FromJSON PromptInput+instance ToJSON PromptInput++main :: IO ()+main = do+  conn <- open "chat.db"+  execute_ conn "CREATE TABLE IF NOT EXISTS conversation (convo_id INTEGER PRIMARY KEY, convo_title TEXT)"+  execute_ conn "CREATE TABLE IF NOT EXISTS chats (chat_id INTEGER PRIMARY KEY, convo_id INTEGER, role TEXT, message TEXT, FOREIGN KEY(convo_id) REFERENCES conversation(convo_id))"+  +  scotty 3000 $ do+    post "/chat" $ do+      p <- jsonData :: ActionM PromptInput+      let cId = conversation_id p+      let trimmedP = T.dropEnd 3 $ T.drop 3 $ prompt p+      newConvoId <- case cId of+        -1 -> do+          liftIO $ execute conn "INSERT INTO conversation (convo_title) VALUES (?)" (Only ("latest title" :: String))+          [Only convoId] <- liftIO $ query_ conn "SELECT last_insert_rowid()" :: ActionM [Only Int]+          pure convoId+        _ -> pure cId++      liftIO $ execute conn "INSERT INTO chats (convo_id, role, message) VALUES (?, 'user', ?)" (newConvoId, trimmedP)+      +      stream $ \sendChunk flush -> do+        eRes <- generate defaultGenerateOps+                { modelName = "llama3.2"+                , prompt = prompt p+                , stream = Just (sendChunk . T.pack, flush)+                }+        case eRes of+            Left e -> return ()+            Right r -> do+                let res = response_ r+                liftIO $ execute conn "INSERT INTO chats (convo_id, role, message) VALUES (?, 'ai', ?)" (newConvoId, res)+-}
+ src/Ollama.hs view
@@ -0,0 +1,85 @@+{- |+  #Ollama-Haskell#+  This library lets you run LlMs from within Haskell projects. Inspired by `ollama-python`.+-}+module Ollama+  ( -- * Main APIs++    -- ** Generate Texts+    generate+  , defaultGenerateOps+  , GenerateOps (..)+  , GenerateResponse (..)++    -- ** Chat with LLMs+  , chat+  , Role (..)+  , defaultChatOps+  , ChatResponse (..)+  , ChatOps (..)++    -- ** Embeddings+  , embedding+  , embeddingOps++    -- * Ollama operations++    -- ** Copy Models+  , copyModel++    -- ** Create Models+  , createModel+  , createModelOps++    -- ** Delete Models+  , deleteModel++    -- ** List Models+  , list++    -- ** List currently running models+  , ps++    -- ** Push and Pull+  , push+  , pushOps+  , pull+  , pullOps++    -- ** Show Model Info+  , showModel+  , showModelOps++    -- * Types+  , ShowModelResponse (..)+  , Models (..)+  , ModelInfo (..)+  , RunningModels (..)+  , RunningModel (..)+  , Message (..)+  )+where++import Data.Ollama.Chat+  ( ChatOps (..)+  , ChatResponse (..)+  , Message (..)+  , Role (..)+  , chat+  , defaultChatOps+  )+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.Generate+  ( GenerateOps (..)+  , GenerateResponse (..)+  , defaultGenerateOps+  , generate+  )+import Data.Ollama.List (ModelInfo (..), Models (..), list)+import Data.Ollama.Ps (RunningModel (..), RunningModels (..), ps)+import Data.Ollama.Pull (pull, pullOps)+import Data.Ollama.Push (push, pushOps)+import Data.Ollama.Show (ShowModelResponse (..), showModel, showModelOps)
+ test/Main.hs view
@@ -0,0 +1,75 @@+{-# LANGUAGE OverloadedStrings #-}+module Main where++import Ollama qualified+import Ollama (defaultGenerateOps, GenerateOps(..), defaultChatOps, Role(..))+import Data.Ollama.Chat qualified as Chat+import Data.Text.IO qualified as T+import Test.Tasty+import Test.Tasty.HUnit+import System.IO.Silently (capture)+import Data.Either+import Data.List.NonEmpty hiding (length)+import Data.Maybe++tests :: TestTree+tests = testGroup "Tests" [+       generateTest+      , chatTest+      , psTest+      , showTest+   ]++generateTest :: TestTree+generateTest = testGroup "Generate tests" [+       testCase "generate stream" $ do+          output <- capture $ Ollama.generate defaultGenerateOps { modelName = "smollm:360m"+               , prompt = "what is 4 + 2?"+               , stream = Just (T.putStr . Ollama.response_, pure ()) }+          assertBool "Checking if generate function is printing anything" (length output > 0)+    , testCase "Generate non-stream" $ do+          eRes <- Ollama.generate defaultGenerateOps { modelName = "smollm:360m"+               , prompt = "what is 4 + 2?" }+          assertBool "Checking if generate function returns a valid value" (isRight eRes)+    , testCase "Generate with invalid model" $ do+          eRes <- Ollama.generate defaultGenerateOps { modelName = "invalid-model" }+          assertBool "Expecting generation to fail with invalid model" (isLeft eRes)+   ]++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+          output <- capture $ Ollama.chat defaultChatOps { +                 Chat.chatModelName = "smollm:360m"+               , Chat.messages = msg :| []+               , Chat.stream = Just (T.putStr . Chat.content . fromMaybe defaultMsg . Chat.message, pure ())+               }+          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+          eRes <- Ollama.chat defaultChatOps { +                 Chat.chatModelName = "smollm:360m"+               , Chat.messages = msg :| []+               }+          assertBool "Checking if chat function returns a valid value" (isRight eRes)+   ]++psTest :: TestTree+psTest = testGroup "PS test" [+       testCase "check ps" $ do+           mRes <- Ollama.ps+           assertBool "Check if ps returns anything" (isJust mRes)+   ]++showTest :: TestTree+showTest = testGroup "Show test" [+       testCase "check show" $ do+           mRes <- Ollama.showModel "smollm:360m"+           assertBool "Check if model exists or not" (isJust mRes)+   ]++main :: IO ()+main = defaultMain tests