packages feed

langchain-hs-0.0.1.0: test/Test/Langchain/Agent/Core.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}

module Test.Langchain.Agent.Core (tests) where

import Control.Exception (throwIO)
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import qualified Data.List.NonEmpty as NE
import qualified Data.Map.Strict as Map
import Data.Text (Text, isInfixOf, pack)
import Langchain.Agents.Core
import Langchain.LLM.Core
import Langchain.Memory.Core (BaseMemory (..))
import Langchain.PromptTemplate
import Langchain.Tool.Core (Tool (..))
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, assertEqual, testCase)

data DummyTool = DummyTool deriving (Show)

instance Tool DummyTool where
  type Input DummyTool = Text
  type Output DummyTool = Text
  toolName _ = "dummy-tool"
  toolDescription _ = "dummy tool description"
  runTool _ input = return $ "Processed: " <> input

data FaultyTool = FaultyTool deriving (Show)

instance Tool FaultyTool where
  type Input FaultyTool = Text
  type Output FaultyTool = Text
  toolName _ = "faulty-tool"
  toolDescription _ = "fulty tool description"
  runTool _ _ = throwIO $ userError "Intentional tool error"

data StepSequenceAgent = StepSequenceAgent (IORef [AgentStep]) [AnyTool]

instance Agent StepSequenceAgent where
  planNextAction (StepSequenceAgent ref _) _ = do
    steps <- readIORef ref
    case steps of
      [] -> return $ Left "No steps left"
      (step : rest) -> do
        writeIORef ref rest
        return $ Right step
  agentTools (StepSequenceAgent _ tools) = return tools
  agentPrompt _ = return $ PromptTemplate "test prompt"

-- Test Memory Implementation

data TestMemory = TestMemory [Message]

instance BaseMemory TestMemory where
  addMessage (TestMemory msgs) newMsg = return $ Right $ TestMemory (msgs ++ [newMsg])
  addUserMessage (TestMemory msgs) input = do
    let userMsg = Message User input defaultMessageData
    return $ Right $ TestMemory (msgs ++ [userMsg])
  addAiMessage (TestMemory msgs) input = do
    let aiMsg = Message System input defaultMessageData
    return $ Right $ TestMemory (msgs ++ [aiMsg])
  messages (TestMemory msgs) = return $ Right $ NE.fromList msgs
  clear _ = return $ Right (TestMemory [])

tests :: TestTree
tests =
  testGroup
    "Agent Tests"
    [ testCase "executeTool valid tool" $ do
        let dummyAnyTool = customAnyTool DummyTool id id
            tools = [dummyAnyTool]
        result <- executeTool tools "dummy-tool" "test input"
        assertEqual "Should process input" (Right "Processed: test input") result
    , testCase "executeTool tool not found" $ do
        let tools = []
        result <- executeTool tools "unknown-tool" "input"
        assertEqual "Should return tool not found error" (Left "Tool not found: unknown-tool") result
    , testCase "executeTool tool throws exception" $ do
        let faultyAnyTool = customAnyTool FaultyTool id id
            tools = [faultyAnyTool]
        result <- executeTool tools "faulty-tool" "input"
        assertBool
          "Should return execution error"
          ("Intentional tool error" `isInfixOf` (pack $ fromLeft "" result))
    , testCase "runAgentLoop max iterations exceeded" $ do
        agentRef <- newIORef []
        let agent = StepSequenceAgent agentRef []
            initialState = AgentState (TestMemory []) [] []
        result <- runAgentLoop agent initialState 10 5
        assertEqual "Should return max iteration error" (Left "Max iterations excedded") result
    , testCase "runAgent immediate finish" $ do
        agentRef <- newIORef [Finish (AgentFinish (Map.singleton "result" "success") "Finished")]
        let agent = StepSequenceAgent agentRef []
            initialState = AgentState (TestMemory []) [] []
        result <- runAgent agent initialState "input"
        assertEqual
          "Should return finish result"
          (Right (AgentFinish (Map.singleton "result" "success") "Finished"))
          result
    , testCase "runAgentLoop continue then finish" $ do
        agentRef <-
          newIORef
            [ Continue (AgentAction "dummy-tool" "input" "log")
            , Finish (AgentFinish Map.empty "Done")
            ]
        let dummyAnyTool = customAnyTool DummyTool id id
            agent = StepSequenceAgent agentRef [dummyAnyTool]
            initialState = AgentState (TestMemory []) [] []
        result <- runAgentLoop agent initialState 0 10
        assertEqual "Should finish after one step" (Right (AgentFinish Map.empty "Done")) result
    , testCase "customAnyTool wraps correctly" $ do
        let tool = customAnyTool DummyTool id id
            input = "test"
            expectedOutput = "Processed: test"
        result <- executeTool [tool] "dummy-tool" input
        assertEqual "Should apply conversions" (Right expectedOutput) result
    ]
  where
    fromLeft :: a -> Either a b -> a
    fromLeft _ (Left x) = x
    fromLeft def _ = def