packages feed

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

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}

{- |
Module      : Langchain.Agent.Core
Description : Core types and abstractions for LangChain agents
Copyright   : (c) 2025 Tushar Adhatrao
License     : MIT
Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
Stability   : experimental

This module provides the foundational types and typeclasses for building agents
in langchain-hs. An LLM Agent runs tools in a loop to achieve a goal.
An agent runs until a stop condition is met -
when the model emits a final output or an iteration limit is reached.
-}
module Langchain.Agent.Core
  ( -- * Agent Typeclass
    Agent (..)

    -- * Agent Actions and Results
  , AgentAction (..)
  , AgentFinish (..)
  , AgentStep (..)
  , PlanResult (..)

    -- * Agent State and Configuration
  , AgentState (..)
  , AgentConfig (..)
  , AgentCallbacks (..)
  , defaultAgentConfig
  , defaultAgentCallbacks

    -- * Tool support
  , ToolAcceptingToolCall (..)

    -- * Memory support
  , SomeMemory (..)
  ) where

import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Aeson
import Data.Map.Strict (Map)
import Data.Text (Text)
import Data.Time (UTCTime)
import GHC.Generics (Generic)
import Langchain.Error (LangchainResult)
import Langchain.LLM.Core (ToolCall)
import Langchain.Memory.Core (BaseMemory)
import Langchain.Tool.Core

-- | Represents an action (ToolCall) that an agent has decided to take.
data AgentAction = AgentAction
  { actionToolCall :: [ToolCall]
  -- ^ tool call
  , actionLog :: Text
  -- ^ LLM's response while suggesting the tool call
  , actionMetadata :: Map Text Text
  -- ^ Additional metadata about the action
  }
  deriving (Show, Eq)

-- | Represents the final result when an agent completes its task.
data AgentFinish = AgentFinish
  { agentOutput :: Text
  -- ^ The final answer or result
  , finishMetadata :: Map Text Text
  -- ^ Additional information about the execution
  , finishLog :: Text
  -- ^ Final thoughts or reasoning
  }
  deriving (Show, Eq, Generic, ToJSON, FromJSON)

-- | Represents one step in the agent's execution.
data AgentStep = AgentStep
  { stepAction :: AgentAction
  -- ^ The action that was executed
  , stepObservation :: Text
  -- ^ The result/observation from the executed tool call
  , stepTimestamp :: UTCTime
  -- ^ When this step occurred
  }
  deriving (Show, Eq)

{- |
A SomeMemory is a wrapper around any type that implements BaseMemory.

> data MyMemory = MyMemory { ... }
> instance BaseMemory MyMemory where ...
>
> let memory = MyMemory { ... }
> let someMemory = SomeMemory memory
>
> let msg = defaultMessage { role = System, content = "You are an AI assistant" }
> let someMemory2 = SomeMemory (WindowBufferMemory 5 (NE.fromList [msg]))
-}
data SomeMemory where
  SomeMemory ::
    (BaseMemory m) =>
    m ->
    SomeMemory

instance Show SomeMemory where
  show (SomeMemory _) = "SomeMemory { <memory instance> }"

{- | Current state of the agent during execution.

Tracks:
- Memory instance for managing chat history
- Current input being processed
- Number of iterations so far
-}
data AgentState = AgentState
  { agentMemory :: SomeMemory
  -- ^ Memory instance for managing chat history with the LLM
  , agentInput :: Text
  -- ^ Current user input/query
  , agentIterations :: Int
  -- ^ Number of iterations so far
  }

instance Show AgentState where
  show (AgentState mem inp iters) =
    "AgentState { agentMemory = "
      ++ show mem
      ++ ", agentInput = "
      ++ show inp
      ++ ", agentIterations = "
      ++ show iters
      ++ " }"

data AgentConfig = AgentConfig
  { maxIterations :: Int
  -- ^ Maximum number of agent steps (default: 15)
  , maxExecutionTime :: Maybe Int
  -- ^ Maximum execution time in seconds (Nothing = no limit)
  , verboseLogging :: Bool
  -- ^ Enable verbose logging (default: False)
  , stateMemory :: Maybe SomeMemory
  {- ^ Configure type of Chat memory you want use.
  ^ (default: windowBufferMessages with 100 window size)
  -}
  }
  deriving (Show)

{- | Callbacks for agent events.
Allows hooking into various points in the agent lifecycle.
-}
data AgentCallbacks = AgentCallbacks
  { onAgentStart :: Text -> IO ()
  -- ^ Called when agent starts with the input
  , onAgentAction :: AgentAction -> IO ()
  -- ^ Called before executing an action
  , onAgentObservation :: Text -> IO ()
  -- ^ Called after receiving an observation / result of the tool call
  , onAgentFinish :: AgentFinish -> IO ()
  -- ^ Called when agent completes
  , onAgentStep :: AgentStep -> IO ()
  -- ^ Called after each complete step
  }

{- |
A ToolAcceptingToolCall is a special type of tool that
can be used by an agent to execute a tool call.

It is a wrapper around a tool type whose input is a ToolCall and output is a Text.
It is user's responsibility wrap your existing tool into this type.

Example:

> data AgeFinderTool = AgeFinderTool
> instance Tool AgeFinderTool where
>   type Input AgeFinderTool = ToolCall
>   type Output AgeFinderTool = Text
>   toolName _ = "age_finder"
>   toolDescription _ = "Finds the age of a person given their name."
>   runTool _ (ToolCall _ _ ToolFunction {..}) = do
>     if toolFunctionName == "age_finder"
>       then do
>         case HM.lookup "name" toolFunctionArguments of
>           Nothing -> pure "Unknown"
>           Just (String name_) -> pure $ getAge name_
>           _ -> pure "Unknown"
>       else pure "Unknown"
>
>   getAge name_ = case name_ of
>     "Alice" -> "30"
>     "Bob" -> "25"
>     _ -> "Unknown"
-}
data ToolAcceptingToolCall where
  ToolAcceptingToolCall ::
    ( Tool t
    , Input t ~ ToolCall
    , Output t ~ Text
    ) =>
    t -> ToolAcceptingToolCall

instance Eq ToolAcceptingToolCall where
  (ToolAcceptingToolCall t1) == (ToolAcceptingToolCall t2) = toolName t1 == toolName t2

instance Show ToolAcceptingToolCall where
  show (ToolAcceptingToolCall t) =
    "ToolAcceptingToolCall { name = " ++ show (toolName t) ++ " }"

data PlanResult = Continue AgentAction | Done AgentFinish
  deriving (Eq, Show)

{- | Core Agent typeclass.

An agent is a system that can plan and execute actions to accomplish a task.
Different agent types (ReAct, Plan-and-Execute, etc.) implement this interface.
-}
class Agent a where
  {- | Plan the next action or finish.

  Given the current state, decide:
  - What tool call to make next (Left AgentAction), or
  - That the task is complete and return the final result (Right AgentFinish)
  -}
  plan ::
    a ->
    AgentState ->
    IO (LangchainResult PlanResult)

  -- | Get the tools available to this agent.
  getTools :: a -> [ToolAcceptingToolCall]

  -- | Execute a tool.
  executeTool :: a -> ToolCall -> IO (LangchainResult Text)

  {- | Prepare the agent for execution.
  Initialize any necessary state before starting.
  Default implementation does nothing.
  -}
  initialize :: a -> AgentState -> IO (LangchainResult AgentState)
  initialize _ state = pure $ Right state

  {- | Clean up after agent execution.
  Release resources, save state, etc.
  Default implementation does nothing.
  -}
  finalize :: a -> AgentState -> IO ()
  finalize _ _ = pure ()

  -- | MonadIO version of plan
  planM ::
    MonadIO m =>
    a ->
    AgentState ->
    m (LangchainResult PlanResult)
  planM agent state = liftIO $ plan agent state

  -- | MonadIO version of executeTool
  executeToolM :: MonadIO m => a -> ToolCall -> m (LangchainResult Text)
  executeToolM a i = liftIO $ executeTool a i

  -- | MonadIO version of initialize
  initializeM ::
    MonadIO m =>
    a ->
    AgentState ->
    m (LangchainResult AgentState)
  initializeM agent state = liftIO $ initialize agent state

  -- | MonadIO version of finalize
  finalizeM :: MonadIO m => a -> AgentState -> m ()
  finalizeM agent state = liftIO $ finalize agent state

{- | Default agent configuration.

Sensible defaults:
- 15 max iterations
- No time limit
- No verbose logging
- WindowBufferMemory with window size 100
-}
defaultAgentConfig :: AgentConfig
defaultAgentConfig =
  AgentConfig
    { maxIterations = 15
    , maxExecutionTime = Nothing
    , verboseLogging = False
    , stateMemory = Nothing
    }

{- | Default agent callbacks (all no-ops).
Useful as a starting point for custom callbacks.
-}
defaultAgentCallbacks :: AgentCallbacks
defaultAgentCallbacks =
  AgentCallbacks
    { onAgentStart = \_ -> pure ()
    , onAgentAction = \_ -> pure ()
    , onAgentObservation = \_ -> pure ()
    , onAgentFinish = \_ -> pure ()
    , onAgentStep = \_ -> pure ()
    }