Source code for langchain.agents.agent_iterator

from __future__ import annotations

import asyncio
import logging
import time
from typing import (
    TYPE_CHECKING,
    Any,
    AsyncIterator,
    Dict,
    Iterator,
    List,
    Optional,
    Tuple,
    Union,
)
from uuid import UUID

from langchain_core.agents import (
    AgentAction,
    AgentFinish,
    AgentStep,
)
from langchain_core.callbacks import (
    AsyncCallbackManager,
    AsyncCallbackManagerForChainRun,
    CallbackManager,
    CallbackManagerForChainRun,
    Callbacks,
)
from langchain_core.load.dump import dumpd
from langchain_core.outputs import RunInfo
from langchain_core.runnables.utils import AddableDict
from langchain_core.tools import BaseTool
from langchain_core.utils.input import get_color_mapping

from langchain.schema import RUN_KEY
from langchain.utilities.asyncio import asyncio_timeout

if TYPE_CHECKING:
    from langchain.agents.agent import AgentExecutor, NextStepOutput

logger = logging.getLogger(__name__)


[docs]class AgentExecutorIterator: """Iterator for AgentExecutor."""
[docs] def __init__( self, agent_executor: AgentExecutor, inputs: Any, callbacks: Callbacks = None, *, tags: Optional[list[str]] = None, metadata: Optional[Dict[str, Any]] = None, run_name: Optional[str] = None, run_id: Optional[UUID] = None, include_run_info: bool = False, yield_actions: bool = False, ): """ Initialize the AgentExecutorIterator with the given AgentExecutor, inputs, and optional callbacks. Args: agent_executor (AgentExecutor): The AgentExecutor to iterate over. inputs (Any): The inputs to the AgentExecutor. callbacks (Callbacks, optional): The callbacks to use during iteration. Defaults to None. tags (Optional[list[str]], optional): The tags to use during iteration. Defaults to None. metadata (Optional[Dict[str, Any]], optional): The metadata to use during iteration. Defaults to None. run_name (Optional[str], optional): The name of the run. Defaults to None. run_id (Optional[UUID], optional): The ID of the run. Defaults to None. include_run_info (bool, optional): Whether to include run info in the output. Defaults to False. yield_actions (bool, optional): Whether to yield actions as they are generated. Defaults to False. """ self._agent_executor = agent_executor self.inputs = inputs self.callbacks = callbacks self.tags = tags self.metadata = metadata self.run_name = run_name self.run_id = run_id self.include_run_info = include_run_info self.yield_actions = yield_actions self.reset()
_inputs: Dict[str, str] callbacks: Callbacks tags: Optional[list[str]] metadata: Optional[Dict[str, Any]] run_name: Optional[str] run_id: Optional[UUID] include_run_info: bool yield_actions: bool @property def inputs(self) -> Dict[str, str]: """The inputs to the AgentExecutor.""" return self._inputs @inputs.setter def inputs(self, inputs: Any) -> None: self._inputs = self.agent_executor.prep_inputs(inputs) @property def agent_executor(self) -> AgentExecutor: """The AgentExecutor to iterate over.""" return self._agent_executor @agent_executor.setter def agent_executor(self, agent_executor: AgentExecutor) -> None: self._agent_executor = agent_executor # force re-prep inputs in case agent_executor's prep_inputs fn changed self.inputs = self.inputs @property def name_to_tool_map(self) -> Dict[str, BaseTool]: """A mapping of tool names to tools.""" return {tool.name: tool for tool in self.agent_executor.tools} @property def color_mapping(self) -> Dict[str, str]: """A mapping of tool names to colors.""" return get_color_mapping( [tool.name for tool in self.agent_executor.tools], excluded_colors=["green", "red"], )
[docs] def reset(self) -> None: """ Reset the iterator to its initial state, clearing intermediate steps, iterations, and time elapsed. """ logger.debug("(Re)setting AgentExecutorIterator to fresh state") self.intermediate_steps: list[tuple[AgentAction, str]] = [] self.iterations = 0 # maybe better to start these on the first __anext__ call? self.time_elapsed = 0.0 self.start_time = time.time()
[docs] def update_iterations(self) -> None: """ Increment the number of iterations and update the time elapsed. """ self.iterations += 1 self.time_elapsed = time.time() - self.start_time logger.debug( f"Agent Iterations: {self.iterations} ({self.time_elapsed:.2f}s elapsed)" )
[docs] def make_final_outputs( self, outputs: Dict[str, Any], run_manager: Union[CallbackManagerForChainRun, AsyncCallbackManagerForChainRun], ) -> AddableDict: # have access to intermediate steps by design in iterator, # so return only outputs may as well always be true. prepared_outputs = AddableDict( self.agent_executor.prep_outputs( self.inputs, outputs, return_only_outputs=True ) ) if self.include_run_info: prepared_outputs[RUN_KEY] = RunInfo(run_id=run_manager.run_id) return prepared_outputs
def __iter__(self: "AgentExecutorIterator") -> Iterator[AddableDict]: logger.debug("Initialising AgentExecutorIterator") self.reset() callback_manager = CallbackManager.configure( self.callbacks, self.agent_executor.callbacks, self.agent_executor.verbose, self.tags, self.agent_executor.tags, self.metadata, self.agent_executor.metadata, ) run_manager = callback_manager.on_chain_start( dumpd(self.agent_executor), self.inputs, self.run_id, name=self.run_name, ) try: while self.agent_executor._should_continue( self.iterations, self.time_elapsed ): # take the next step: this plans next action, executes it, # yielding action and observation as they are generated next_step_seq: NextStepOutput = [] for chunk in self.agent_executor._iter_next_step( self.name_to_tool_map, self.color_mapping, self.inputs, self.intermediate_steps, run_manager, ): next_step_seq.append(chunk) # if we're yielding actions, yield them as they come # do not yield AgentFinish, which will be handled below if self.yield_actions: if isinstance(chunk, AgentAction): yield AddableDict(actions=[chunk], messages=chunk.messages) elif isinstance(chunk, AgentStep): yield AddableDict(steps=[chunk], messages=chunk.messages) # convert iterator output to format handled by _process_next_step_output next_step = self.agent_executor._consume_next_step(next_step_seq) # update iterations and time elapsed self.update_iterations() # decide if this is the final output output = self._process_next_step_output(next_step, run_manager) is_final = "intermediate_step" not in output # yield the final output always # for backwards compat, yield int. output if not yielding actions if not self.yield_actions or is_final: yield output # if final output reached, stop iteration if is_final: return except BaseException as e: run_manager.on_chain_error(e) raise # if we got here means we exhausted iterations or time yield self._stop(run_manager) async def __aiter__(self) -> AsyncIterator[AddableDict]: """ N.B. __aiter__ must be a normal method, so need to initialize async run manager on first __anext__ call where we can await it """ logger.debug("Initialising AgentExecutorIterator (async)") self.reset() callback_manager = AsyncCallbackManager.configure( self.callbacks, self.agent_executor.callbacks, self.agent_executor.verbose, self.tags, self.agent_executor.tags, self.metadata, self.agent_executor.metadata, ) run_manager = await callback_manager.on_chain_start( dumpd(self.agent_executor), self.inputs, self.run_id, name=self.run_name, ) try: async with asyncio_timeout(self.agent_executor.max_execution_time): while self.agent_executor._should_continue( self.iterations, self.time_elapsed ): # take the next step: this plans next action, executes it, # yielding action and observation as they are generated next_step_seq: NextStepOutput = [] async for chunk in self.agent_executor._aiter_next_step( self.name_to_tool_map, self.color_mapping, self.inputs, self.intermediate_steps, run_manager, ): next_step_seq.append(chunk) # if we're yielding actions, yield them as they come # do not yield AgentFinish, which will be handled below if self.yield_actions: if isinstance(chunk, AgentAction): yield AddableDict( actions=[chunk], messages=chunk.messages ) elif isinstance(chunk, AgentStep): yield AddableDict( steps=[chunk], messages=chunk.messages ) # convert iterator output to format handled by _process_next_step next_step = self.agent_executor._consume_next_step(next_step_seq) # update iterations and time elapsed self.update_iterations() # decide if this is the final output output = await self._aprocess_next_step_output( next_step, run_manager ) is_final = "intermediate_step" not in output # yield the final output always # for backwards compat, yield int. output if not yielding actions if not self.yield_actions or is_final: yield output # if final output reached, stop iteration if is_final: return except (TimeoutError, asyncio.TimeoutError): yield await self._astop(run_manager) return except BaseException as e: await run_manager.on_chain_error(e) raise # if we got here means we exhausted iterations or time yield await self._astop(run_manager) def _process_next_step_output( self, next_step_output: Union[AgentFinish, List[Tuple[AgentAction, str]]], run_manager: CallbackManagerForChainRun, ) -> AddableDict: """ Process the output of the next step, handling AgentFinish and tool return cases. """ logger.debug("Processing output of Agent loop step") if isinstance(next_step_output, AgentFinish): logger.debug( "Hit AgentFinish: _return -> on_chain_end -> run final output logic" ) return self._return(next_step_output, run_manager=run_manager) self.intermediate_steps.extend(next_step_output) logger.debug("Updated intermediate_steps with step output") # Check for tool return if len(next_step_output) == 1: next_step_action = next_step_output[0] tool_return = self.agent_executor._get_tool_return(next_step_action) if tool_return is not None: return self._return(tool_return, run_manager=run_manager) return AddableDict(intermediate_step=next_step_output) async def _aprocess_next_step_output( self, next_step_output: Union[AgentFinish, List[Tuple[AgentAction, str]]], run_manager: AsyncCallbackManagerForChainRun, ) -> AddableDict: """ Process the output of the next async step, handling AgentFinish and tool return cases. """ logger.debug("Processing output of async Agent loop step") if isinstance(next_step_output, AgentFinish): logger.debug( "Hit AgentFinish: _areturn -> on_chain_end -> run final output logic" ) return await self._areturn(next_step_output, run_manager=run_manager) self.intermediate_steps.extend(next_step_output) logger.debug("Updated intermediate_steps with step output") # Check for tool return if len(next_step_output) == 1: next_step_action = next_step_output[0] tool_return = self.agent_executor._get_tool_return(next_step_action) if tool_return is not None: return await self._areturn(tool_return, run_manager=run_manager) return AddableDict(intermediate_step=next_step_output) def _stop(self, run_manager: CallbackManagerForChainRun) -> AddableDict: """ Stop the iterator and raise a StopIteration exception with the stopped response. """ logger.warning("Stopping agent prematurely due to triggering stop condition") # this manually constructs agent finish with output key output = self.agent_executor._action_agent.return_stopped_response( self.agent_executor.early_stopping_method, self.intermediate_steps, **self.inputs, ) return self._return(output, run_manager=run_manager) async def _astop(self, run_manager: AsyncCallbackManagerForChainRun) -> AddableDict: """ Stop the async iterator and raise a StopAsyncIteration exception with the stopped response. """ logger.warning("Stopping agent prematurely due to triggering stop condition") output = self.agent_executor._action_agent.return_stopped_response( self.agent_executor.early_stopping_method, self.intermediate_steps, **self.inputs, ) return await self._areturn(output, run_manager=run_manager) def _return( self, output: AgentFinish, run_manager: CallbackManagerForChainRun ) -> AddableDict: """ Return the final output of the iterator. """ returned_output = self.agent_executor._return( output, self.intermediate_steps, run_manager=run_manager ) returned_output["messages"] = output.messages run_manager.on_chain_end(returned_output) return self.make_final_outputs(returned_output, run_manager) async def _areturn( self, output: AgentFinish, run_manager: AsyncCallbackManagerForChainRun ) -> AddableDict: """ Return the final output of the async iterator. """ returned_output = await self.agent_executor._areturn( output, self.intermediate_steps, run_manager=run_manager ) returned_output["messages"] = output.messages await run_manager.on_chain_end(returned_output) return self.make_final_outputs(returned_output, run_manager)