packages feed

langchain-hs-0.0.1.0: src/Langchain/Agents/Core.hs

{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeFamilies #-}

{- |
Module      : Langchain.Agents.Core
Description : Core implementation of LangChain agents
Copyright   : (c) 2025 Tushar Adhatrao
License     : MIT
Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>

Agents use LLMs as reasoning engines to determine actions dynamically
This module implements the core agent execution loop and interfaces,
supporting tool interaction and memory management.

Example agent execution flow:

> executor <- AgentExecutor
>   { executor = myAgent
>   , executorMemory = emptyMemory
>   , maxIterations = 5
>   , returnIntermediateSteps = True
>   }
> result <- runAgentExecutor executor "Explain quantum computing"
-}
module Langchain.Agents.Core
  ( AgentAction (..)
  , AgentFinish (..)
  , AgentStep (..)
  , Agent (..)
  , AnyTool (..)
  , AgentState (..)
  , AgentExecutor (..)
  , runAgent
  , runAgentLoop
  , runAgentExecutor
  , executeTool
  , runSingleStep
  , customAnyTool
  ) where

import Control.Exception (SomeException, try)
import Data.List (find)
import qualified Data.Map.Strict as Map
import Data.Text (Text)
import qualified Data.Text as T
import Langchain.LLM.Core (Message (Message), Role (..), defaultMessageData)
import Langchain.Memory.Core (BaseMemory (..))
import Langchain.PromptTemplate (PromptTemplate)
import qualified Langchain.Runnable.Core as Run
import Langchain.Tool.Core (Tool (..))

{- |
Represents an action to be taken by the agent
-}
data AgentAction = AgentAction
  { actionToolName :: Text
  -- ^ Tool name
  , actionInput :: Text
  -- ^ Input
  , actionLog :: Text
  -- ^ Execution log
  }
  deriving (Eq, Show)

-- | Represents that agent has finished work with final value
data AgentFinish = AgentFinish
  { returnValues :: Map.Map Text Text 
  , finishLog :: Text
  }
  deriving (Show, Eq)

-- | Type that will be return from LLM 
-- Could be either Continue, making another call to LLM or Finish with final value
data AgentStep
  = Continue AgentAction
  | Finish AgentFinish
  deriving (Eq, Show)

-- | Type for maintaining state of the agent 
data (BaseMemory m) => AgentState m = AgentState
  { agentMemory :: m -- ^ Memory for storing chat history
  , agentToolResults :: [(Text, Text)] -- ^ Tool results
  , agentSteps :: [AgentAction] -- ^ Agent steps happened so far
  }
  deriving (Eq, Show)

{- |
Dynamic tool wrapper allowing heterogeneous tool collections
Converts between Text and tool-specific input/output types.

Example usage:

> calculatorTool :: AnyTool
> calculatorTool = customAnyTool
>   Calculator
>   (\t -> read (T.unpack t) :: (Int, Int))
>   (T.pack . show)
-}
data AnyTool = forall a. Tool a => AnyTool
  { anyTool :: a
  , textToInput :: Text -> Input a
  , outputToText :: Output a -> Text
  }

{- |
Core agent class defining required operations

* Plan next action based on state
* Provide prompt template
* Expose available tools
-}
class Agent a where
  planNextAction :: BaseMemory m => a -> AgentState m -> IO (Either String AgentStep)
  agentPrompt :: a -> IO PromptTemplate
  agentTools :: a -> IO [AnyTool]

{- |
Agent execution engine
-}
data AgentExecutor a m = AgentExecutor
  { executor :: a -- Agent instance
  , executorMemory :: m
  -- ^ Memory state
  , maxIterations :: Int
  -- ^ Iteration limits
  , returnIntermediateSteps :: Bool
  -- ^ Step tracking
  }
  deriving (Eq, Show)

{- |
Run the full agent execution loop
Handles:

1. Memory updates
2. Action planning
3. Tool execution
4. Iteration control

Example flow:

1. User input -> memory
2. Plan action -> execute tool
3. Store result -> memory
4. Repeat until finish

Throws errors for:

- Tool not found [[5]]
- Execution errors
- Iteration limits
-}
runAgent :: (Agent a, BaseMemory m) => a -> AgentState m -> Text -> IO (Either String AgentFinish)
runAgent agent initialState@AgentState {..} initialInput = do
  memWithInput <- addUserMessage agentMemory initialInput
  case memWithInput of
    Left err -> return $ Left err
    Right updatedMem ->
      let newState = initialState {agentMemory = updatedMem}
       in runAgentLoop agent newState 0 10

-- | Helper function for runAgent
runAgentLoop ::
  (Agent a, BaseMemory m) => a -> AgentState m -> Int -> Int -> IO (Either String AgentFinish)
runAgentLoop agent agentState@AgentState {..} currIter maxIter
  | currIter > maxIter = return $ Left "Max iterations excedded"
  | otherwise = do
      eStepResult <- runSingleStep agent agentState
      case eStepResult of
        Left err -> return $ Left err
        Right (Finish agentFinish) -> return $ Right agentFinish
        Right (Continue act@AgentAction {..}) -> do
          toolList <- agentTools agent
          toolResult <- executeTool toolList actionToolName actionInput
          case toolResult of
            Left err -> return $ Left err
            Right result -> do
              -- Add the tool result to memory as a tool message
              let toolMsg = Message Tool result defaultMessageData
              updatedMemResult <- addMessage agentMemory toolMsg
              case updatedMemResult of
                Left err -> return $ Left err
                Right updatedMem ->
                  let updatedState =
                        agentState
                          { agentMemory = updatedMem
                          , agentToolResults = agentToolResults ++ [(actionToolName, result)]
                          , agentSteps = agentSteps ++ [act]
                          }
                   in runAgentLoop agent updatedState (currIter + 1) maxIter

-- | Alias for planNextAction
runSingleStep :: (Agent a, BaseMemory m) => a -> AgentState m -> IO (Either String AgentStep)
runSingleStep = planNextAction

{- |
Execute a single tool call
Handles tool lookup and input/output conversion.

Example:

> tools = [calculatorTool, wikipediaTool]
> executeTool tools "calculator" "(5, 3)"
> -- Returns Right "8"
-}
executeTool :: [AnyTool] -> Text -> Text -> IO (Either String Text)
executeTool tools toolName_ input =
  case find (\(AnyTool t _ _) -> toolName t == toolName_) tools of
    Nothing -> return $ Left $ "Tool not found: " <> T.unpack toolName_
    Just (AnyTool {..}) -> do
      resultE <- try $ do
        let typedInput = textToInput input
        result <- runTool anyTool typedInput
        return $ outputToText result
      case resultE of
        Left ex -> return $ Left $ "Tool execution error: " <> show (ex :: SomeException)
        Right output -> return $ Right output

{- |
Helper for creating custom tool wrappers
Requires conversion functions between Text and tool-specific types.

Example:

> weatherTool = customAnyTool
>   WeatherAPI
>   parseLocation
>   formatWeatherResponse
-}
customAnyTool :: Tool a => a -> (Text -> Input a) -> (Output a -> Text) -> AnyTool
customAnyTool tool inputConv outputConv = AnyTool tool inputConv outputConv

-- | Similar to runAgent, but for AgentExecutor
runAgentExecutor ::
  (Agent a, BaseMemory m) => AgentExecutor a m -> Text -> IO (Either String (Maybe AgentFinish))
runAgentExecutor AgentExecutor {..} input = do
  let initialState =
        AgentState
          { agentMemory = executorMemory
          , agentToolResults = []
          , agentSteps = []
          }
  result <- runAgent executor initialState input
  case result of
    Left err -> return $ Left err
    Right a ->
      if returnIntermediateSteps
        then return $ Right $ Just a
        else return $ Right Nothing

{- |
Runnable instance for agent execution
Allows integration with LangChain workflows.

Example:

> response <- invoke myAgentExecutor "Solve 5+3"
> case response of
>   Right result -> print result
>   Left err -> print err
-}
instance (Agent a, BaseMemory m) => Run.Runnable (AgentExecutor a m) where
  type RunnableInput (AgentExecutor a m) = Text
  type RunnableOutput (AgentExecutor a m) = AgentFinish

  invoke AgentExecutor {..} input = do
    let initialState =
          AgentState
            { agentMemory = executorMemory
            , agentToolResults = []
            , agentSteps = []
            }
    runAgent executor initialState input