packages feed

langchain-hs-0.0.3.0: src/Langchain/Agent/ReAct.hs

{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}

{- |
Module      : Langchain.Agent.ReAct
Description : ReAct (Reasoning + Acting) agent implementation
Copyright   : (c) 2025 Tushar Adhatrao
License     : MIT
Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
Stability   : experimental

This module implements the ReAct (Reasoning + Acting) agent pattern.
ReAct combines reasoning traces and task-specific actions in an interleaved manner.
-}
module Langchain.Agent.ReAct
  ( -- * Agent Creation
    ReActAgent (..)
  , createReActAgent
  , createReActAgentWithPrompt

    -- * Prompt Templates
  , reActSystemPrompt
  ) where

import Control.Monad.Trans.Except
import Data.List (find)
import qualified Data.Map as Map
import Data.Text (Text)
import qualified Data.Text as T
import Langchain.Agent.Core
import qualified Langchain.Error as Error
import Langchain.LLM.Core
import Langchain.Memory.Core (BaseMemory (..))
import Langchain.Tool.Core

{- | ReAct agent.

Arguments:
- llm: The language model
- llmParams: The language model parameters
- systemPrompt: The system prompt
- maxThinkingSteps: The maximum number of thinking steps before forcing action
- tools: The tools available to the agent.
-}
data ReActAgent llm = ReActAgent
  { reactLLM :: llm
  -- ^ The language model for reasoning
  , reactLLMParams :: Maybe (LLMParams llm)
  -- ^ the llm params for language model
  , reactSystemPrompt :: Text
  -- ^ System prompt template
  , reactMaxThinkingSteps :: Int
  -- ^ Maximum consecutive thinking steps before forcing action (default: 3)
  , reactTools :: [ToolAcceptingToolCall]
  }

{- | Create a ReAct agent.

Arguments:
- llm: The language model
- llmParams: The language model parameters
- tools: The tools available to the agent

Important:
- It is user's responsibility to wrap the tools into ToolAcceptingToolCall.
- It is user's responsibility to pass tool_calls as part of LLMParams.
- The tool_calls shall be same as the reactTools (ToolAcceptingToolCall) list.
-}
createReActAgent ::
  -- | The language model
  llm ->
  -- | The language model parameters
  Maybe (LLMParams llm) ->
  -- | The tools available to the agent
  [ToolAcceptingToolCall] ->
  -- | The ReAct agent
  ReActAgent llm
createReActAgent llm mbLlmParams tools =
  ReActAgent
    { reactLLM = llm
    , reactLLMParams = mbLlmParams
    , reactSystemPrompt = reActSystemPrompt
    , reactMaxThinkingSteps = 3
    , reactTools = tools
    }

-- | Create a ReAct agent with a custom system prompt.
createReActAgentWithPrompt ::
  -- | The language model
  llm ->
  -- | The language model parameters
  Maybe (LLMParams llm) ->
  -- | The tools available to the agent
  [ToolAcceptingToolCall] ->
  -- | The custom system prompt
  Text ->
  -- | The ReAct agent
  ReActAgent llm
createReActAgentWithPrompt llm mbLlmParams tools prompt =
  ReActAgent
    { reactLLM = llm
    , reactLLMParams = mbLlmParams
    , reactSystemPrompt = prompt
    , reactMaxThinkingSteps = 3
    , reactTools = tools
    }

-- | Default system prompt for the ReAct agent.
reActSystemPrompt :: Text
reActSystemPrompt =
  "You are a helpful AI assistant that uses tools to answer user questions."

instance LLM llm => Agent (ReActAgent llm) where
  plan agent state = do
    let llm = reactLLM agent
        mbParams = reactLLMParams agent
    -- Get messages from memory - use case to handle existential type
    case agentMemory state of
      SomeMemory mem -> runExceptT $ do
        msgs <- ExceptT $ messages mem
        respMsg <- ExceptT $ chat llm msgs mbParams
        case toolCalls (messageData respMsg) of
          Nothing -> do
            -- No tool calls requested. Assume content as the final result
            pure $
              Done $
                AgentFinish
                  { agentOutput = content respMsg
                  , finishMetadata = Map.empty -- TODO: Add stuff from state
                  , finishLog = content respMsg
                  }
          Just toolCallList -> do
            pure $
              Continue
                AgentAction
                  { actionToolCall = toolCallList
                  , actionLog = content respMsg
                  , actionMetadata = Map.empty -- TODO: what to add here?
                  }

  getTools = reactTools

  executeTool agent toolCall = do
    let tools = getTools agent
    let inputFunctionName = toolFunctionName (toolCallFunction toolCall)
    case find (\(ToolAcceptingToolCall t) -> toolName t == inputFunctionName) tools of
      Nothing ->
        pure $
          Left $
            Error.fromString $
              "Cannot find tool with name: "
                <> T.unpack inputFunctionName
      Just (ToolAcceptingToolCall selectedTool) -> Right <$> runTool selectedTool toolCall

  initialize agent state = do
    let sysPrompt = reactSystemPrompt agent
        userInput = agentInput state
        sysMsg = defaultMessage {role = System, content = sysPrompt}
        userMsg = defaultMessage {role = User, content = userInput}
    case agentMemory state of
      SomeMemory mem -> runExceptT $ do
        memWithSys <- ExceptT $ addMessage mem sysMsg
        memWithUser <- ExceptT $ addMessage memWithSys userMsg
        pure
          AgentState
            { agentMemory = SomeMemory memWithUser
            , agentInput = userInput
            , agentIterations = 0
            }