mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-28 10:49:56 +02:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e38bae6b3a | ||
|
|
c31ee23e2e | ||
|
|
ae525fb74f | ||
|
|
a6dde39be7 |
@@ -180,7 +180,7 @@ REDIRECT_MAP = {
|
||||
"cloud/concepts/data_storage_and_privacy.md": "https://docs.langchain.com/langsmith/data-storage-and-privacy",
|
||||
"cloud/deployment/semantic_search.md": "https://docs.langchain.com/langsmith/semantic-search",
|
||||
"how-tos/ttl/configure_ttl.md": "https://docs.langchain.com/langsmith/configure-ttl",
|
||||
"concepts/deployment_options.md": "https://docs.langchain.com/langsmith/hosting",
|
||||
"concepts/deployment_options.md": "https://docs.langchain.com/langsmith/platform-setup",
|
||||
"cloud/quick_start.md": "https://docs.langchain.com/langsmith/deployment-quickstart",
|
||||
"cloud/deployment/setup.md": "https://docs.langchain.com/langsmith/setup-app-requirements-txt",
|
||||
"cloud/deployment/setup_pyproject.md": "https://docs.langchain.com/langsmith/setup-pyproject",
|
||||
|
||||
@@ -5,5 +5,5 @@ description = "Test for prerelease stuff"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"langgraph==0.6.0"
|
||||
"langgraph==0.6.11"
|
||||
]
|
||||
@@ -8,7 +8,7 @@ authors = [
|
||||
license = { text = "MIT" }
|
||||
requires-python = ">=3.11,<4.0"
|
||||
dependencies = [
|
||||
"langgraph>=0.6.0,<0.7.0",
|
||||
"langgraph>=0.6.0,<2",
|
||||
"langchain-core>=0.2.14",
|
||||
]
|
||||
|
||||
|
||||
@@ -81,9 +81,8 @@ def push_ui_message(
|
||||
metadata: Optional additional metadata about the UI message.
|
||||
message: Optional message object to associate with the UI message.
|
||||
state_key: Key in the graph state where the UI messages are stored.
|
||||
Defaults to "ui".
|
||||
merge: Whether to merge props with existing UI message (True) or replace
|
||||
them (False). Defaults to False.
|
||||
them (False).
|
||||
|
||||
Returns:
|
||||
The created UI message.
|
||||
|
||||
@@ -55,6 +55,7 @@ __all__ = (
|
||||
"Command",
|
||||
"Durability",
|
||||
"interrupt",
|
||||
"Overwrite",
|
||||
)
|
||||
|
||||
Durability = Literal["sync", "async", "exit"]
|
||||
@@ -283,26 +284,32 @@ class Send:
|
||||
node (str): The name of the target node to send the message to.
|
||||
arg (Any): The state or message to send to the target node.
|
||||
|
||||
Examples:
|
||||
>>> from typing import Annotated
|
||||
>>> import operator
|
||||
>>> class OverallState(TypedDict):
|
||||
... subjects: list[str]
|
||||
... jokes: Annotated[list[str], operator.add]
|
||||
>>> from langgraph.types import Send
|
||||
>>> from langgraph.graph import END, START
|
||||
>>> def continue_to_jokes(state: OverallState):
|
||||
... return [Send("generate_joke", {"subject": s}) for s in state["subjects"]]
|
||||
>>> from langgraph.graph import StateGraph
|
||||
>>> builder = StateGraph(OverallState)
|
||||
>>> builder.add_node("generate_joke", lambda state: {"jokes": [f"Joke about {state['subject']}"]})
|
||||
>>> builder.add_conditional_edges(START, continue_to_jokes)
|
||||
>>> builder.add_edge("generate_joke", END)
|
||||
>>> graph = builder.compile()
|
||||
>>>
|
||||
>>> # Invoking with two subjects results in a generated joke for each
|
||||
>>> graph.invoke({"subjects": ["cats", "dogs"]})
|
||||
{'subjects': ['cats', 'dogs'], 'jokes': ['Joke about cats', 'Joke about dogs']}
|
||||
!!! example
|
||||
|
||||
```python
|
||||
from typing import Annotated
|
||||
from langgraph.types import Send
|
||||
from langgraph.graph import END, START
|
||||
from langgraph.graph import StateGraph
|
||||
import operator
|
||||
|
||||
class OverallState(TypedDict):
|
||||
subjects: list[str]
|
||||
jokes: Annotated[list[str], operator.add]
|
||||
|
||||
def continue_to_jokes(state: OverallState):
|
||||
return [Send("generate_joke", {"subject": s}) for s in state["subjects"]]
|
||||
|
||||
builder = StateGraph(OverallState)
|
||||
builder.add_node("generate_joke", lambda state: {"jokes": [f"Joke about {state['subject']}"]})
|
||||
builder.add_conditional_edges(START, continue_to_jokes)
|
||||
builder.add_edge("generate_joke", END)
|
||||
graph = builder.compile()
|
||||
|
||||
# Invoking with two subjects results in a generated joke for each
|
||||
graph.invoke({"subjects": ["cats", "dogs"]})
|
||||
# {'subjects': ['cats', 'dogs'], 'jokes': ['Joke about cats', 'Joke about dogs']}
|
||||
```
|
||||
"""
|
||||
|
||||
__slots__ = ("node", "arg")
|
||||
@@ -342,10 +349,8 @@ N = TypeVar("N", bound=Hashable)
|
||||
class Command(Generic[N], ToolOutputMixin):
|
||||
"""One or more commands to update the graph's state and send messages to nodes.
|
||||
|
||||
!!! version-added "Added in version 0.2.24"
|
||||
|
||||
Args:
|
||||
graph: graph to send the command to. Supported values are:
|
||||
graph: Graph to send the command to. Supported values are:
|
||||
|
||||
- `None`: the current graph
|
||||
- `Command.PARENT`: closest parent graph
|
||||
@@ -415,7 +420,8 @@ def interrupt(value: Any) -> Any:
|
||||
To use an `interrupt`, you must enable a checkpointer, as the feature relies
|
||||
on persisting the graph state.
|
||||
|
||||
Example:
|
||||
!!! example
|
||||
|
||||
```python
|
||||
import uuid
|
||||
from typing import Optional
|
||||
@@ -520,38 +526,42 @@ def interrupt(value: Any) -> Any:
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Overwrite:
|
||||
"""Bypass a reducer and write the wrapped value directly to a BinaryOperatorAggregate channel.
|
||||
"""Bypass a reducer and write the wrapped value directly to a `BinaryOperatorAggregate` channel.
|
||||
|
||||
Receiving multiple Overwrite values for the same channel in a single super-step will raise an InvalidUpdateError.
|
||||
Receiving multiple `Overwrite` values for the same channel in a single super-step
|
||||
will raise an `InvalidUpdateError`.
|
||||
|
||||
Example:
|
||||
>>> from typing import Annotated
|
||||
>>> import operator
|
||||
>>> from langgraph.graph import StateGraph
|
||||
>>> from langgraph.types import Overwrite
|
||||
>>>
|
||||
>>> class State(TypedDict):
|
||||
... messages: Annotated[list, operator.add]
|
||||
>>>
|
||||
>>> def node_a(state: TypedDict):
|
||||
... # Normal update: uses the reducer (operator.add)
|
||||
... return {"messages": ["a"]}
|
||||
>>>
|
||||
>>> def node_b(state: State):
|
||||
... # Overwrite: bypasses the reducer and replaces the entire value
|
||||
... return {"messages": Overwrite(value=["b"])}
|
||||
>>>
|
||||
>>> builder = StateGraph(State)
|
||||
>>> builder.add_node("node_a", node_a)
|
||||
>>> builder.add_node("node_b", node_b)
|
||||
>>> builder.set_entry_point("node_a")
|
||||
>>> builder.add_edge("node_a", "node_b")
|
||||
>>> graph = builder.compile()
|
||||
>>>
|
||||
>>> # Without Overwrite in node_b, messages would be ["START", "a", "b"]
|
||||
>>> # With Overwrite, messages is just ["b"]
|
||||
>>> result = graph.invoke({"messages": ["START"]})
|
||||
>>> assert result == {"messages": ["b"]}
|
||||
!!! example
|
||||
|
||||
```python
|
||||
from typing import Annotated
|
||||
import operator
|
||||
from langgraph.graph import StateGraph
|
||||
from langgraph.types import Overwrite
|
||||
|
||||
class State(TypedDict):
|
||||
messages: Annotated[list, operator.add]
|
||||
|
||||
def node_a(state: TypedDict):
|
||||
# Normal update: uses the reducer (operator.add)
|
||||
return {"messages": ["a"]}
|
||||
|
||||
def node_b(state: State):
|
||||
# Overwrite: bypasses the reducer and replaces the entire value
|
||||
return {"messages": Overwrite(value=["b"])}
|
||||
|
||||
builder = StateGraph(State)
|
||||
builder.add_node("node_a", node_a)
|
||||
builder.add_node("node_b", node_b)
|
||||
builder.set_entry_point("node_a")
|
||||
builder.add_edge("node_a", "node_b")
|
||||
graph = builder.compile()
|
||||
|
||||
# Without Overwrite in node_b, messages would be ["START", "a", "b"]
|
||||
# With Overwrite, messages is just ["b"]
|
||||
result = graph.invoke({"messages": ["START"]})
|
||||
assert result == {"messages": ["b"]}
|
||||
```
|
||||
"""
|
||||
|
||||
value: Any
|
||||
|
||||
@@ -309,37 +309,40 @@ def create_react_agent(
|
||||
model: The language model for the agent. Supports static and dynamic
|
||||
model selection.
|
||||
|
||||
- **Static model**: A chat model instance (e.g., `ChatOpenAI()`) or
|
||||
string identifier (e.g., `"openai:gpt-4"`)
|
||||
- **Static model**: A chat model instance (e.g.,
|
||||
[`ChatOpenAI`][langchain_openai.ChatOpenAI]) or string identifier (e.g.,
|
||||
`"openai:gpt-4"`)
|
||||
- **Dynamic model**: A callable with signature
|
||||
`(state, runtime) -> BaseChatModel` that returns different models
|
||||
based on runtime context
|
||||
If the model has tools bound via `.bind_tools()` or other configurations,
|
||||
the return type should be a Runnable[LanguageModelInput, BaseMessage]
|
||||
Coroutines are also supported, allowing for asynchronous model selection.
|
||||
`(state, runtime) -> BaseChatModel` that returns different models
|
||||
based on runtime context
|
||||
|
||||
If the model has tools bound via `bind_tools` or other configurations,
|
||||
the return type should be a `Runnable[LanguageModelInput, BaseMessage]`
|
||||
Coroutines are also supported, allowing for asynchronous model selection.
|
||||
|
||||
Dynamic functions receive graph state and runtime, enabling
|
||||
context-dependent model selection. Must return a `BaseChatModel`
|
||||
instance. For tool calling, bind tools using `.bind_tools()`.
|
||||
Bound tools must be a subset of the `tools` parameter.
|
||||
|
||||
Dynamic model example:
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
!!! example "Dynamic model"
|
||||
|
||||
@dataclass
|
||||
class ModelContext:
|
||||
model_name: str = "gpt-3.5-turbo"
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
|
||||
# Instantiate models globally
|
||||
gpt4_model = ChatOpenAI(model="gpt-4")
|
||||
gpt35_model = ChatOpenAI(model="gpt-3.5-turbo")
|
||||
@dataclass
|
||||
class ModelContext:
|
||||
model_name: str = "gpt-3.5-turbo"
|
||||
|
||||
def select_model(state: AgentState, runtime: Runtime[ModelContext]) -> ChatOpenAI:
|
||||
model_name = runtime.context.model_name
|
||||
model = gpt4_model if model_name == "gpt-4" else gpt35_model
|
||||
return model.bind_tools(tools)
|
||||
```
|
||||
# Instantiate models globally
|
||||
gpt4_model = ChatOpenAI(model="gpt-4")
|
||||
gpt35_model = ChatOpenAI(model="gpt-3.5-turbo")
|
||||
|
||||
def select_model(state: AgentState, runtime: Runtime[ModelContext]) -> ChatOpenAI:
|
||||
model_name = runtime.context.model_name
|
||||
model = gpt4_model if model_name == "gpt-4" else gpt35_model
|
||||
return model.bind_tools(tools)
|
||||
```
|
||||
|
||||
!!! note "Dynamic Model Requirements"
|
||||
|
||||
@@ -351,23 +354,26 @@ def create_react_agent(
|
||||
If an empty list is provided, the agent will consist of a single LLM node without tool calling.
|
||||
prompt: An optional prompt for the LLM. Can take a few different forms:
|
||||
|
||||
- str: This is converted to a SystemMessage and added to the beginning of the list of messages in state["messages"].
|
||||
- SystemMessage: this is added to the beginning of the list of messages in state["messages"].
|
||||
- Callable: This function should take in full graph state and the output is then passed to the language model.
|
||||
- Runnable: This runnable should take in full graph state and the output is then passed to the language model.
|
||||
- `str`: This is converted to a `SystemMessage` and added to the beginning of the list of messages in `state["messages"]`.
|
||||
- `SystemMessage`: this is added to the beginning of the list of messages in `state["messages"]`.
|
||||
- `Callable`: This function should take in full graph state and the output is then passed to the language model.
|
||||
- `Runnable`: This runnable should take in full graph state and the output is then passed to the language model.
|
||||
|
||||
response_format: An optional schema for the final agent output.
|
||||
|
||||
If provided, output will be formatted to match the given schema and returned in the 'structured_response' state key.
|
||||
|
||||
If not provided, `structured_response` will not be present in the output state.
|
||||
|
||||
Can be passed in as:
|
||||
|
||||
- an OpenAI function/tool schema,
|
||||
- a JSON Schema,
|
||||
- a TypedDict class,
|
||||
- or a Pydantic class.
|
||||
- a tuple (prompt, schema), where schema is one of the above.
|
||||
The prompt will be used together with the model that is being used to generate the structured response.
|
||||
- An OpenAI function/tool schema,
|
||||
- A JSON Schema,
|
||||
- A TypedDict class,
|
||||
- A Pydantic class.
|
||||
- A tuple `(prompt, schema)`, where schema is one of the above.
|
||||
The prompt will be used together with the model that is being used to
|
||||
generate the structured response.
|
||||
|
||||
!!! Important
|
||||
`response_format` requires the model to support `.with_structured_output`
|
||||
@@ -428,13 +434,16 @@ def create_react_agent(
|
||||
store: An optional store object. This is used for persisting data
|
||||
across multiple threads (e.g., multiple conversations / users).
|
||||
interrupt_before: An optional list of node names to interrupt before.
|
||||
Should be one of the following: "agent", "tools".
|
||||
Should be one of the following: `"agent"`, `"tools"`.
|
||||
|
||||
This is useful if you want to add a user confirmation or other interrupt before taking an action.
|
||||
interrupt_after: An optional list of node names to interrupt after.
|
||||
Should be one of the following: "agent", "tools".
|
||||
Should be one of the following: `"agent"`, `"tools"`.
|
||||
|
||||
This is useful if you want to return directly or run additional processing on an output.
|
||||
debug: A flag indicating whether to enable debug mode.
|
||||
version: Determines the version of the graph to create.
|
||||
|
||||
Can be one of:
|
||||
|
||||
- `"v1"`: The tool node processes a single message. All tool
|
||||
@@ -443,7 +452,7 @@ def create_react_agent(
|
||||
Tool calls are distributed across multiple instances of the tool
|
||||
node using the [Send](https://langchain-ai.github.io/langgraph/concepts/low_level/#send)
|
||||
API.
|
||||
name: An optional name for the CompiledStateGraph.
|
||||
name: An optional name for the `CompiledStateGraph`.
|
||||
This name will be automatically used when adding ReAct agent graph to another graph as a subgraph node -
|
||||
particularly useful for building multi-agent systems.
|
||||
|
||||
@@ -453,14 +462,14 @@ def create_react_agent(
|
||||
|
||||
|
||||
Returns:
|
||||
A compiled LangChain runnable that can be used for chat interactions.
|
||||
A compiled LangChain `Runnable` that can be used for chat interactions.
|
||||
|
||||
The "agent" node calls the language model with the messages list (after applying the prompt).
|
||||
If the resulting AIMessage contains `tool_calls`, the graph will then call the ["tools"][langgraph.prebuilt.tool_node.ToolNode].
|
||||
The "tools" node executes the tools (1 tool per `tool_call`) and adds the responses to the messages list
|
||||
as `ToolMessage` objects. The agent node then calls the language model again.
|
||||
The process repeats until no more `tool_calls` are present in the response.
|
||||
The agent then returns the full list of messages as a dictionary containing the key "messages".
|
||||
The agent then returns the full list of messages as a dictionary containing the key `'messages'`.
|
||||
|
||||
``` mermaid
|
||||
sequenceDiagram
|
||||
|
||||
@@ -36,7 +36,7 @@ class ActionRequest(TypedDict):
|
||||
Contains the action type and any associated arguments needed for the action.
|
||||
|
||||
Attributes:
|
||||
action: The type or name of action being requested (e.g., "Approve XYZ action")
|
||||
action: The type or name of action being requested (e.g., `"Approve XYZ action"`)
|
||||
args: Key-value pairs of arguments needed for the action
|
||||
"""
|
||||
|
||||
@@ -89,14 +89,16 @@ class HumanResponse(TypedDict):
|
||||
|
||||
Attributes:
|
||||
type: The type of response:
|
||||
- "accept": Approves the current state without changes
|
||||
- "ignore": Skips/ignores the current step
|
||||
- "response": Provides text feedback or instructions
|
||||
- "edit": Modifies the current state/content
|
||||
|
||||
- `'accept'`: Approves the current state without changes
|
||||
- `'ignore'`: Skips/ignores the current step
|
||||
- `'response'`: Provides text feedback or instructions
|
||||
- `'edit'`: Modifies the current state/content
|
||||
args: The response payload:
|
||||
- None: For ignore/accept actions
|
||||
- str: For text responses
|
||||
- ActionRequest: For edit actions with updated content
|
||||
|
||||
- `None`: For ignore/accept actions
|
||||
- `str`: For text responses
|
||||
- `ActionRequest`: For edit actions with updated content
|
||||
"""
|
||||
|
||||
type: Literal["accept", "ignore", "response", "edit"]
|
||||
|
||||
@@ -6,6 +6,7 @@ Tools are functions that models can call to interact with external systems,
|
||||
APIs, databases, or perform computations.
|
||||
|
||||
The module implements design patterns for:
|
||||
|
||||
- Parallel execution of multiple tool calls for efficiency
|
||||
- Robust error handling with customizable error messages
|
||||
- State injection for tools that need access to graph state
|
||||
@@ -13,11 +14,13 @@ The module implements design patterns for:
|
||||
- Command-based state updates for advanced control flow
|
||||
|
||||
Key Components:
|
||||
`ToolNode`: Main class for executing tools in LangGraph workflows
|
||||
`InjectedState`: Annotation for injecting graph state into tools
|
||||
`InjectedStore`: Annotation for injecting persistent store into tools
|
||||
`ToolRuntime`: Runtime information for tools, bundling together state, context, config, stream_writer, tool_call_id, and store
|
||||
`tools_condition`: Utility function for conditional routing based on tool calls
|
||||
|
||||
- `ToolNode`: Main class for executing tools in LangGraph workflows
|
||||
- `InjectedState`: Annotation for injecting graph state into tools
|
||||
- `InjectedStore`: Annotation for injecting persistent store into tools
|
||||
- `ToolRuntime`: Runtime information for tools, bundling together `state`, `context`,
|
||||
`config`, `stream_writer`, `tool_call_id`, and `store`
|
||||
- `tools_condition`: Utility function for conditional routing based on tool calls
|
||||
|
||||
Typical Usage:
|
||||
```python
|
||||
@@ -552,44 +555,52 @@ class ToolNode(RunnableCallable):
|
||||
Output format depends on input type and tool behavior:
|
||||
|
||||
**For Regular tools**:
|
||||
|
||||
- Dict input → `{"messages": [ToolMessage(...)]}`
|
||||
- List input → `[ToolMessage(...)]`
|
||||
|
||||
**For Command tools**:
|
||||
|
||||
- Returns `[Command(...)]` or mixed list with regular tool outputs
|
||||
- Commands can update state, trigger navigation, or send messages
|
||||
- `Command` can update state, trigger navigation, or send messages
|
||||
|
||||
Args:
|
||||
tools: A sequence of tools that can be invoked by this node. Supports:
|
||||
tools: A sequence of tools that can be invoked by this node.
|
||||
|
||||
Supports:
|
||||
|
||||
- **BaseTool instances**: Tools with schemas and metadata
|
||||
- **Plain functions**: Automatically converted to tools with inferred schemas
|
||||
|
||||
name: The name identifier for this node in the graph. Used for debugging
|
||||
and visualization. Defaults to "tools".
|
||||
and visualization.
|
||||
tags: Optional metadata tags to associate with the node for filtering
|
||||
and organization. Defaults to `None`.
|
||||
and organization.
|
||||
handle_tool_errors: Configuration for error handling during tool execution.
|
||||
Supports multiple strategies:
|
||||
|
||||
- **True**: Catch all errors and return a ToolMessage with the default
|
||||
- `True`: Catch all errors and return a `ToolMessage` with the default
|
||||
error template containing the exception details.
|
||||
- **str**: Catch all errors and return a ToolMessage with this custom
|
||||
- `str`: Catch all errors and return a `ToolMessage` with this custom
|
||||
error message string.
|
||||
- **type[Exception]**: Only catch exceptions with the specified type and
|
||||
- `type[Exception]`: Only catch exceptions with the specified type and
|
||||
return the default error message for it.
|
||||
- **tuple[type[Exception], ...]**: Only catch exceptions with the specified
|
||||
- `tuple[type[Exception], ...]`: Only catch exceptions with the specified
|
||||
types and return default error messages for them.
|
||||
- **Callable[..., str]**: Catch exceptions matching the callable's signature
|
||||
- `Callable[..., str]`: Catch exceptions matching the callable's signature
|
||||
and return the string result of calling it with the exception.
|
||||
- **False**: Disable error handling entirely, allowing exceptions to
|
||||
- `False`: Disable error handling entirely, allowing exceptions to
|
||||
propagate.
|
||||
|
||||
Defaults to a callable that:
|
||||
- catches tool invocation errors (due to invalid arguments provided by the model) and returns a descriptive error message
|
||||
- ignores tool execution errors (they will be re-raised)
|
||||
|
||||
- Catches tool invocation errors (due to invalid arguments provided by the
|
||||
model) and returns a descriptive error message
|
||||
- Ignores tool execution errors (they will be re-raised)
|
||||
|
||||
messages_key: The key in the state dictionary that contains the message list.
|
||||
This same key will be used for the output `ToolMessage` objects.
|
||||
Defaults to "messages".
|
||||
|
||||
Allows custom state schemas with different message field names.
|
||||
|
||||
Examples:
|
||||
@@ -891,9 +902,6 @@ class ToolNode(RunnableCallable):
|
||||
return self._validate_tool_command(response, request.tool_call, input_type)
|
||||
if isinstance(response, ToolMessage):
|
||||
response.content = cast("str | list", msg_content_output(response.content))
|
||||
# Enrich ToolMessage with name if not set (e.g., from fallback handlers)
|
||||
if response.name is None:
|
||||
response.name = call["name"]
|
||||
return response
|
||||
|
||||
msg = f"Tool {call['name']} returned unexpected type: {type(response)}"
|
||||
@@ -1051,9 +1059,6 @@ class ToolNode(RunnableCallable):
|
||||
return self._validate_tool_command(response, request.tool_call, input_type)
|
||||
if isinstance(response, ToolMessage):
|
||||
response.content = cast("str | list", msg_content_output(response.content))
|
||||
# Enrich ToolMessage with name if not set (e.g., from fallback handlers)
|
||||
if response.name is None:
|
||||
response.name = call["name"]
|
||||
return response
|
||||
|
||||
msg = f"Tool {call['name']} returned unexpected type: {type(response)}"
|
||||
@@ -1399,7 +1404,7 @@ def tools_condition(
|
||||
"""Conditional routing function for tool-calling workflows.
|
||||
|
||||
This utility function implements the standard conditional logic for ReAct-style
|
||||
agents: if the last AI message contains tool calls, route to the tool execution
|
||||
agents: if the last `AIMessage` contains tool calls, route to the tool execution
|
||||
node; otherwise, end the workflow. This pattern is fundamental to most tool-calling
|
||||
agent architectures.
|
||||
|
||||
@@ -1408,16 +1413,15 @@ def tools_condition(
|
||||
|
||||
Args:
|
||||
state: The current graph state to examine for tool calls. Supported formats:
|
||||
- Dictionary containing a messages key (for StateGraph)
|
||||
- BaseModel instance with a messages attribute
|
||||
- Dictionary containing a messages key (for `StateGraph`)
|
||||
- `BaseModel` instance with a messages attribute
|
||||
messages_key: The key or attribute name containing the message list in the state.
|
||||
This allows customization for graphs using different state schemas.
|
||||
Defaults to "messages".
|
||||
|
||||
Returns:
|
||||
Either "tools" if tool calls are present in the last AI message, or "__end__"
|
||||
to terminate the workflow. These are the standard routing destinations for
|
||||
tool-calling conditional edges.
|
||||
Either `'tools'` if tool calls are present in the last `AIMessage`, or `'__end__'`
|
||||
to terminate the workflow. These are the standard routing destinations for
|
||||
tool-calling conditional edges.
|
||||
|
||||
Raises:
|
||||
ValueError: If no messages can be found in the provided state format.
|
||||
@@ -1614,7 +1618,7 @@ class InjectedStore(InjectedToolArg):
|
||||
|
||||
This annotation enables tools to access LangGraph's persistent storage system
|
||||
without exposing storage details to the language model. Tools annotated with
|
||||
InjectedStore receive the store instance automatically during execution while
|
||||
`InjectedStore` receive the store instance automatically during execution while
|
||||
remaining invisible to the model's tool-calling interface.
|
||||
|
||||
The store provides persistent, cross-session data storage that tools can use
|
||||
|
||||
@@ -45,9 +45,9 @@ def _default_format_error(
|
||||
category=LangGraphDeprecatedSinceV10,
|
||||
)
|
||||
class ValidationNode(RunnableCallable):
|
||||
"""A node that validates all tools requests from the last AIMessage.
|
||||
"""A node that validates all tools requests from the last `AIMessage`.
|
||||
|
||||
It can be used either in StateGraph with a "messages" key.
|
||||
It can be used either in `StateGraph` with a `'messages'` key.
|
||||
|
||||
!!! note
|
||||
|
||||
@@ -57,7 +57,8 @@ class ValidationNode(RunnableCallable):
|
||||
messages and tool IDs (for use in multi-turn conversations).
|
||||
|
||||
Returns:
|
||||
(Union[Dict[str, List[ToolMessage]], Sequence[ToolMessage]]): A list of ToolMessages with the validated content or error messages.
|
||||
(Union[Dict[str, List[ToolMessage]], Sequence[ToolMessage]]): A list of
|
||||
`ToolMessage` objects with the validated content or error messages.
|
||||
|
||||
Example:
|
||||
```python title="Example usage for re-prompting the model to generate a valid response:"
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
"""Test to reproduce the tool error fallback issue.
|
||||
|
||||
When using ToolNode(...).with_fallbacks(...) with an error handler that returns
|
||||
ToolMessage objects, the messages are losing:
|
||||
1. The `name` field (becomes `None` instead of the tool name)
|
||||
2. The `status` field (becomes `success` instead of `error`)
|
||||
"""
|
||||
|
||||
from unittest.mock import Mock
|
||||
|
||||
from langchain_core.messages import AIMessage, ToolMessage
|
||||
from langchain_core.runnables import RunnableLambda
|
||||
from langchain_core.runnables.config import RunnableConfig
|
||||
from langchain_core.tools import tool
|
||||
|
||||
from langgraph.prebuilt import ToolNode
|
||||
|
||||
|
||||
def _create_mock_runtime():
|
||||
"""Create a mock Runtime object for testing ToolNode outside of graph context."""
|
||||
mock_runtime = Mock()
|
||||
mock_runtime.store = None
|
||||
mock_runtime.context = None
|
||||
mock_runtime.stream_writer = lambda *args, **kwargs: None
|
||||
return mock_runtime
|
||||
|
||||
|
||||
def _create_config_with_runtime() -> RunnableConfig:
|
||||
"""Create a RunnableConfig with mock Runtime for testing ToolNode."""
|
||||
return {"configurable": {"__pregel_runtime": _create_mock_runtime()}}
|
||||
|
||||
|
||||
@tool
|
||||
def failing_tool(x: int) -> str:
|
||||
"""A tool that always fails."""
|
||||
raise RuntimeError("This tool always fails!")
|
||||
|
||||
|
||||
def handle_tool_error(state) -> dict:
|
||||
"""Error handler that returns ToolMessages."""
|
||||
print(f"handle_tool_error called with state: {state}")
|
||||
print(f"State type: {type(state)}")
|
||||
print(f"State keys: {state.keys() if isinstance(state, dict) else 'N/A'}")
|
||||
error = state.get("error")
|
||||
print(f"Error: {error}")
|
||||
tool_calls = state["messages"][-1].tool_calls
|
||||
print(f"Tool calls: {tool_calls}")
|
||||
return {
|
||||
"messages": [
|
||||
ToolMessage(
|
||||
content=f"Error: {repr(error)}\n please fix your mistakes.",
|
||||
tool_call_id=tc["id"],
|
||||
)
|
||||
for tc in tool_calls
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
fallback_runnable = RunnableLambda(handle_tool_error)
|
||||
|
||||
|
||||
def test_tool_error_with_fallbacks():
|
||||
"""Test that ToolMessages from fallback handlers preserve name and status."""
|
||||
# Create a ToolNode with a fallback
|
||||
tool_node = ToolNode([failing_tool], handle_tool_errors=True).with_fallbacks(
|
||||
[fallback_runnable],
|
||||
)
|
||||
|
||||
# Create an AI message with a tool call
|
||||
messages = [
|
||||
AIMessage(
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "failing_tool",
|
||||
"args": {"x": 1},
|
||||
"id": "call_123",
|
||||
"type": "tool_call",
|
||||
}
|
||||
],
|
||||
)
|
||||
]
|
||||
|
||||
# Invoke the tool node
|
||||
result = tool_node.invoke({"messages": messages}, config=_create_config_with_runtime())
|
||||
|
||||
print("Result:", result)
|
||||
print("Result type:", type(result))
|
||||
print("Result keys:", result.keys() if isinstance(result, dict) else "N/A")
|
||||
print("\nTool messages:")
|
||||
for msg in result["messages"]:
|
||||
if isinstance(msg, ToolMessage):
|
||||
print(f" - content: {msg.content[:50]}...")
|
||||
print(f" name: {msg.name}")
|
||||
print(f" tool_call_id: {msg.tool_call_id}")
|
||||
print(f" status: {msg.status}")
|
||||
print()
|
||||
|
||||
# Check expectations
|
||||
assert msg.name == "failing_tool", f"Expected name='failing_tool', got {msg.name}"
|
||||
assert msg.status == "error", f"Expected status='error', got {msg.status}"
|
||||
print("✓ Test passed!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_tool_error_with_fallbacks()
|
||||
Reference in New Issue
Block a user