a7d6d88f6f
CI / changes (push) Has been cancelled
CI / cd libs/checkpoint (push) Has been cancelled
CI / cd libs/checkpoint-conformance (push) Has been cancelled
CI / cd libs/checkpoint-postgres (push) Has been cancelled
CI / cd libs/checkpoint-sqlite (push) Has been cancelled
CI / cd libs/cli (push) Has been cancelled
CI / cd libs/prebuilt (push) Has been cancelled
CI / cd libs/sdk-py (push) Has been cancelled
CI / cd libs/langgraph (push) Has been cancelled
CI / Check SDK methods matching (push) Has been cancelled
CI / Check CLI schema hasn't changed #3.13 (push) Has been cancelled
CI / CLI integration test (push) Has been cancelled
CI / sdk-py integration test (push) Has been cancelled
CI / CI Success (push) Has been cancelled
baseline / benchmark (push) Has been cancelled
Deploy Redirects to GitHub Pages / deploy (push) Has been cancelled
98 lines
3.9 KiB
Python
98 lines
3.9 KiB
Python
"""create_agent-based example exercising the v3 `tools` channel.
|
|
|
|
`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
|
|
`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 hermetic fake chat model
|
|
(no `ANTHROPIC_API_KEY` required).
|
|
|
|
Flow on `run.start`:
|
|
|
|
1. Supervisor model returns an `AIMessage(tool_calls=[search(query="v3")])`.
|
|
2. langchain's tool node executes `search` and produces a `ToolMessage`.
|
|
3. Supervisor model returns a final `AIMessage("done.")` to terminate.
|
|
|
|
The v3 streaming layer surfaces this as `messages` + `tools` channel
|
|
events at root namespace.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
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, BaseMessage, ToolMessage
|
|
from langchain_core.outputs import ChatGeneration, ChatResult
|
|
from langchain_core.tools import tool
|
|
|
|
|
|
@tool
|
|
def search(query: str) -> str:
|
|
"""Look up `query` in a fake search index."""
|
|
return f"result for {query!r}"
|
|
|
|
|
|
class _ToolBindingFakeChatModel(FakeMessagesListChatModel):
|
|
"""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`, so `bind_tools` is overridden as
|
|
a no-op (the reply is hand-built and already carries `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
|
|
|
|
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)])
|
|
|
|
|
|
# `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(
|
|
model=_supervisor_model,
|
|
tools=[search],
|
|
system_prompt="You are a research assistant. Use the search tool when asked.",
|
|
name="v3_tools_agent",
|
|
)
|