mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-24 00:22:25 +02:00
fix(sdk-py): make tools_agent fake model stateless (#7930)
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
"""create_agent-based example exercising the v3 ``tools`` channel.
|
||||
"""create_agent-based example exercising the v3 `tools` channel.
|
||||
|
||||
`thread.tool_calls` and the underlying ``tools`` channel only emit
|
||||
`thread.tool_calls` and the underlying `tools` channel only emit
|
||||
events when an actual model issues a tool call through langchain's
|
||||
agent stack. The synthetic ``streaming_graph.py`` hand-builds
|
||||
agent stack. The synthetic `streaming_graph.py` hand-builds
|
||||
`AIMessage(tool_calls=[...])` and a `ToolMessage` via the messages
|
||||
reducer — that gets persisted in state but never produces tool-call
|
||||
telemetry on the wire. This graph fixes that by going through
|
||||
`create_agent` with a real tool, driven by a `GenericFakeChatModel` so
|
||||
the test stays hermetic (no `ANTHROPIC_API_KEY` required).
|
||||
`create_agent` with a real tool, driven by a hermetic fake chat model
|
||||
(no `ANTHROPIC_API_KEY` required).
|
||||
|
||||
Flow on `run.start`:
|
||||
|
||||
@@ -25,7 +25,8 @@ from typing import Any
|
||||
|
||||
from langchain.agents import create_agent
|
||||
from langchain_core.language_models.fake_chat_models import FakeMessagesListChatModel
|
||||
from langchain_core.messages import AIMessage
|
||||
from langchain_core.messages import AIMessage, BaseMessage, ToolMessage
|
||||
from langchain_core.outputs import ChatGeneration, ChatResult
|
||||
from langchain_core.tools import tool
|
||||
|
||||
|
||||
@@ -36,51 +37,56 @@ def search(query: str) -> str:
|
||||
|
||||
|
||||
class _ToolBindingFakeChatModel(FakeMessagesListChatModel):
|
||||
"""Fake chat model that satisfies `create_agent`'s ``bind_tools`` call.
|
||||
"""Stateless fake chat model driving a single `search` tool call.
|
||||
|
||||
``create_agent`` calls ``model.bind_tools(tools)`` to attach the tool
|
||||
schema (``langchain/agents/factory.py:1284``). The base
|
||||
``FakeMessagesListChatModel`` inherits ``BaseChatModel.bind_tools``,
|
||||
which raises ``NotImplementedError``. We don't actually need the
|
||||
bound schema — the fake replays scripted ``AIMessage``s with their
|
||||
own ``tool_calls`` field — so override ``bind_tools`` as a no-op.
|
||||
`create_agent` calls `model.bind_tools(tools)` to attach the tool
|
||||
schema (`langchain/agents/factory.py:1284`). The base
|
||||
`FakeMessagesListChatModel` inherits `BaseChatModel.bind_tools`,
|
||||
which raises `NotImplementedError`, so `bind_tools` is overridden as
|
||||
a no-op (the reply is hand-built and already carries `tool_calls`).
|
||||
|
||||
``FakeMessagesListChatModel`` is preferred over
|
||||
``GenericFakeChatModel`` here because the latter's ``_stream``
|
||||
breaks the message into content chunks and **drops ``tool_calls``**
|
||||
when content is empty, causing the v2 streaming path inside
|
||||
``create_agent`` to raise ``RuntimeError("v2 stream finished
|
||||
without producing a message")``. ``FakeMessagesListChatModel``
|
||||
falls back to the default ``_stream`` that yields the whole
|
||||
message in one chunk, preserving ``tool_calls``.
|
||||
The reply is derived from conversation state rather than a cycling
|
||||
response list: the `search` tool call is issued until a `ToolMessage`
|
||||
appears, then a terminating `AIMessage`. This avoids the response-index
|
||||
parity flake where `FakeMessagesListChatModel.responses` is shared
|
||||
process-wide and cycles `0 -> 1 -> 0`; a run that started mid-cycle
|
||||
(e.g. on a reused server worker) would reply `"done."` first and emit
|
||||
no tool call. Being order-independent, every run emits exactly one
|
||||
tool call regardless of how many times the model was previously called.
|
||||
|
||||
`FakeMessagesListChatModel` is subclassed (rather than
|
||||
`GenericFakeChatModel`) because the latter's `_stream` breaks the
|
||||
message into content chunks and drops `tool_calls` when content is
|
||||
empty, causing the v2 streaming path inside `create_agent` to raise
|
||||
`RuntimeError("v2 stream finished without producing a message")`.
|
||||
The inherited `_stream` yields the whole message in one chunk,
|
||||
preserving `tool_calls`.
|
||||
"""
|
||||
|
||||
def bind_tools(self, tools: Any, **kwargs: Any) -> _ToolBindingFakeChatModel:
|
||||
return self
|
||||
|
||||
|
||||
# Two scripted turns. ``FakeMessagesListChatModel`` cycles through
|
||||
# ``responses`` (resetting to index 0 after the last) so the graph
|
||||
# can be run many times without restart; per run, ``create_agent``
|
||||
# invokes the model exactly twice (once to issue the tool call,
|
||||
# once after the tool result to produce the terminating answer).
|
||||
_supervisor_responses: list[AIMessage] = [
|
||||
AIMessage(
|
||||
content="",
|
||||
id="ai-tools-1",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "tc-1",
|
||||
"name": "search",
|
||||
"args": {"query": "v3"},
|
||||
}
|
||||
],
|
||||
),
|
||||
AIMessage(content="done.", id="ai-tools-2"),
|
||||
]
|
||||
def _generate(
|
||||
self,
|
||||
messages: list[BaseMessage],
|
||||
stop: list[str] | None = None,
|
||||
run_manager: Any = None,
|
||||
**kwargs: Any,
|
||||
) -> ChatResult:
|
||||
if any(isinstance(m, ToolMessage) for m in messages):
|
||||
response = AIMessage(content="done.", id="ai-tools-done")
|
||||
else:
|
||||
response = AIMessage(
|
||||
content="",
|
||||
id="ai-tools-call",
|
||||
tool_calls=[{"id": "tc-1", "name": "search", "args": {"query": "v3"}}],
|
||||
)
|
||||
return ChatResult(generations=[ChatGeneration(message=response)])
|
||||
|
||||
|
||||
_supervisor_model = _ToolBindingFakeChatModel(responses=_supervisor_responses)
|
||||
# `responses` is a required field on `FakeMessagesListChatModel`, but the
|
||||
# overridden `_generate` derives its reply from state and never reads it.
|
||||
_supervisor_model = _ToolBindingFakeChatModel(responses=[])
|
||||
|
||||
|
||||
graph = create_agent(
|
||||
|
||||
Reference in New Issue
Block a user