langchain-hs-0.0.3.0: test/Test/Langchain/LLM/Ollama.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Test.Langchain.LLM.Ollama (tests) where
import Test.Tasty
import Test.Tasty.HUnit
import Data.IORef
import Data.List.NonEmpty (NonEmpty (..))
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Data.Aeson
import qualified Data.ByteString.Lazy.Char8 as BSL
import qualified Data.Ollama.Chat as O
import Langchain.Callback (Callback, Event (..))
import Langchain.LLM.Core
import Langchain.LLM.Ollama
import qualified Langchain.Runnable.Core as Run
captureEvents :: IO (Callback, IO [Event])
captureEvents = do
eventsRef <- newIORef []
let callback event = modifyIORef eventsRef (event :)
let getEvents = reverse <$> readIORef eventsRef
return (callback, getEvents)
testModelName :: Text
testModelName = "qwen3:0.6b"
tests :: TestTree
tests =
testGroup
"Ollama"
[ testCase "Show instance formats Ollama correctly" $ do
let ollama = Ollama "llama3" []
show ollama @?= "Ollama \"llama3\""
, testCase "generate returns text response for a prompt" $ do
(callback, getEvents) <- captureEvents
let ollama = Ollama testModelName [callback]
let prompt = "What is functional programming?"
result <- generate ollama prompt Nothing
case result of
Left err -> assertFailure $ "Expected success, got error: " ++ show err
Right response -> do
assertBool "Non-empty response expected" (T.length response > 0)
events <- getEvents
assertBool
"should contain all events"
(events `shouldContainAll` [LLMStart, LLMEnd])
, testCase "generate returns error for invalid model" $ do
(callback, getEvents) <- captureEvents
let ollama = Ollama "non_existent_model" [callback]
let prompt = "Hello"
result <- generate ollama prompt Nothing
case result of
Left err -> do
assertBool
"Error should mention model"
("model" `T.isInfixOf` T.pack (show err))
events <- getEvents
assertBool
"LLM should tried to be started"
(events `shouldContainAll` [LLMStart])
length (filter isErrorEvent events) @?= 1
Right _ -> assertFailure "Expected error, but got success"
, testCase "chat returns text response for messages" $ do
(callback, getEvents) <- captureEvents
let ollama = Ollama testModelName [callback]
let messages =
Message
User
"What's the capital of France?"
defaultMessageData
:| []
result <- chat ollama messages Nothing
case result of
Left err -> assertFailure $ "Expected success, got error: " ++ show err
Right response -> do
assertBool
"Response should mention Paris"
("paris" `T.isInfixOf` T.toLower (content response))
events <- getEvents
assertBool
"LLM should be completed"
(events `shouldContainAll` [LLMStart, LLMEnd])
, testCase "chat handles multi-turn conversations" $ do
(callback, _) <- captureEvents
let ollama = Ollama testModelName [callback]
let messages =
Message System "You are a helpful assistant." defaultMessageData
:| [ Message
User
"What's the capital of France?"
defaultMessageData
, Message
Assistant
"The capital of France is Paris."
defaultMessageData
, Message
User
"And what about Italy?"
defaultMessageData
]
result <- chat ollama messages Nothing
case result of
Left err -> assertFailure $ "Expected success, got error: " ++ show err
Right response ->
assertBool
"Response should mention Rome"
("rome" `T.isInfixOf` T.toLower (content response))
, testCase "stream calls handlers for streaming responses" $ do
let ollama = Ollama testModelName []
let messages = Message User "Count from 1 to 5 briefly." defaultMessageData :| []
tokensRef <- newIORef []
let handler =
StreamHandler
{ onToken = \token -> modifyIORef tokensRef (token :)
, onComplete = pure ()
}
-- \| onComplete does not support Ollama
result <- stream ollama messages handler Nothing
case result of
Left err -> assertFailure $ "Expected success, got error: " ++ show err
Right () -> do
tokens <- readIORef tokensRef
assertBool "Should receive tokens" (not (null tokens))
, testCase "invoke calls chat with the input messages" $ do
let ollama = Ollama testModelName []
let input = Message User "What is 2+2?" defaultMessageData :| []
result <- Run.invoke ollama (input, Nothing)
case result of
Left err -> assertFailure $ "Expected success, got error: " ++ show err
Right response ->
assertBool
"Should mention 4"
("4" `T.isInfixOf` T.toLower (content response))
, {- qwen3:06b does not support insert
, testCase "generate appends suffix when provided" $ do
(callback, getEvents) <- captureEvents
let ollama = Ollama testModelName [callback]
let prompt = "What is functional programming?"
result <- generate ollama prompt Nothing
case result of
Left err -> assertFailure $ "Expected success, got error: " ++ err
Right response -> do
assertBool "Response should end with suffix" (T.isSuffixOf " [End]" response)
events <- getEvents
assertBool "should contain all events"
(events `shouldContainAll` [LLMStart, LLMEnd])
-}
testCase "generate uses system message for context" $ do
(callback, getEvents) <- captureEvents
let ollama = Ollama testModelName [callback]
let prompt = "What is 2 + 2?"
result <- generate ollama prompt Nothing
case result of
Left err -> assertFailure $ "Expected success, got error: " ++ show err
Right response -> do
assertBool "Response should mention 4" ("4" `T.isInfixOf` T.toLower response)
events <- getEvents
assertBool "should contain all events" (events `shouldContainAll` [LLMStart, LLMEnd])
, testCase "generate returns JSON response when format is set" $ do
(callback, getEvents) <- captureEvents
let ollama = Ollama testModelName [callback]
let prompt = "What is JSON?"
let params = O.defaultChatOps {O.format = Just O.JsonFormat}
result <- generate ollama prompt (Just params)
case result of
Left err -> assertFailure $ "Expected success, got error: " ++ show err
Right response -> do
case eitherDecode (BSL.fromStrict $ T.encodeUtf8 response) :: Either String Value of
Left _ -> assertFailure "Response is not valid JSON"
Right _ -> return ()
events <- getEvents
assertBool "should contain all events" (events `shouldContainAll` [LLMStart, LLMEnd])
, testCase "chat returns JSON response when format is set" $ do
(callback, getEvents) <- captureEvents
let ollama = Ollama testModelName [callback]
let messages = Message User "What is JSON?" defaultMessageData :| []
let params = O.defaultChatOps {O.format = Just O.JsonFormat}
result <- chat ollama messages (Just params)
case result of
Left err -> assertFailure $ "Expected success, got error: " ++ show err
Right response -> do
case eitherDecode (BSL.fromStrict $ T.encodeUtf8 (content response)) :: Either String Value of
Left _ -> assertFailure "Response is not valid JSON"
Right _ -> return ()
events <- getEvents
assertBool "should contain all events" (events `shouldContainAll` [LLMStart, LLMEnd])
]
where
isErrorEvent (LLMError _) = True
isErrorEvent _ = False
shouldContainAll xs = all (`elem` xs)