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 +5/−0
- LICENSE +20/−0
- ollama-haskell.cabal +71/−0
- src/Data/Ollama/Chat.hs +212/−0
- src/Data/Ollama/Common/Types.hs +32/−0
- src/Data/Ollama/Common/Utils.hs +8/−0
- src/Data/Ollama/Copy.hs +54/−0
- src/Data/Ollama/Create.hs +120/−0
- src/Data/Ollama/Delete.hs +43/−0
- src/Data/Ollama/Embeddings.hs +87/−0
- src/Data/Ollama/Generate.hs +272/−0
- src/Data/Ollama/List.hs +59/−0
- src/Data/Ollama/Ps.hs +61/−0
- src/Data/Ollama/Pull.hs +113/−0
- src/Data/Ollama/Push.hs +84/−0
- src/Data/Ollama/Show.hs +174/−0
- src/Lib.hs +138/−0
- src/Ollama.hs +85/−0
- test/Main.hs +75/−0
+ 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