packages feed

langchain-hs-0.0.5.0: test/Test/Langchain/Provider/Ollama.hs

{-# LANGUAGE OverloadedStrings #-}

module Test.Langchain.Provider.Ollama (tests) where

import Test.Tasty
import Test.Tasty.HUnit

import Control.Monad.Except (runExceptT)
import Data.Text (Text)
import qualified Data.Text as T

import Langchain.Core.Model
import Langchain.Core.Tool (Tool)
import Langchain.Provider.Ollama
import Langchain.Tool.Calculator (calculatorTool)
import Test.Langchain.TestHelpers (withOllamaModel)

import qualified Data.List.NonEmpty as NonEmpty
import qualified Ollama.Client as OC
import qualified Ollama.Types.Format as OFormat
import qualified Ollama.Types.Message as O
import qualified Ollama.Types.Tool as OTool

testModelName :: Text
testModelName = "gemma3:latest"

tests :: TestTree
tests =
  testGroup
    "Langchain.Provider.Ollama"
    [ testCase "newOllama initializes provider with defaultConfig" $ do
        p <- newOllama testModelName defaultConfig
        ollamaModelName p @?= testModelName
    , testCase "newOllama accepts custom OllamaClientConfig" $ do
        let cfg =
              defaultConfig
                { configBaseUrl = "http://custom-host:11434"
                , configTimeout = 120
                }
        p <- newOllama "qwen3.5:2b" cfg
        ollamaModelName p @?= "qwen3.5:2b"
        configBaseUrl (OC.clientConfig (client p)) @?= "http://custom-host:11434"
        configTimeout (OC.clientConfig (client p)) @?= 120
    , testCase "newOllamaWithClient wraps existing OllamaClient" $ do
        c <- OC.defaultClient
        let p = newOllamaWithClient testModelName c
        ollamaModelName p @?= testModelName
    , testCase "invoke returns Assistant message" $ do
        withOllamaModel testModelName $ \modelName -> do
          p <- newOllama modelName defaultConfig
          let input = [userMessage "What is 2 + 2? Answer with just the number."]
          res <- runExceptT $ invoke p input Nothing
          case res of
            Left err -> assertFailure $ "Expected success, got error: " ++ show err
            Right msg -> do
              messageRole msg @?= Assistant
              assertBool "Should contain 4" ("4" `T.isInfixOf` extractMessageText msg)
    , testCase "batch processes multiple inputs" $ do
        withOllamaModel testModelName $ \modelName -> do
          p <- newOllama modelName defaultConfig
          let inputs = [[userMessage "What is 1 + 1?"], [userMessage "What is 2 + 2?"]]
          res <- runExceptT $ batch p inputs Nothing
          case res of
            Left err -> assertFailure $ "Expected success, got error: " ++ show err
            Right msgs -> do
              length msgs @?= 2
    , testCase "withOptions sets ModelOptions on ChatRequest" $ do
        p <- newOllama testModelName defaultConfig
        let opts = defaultOptions {optTemperature = Just 0.3, optNumCtx = Just 4096}
            req = withOptions opts (chatRequestFor p [userMessage "Hello"])
        case chatOptions req of
          Nothing -> assertFailure "Expected chatOptions in ChatRequest"
          Just o -> do
            optTemperature o @?= Just 0.3
            optNumCtx o @?= Just 4096
    , testCase "toOllamaTool converts calculatorTool to Ollama Tool" $ do
        let cTool = calculatorTool :: Tool IO
        case toOllamaTool cTool of
          Nothing -> assertFailure "Failed to convert calculatorTool to Ollama Tool"
          Just ot -> do
            OTool.toolType ot @?= "function"
            OTool.fnName (OTool.toolFunction ot) @?= "calculator"
    , testCase "withTools on ChatRequest sets chatTools" $ do
        p <- newOllama testModelName defaultConfig
        let req = withTools [calculatorTool :: Tool IO] (chatRequestFor p [userMessage "Hello"])
        case chatTools req of
          Nothing -> assertFailure "Expected chatTools in ChatRequest"
          Just ts -> length ts @?= 1
    , testCase "chatRequestFor creates base request" $ do
        p <- newOllama testModelName defaultConfig
        let req = chatRequestFor p [userMessage "Hello"]
        chatModel req @?= ModelName testModelName
    , testCase "invoke propagates chatFormat from mbReq" $ do
        withOllamaModel testModelName $ \modelName -> do
          p <- newOllama modelName defaultConfig
          let input = [userMessage "Return JSON: {\"answer\": 42}"]
              req = withJsonFormat (chatRequestFor p input)
          chatFormat req @?= Just OFormat.JsonFormat
          res <- runExceptT $ invoke p input (Just req)
          case res of
            Left err -> assertFailure $ "Expected success, got error: " ++ show err
            Right msg -> messageRole msg @?= Assistant
    , testGroup
        "Precedence Rules (resolveChatRequest)"
        [ testCase "inputMsgs takes priority over ChatRequest chatMessages when non-empty" $ do
            p <- newOllama "base-model" defaultConfig
            let invokeMsgs = [userMessage "From invoke argument"]
                reqMsgs = [userMessage "From ChatRequest"]
                customReq = chatRequestFor p reqMsgs
                (resolvedReq, resolvedModel, resolvedMsgs) =
                  resolveChatRequest p invokeMsgs (Just customReq)
            resolvedModel @?= "base-model"
            resolvedMsgs @?= invokeMsgs
            NonEmpty.toList (chatMessages resolvedReq) @?= map toOllamaMessage invokeMsgs
        , testCase "fallback to ChatRequest chatMessages when inputMsgs is empty" $ do
            p <- newOllama "base-model" defaultConfig
            let reqMsgs = [userMessage "From ChatRequest only"]
                customReq = chatRequestFor p reqMsgs
                (resolvedReq, resolvedModel, resolvedMsgs) =
                  resolveChatRequest p [] (Just customReq)
            resolvedModel @?= "base-model"
            resolvedMsgs @?= reqMsgs
            NonEmpty.toList (chatMessages resolvedReq) @?= map toOllamaMessage reqMsgs
        , testCase "defaults to single empty message when both inputMsgs and mbReq are empty" $ do
            p <- newOllama "base-model" defaultConfig
            let (resolvedReq, resolvedModel, resolvedMsgs) =
                  resolveChatRequest p [] Nothing
            resolvedModel @?= "base-model"
            resolvedMsgs @?= []
            NonEmpty.toList (chatMessages resolvedReq) @?= [O.userMessage ""]
        , testCase "ChatRequest chatModel overrides provider ollamaModelName when non-empty" $ do
            p <- newOllama "base-model" defaultConfig
            let customReq = (chatRequestFor p [userMessage "hi"]) {chatModel = ModelName "custom-model"}
                (resolvedReq, resolvedModel, _) =
                  resolveChatRequest p [userMessage "hi"] (Just customReq)
            resolvedModel @?= "custom-model"
            chatModel resolvedReq @?= ModelName "custom-model"
        , testCase "falls back to ollamaModelName when ChatRequest chatModel is empty" $ do
            p <- newOllama "base-model" defaultConfig
            let customReq = (chatRequestFor p [userMessage "hi"]) {chatModel = ModelName ""}
                (resolvedReq, resolvedModel, _) =
                  resolveChatRequest p [userMessage "hi"] (Just customReq)
            resolvedModel @?= "base-model"
            chatModel resolvedReq @?= ModelName "base-model"
        , testCase "falls back to ollamaModelName when mbReq is Nothing" $ do
            p <- newOllama "base-model" defaultConfig
            let (resolvedReq, resolvedModel, _) =
                  resolveChatRequest p [userMessage "hi"] Nothing
            resolvedModel @?= "base-model"
            chatModel resolvedReq @?= ModelName "base-model"
        , testCase "preserves options, tools, format, and keep-alive from ChatRequest" $ do
            p <- newOllama "base-model" defaultConfig
            let opts = defaultOptions {optTemperature = Just 0.5}
                customReq =
                  (withOptions opts (chatRequestFor p [userMessage "dummy"]))
                    { chatKeepAlive = Just "5m"
                    , chatFormat = Just OFormat.JsonFormat
                    }
                (resolvedReq, _, _) =
                  resolveChatRequest p [userMessage "override message"] (Just customReq)
            chatOptions resolvedReq @?= Just opts
            chatKeepAlive resolvedReq @?= Just "5m"
            chatFormat resolvedReq @?= Just OFormat.JsonFormat
        ]
    ]