packages feed

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

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

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

Modernized ReAct agent operating over ChatModel, Tool m, and multi-modal Message history.
Uses 'ToolBinder' to pass tool definitions to the LLM provider in a provider-agnostic way.
-}
module Langchain.Agent.ReAct
  ( AgentStep (..)
  , ReActAgent (..)
  , createReActAgent
  , reactStep
  , runReActAgent
  ) where

import Control.Monad (forM)
import Control.Monad.Except (MonadError, throwError)
import Control.Monad.IO.Class (MonadIO)
import Data.List (find)

import Langchain.Core.Error (LangchainError, agentError, errorMessage)
import Langchain.Core.Model
import qualified Langchain.Core.Model.Types as M
import Langchain.Core.Tool
import Langchain.Tool.Binding (ToolBinder (..))

-- | Step result of ReAct reasoning iteration
data AgentStep
  = AgentAction Message [ToolCall]
  | AgentFinish Message
  deriving (Eq, Show)

-- | Effect-polymorphic ReAct Agent configuration
data ReActAgent model m = ReActAgent
  { agentModel :: model
  , agentTools :: [Tool m]
  , agentMaxIterations :: Int
  }

-- | Construct a ReAct Agent instance
createReActAgent :: model -> [Tool m] -> ReActAgent model m
createReActAgent model tools =
  ReActAgent
    { agentModel = model
    , agentTools = tools
    , agentMaxIterations = 10
    }

-- | Run a single step of ReAct reasoning using ChatModel
reactStep ::
  forall model m.
  (ToolBinder model m, MonadIO m, MonadError LangchainError m) =>
  model ->
  [Tool m] ->
  [Message] ->
  m AgentStep
reactStep model tools history = do
  let cfg = bindToolsConfig @model tools Nothing
  responseMsg <- invoke model history cfg
  case messageToolCalls responseMsg of
    Just tcs@(_ : _) -> pure $ AgentAction responseMsg tcs
    _ -> pure $ AgentFinish responseMsg

-- | Execute the full ReAct reasoning loop until AgentFinish or max iterations reached
runReActAgent ::
  (ToolBinder model m, MonadIO m, MonadError LangchainError m) =>
  ReActAgent model m ->
  [Message] ->
  m Message
runReActAgent agent initialHistory = go initialHistory (agentMaxIterations agent)
  where
    go history maxIter
      | maxIter <= 0 = throwError $ agentError "ReAct Agent exceeded maximum iterations" Nothing Nothing
      | otherwise = do
          step <- reactStep (agentModel agent) (agentTools agent) history
          case step of
            AgentFinish finalMsg -> pure finalMsg
            AgentAction respMsg tcs -> do
              obsMsgs <- forM tcs $ \tc -> do
                let tName = toolCallName tc
                outTxt <- case find (\t -> toolName t == tName) (agentTools agent) of
                  Nothing ->
                    pure $ "Error: Tool not found: " <> tName
                  Just tool -> do
                    eOut <- toolExecute tool (toolCallArguments tc)
                    case eOut of
                      Left err ->
                        pure $ "Error executing tool " <> tName <> ": " <> errorMessage err
                      Right out ->
                        pure out
                pure $
                  (textMessage M.Tool outTxt)
                    { M.messageName = Just tName
                    , M.messageToolId = Just (toolCallId tc)
                    }
              let newHistory = history ++ [respMsg] ++ obsMsgs
              go newHistory (maxIter - 1)