packages feed

langchain-hs-0.0.5.0: test/Test/Langchain/RegressionSpec.hs

{-# LANGUAGE OverloadedStrings #-}

module Test.Langchain.RegressionSpec (tests) where

import Control.Monad.Except (runExceptT)
import Control.Monad.Trans.Resource (runResourceT)
import Data.Aeson (decode)
import qualified Data.ByteString.Lazy.Char8 as LBSC
import Test.Tasty
import Test.Tasty.HUnit

import Langchain.Agent.ReAct
import Langchain.Core.Model
import Langchain.Core.Stream
import Langchain.Memory.Core (BaseMemory (..), newWindowBufferMemory)
import qualified Langchain.Memory.Core as TB
import Langchain.Provider.OpenAI (parseOpenAIResponse)
import Langchain.Tool.Calculator (calculatorTool)
import Test.Langchain.Provider.Mock (newMockModel)

tests :: TestTree
tests =
  testGroup
    "Langchain.RegressionSpec"
    [ testCase "regression_ollama_stream_lifecycle: StreamEvent stream ends with LLMEnd" $ do
        let mockModel = newMockModel "Streaming chunk data"
            input = [userMessage "Ping"]
        res <- runResourceT $ runExceptT $ collectEvents (stream mockModel input Nothing)
        case res of
          Left err -> assertFailure ("Stream failed: " ++ show err)
          Right events -> do
            length events @?= 3
            case last events of
              LLMEnd _ finalMsg _ -> extractMessageText finalMsg @?= "Streaming chunk data"
              _ -> assertFailure "Expected LLMEnd as last event in stream"
    , testCase "regression_system_fingerprint_nullable: OpenAI JSON parses without fingerprint" $ do
        let jsonWithoutFingerprint =
              "{\"id\":\"cmpl-1\",\"object\":\"chat.completion\",\"created\":1600000000,\"model\":\"gpt-4o\",\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"OK\"},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":1,\"completion_tokens\":1,\"total_tokens\":2}}"
        case decode (LBSC.pack jsonWithoutFingerprint) of
          Nothing -> assertFailure "Failed to decode JSON value"
          Just val -> case parseOpenAIResponse val of
            Left err -> assertFailure ("OpenAI parsing failed on nullable fingerprint: " ++ err)
            Right (msg, _) -> extractMessageText msg @?= "OK"
    , testCase "regression_react_agent_plain_response: Completes immediately when no tool calls" $ do
        let mockModel = newMockModel "Direct Answer without tool calls"
            agent = createReActAgent mockModel [calculatorTool]
        res <- runExceptT $ runReActAgent agent [userMessage "What is the capital of France?"]
        case res of
          Left err -> assertFailure ("ReAct agent failed: " ++ show err)
          Right finalMsg -> extractMessageText finalMsg @?= "Direct Answer without tool calls"
    , testCase "regression_memory_window_trimming: System message preserved during trimming" $ do
        let sys = systemMessage "System Prompt"
            u1 = userMessage "User 1"
            u2 = userMessage "User 2"
        mem <- newWindowBufferMemory 2 [sys, u1]
        res <- runExceptT $ do
          addMessage mem u2
          messages mem
        case res of
          Left err -> assertFailure ("Memory failed: " ++ show err)
          Right msgs -> msgs @?= [sys, u2]
    , testCase "regression_token_buffer_system_preservation: System message kept within token budget" $ do
        let sys = systemMessage "Sys"
            u1 = userMessage "Long user message 12345678"
            u2 = userMessage "Long user message 12345678"
        mem <- TB.newTokenBufferMemory 8 [sys, u1]
        res <- runExceptT $ do
          addMessage mem u2
          messages mem
        case res of
          Left err -> assertFailure ("TokenBuffer failed: " ++ show err)
          Right msgs -> do
            assertBool "Contains system message" (any (\m -> messageRole m == System) msgs)
    ]