Fixes: #5787
Ensures that if `config` is not typed as one of `RunanbleConfig` or
`Optional[RunnableConfig]` a warning is raised to help developers avoid
unexpected results at invocation time.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Sydney Runkle <sydneymarierunkle@gmail.com>
Fixes https://github.com/langchain-ai/langgraph/issues/5795
* Must use `category=None` on decorator so that we get type checking
support but no dupe warning
* Fixed tuple on `confix_type` warning causing false warning
I suppose that the code snippets are intended to run each on its own. To
guarantee this the snippet for the example:
"Write long-temr memory from tools"
needs to include `RunnableConfig`
Same also for the second commit of this pull request.
The other commits are about similar issues, where imports are missing to
make a snippet executable on its own.
---------
Signed-off-by: Kai Wendel <kai.wendel@iws.uni-stuttgart.de>
Co-authored-by: Eugene Yurtsev <eugene@langchain.dev>
Co-authored-by: Lauren Hirata Singh <lauren@langchain.dev>
- **Description:** Update the type annotations in create_react_agent to
allow one to provide a callable for the model that uses bind_tools and
returns a Runnable[LanguageModelInput, BaseMessage]
- **Issue:** #5739
---------
Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
Fixes#5735
Implement context coercion functionality for LangGraph runtime to
improve API usability.
Key changes:
- Added `_coerce_context` function in `pregel/main.py`
- Supports coercion for:
- Pydantic BaseModel
- Dataclasses
- TypedDict
- Comprehensive test coverage added in `tests/test_runtime.py`
- Handles edge cases like None context and missing fields
The implementation allows users to pass dictionaries as context, which
will be automatically converted to the expected schema type, making the
API more flexible and user-friendly.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Sydney Runkle <sydneymarierunkle@gmail.com>
Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
## Description
Previously, when a tool returned `Command` to update the graph's state,
the `_validate_tool_command` method in `ToolNode` would raise a
`ValueError` if the `messages_update` list contained only a
`RemoveMessage(id=REMOVE_ALL_MESSAGES)` object. This was because the
validation logic expected a matching `ToolMessage` for the tool call and
did not account for this specific state-clearing scenario.
This commit modifies the validation logic to check if the
`messages_update` list contains a single
`RemoveMessage(id=REMOVE_ALL_MESSAGES)` element. If this condition is
met, the `ToolMessage` validation is bypassed, allowing a tool to clear
the entire message history without causing a validation error.
A new test case, `test_tool_node_command_remove_all_messages`, has been
added to `tests/test_tool_node.py` to verify this change and prevent
future regressions.
## Example
Here is a self-contained example that illustrates the problem and the
fix. Without this change, the code block for `Example 2` would raise a
`ValueError`.
```python
from typing import Annotated, List
from langchain_core.messages import (
AIMessage,
AnyMessage,
HumanMessage,
RemoveMessage,
ToolMessage,
)
from langchain_core.tools import InjectedToolCallId, tool
from langchain_openai import ChatOpenAI
from langgraph.graph import END, StateGraph, add_messages
from langgraph.graph.message import REMOVE_ALL_MESSAGES
from langgraph.prebuilt import InjectedState, ToolNode
from langgraph.types import Command
from pydantic import BaseModel, Field
# Agent state tracks current and all messages
class AgentState(BaseModel):
messages: Annotated[List[AnyMessage], add_messages] = Field(
default_factory=list, description="Current conversation messages."
)
all_messages: Annotated[List[AnyMessage], add_messages] = Field(
default_factory=list, description="All messages, including removed ones."
)
# Tool to clear history if long enough, otherwise returns a warning
@tool
def clear_history_tool(
state: Annotated[AgentState, InjectedState],
tool_call_id: Annotated[str, InjectedToolCallId],
):
"""Clears message history if it's long enough."""
if len(state.messages) < 3:
return Command(
update={
"messages": [
ToolMessage(
"History is not long enough to be cleared. Please try again.",
tool_call_id=tool_call_id,
)
]
}
)
else:
return Command(
update={
"messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES)],
"all_messages": state.messages
+ [
ToolMessage(
"History has been successfully cleared.",
tool_call_id=tool_call_id,
)
],
}
)
# Bind the tool to the model
model = ChatOpenAI(model="gpt-4o-mini").bind_tools([clear_history_tool])
def model_node(state: AgentState):
return {"messages": [model.invoke(state.messages)]}
# Build the agent graph
graph_builder = StateGraph(AgentState)
graph_builder.add_node("model", model_node)
graph_builder.add_node("tools", ToolNode([clear_history_tool]))
graph_builder.set_entry_point("model")
graph_builder.add_edge("model", "tools")
graph_builder.add_edge("tools", END)
graph = graph_builder.compile()
def print_messages(header, messages):
print(f"\n{header}")
for message in messages:
message.pretty_print()
### Example 1: Not enough history to clear
state_1 = AgentState(
messages=[HumanMessage(content="Please clear my message history.")]
)
output_1 = graph.invoke(state_1)
print_messages("First call: State 'messages'", output_1["messages"])
print_messages("First call: State 'all_messages'", output_1["all_messages"])
### Example 2: History is cleared
state_2 = AgentState(
messages=[
HumanMessage(content="Will this PR get merged?"),
AIMessage(content="Maybe, if it's good enough."),
HumanMessage(content="Please clear my message history."),
]
)
# Without the changes in this PR, the following line will raise a ValueError
output_2 = graph.invoke(state_2)
print_messages("Second call: State 'messages'", output_2["messages"])
print_messages("Second call: State 'all_messages'", output_2["all_messages"])
```
### Outputs
*Without the changes in this PR:*
```
First call: State 'messages'
================================ Human Message =================================
Please clear my message history.
================================== Ai Message ==================================
Tool Calls:
clear_history_tool (ba421ac3-1e1a-4208-a8f6-c5500ee0abcc)
Call ID: ba421ac3-1e1a-4208-a8f6-c5500ee0abcc
Args:
================================= Tool Message =================================
Name: clear_history_tool
History is not long enough to be cleared. Please try again.
First call: State 'all_messages'
Traceback (most recent call last):
File "main.py", line 114, in <module>
output_2 = graph.invoke(state_2)
^^^^^^^^^^^^^^^^^^^^^
File ".venv/lib/python3.11/site-packages/langgraph/pregel/__init__.py", line 2844, in invoke
for chunk in self.stream(
File ".venv/lib/python3.11/site-packages/langgraph/pregel/__init__.py", line 2534, in stream
for _ in runner.tick(
File ".venv/lib/python3.11/site-packages/langgraph/prebuilt/tool_node.py", line 241, in _func
outputs = [
^
File ".venv/lib/python3.11/concurrent/futures/_base.py", line 619, in result_iterator
yield _result_or_cancel(fs.pop())
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File ".venv/lib/python3.11/concurrent/futures/_base.py", line 317, in _result_or_cancel
return fut.result(timeout)
^^^^^^^^^^^^^^^^^^^
File ".venv/lib/python3.11/concurrent/futures/_base.py", line 449, in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
File ".venv/lib/python3.11/concurrent/futures/_base.py", line 401, in __get_result
raise self._exception
File ".venv/lib/python3.11/concurrent/futures/thread.py", line 58, in run
result = self.fn(*self.args, **self.kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File ".venv/lib/python3.11/site-packages/langchain_core/runnables/config.py", line 555, in _wrapped_fn
return contexts.pop().run(fn, *args)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File ".venv/lib/python3.11/site-packages/langgraph/prebuilt/tool_node.py", line 353, in _run_one
return self._validate_tool_command(response, call, input_type)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File ".venv/lib/python3.11/site-packages/langgraph/prebuilt/tool_node.py", line 616, in _validate_tool_command
raise ValueError(
ValueError: Expected to have a matching ToolMessage in Command.update for tool 'clear_history_tool', got: [RemoveMessage(content='', additional_kwargs={}, response_metadata={}, id='__remove_all__')]. Every tool call (LLM requesting to call a tool) in the message history MUST have a corresponding ToolMessage. You can fix it by modifying the tool to return `Command(update={"messages": [ToolMessage("Success", tool_call_id=tool_call_id), ...]}, ...)`.
```
*With the changes in this PR:*
```
First call: State 'messages'
================================ Human Message =================================
Please clear my message history.
================================== Ai Message ==================================
Tool Calls:
clear_history_tool (ba421ac3-1e1a-4208-a8f6-c5500ee0abcc)
Call ID: ba421ac3-1e1a-4208-a8f6-c5500ee0abcc
Args:
================================= Tool Message =================================
Name: clear_history_tool
History is not long enough to be cleared. Please try again.
First call: State 'all_messages'
Second call: State 'messages'
Second call: State 'all_messages'
================================ Human Message =================================
Will this PR get merged?
================================== Ai Message ==================================
Maybe, if it's good enough.
================================ Human Message =================================
Please clear my message history.
================================== Ai Message ==================================
Tool Calls:
clear_history_tool (499b1be3-6df1-493f-85e5-8d7e429dead8)
Call ID: 499b1be3-6df1-493f-85e5-8d7e429dead8
Args:
================================= Tool Message =================================
Name: clear_history_tool
History has been successfully cleared.
```
## Twitter handle
[@samuelpullely](https://x.com/samuelpullely)
---------
Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
This PR allows a developer to change the model configuration at run time based on context. This includes that list of tools available to the model to call.
```python
def create_react_agent(
model: Union[
str,
LanguageModelLike,
Callable[[SateLike, Runtime...], BaseChatModel], # <--- New
],
tools: Union[
Sequence[Union[BaseTool, Callable, dict[str, Any]]], ToolNode]
],
*,
....
llm = init_chat_model(...)
def prepare_model(state, runtime):
selected_tool_names = func(state, context)
return llm.bind(tools=selected_tool_names)
create_react_agent(
prepare_model,
tools=all_known_tools
)
```
## Semantics
1. `tools` = are the known tools, used to configure ToolNode and will
configure:
1. model provided as string
2. model provided as BaseChatModel (if it has no tools bound to it)
2. If a user provides a dynamic model (callable), the user is
responsible for binding tools
Alternative considered:
1. Passing `Callable[[SateLike, Config...], list[BaseTool]]` to tools
2. Passing `Callable[[SateLike, Config...], list[str]]` to a tool
selector
Both have the issue that there's non obvious interplay between tool
selection and dynamic models. (i.e., if we want to introduce dynamic
models at in the future, the API will become tricky to explain)
---------
Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>