packages feed

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

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

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

import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (testCase, assertEqual, assertBool)
import qualified Data.Map.Strict as Map
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.List.NonEmpty as NE
import Langchain.Agents.Core
import Langchain.Agents.React
import Langchain.LLM.Core
import Langchain.Memory.Core (BaseMemory(..))
import Langchain.Tool.Core (Tool(..))

--TODO: Need to fix answering parsing by stripping ": "
data MockLLM = MockLLM { mockResponse :: Text }

instance LLM MockLLM where
  generate _ _ _ = undefined
  chat (MockLLM resp) _ _ = return $ Right resp
  stream _ _ _ _ = undefined

data DummyTool = DummyTool deriving (Show)

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

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 $ return $ NE.fromList msgs
  clear _ = pure $ Right $ TestMemory []

tests :: TestTree
tests = testGroup "React Agent Tests"
  [ testCase "parseReactOutput final answer" $ do
      let input = "Thought: I know the answer\nFinal Answer: Success"
      let result = parseReactOutput input
      case result of
        Right (ReactAgentOutputParser (Finish (AgentFinish vals _))) ->
          assertEqual "Should parse final answer" (Map.singleton "output" ": Success") vals
        _ -> assertBool "Failed to parse final answer" False

  , testCase "parseReactOutput action step" $ do
      let input = "Action: dummy-tool\nAction Input: test input"
      let result = parseReactOutput input
      case result of
        Right (ReactAgentOutputParser (Continue act)) -> do
          assertEqual "Correct tool name" ": dummy-tool" (actionToolName act)
          assertEqual "Correct input" ": test input" (actionInput act)
        _ -> assertBool "Failed to parse action" False

  , testCase "parseReactOutput invalid input" $ do
      let input = "Invalid format"
      let result = parseReactOutput input
      case result of
        Left err -> assertBool "Should return parse error" ("Could not parse" `T.isInfixOf` (T.pack err))
        _ -> assertBool "Should fail on invalid input" False

  , testCase "planNextAction generates action step" $ do
      let llm = MockLLM { mockResponse = "Action: dummy-tool\nAction Input: test" }
      let tools = [customAnyTool DummyTool id id]
      agent <- createReactAgent llm tools
      case agent of
        Left _ -> assertBool "Agent creation failed" False
        Right reactAgent -> do
          let mem = TestMemory [Message User "Solve this" defaultMessageData]
          let state = AgentState mem [] []
          result <- planNextAction reactAgent state
          case result of
            Right (Continue act) -> do
              assertEqual "Correct tool name" ": dummy-tool" (actionToolName act)
              assertEqual "Correct input" ": test" (actionInput act)
            _ -> assertBool "Should generate action step" False

  , testCase "planNextAction final answer" $ do
      let llm = MockLLM { mockResponse = "Final Answer: 42" }
      let tools = []
      agent <- createReactAgent llm tools
      case agent of
        Left _ -> assertBool "Agent creation failed" False
        Right reactAgent -> do
          let mem = TestMemory [Message User "What's the answer?" defaultMessageData]
          let state = AgentState mem [] []
          result <- planNextAction reactAgent state
          case result of
            Right (Finish (AgentFinish vals _)) ->
              assertEqual "Correct final answer" (Map.singleton "output" ": 42") vals
            _ -> assertBool "Should generate final answer" False

  , testCase "createReactAgent prompt formatting" $ do
      let llm = MockLLM { mockResponse = "" }
      let tools = [customAnyTool DummyTool id id]
      agent <- createReactAgent llm tools
      case agent of
        Right ReactAgent {..} -> do
          let expectedTools = "Tool: dummy-tool\nDescription: A dummy tool for testing"
              expectedNames = "dummy-tool"
          assertEqual "Correct tool descriptions" expectedTools (formatToolDescriptions reactTools)
          assertEqual "Correct tool names" expectedNames (formatToolNames reactTools)
        _ -> assertBool "Agent creation failed" False

  , testCase "getLastUserInput retrieves last user message" $ do
      let msgs = NE.fromList [ Message User "First" defaultMessageData
                 , Message Assistant "Response" defaultMessageData
                 , Message User "Last" defaultMessageData ] 
      let result = getLastUserInput msgs
      assertEqual "Should get last user input" "Last" result

  , testCase "getLastUserInput no user messages" $ do
      let msgs = NE.fromList [ Message Assistant "Only" defaultMessageData ] 
      let result = getLastUserInput msgs
      assertEqual "Should return empty" "" result
  ]
  where
    -- isLeft (Left _) = True
    -- isLeft _ = False