codekingpro/portable-devtools
115k
1import inspect2import warnings3from collections.abc import Awaitable, Callable, Sequence4from typing import (5 Annotated,6 Any,7 Literal,8 TypeVar,9 cast,10 get_type_hints,11)12 13from langchain_core.language_models import (14 BaseChatModel,15 LanguageModelInput,16 LanguageModelLike,17)18from langchain_core.messages import (19 AIMessage,20 AnyMessage,21 BaseMessage,22 SystemMessage,23 ToolMessage,24)25from langchain_core.runnables import (26 Runnable,27 RunnableBinding,28 RunnableConfig,29 RunnableSequence,30)31from langchain_core.tools import BaseTool32from langgraph._internal._runnable import RunnableCallable, RunnableLike33from langgraph._internal._typing import MISSING34from langgraph.errors import ErrorCode, create_error_message35from langgraph.graph import END, StateGraph36from langgraph.graph.message import add_messages37from langgraph.graph.state import CompiledStateGraph38from langgraph.managed import RemainingSteps39from langgraph.runtime import Runtime40from langgraph.store.base import BaseStore41from langgraph.types import Checkpointer, Send42from langgraph.typing import ContextT43from langgraph.warnings import LangGraphDeprecatedSinceV1044from pydantic import BaseModel45from typing_extensions import NotRequired, TypedDict, deprecated46 47from langgraph.prebuilt.tool_node import ToolCallWithContext, ToolNode48 49StructuredResponse = dict | BaseModel50StructuredResponseSchema = dict | type[BaseModel]51 52 53@deprecated(54 "AgentState has been moved to `langchain.agents`. Please update your import to `from langchain.agents import AgentState`.",55 category=LangGraphDeprecatedSinceV10,56)57class AgentState(TypedDict):58 """The state of the agent."""59 60 messages: Annotated[Sequence[BaseMessage], add_messages]61 62 remaining_steps: NotRequired[RemainingSteps]63 64 65@deprecated(66 "AgentStatePydantic has been deprecated in favor of AgentState in `langchain.agents`.",67 category=LangGraphDeprecatedSinceV10,68)69class AgentStatePydantic(BaseModel):70 """The state of the agent."""71 72 messages: Annotated[Sequence[BaseMessage], add_messages]73 74 remaining_steps: RemainingSteps = 2575 76 77with warnings.catch_warnings():78 warnings.filterwarnings(79 "ignore",80 category=LangGraphDeprecatedSinceV10,81 message="AgentState has been moved to `langchain.agents`.*",82 )83 84 @deprecated(85 "AgentStateWithStructuredResponse has been deprecated in favor of AgentState in `langchain.agents`.",86 category=LangGraphDeprecatedSinceV10,87 )88 class AgentStateWithStructuredResponse(AgentState):89 """The state of the agent with a structured response."""90 91 structured_response: StructuredResponse92 93 94with warnings.catch_warnings():95 warnings.filterwarnings(96 "ignore",97 category=LangGraphDeprecatedSinceV10,98 message="AgentStatePydantic has been deprecated in favor of AgentState in `langchain.agents`.",99 )100 101 @deprecated(102 "AgentStateWithStructuredResponsePydantic has been deprecated in favor of AgentState in `langchain.agents`.",103 category=LangGraphDeprecatedSinceV10,104 )105 class AgentStateWithStructuredResponsePydantic(AgentStatePydantic):106 """The state of the agent with a structured response."""107 108 structured_response: StructuredResponse109 110 111with warnings.catch_warnings():112 warnings.filterwarnings(113 "ignore",114 category=LangGraphDeprecatedSinceV10,115 )116 StateSchema = TypeVar("StateSchema", bound=AgentState | AgentStatePydantic)117 StateSchemaType = type[StateSchema]118 119PROMPT_RUNNABLE_NAME = "Prompt"120 121Prompt = (122 SystemMessage123 | str124 | Callable[[StateSchema], LanguageModelInput]125 | Runnable[StateSchema, LanguageModelInput]126)127 128 129def _get_state_value(state: StateSchema, key: str, default: Any = None) -> Any:130 return (131 state.get(key, default)132 if isinstance(state, dict)133 else getattr(state, key, default)134 )135 136 137def _get_prompt_runnable(prompt: Prompt | None) -> Runnable:138 prompt_runnable: Runnable139 if prompt is None:140 prompt_runnable = RunnableCallable(141 lambda state: _get_state_value(state, "messages"), name=PROMPT_RUNNABLE_NAME142 )143 elif isinstance(prompt, str):144 _system_message: BaseMessage = SystemMessage(content=prompt)145 prompt_runnable = RunnableCallable(146 lambda state: [_system_message] + _get_state_value(state, "messages"),147 name=PROMPT_RUNNABLE_NAME,148 )149 elif isinstance(prompt, SystemMessage):150 prompt_runnable = RunnableCallable(151 lambda state: [prompt] + _get_state_value(state, "messages"),152 name=PROMPT_RUNNABLE_NAME,153 )154 elif inspect.iscoroutinefunction(prompt):155 prompt_runnable = RunnableCallable(156 None,157 prompt,158 name=PROMPT_RUNNABLE_NAME,159 )160 elif callable(prompt):161 prompt_runnable = RunnableCallable(162 prompt,163 name=PROMPT_RUNNABLE_NAME,164 )165 elif isinstance(prompt, Runnable):166 prompt_runnable = prompt167 else:168 raise ValueError(f"Got unexpected type for `prompt`: {type(prompt)}")169 170 return prompt_runnable171 172 173def _should_bind_tools(174 model: LanguageModelLike, tools: Sequence[BaseTool], num_builtin: int = 0175) -> bool:176 if isinstance(model, RunnableSequence):177 model = next(178 (179 step180 for step in model.steps181 if isinstance(step, (RunnableBinding, BaseChatModel))182 ),183 model,184 )185 186 if not isinstance(model, RunnableBinding):187 return True188 189 if "tools" not in model.kwargs:190 return True191 192 bound_tools = model.kwargs["tools"]193 if len(tools) != len(bound_tools) - num_builtin:194 raise ValueError(195 "Number of tools in the model.bind_tools() and tools passed to create_react_agent must match"196 f" Got {len(tools)} tools, expected {len(bound_tools) - num_builtin}"197 )198 199 tool_names = set(tool.name for tool in tools)200 bound_tool_names = set()201 for bound_tool in bound_tools:202 # OpenAI-style tool203 if bound_tool.get("type") == "function":204 bound_tool_name = bound_tool["function"]["name"]205 # Anthropic-style tool206 elif bound_tool.get("name"):207 bound_tool_name = bound_tool["name"]208 else:209 # unknown tool type so we'll ignore it210 continue211 212 bound_tool_names.add(bound_tool_name)213 214 if missing_tools := tool_names - bound_tool_names:215 raise ValueError(f"Missing tools '{missing_tools}' in the model.bind_tools()")216 217 return False218 219 220def _get_model(model: LanguageModelLike) -> BaseChatModel:221 """Get the underlying model from a RunnableBinding or return the model itself."""222 if isinstance(model, RunnableSequence):223 model = next(224 (225 step226 for step in model.steps227 if isinstance(step, (RunnableBinding, BaseChatModel))228 ),229 model,230 )231 232 if isinstance(model, RunnableBinding):233 model = model.bound234 235 if not isinstance(model, BaseChatModel):236 raise TypeError(237 f"Expected `model` to be a ChatModel or RunnableBinding (e.g. model.bind_tools(...)), got {type(model)}"238 )239 240 return model241 242 243def _validate_chat_history(244 messages: Sequence[BaseMessage],245) -> None:246 """Validate that all tool calls in AIMessages have a corresponding ToolMessage."""247 all_tool_calls = [248 tool_call249 for message in messages250 if isinstance(message, AIMessage)251 for tool_call in message.tool_calls252 ]253 tool_call_ids_with_results = {254 message.tool_call_id for message in messages if isinstance(message, ToolMessage)255 }256 tool_calls_without_results = [257 tool_call258 for tool_call in all_tool_calls259 if tool_call["id"] not in tool_call_ids_with_results260 ]261 if not tool_calls_without_results:262 return263 264 error_message = create_error_message(265 message="Found AIMessages with tool_calls that do not have a corresponding ToolMessage. "266 f"Here are the first few of those tool calls: {tool_calls_without_results[:3]}.\n\n"267 "Every tool call (LLM requesting to call a tool) in the message history MUST have a corresponding ToolMessage "268 "(result of a tool invocation to return to the LLM) - this is required by most LLM providers.",269 error_code=ErrorCode.INVALID_CHAT_HISTORY,270 )271 raise ValueError(error_message)272 273 274@deprecated(275 "create_react_agent has been moved to `langchain.agents`. Please update your import to `from langchain.agents import create_agent`.",276 category=LangGraphDeprecatedSinceV10,277)278def create_react_agent(279 model: str280 | LanguageModelLike281 | Callable[[StateSchema, Runtime[ContextT]], BaseChatModel]282 | Callable[[StateSchema, Runtime[ContextT]], Awaitable[BaseChatModel]]283 | Callable[284 [StateSchema, Runtime[ContextT]], Runnable[LanguageModelInput, BaseMessage]285 ]286 | Callable[287 [StateSchema, Runtime[ContextT]],288 Awaitable[Runnable[LanguageModelInput, BaseMessage]],289 ],290 tools: Sequence[BaseTool | Callable | dict[str, Any]] | ToolNode,291 *,292 prompt: Prompt | None = None,293 response_format: StructuredResponseSchema294 | tuple[str, StructuredResponseSchema]295 | None = None,296 pre_model_hook: RunnableLike | None = None,297 post_model_hook: RunnableLike | None = None,298 state_schema: StateSchemaType | None = None,299 context_schema: type[Any] | None = None,300 checkpointer: Checkpointer | None = None,301 store: BaseStore | None = None,302 interrupt_before: list[str] | None = None,303 interrupt_after: list[str] | None = None,304 debug: bool = False,305 version: Literal["v1", "v2"] = "v2",306 name: str | None = None,307 **deprecated_kwargs: Any,308) -> CompiledStateGraph:309 """Creates an agent graph that calls tools in a loop until a stopping condition is met.310 311 !!! warning312 313 This function is deprecated in favor of314 [`create_agent`][langchain.agents.create_agent] from the `langchain`315 package, which provides an equivalent agent factory with a flexible316 middleware system. For migration guidance, see317 [Migrating from LangGraph v0](https://docs.langchain.com/oss/python/migrate/langgraph-v1).318 319 Args:320 model: The language model for the agent. Supports static and dynamic321 model selection.322 323 - **Static model**: A chat model instance (e.g.,324 [`ChatOpenAI`][langchain_openai.ChatOpenAI]) or string identifier (e.g.,325 `"openai:gpt-4"`)326 - **Dynamic model**: A callable with signature327 `(state, runtime) -> BaseChatModel` that returns different models328 based on runtime context329 330 If the model has tools bound via `bind_tools` or other configurations,331 the return type should be a `Runnable[LanguageModelInput, BaseMessage]`332 Coroutines are also supported, allowing for asynchronous model selection.333 334 Dynamic functions receive graph state and runtime, enabling335 context-dependent model selection. Must return a `BaseChatModel`336 instance. For tool calling, bind tools using `.bind_tools()`.337 Bound tools must be a subset of the `tools` parameter.338 339 !!! example "Dynamic model"340 341 ```python342 from dataclasses import dataclass343 344 @dataclass345 class ModelContext:346 model_name: str = "gpt-3.5-turbo"347 348 # Instantiate models globally349 gpt4_model = ChatOpenAI(model="gpt-4")350 gpt35_model = ChatOpenAI(model="gpt-3.5-turbo")351 352 def select_model(state: AgentState, runtime: Runtime[ModelContext]) -> ChatOpenAI:353 model_name = runtime.context.model_name354 model = gpt4_model if model_name == "gpt-4" else gpt35_model355 return model.bind_tools(tools)356 ```357 358 !!! note "Dynamic Model Requirements"359 360 Ensure returned models have appropriate tools bound via361 `.bind_tools()` and support required functionality. Bound tools362 must be a subset of those specified in the `tools` parameter.363 364 tools: A list of tools or a `ToolNode` instance.365 If an empty list is provided, the agent will consist of a single LLM node without tool calling.366 prompt: An optional prompt for the LLM. Can take a few different forms:367 368 - `str`: This is converted to a `SystemMessage` and added to the beginning of the list of messages in `state["messages"]`.369 - `SystemMessage`: this is added to the beginning of the list of messages in `state["messages"]`.370 - `Callable`: This function should take in full graph state and the output is then passed to the language model.371 - `Runnable`: This runnable should take in full graph state and the output is then passed to the language model.372 373 response_format: An optional schema for the final agent output.374 375 If provided, output will be formatted to match the given schema and returned in the 'structured_response' state key.376 377 If not provided, `structured_response` will not be present in the output state.378 379 Can be passed in as:380 381 - An OpenAI function/tool schema,382 - A JSON Schema,383 - A TypedDict class,384 - A Pydantic class.385 - A tuple `(prompt, schema)`, where schema is one of the above.386 The prompt will be used together with the model that is being used to387 generate the structured response.388 389 !!! Important390 `response_format` requires the model to support `.with_structured_output`391 392 !!! Note393 The graph will make a separate call to the LLM to generate the structured response after the agent loop is finished.394 This is not the only strategy to get structured responses, see more options in [this guide](https://langchain-ai.github.io/langgraph/how-tos/react-agent-structured-output/).395 396 pre_model_hook: An optional node to add before the `agent` node (i.e., the node that calls the LLM).397 Useful for managing long message histories (e.g., message trimming, summarization, etc.).398 Pre-model hook must be a callable or a runnable that takes in current graph state and returns a state update in the form of399 ```python400 # At least one of `messages` or `llm_input_messages` MUST be provided401 {402 # If provided, will UPDATE the `messages` in the state403 "messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES), ...],404 # If provided, will be used as the input to the LLM,405 # and will NOT UPDATE `messages` in the state406 "llm_input_messages": [...],407 # Any other state keys that need to be propagated408 ...409 }410 ```411 412 !!! Important413 At least one of `messages` or `llm_input_messages` MUST be provided and will be used as an input to the `agent` node.414 The rest of the keys will be added to the graph state.415 416 !!! Warning417 If you are returning `messages` in the pre-model hook, you should OVERWRITE the `messages` key by doing the following:418 419 ```python420 {421 "messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES), *new_messages]422 ...423 }424 ```425 post_model_hook: An optional node to add after the `agent` node (i.e., the node that calls the LLM).426 Useful for implementing human-in-the-loop, guardrails, validation, or other post-processing.427 Post-model hook must be a callable or a runnable that takes in current graph state and returns a state update.428 429 !!! Note430 Only available with `version="v2"`.431 state_schema: An optional state schema that defines graph state.432 Must have `messages` and `remaining_steps` keys.433 Defaults to `AgentState` that defines those two keys.434 !!! Note435 `remaining_steps` is used to limit the number of steps the react agent can take.436 Calculated roughly as `recursion_limit` - `total_steps_taken`.437 If `remaining_steps` is less than 2 and tool calls are present in the response,438 the react agent will return a final AI Message with439 the content "Sorry, need more steps to process this request.".440 No `GraphRecusionError` will be raised in this case.441 442 context_schema: An optional schema for runtime context.443 checkpointer: An optional checkpoint saver object. This is used for persisting444 the state of the graph (e.g., as chat memory) for a single thread (e.g., a single conversation).445 store: An optional store object. This is used for persisting data446 across multiple threads (e.g., multiple conversations / users).447 interrupt_before: An optional list of node names to interrupt before.448 Should be one of the following: `"agent"`, `"tools"`.449 450 This is useful if you want to add a user confirmation or other interrupt before taking an action.451 interrupt_after: An optional list of node names to interrupt after.452 Should be one of the following: `"agent"`, `"tools"`.453 454 This is useful if you want to return directly or run additional processing on an output.455 debug: A flag indicating whether to enable debug mode.456 version: Determines the version of the graph to create.457 458 Can be one of:459 460 - `"v1"`: The tool node processes a single message. All tool461 calls in the message are executed in parallel within the tool node.462 - `"v2"`: The tool node processes a tool call.463 Tool calls are distributed across multiple instances of the tool464 node using the [Send](https://langchain-ai.github.io/langgraph/concepts/low_level/#send)465 API.466 name: An optional name for the `CompiledStateGraph`.467 This name will be automatically used when adding ReAct agent graph to another graph as a subgraph node -468 particularly useful for building multi-agent systems.469 470 !!! warning "`config_schema` Deprecated"471 The `config_schema` parameter is deprecated in v0.6.0 and support will be removed in v2.0.0.472 Please use `context_schema` instead to specify the schema for run-scoped context.473 474 475 Returns:476 A compiled LangChain `Runnable` that can be used for chat interactions.477 478 The "agent" node calls the language model with the messages list (after applying the prompt).479 If the resulting AIMessage contains `tool_calls`, the graph will then call the ["tools"][langgraph.prebuilt.tool_node.ToolNode].480 The "tools" node executes the tools (1 tool per `tool_call`) and adds the responses to the messages list481 as `ToolMessage` objects. The agent node then calls the language model again.482 The process repeats until no more `tool_calls` are present in the response.483 The agent then returns the full list of messages as a dictionary containing the key `'messages'`.484 485 ``` mermaid486 sequenceDiagram487 participant U as User488 participant A as LLM489 participant T as Tools490 U->>A: Initial input491 Note over A: Prompt + LLM492 loop while tool_calls present493 A->>T: Execute tools494 T-->>A: ToolMessage for each tool_calls495 end496 A->>U: Return final state497 ```498 499 Example:500 ```python501 from langgraph.prebuilt import create_react_agent502 503 def check_weather(location: str) -> str:504 '''Return the weather forecast for the specified location.'''505 return f"It's always sunny in {location}"506 507 graph = create_react_agent(508 "anthropic:claude-3-7-sonnet-latest",509 tools=[check_weather],510 prompt="You are a helpful assistant",511 )512 inputs = {"messages": [{"role": "user", "content": "what is the weather in sf"}]}513 for chunk in graph.stream(inputs, stream_mode="updates"):514 print(chunk)515 ```516 """517 if (518 config_schema := deprecated_kwargs.pop("config_schema", MISSING)519 ) is not MISSING:520 warnings.warn(521 "`config_schema` is deprecated and will be removed. Please use `context_schema` instead.",522 category=LangGraphDeprecatedSinceV10,523 )524 525 if context_schema is None:526 context_schema = config_schema527 528 if len(deprecated_kwargs) > 0:529 raise TypeError(530 f"create_react_agent() got unexpected keyword arguments: {deprecated_kwargs}"531 )532 533 if version not in ("v1", "v2"):534 raise ValueError(535 f"Invalid version {version}. Supported versions are 'v1' and 'v2'."536 )537 538 if state_schema is not None:539 required_keys = {"messages", "remaining_steps"}540 if response_format is not None:541 required_keys.add("structured_response")542 543 schema_keys = set(get_type_hints(state_schema))544 if missing_keys := required_keys - set(schema_keys):545 raise ValueError(f"Missing required key(s) {missing_keys} in state_schema")546 547 if state_schema is None:548 state_schema = (549 AgentStateWithStructuredResponse550 if response_format is not None551 else AgentState552 )553 554 llm_builtin_tools: list[dict] = []555 if isinstance(tools, ToolNode):556 tool_classes = list(tools.tools_by_name.values())557 tool_node = tools558 else:559 llm_builtin_tools = [t for t in tools if isinstance(t, dict)]560 tool_node = ToolNode([t for t in tools if not isinstance(t, dict)])561 tool_classes = list(tool_node.tools_by_name.values())562 563 is_dynamic_model = not isinstance(model, (str, Runnable)) and callable(model)564 is_async_dynamic_model = is_dynamic_model and inspect.iscoroutinefunction(model)565 566 tool_calling_enabled = len(tool_classes) > 0567 568 if not is_dynamic_model:569 if isinstance(model, str):570 try:571 from langchain.chat_models import ( # type: ignore[import-not-found]572 init_chat_model,573 )574 except ImportError:575 raise ImportError(576 "Please install langchain (`pip install langchain`) to "577 "use '<provider>:<model>' string syntax for `model` parameter."578 )579 580 model = cast(BaseChatModel, init_chat_model(model))581 582 if (583 _should_bind_tools(model, tool_classes, num_builtin=len(llm_builtin_tools)) # type: ignore[arg-type]584 and len(tool_classes + llm_builtin_tools) > 0585 ):586 model = cast(BaseChatModel, model).bind_tools(587 tool_classes + llm_builtin_tools # type: ignore[operator]588 )589 590 static_model: Runnable | None = _get_prompt_runnable(prompt) | model # type: ignore[operator]591 else:592 # For dynamic models, we'll create the runnable at runtime593 static_model = None594 595 # If any of the tools are configured to return_directly after running,596 # our graph needs to check if these were called597 should_return_direct = {t.name for t in tool_classes if t.return_direct}598 599 def _resolve_model(600 state: StateSchema, runtime: Runtime[ContextT]601 ) -> LanguageModelLike:602 """Resolve the model to use, handling both static and dynamic models."""603 if is_dynamic_model:604 return _get_prompt_runnable(prompt) | model(state, runtime) # type: ignore[operator]605 else:606 return static_model607 608 async def _aresolve_model(609 state: StateSchema, runtime: Runtime[ContextT]610 ) -> LanguageModelLike:611 """Async resolve the model to use, handling both static and dynamic models."""612 if is_async_dynamic_model:613 resolved_model = await model(state, runtime) # type: ignore[misc,operator]614 return _get_prompt_runnable(prompt) | resolved_model615 elif is_dynamic_model:616 return _get_prompt_runnable(prompt) | model(state, runtime) # type: ignore[operator]617 else:618 return static_model619 620 def _are_more_steps_needed(state: StateSchema, response: BaseMessage) -> bool:621 has_tool_calls = isinstance(response, AIMessage) and response.tool_calls622 all_tools_return_direct = (623 all(call["name"] in should_return_direct for call in response.tool_calls)624 if isinstance(response, AIMessage)625 else False626 )627 remaining_steps = _get_state_value(state, "remaining_steps", None)628 if remaining_steps is not None:629 if remaining_steps < 1 and all_tools_return_direct:630 return True631 elif remaining_steps < 2 and has_tool_calls:632 return True633 634 return False635 636 def _get_model_input_state(state: StateSchema) -> StateSchema:637 if pre_model_hook is not None:638 messages = (639 _get_state_value(state, "llm_input_messages")640 ) or _get_state_value(state, "messages")641 error_msg = f"Expected input to call_model to have 'llm_input_messages' or 'messages' key, but got {state}"642 else:643 messages = _get_state_value(state, "messages")644 error_msg = (645 f"Expected input to call_model to have 'messages' key, but got {state}"646 )647 648 if messages is None:649 raise ValueError(error_msg)650 651 _validate_chat_history(messages)652 # we're passing messages under `messages` key, as this is expected by the prompt653 if isinstance(state_schema, type) and issubclass(state_schema, BaseModel):654 state.messages = messages # type: ignore655 else:656 state["messages"] = messages # type: ignore657 658 return state659 660 # Define the function that calls the model661 def call_model(662 state: StateSchema, runtime: Runtime[ContextT], config: RunnableConfig663 ) -> StateSchema:664 if is_async_dynamic_model:665 msg = (666 "Async model callable provided but agent invoked synchronously. "667 "Use agent.ainvoke() or agent.astream(), or "668 "provide a sync model callable."669 )670 raise RuntimeError(msg)671 672 model_input = _get_model_input_state(state)673 674 if is_dynamic_model:675 # Resolve dynamic model at runtime and apply prompt676 dynamic_model = _resolve_model(state, runtime)677 response = cast(AIMessage, dynamic_model.invoke(model_input, config)) # type: ignore[arg-type]678 else:679 response = cast(AIMessage, static_model.invoke(model_input, config)) # type: ignore[union-attr]680 681 # add agent name to the AIMessage682 response.name = name683 684 if _are_more_steps_needed(state, response):685 return {686 "messages": [687 AIMessage(688 id=response.id,689 content="Sorry, need more steps to process this request.",690 )691 ]692 }693 # We return a list, because this will get added to the existing list694 return {"messages": [response]}695 696 async def acall_model(697 state: StateSchema, runtime: Runtime[ContextT], config: RunnableConfig698 ) -> StateSchema:699 model_input = _get_model_input_state(state)700 701 if is_dynamic_model:702 # Resolve dynamic model at runtime and apply prompt703 # (supports both sync and async)704 dynamic_model = await _aresolve_model(state, runtime)705 response = cast(AIMessage, await dynamic_model.ainvoke(model_input, config)) # type: ignore[arg-type]706 else:707 response = cast(AIMessage, await static_model.ainvoke(model_input, config)) # type: ignore[union-attr]708 709 # add agent name to the AIMessage710 response.name = name711 if _are_more_steps_needed(state, response):712 return {713 "messages": [714 AIMessage(715 id=response.id,716 content="Sorry, need more steps to process this request.",717 )718 ]719 }720 # We return a list, because this will get added to the existing list721 return {"messages": [response]}722 723 input_schema: StateSchemaType724 if pre_model_hook is not None:725 # Dynamically create a schema that inherits from state_schema and adds 'llm_input_messages'726 if isinstance(state_schema, type) and issubclass(state_schema, BaseModel):727 # For Pydantic schemas728 from pydantic import create_model729 730 input_schema = create_model(731 "CallModelInputSchema",732 llm_input_messages=(list[AnyMessage], ...),733 __base__=state_schema,734 )735 else:736 # For TypedDict schemas737 class CallModelInputSchema(state_schema): # type: ignore738 llm_input_messages: list[AnyMessage]739 740 input_schema = CallModelInputSchema741 else:742 input_schema = state_schema743 744 def generate_structured_response(745 state: StateSchema, runtime: Runtime[ContextT], config: RunnableConfig746 ) -> StateSchema:747 if is_async_dynamic_model:748 msg = (749 "Async model callable provided but agent invoked synchronously. "750 "Use agent.ainvoke() or agent.astream(), or provide a sync model callable."751 )752 raise RuntimeError(msg)753 754 messages = _get_state_value(state, "messages")755 structured_response_schema = response_format756 if isinstance(response_format, tuple):757 system_prompt, structured_response_schema = response_format758 messages = [SystemMessage(content=system_prompt)] + list(messages)759 760 resolved_model = _resolve_model(state, runtime)761 model_with_structured_output = _get_model(762 resolved_model763 ).with_structured_output(764 cast(StructuredResponseSchema, structured_response_schema)765 )766 response = model_with_structured_output.invoke(messages, config)767 return {"structured_response": response}768 769 async def agenerate_structured_response(770 state: StateSchema, runtime: Runtime[ContextT], config: RunnableConfig771 ) -> StateSchema:772 messages = _get_state_value(state, "messages")773 structured_response_schema = response_format774 if isinstance(response_format, tuple):775 system_prompt, structured_response_schema = response_format776 messages = [SystemMessage(content=system_prompt)] + list(messages)777 778 resolved_model = await _aresolve_model(state, runtime)779 model_with_structured_output = _get_model(780 resolved_model781 ).with_structured_output(782 cast(StructuredResponseSchema, structured_response_schema)783 )784 response = await model_with_structured_output.ainvoke(messages, config)785 return {"structured_response": response}786 787 if not tool_calling_enabled:788 # Define a new graph789 workflow = StateGraph(state_schema=state_schema, context_schema=context_schema)790 workflow.add_node(791 "agent",792 RunnableCallable(call_model, acall_model),793 input_schema=input_schema,794 )795 if pre_model_hook is not None:796 workflow.add_node("pre_model_hook", pre_model_hook) # type: ignore[arg-type]797 workflow.add_edge("pre_model_hook", "agent")798 entrypoint = "pre_model_hook"799 else:800 entrypoint = "agent"801 802 workflow.set_entry_point(entrypoint)803 804 if post_model_hook is not None:805 workflow.add_node("post_model_hook", post_model_hook) # type: ignore[arg-type]806 workflow.add_edge("agent", "post_model_hook")807 808 if response_format is not None:809 workflow.add_node(810 "generate_structured_response",811 RunnableCallable(812 generate_structured_response,813 agenerate_structured_response,814 ),815 )816 if post_model_hook is not None:817 workflow.add_edge("post_model_hook", "generate_structured_response")818 else:819 workflow.add_edge("agent", "generate_structured_response")820 821 return workflow.compile(822 checkpointer=checkpointer,823 store=store,824 interrupt_before=interrupt_before,825 interrupt_after=interrupt_after,826 debug=debug,827 name=name,828 )829 830 # Define the function that determines whether to continue or not831 def should_continue(state: StateSchema) -> str | list[Send]:832 messages = _get_state_value(state, "messages")833 last_message = messages[-1]834 # If there is no function call, then we finish835 if not isinstance(last_message, AIMessage) or not last_message.tool_calls:836 if post_model_hook is not None:837 return "post_model_hook"838 elif response_format is not None:839 return "generate_structured_response"840 else:841 return END842 # Otherwise if there is, we continue843 else:844 if version == "v1":845 return "tools"846 elif version == "v2":847 if post_model_hook is not None:848 return "post_model_hook"849 return [850 Send(851 "tools",852 ToolCallWithContext(853 __type="tool_call_with_context",854 tool_call=call,855 state=state,856 ),857 )858 for call in last_message.tool_calls859 ]860 861 # Define a new graph862 workflow = StateGraph(863 state_schema=state_schema or AgentState, context_schema=context_schema864 )865 866 # Define the two nodes we will cycle between867 workflow.add_node(868 "agent",869 RunnableCallable(call_model, acall_model),870 input_schema=input_schema,871 )872 workflow.add_node("tools", tool_node)873 874 # Optionally add a pre-model hook node that will be called875 # every time before the "agent" (LLM-calling node)876 if pre_model_hook is not None:877 workflow.add_node("pre_model_hook", pre_model_hook) # type: ignore[arg-type]878 workflow.add_edge("pre_model_hook", "agent")879 entrypoint = "pre_model_hook"880 else:881 entrypoint = "agent"882 883 # Set the entrypoint as `agent`884 # This means that this node is the first one called885 workflow.set_entry_point(entrypoint)886 887 agent_paths = []888 post_model_hook_paths = [entrypoint, "tools"]889 890 # Add a post model hook node if post_model_hook is provided891 if post_model_hook is not None:892 workflow.add_node("post_model_hook", post_model_hook) # type: ignore[arg-type]893 agent_paths.append("post_model_hook")894 workflow.add_edge("agent", "post_model_hook")895 else:896 agent_paths.append("tools")897 898 # Add a structured output node if response_format is provided899 if response_format is not None:900 workflow.add_node(901 "generate_structured_response",902 RunnableCallable(903 generate_structured_response,904 agenerate_structured_response,905 ),906 )907 if post_model_hook is not None:908 post_model_hook_paths.append("generate_structured_response")909 else:910 agent_paths.append("generate_structured_response")911 else:912 if post_model_hook is not None:913 post_model_hook_paths.append(END)914 else:915 agent_paths.append(END)916 917 if post_model_hook is not None:918 919 def post_model_hook_router(state: StateSchema) -> str | list[Send]:920 """Route to the next node after post_model_hook.921 922 Routes to one of:923 * "tools": if there are pending tool calls without a corresponding message.924 * "generate_structured_response": if no pending tool calls exist and response_format is specified.925 * END: if no pending tool calls exist and no response_format is specified.926 """927 928 messages = _get_state_value(state, "messages")929 tool_messages = [930 m.tool_call_id for m in messages if isinstance(m, ToolMessage)931 ]932 last_ai_message = next(933 m for m in reversed(messages) if isinstance(m, AIMessage)934 )935 pending_tool_calls = [936 c for c in last_ai_message.tool_calls if c["id"] not in tool_messages937 ]938 939 if pending_tool_calls:940 return [941 Send(942 "tools",943 ToolCallWithContext(944 __type="tool_call_with_context",945 tool_call=call,946 state=state,947 ),948 )949 for call in pending_tool_calls950 ]951 elif isinstance(messages[-1], ToolMessage):952 return entrypoint953 elif response_format is not None:954 return "generate_structured_response"955 else:956 return END957 958 workflow.add_conditional_edges(959 "post_model_hook",960 post_model_hook_router,961 path_map=post_model_hook_paths,962 )963 964 workflow.add_conditional_edges(965 "agent",966 should_continue,967 path_map=agent_paths,968 )969 970 def route_tool_responses(state: StateSchema) -> str:971 for m in reversed(_get_state_value(state, "messages")):972 if not isinstance(m, ToolMessage):973 break974 if m.name in should_return_direct:975 return END976 977 # handle a case of parallel tool calls where978 # the tool w/ `return_direct` was executed in a different `Send`979 if isinstance(m, AIMessage) and m.tool_calls:980 if any(call["name"] in should_return_direct for call in m.tool_calls):981 return END982 983 return entrypoint984 985 if should_return_direct:986 workflow.add_conditional_edges(987 "tools", route_tool_responses, path_map=[entrypoint, END]988 )989 else:990 workflow.add_edge("tools", entrypoint)991 992 # Finally, we compile it!993 # This compiles it into a LangChain Runnable,994 # meaning you can use it as you would any other runnable995 return workflow.compile(996 checkpointer=checkpointer,997 store=store,998 interrupt_before=interrupt_before,999 interrupt_after=interrupt_after,1000 debug=debug,1001 name=name,1002 )1003 1004 1005# Keep for backwards compatibility1006create_tool_calling_executor = create_react_agent1007 1008__all__ = [1009 "create_react_agent",1010 "create_tool_calling_executor",1011 "AgentState",1012 "AgentStatePydantic",1013 "AgentStateWithStructuredResponse",1014 "AgentStateWithStructuredResponsePydantic",1015]1016 