packages feed

ollama-haskell 0.1.0.3 → 0.1.1.3

raw patch · 7 files changed

+338/−73 lines, 7 filesdep +base64-bytestringdep +directorydep +filepathPVP ok

version bump matches the API change (PVP)

Dependencies added: base64-bytestring, directory, filepath

API changes (from Hackage documentation)

+ Data.Ollama.Chat: chatJson :: (FromJSON jsonResult, ToJSON jsonResult) => ChatOps -> jsonResult -> Maybe Int -> IO (Either String jsonResult)
+ Data.Ollama.Common.Utils: encodeImage :: FilePath -> IO (Maybe Text)
+ Data.Ollama.Generate: generateJson :: (ToJSON jsonResult, FromJSON jsonResult) => GenerateOps -> jsonResult -> Maybe Int -> IO (Either String jsonResult)
+ Ollama: chatJson :: (FromJSON jsonResult, ToJSON jsonResult) => ChatOps -> jsonResult -> Maybe Int -> IO (Either String jsonResult)
+ Ollama: generateJson :: (ToJSON jsonResult, FromJSON jsonResult) => GenerateOps -> jsonResult -> Maybe Int -> IO (Either String jsonResult)
+ OllamaExamples: instance Data.Aeson.Types.FromJSON.FromJSON OllamaExamples.Example
+ OllamaExamples: instance Data.Aeson.Types.ToJSON.ToJSON OllamaExamples.Example
+ OllamaExamples: instance GHC.Classes.Eq OllamaExamples.Example
+ OllamaExamples: instance GHC.Generics.Generic OllamaExamples.Example
+ OllamaExamples: instance GHC.Show.Show OllamaExamples.Example

Files

CHANGELOG.md view
@@ -1,5 +1,11 @@ # Revision history for ollama-haskell +## 0.1.1.3 -- 2024-11-08++* Increase response timeout to 15 minutes+* Added encodeImage utility function that converts image filePath to base64 image data.+* Added generateJson and chatJson. High level function to return response in Haskell type.+ ## 0.1.0.3 -- 2024-11-05  * Moving to stack instead of cabal.
ollama-haskell.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           ollama-haskell-version:        0.1.0.3+version:        0.1.1.3 synopsis:       Haskell bindings for ollama. description:    Please see the README on GitHub at <https://github.com/tusharad/ollama-haskell#readme> category:       Web@@ -52,7 +52,10 @@   build-depends:       aeson     , base >=4.7 && <5+    , base64-bytestring     , bytestring+    , directory+    , filepath     , http-client     , http-types     , text@@ -72,7 +75,10 @@   build-depends:       aeson     , base >=4.7 && <5+    , base64-bytestring     , bytestring+    , directory+    , filepath     , http-client     , http-types     , ollama-haskell
src/Data/Ollama/Chat.hs view
@@ -1,10 +1,12 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}  module Data.Ollama.Chat   ( -- * Chat APIs     chat+  , chatJson   , Message (..)   , Role (..)   , defaultChatOps@@ -15,11 +17,12 @@ import Data.Aeson import Data.ByteString.Char8 qualified as BS import Data.ByteString.Lazy.Char8 qualified as BSL-import Data.List.NonEmpty+import Data.List.NonEmpty as NonEmpty import Data.Maybe (isNothing) import Data.Ollama.Common.Utils as CU import Data.Text (Text) import Data.Text qualified as T+import Data.Text.Encoding qualified as T import Data.Time (UTCTime) import GHC.Generics import GHC.Int (Int64)@@ -184,7 +187,7 @@   manager <-     newManager       defaultManagerSettings -- Setting response timeout to 5 minutes, since llm takes time-        { managerResponseTimeout = responseTimeoutMicro (5 * 60 * 1000000)+        { managerResponseTimeout = responseTimeoutMicro (15 * 60 * 1000000)         }   initialRequest <- parseRequest $ T.unpack (url <> "/api/chat")   let reqBody = cOps@@ -218,3 +221,75 @@     case stream cOps of       Nothing -> genResponse ""       Just (sendChunk, flush) -> streamResponse sendChunk flush++{- |+ chatJson is a higher level function that takes ChatOps (similar to chat) and also takes+ a Haskell type (that has To and From JSON instance) and returns the response in provided type.++ This function simply calls chat with extra prompt appended to it, telling LLM to return the+ response in certain JSON format and serializes the response. This function will be helpful when you+ want to use the LLM to do something programmatic.++ For Example:+  > let expectedJsonStrucutre = Example {+  >   sortedList = ["sorted List here"]+  > , wasListAlreadSorted = False+  > }+  > let msg0 = Ollama.Message User "Sort given list: [4, 2 , 3, 67]. Also tell whether list was already sorted or not." Nothing+  > eRes3 <-+  >  chatJson+  >   defaultChatOps+  >    { Chat.chatModelName = "llama3.2"+  >      , Chat.messages = msg0 :| []+  >   }+  >      expectedJsonStrucutre+  >      (Just 2)+  > print eRes3+ Output:+  > Example {sortedList = ["1","2","3","4"], wasListAlreadSorted = False}++Note: While Passing the type, construct the type that will help LLM understand the field better.+ For example, in the above example, the sortedList's value is written as "Sorted List here". This+ will help LLM understand context better.++ You can also provide number of retries in case the LLM field to return the response in correct JSON+ in first attempt.+-}+chatJson ::+  (FromJSON jsonResult, ToJSON jsonResult) =>+  ChatOps ->+  jsonResult -> -- ^ Haskell type that you want your result in+  Maybe Int -> -- ^ Max retries+  IO (Either String jsonResult)+chatJson cOps@ChatOps {..} jsonStructure mMaxRetries = do+  let lastMessage = NonEmpty.last messages+      jsonHelperPrompt =+        "You are an AI that returns only JSON object. \n"+          <> "* Your output should be a JSON object that matches the following schema: \n"+          <> T.decodeUtf8 (BSL.toStrict $ encode jsonStructure)+          <> content lastMessage+          <> "\n"+          <> "# How to treat the task:\n"+          <> "* Stricly follow the schema for the output.\n"+          <> "* Never return anything other than a JSON object.\n"+          <> "* Do not talk to the user.\n"+  chatResponse <-+    chat+      cOps+        { messages =+            NonEmpty.fromList $+              lastMessage {content = jsonHelperPrompt} : NonEmpty.init messages+        }+  case chatResponse of+    Left err -> return $ Left err+    Right r -> do+      let mMessage = message r+      case mMessage of+        Nothing -> return $ Left "Something went wrong"+        Just res -> do+          case decode (BSL.fromStrict . T.encodeUtf8 $ content res) of+            Nothing -> do +                case mMaxRetries of+                    Nothing -> return $ Left "Decoding Failed :("+                    Just n -> if n < 1 then return $ Left "Decoding Failed :(" else chatJson cOps jsonStructure (Just (n - 1))+            Just resultInType -> return $ Right resultInType
src/Data/Ollama/Common/Utils.hs view
@@ -1,8 +1,45 @@ {-# LANGUAGE OverloadedStrings #-} -module Data.Ollama.Common.Utils (defaultOllama, OllamaClient (..)) where+module Data.Ollama.Common.Utils (defaultOllama, OllamaClient (..), encodeImage) where +import Control.Exception (IOException, try)+import Data.ByteString qualified as BS+import Data.ByteString.Base64 qualified as Base64+import Data.Char (toLower) import Data.Ollama.Common.Types+import Data.Text (Text)+import Data.Text.Encoding qualified as TE+import System.Directory+import System.FilePath  defaultOllama :: OllamaClient defaultOllama = OllamaClient "http://127.0.0.1:11434"++supportedExtensions :: [String]+supportedExtensions = [".jpg", ".jpeg", ".png"]++safeReadFile :: FilePath -> IO (Either IOException BS.ByteString)+safeReadFile = try . BS.readFile++asPath :: FilePath -> IO (Maybe BS.ByteString)+asPath filePath = do+  exists <- doesFileExist filePath+  if exists+    then either (const Nothing) Just <$> safeReadFile filePath+    else return Nothing++isSupportedExtension :: FilePath -> Bool+isSupportedExtension path = map toLower (takeExtension path) `elem` supportedExtensions++{- |+  encodeImage is a utility function that takes an image file path (jpg, jpeg, png) and+  returns the image data in Base64 encoded format. Since GenerateOps' images field +  expects image data in base64. It is helper function that we are providing out of the box.+-}+encodeImage :: FilePath -> IO (Maybe Text)+encodeImage filePath = do+  if not (isSupportedExtension filePath)+    then return Nothing+    else do+      maybeContent <- asPath filePath+      return $ fmap (TE.decodeUtf8 . Base64.encode) maybeContent
src/Data/Ollama/Generate.hs view
@@ -6,6 +6,7 @@   ( -- * Generate Texts     generate   , defaultGenerateOps+  , generateJson   , GenerateOps (..)   , GenerateResponse (..)   ) where@@ -17,6 +18,7 @@ import Data.Ollama.Common.Utils as CU import Data.Text (Text) import Data.Text qualified as T+import Data.Text.Encoding qualified as T import Data.Time (UTCTime) import GHC.Int (Int64) import Network.HTTP.Client@@ -24,13 +26,13 @@ -- TODO: Add Options parameter -- TODO: Add Context parameter -{- | -  Input type for generate functions. This data type represents all possible configurations +{- |+  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 ++  > let ops = GenerateOps   >         { modelName = "llama3.2"   >         , prompt = "What is the meaning of life?"   >         , suffix = Nothing@@ -91,21 +93,22 @@       <> 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+  (==) 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.+{- |+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.@@ -171,15 +174,16 @@       <*> 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.+{- |+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@@ -195,49 +199,50 @@     , 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 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)}+      defaultManagerSettings {managerResponseTimeout = responseTimeoutMicro (15 * 60 * 1000000)}   initialRequest <- parseRequest $ T.unpack (url <> "/api/generate")   let reqBody = genOps       request =@@ -270,3 +275,70 @@     case stream genOps of       Nothing -> genResponse ""       Just (sendChunk, flush) -> streamResponse sendChunk flush++{- |+ generateJson is a higher level function that takes generateOps (similar to generate) and also takes+ a Haskell type (that has To and From JSON instance) and returns the response in provided type.++ This function simply calls generate with extra prompt appended to it, telling LLM to return the+ response in certain JSON format and serializes the response. This function will be helpful when you+ want to use the LLM to do something programmatic.++ For Example:+  > let expectedJsonStrucutre = Example {+  >   sortedList = ["sorted List here"]+  > , wasListAlreadSorted = False+  > }+  > eRes2 <- generateJson+  >     defaultGenerateOps+  >      { modelName = "llama3.2"+  >     , prompt = "Sort given list: [4, 2 , 3, 67]. Also tell whether list was already sorted or not."+  >       }+  >     expectedJsonStrucutre+  >     Nothing+  > case eRes2 of+  >   Left e -> putStrLn e+  >   Right r -> print ("JSON response: " :: String, r)++Output:+  > ("JSON response: ",Example {sortedList = ["1","2","3","4"], wasListAlreadSorted = False})++Note: While Passing the type, construct the type that will help LLM understand the field better.+ For example, in the above example, the sortedList's value is written as "Sorted List here". This+ will help LLM understand context better.++ You can also provide number of retries in case the LLM field to return the response in correct JSON+ in first attempt.+-}+generateJson ::+  (ToJSON jsonResult, FromJSON jsonResult) =>+  GenerateOps ->+  -- | Haskell type that you want your result in+  jsonResult ->+  -- | Max retries+  Maybe Int ->+  IO (Either String jsonResult)+generateJson genOps@GenerateOps {..} jsonStructure mMaxRetries = do+  let jsonHelperPrompt =+        "You are an AI that returns only JSON object. \n"+          <> "* Your output should be a JSON object that matches the following schema: \n"+          <> T.decodeUtf8 (BSL.toStrict $ encode jsonStructure)+          <> prompt+          <> "\n"+          <> "# How to treat the task:\n"+          <> "* Stricly follow the schema for the output.\n"+          <> "* Never return anything other than a JSON object.\n"+          <> "* Do not talk to the user.\n"+  generatedResponse <- generate genOps {prompt = jsonHelperPrompt}+  case generatedResponse of+    Left err -> return $ Left err+    Right r -> do+      case decode (BSL.fromStrict . T.encodeUtf8 $ response_ r) of+        Nothing -> do+          case mMaxRetries of+            Nothing -> return $ Left "Decoding Failed :("+            Just n ->+              if n < 1+                then return $ Left "Decoding failed :("+                else generateJson genOps jsonStructure (Just (n - 1))+        Just resultInType -> return $ Right resultInType
src/Ollama.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE DuplicateRecordFields #-} {- |   #Ollama-Haskell#   This library lets you run LlMs from within Haskell projects. Inspired by `ollama-python`.@@ -7,12 +8,14 @@      -- ** Generate Texts     generate+  , generateJson   , defaultGenerateOps   , GenerateOps (..)   , GenerateResponse (..)      -- ** Chat with LLMs   , chat+  , chatJson   , Role (..)   , defaultChatOps   , ChatResponse (..)@@ -66,6 +69,7 @@   , Message (..)   , Role (..)   , chat+  , chatJson   , defaultChatOps   ) import Data.Ollama.Copy (copyModel)@@ -77,6 +81,7 @@   , GenerateResponse (..)   , defaultGenerateOps   , generate+  , generateJson   ) import Data.Ollama.List (ModelInfo (..), Models (..), list) import Data.Ollama.Ps (RunningModel (..), RunningModels (..), ps)
src/OllamaExamples.hs view
@@ -1,16 +1,29 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} -module OllamaExamples where+module OllamaExamples (main) where  import Control.Monad (void)-import Data.List.NonEmpty (NonEmpty((:|)))+import Data.Aeson+import Data.List.NonEmpty (NonEmpty ((:|))) import Data.Maybe (fromMaybe)+import Data.Ollama.Chat (chatJson) import Data.Ollama.Chat qualified as Chat+import Data.Ollama.Common.Utils (encodeImage)+import Data.Ollama.Generate (generateJson) import Data.Text.IO qualified as T-import Ollama (GenerateOps(..), Role(..), chat, defaultChatOps, defaultGenerateOps, generate)+import GHC.Generics+import Ollama (GenerateOps (..), Role (..), chat, defaultChatOps, defaultGenerateOps, generate) import Ollama qualified +data Example = Example+  { sortedList :: [String]+  , wasListAlreadSorted :: Bool+  }+  deriving (Show, Eq, Generic, FromJSON, ToJSON)+ main :: IO () main = do   -- Example 1: Streamed Text Generation@@ -81,6 +94,57 @@   -- 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 +  -- Example 8: Stream Text Generation with JSON Body+  -- It is a higher level version of generate, here with genOps, you can also provide a Haskell type.+  -- You will get the response from LLM in this Haskell type.+  let expectedJsonStrucutre =+        Example+          { sortedList = ["sorted List here"]+          , wasListAlreadSorted = False+          }+  eRes2 <-+    generateJson+      defaultGenerateOps+        { modelName = "llama3.2"+        , prompt = "Sort given list: [14, 12 , 13, 67]. Also tell whether list was already sorted or not."+        }+      expectedJsonStrucutre+      (Just 2)+  case eRes2 of+    Left e -> putStrLn e+    Right r -> print ("JSON response: " :: String, r)+  -- ("JSON response: ",Example {sortedList = ["1","2","3","4"], wasListAlreadSorted = False})++  -- Example 9: Chat with JSON Body+  -- This example demonstrates setting up a chat session but you receive the response in+  -- given haskell type.+  let msg0 =+        Ollama.Message+          User+          "Sort given list: [4, 2 , 3, 67]. Also tell whether list was already sorted or not."+          Nothing+  eRes3 <-+    chatJson+      defaultChatOps+        { Chat.chatModelName = "llama3.2"+        , Chat.messages = msg0 :| []+        }+      expectedJsonStrucutre+      (Just 2)+  print eRes3++  -- Example 10: Chat with Image+  -- This example demonstrates chatting with example using an image.+  mImg <- encodeImage "/home/user/sample.png"+  void $+    generate+      defaultGenerateOps+        { modelName = "llama3.2-vision"+        , prompt = "Describe the given image"+        , images = (\x -> Just [x]) =<< mImg+        , stream = Just (T.putStr . Ollama.response_, pure ())+        }+ {- Scotty example: {-# LANGUAGE OverloadedStrings #-}@@ -109,7 +173,7 @@   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@@ -123,7 +187,7 @@         _ -> 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"