From c3cd585891c3d8a6adbb936fb81eebf5b59bc7b4 Mon Sep 17 00:00:00 2001 From: tusharad Date: Fri, 8 Nov 2024 22:25:44 +0530 Subject: [PATCH] Added encodeImage utility function --- CHANGELOG.md | 4 +- example/sample.png | Bin 0 -> 3292 bytes ollama-haskell.cabal | 8 +++- package.yaml | 5 ++- src/Data/Ollama/Chat.hs | 2 +- src/Data/Ollama/Common/Utils.hs | 39 ++++++++++++++++++- src/Data/Ollama/Generate.hs | 19 +++++---- src/OllamaExamples.hs | 66 +++++++++++++++++++++----------- 8 files changed, 108 insertions(+), 35 deletions(-) create mode 100644 example/sample.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 313a068..a68b5e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,9 @@ # Revision history for ollama-haskell -## 0.1.1.3 -- 2024-11-06 +## 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 diff --git a/example/sample.png b/example/sample.png new file mode 100644 index 0000000000000000000000000000000000000000..c13fb488c73ce794ba83f158607fe0b88c339d9b GIT binary patch literal 3292 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 a2k`&VmH7Fh2c4Jz0000 category: Web @@ -52,7 +52,10 @@ library build-depends: aeson , base >=4.7 && <5 + , base64-bytestring , bytestring + , directory + , filepath , http-client , http-types , text @@ -72,7 +75,10 @@ test-suite ollama-haskell-test build-depends: aeson , base >=4.7 && <5 + , base64-bytestring , bytestring + , directory + , filepath , http-client , http-types , ollama-haskell diff --git a/package.yaml b/package.yaml index b4eac19..9408ddc 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: ollama-haskell -version: 0.1.0.3 +version: 0.1.1.3 github: "tusharad/ollama-haskell" license: MIT author: "tushar" @@ -23,6 +23,9 @@ dependencies: - time - http-client - http-types +- base64-bytestring +- filepath +- directory ghc-options: - -Wall diff --git a/src/Data/Ollama/Chat.hs b/src/Data/Ollama/Chat.hs index 095a69a..27e0101 100644 --- a/src/Data/Ollama/Chat.hs +++ b/src/Data/Ollama/Chat.hs @@ -187,7 +187,7 @@ chat cOps = do 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 diff --git a/src/Data/Ollama/Common/Utils.hs b/src/Data/Ollama/Common/Utils.hs index 307720b..5dd0d7a 100644 --- a/src/Data/Ollama/Common/Utils.hs +++ b/src/Data/Ollama/Common/Utils.hs @@ -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 diff --git a/src/Data/Ollama/Generate.hs b/src/Data/Ollama/Generate.hs index d28c083..bfce9df 100644 --- a/src/Data/Ollama/Generate.hs +++ b/src/Data/Ollama/Generate.hs @@ -242,7 +242,7 @@ 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 = @@ -313,8 +313,10 @@ Note: While Passing the type, construct the type that will help LLM understand t generateJson :: (ToJSON jsonResult, FromJSON jsonResult) => GenerateOps -> - jsonResult -> -- ^ Haskell type that you want your result in - Maybe Int -> -- ^ Max retries + -- | 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 = @@ -332,8 +334,11 @@ generateJson genOps@GenerateOps {..} jsonStructure mMaxRetries = do 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)) + 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 diff --git a/src/OllamaExamples.hs b/src/OllamaExamples.hs index 033009b..7ab21f9 100644 --- a/src/OllamaExamples.hs +++ b/src/OllamaExamples.hs @@ -1,26 +1,28 @@ -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} 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 Ollama qualified -import Data.Aeson import GHC.Generics -import Data.Ollama.Generate (generateJson) -import Data.Ollama.Chat (chatJson) +import Ollama (GenerateOps (..), Role (..), chat, defaultChatOps, defaultGenerateOps, generate) +import Ollama qualified -data Example = Example { - sortedList :: [String] +data Example = Example + { sortedList :: [String] , wasListAlreadSorted :: Bool - } deriving (Show, Eq, Generic, FromJSON, ToJSON) + } + deriving (Show, Eq, Generic, FromJSON, ToJSON) main :: IO () main = do @@ -93,13 +95,15 @@ main = do 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. + -- 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 + 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." @@ -110,21 +114,37 @@ main = do 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 + 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) + 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 #-} @@ -153,7 +173,7 @@ 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 @@ -167,7 +187,7 @@ main = do _ -> 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"