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
97 lines
3.4 KiB
Python
97 lines
3.4 KiB
Python
"""Exercise `thread.tool_calls` against the `tools_agent` graph.
|
|
|
|
`tools_agent` (`graph/tools_agent.py`) wraps a
|
|
`FakeMessagesListChatModel` in `create_agent` with a real `search`
|
|
tool. The first scripted model turn returns an `AIMessage` with a
|
|
`tool_calls=[search(query="v3")]`; langchain's tool node then executes
|
|
`search` and surfaces a `ToolMessage`; the second turn returns a final
|
|
`AIMessage("done.")` that terminates the agent.
|
|
|
|
Compared to `test_tool_calls.py` (which targets the synthetic
|
|
`streaming_graph` and never produces real tool-call telemetry), this
|
|
test verifies the v3 ``tools`` channel actually fires when the
|
|
canonical langchain-agent surface is in play.
|
|
|
|
Pattern note: `thread.tool_calls` yields handles incrementally, but
|
|
each handle's `deltas` and `output` are only completed when the
|
|
*outer* iterator processes the matching `tool-finished` event. Drain
|
|
the outer iterator to completion FIRST (via a list comprehension),
|
|
then inspect handles -- this is the same pattern used in
|
|
`tests/streaming/test_tool_calls_projection.py`. Iterating
|
|
`handle.deltas` while the outer iterator is still suspended at its
|
|
`yield` deadlocks.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
|
|
from _common import check_api_reachable, header, make_async_client, make_sync_client
|
|
|
|
TOOLS_ASSISTANT_ID = "tools_agent"
|
|
|
|
|
|
async def run_async() -> None:
|
|
header("async tools_agent tool_calls")
|
|
threads, raw = make_async_client()
|
|
try:
|
|
async with threads.stream(assistant_id=TOOLS_ASSISTANT_ID) as thread:
|
|
await thread.run.start(
|
|
input={"messages": [{"role": "human", "content": "search for v3"}]}
|
|
)
|
|
|
|
# Drain the outer iterator first; lifecycle-terminal triggers
|
|
# the None sentinel via the shared SSE fanout once the run
|
|
# completes naturally.
|
|
handles = [h async for h in thread.tool_calls]
|
|
print(f" total handles: {len(handles)}")
|
|
for handle in handles:
|
|
deltas = [d async for d in handle.deltas]
|
|
output = await handle.output
|
|
joined = "".join(deltas)
|
|
print(
|
|
f" tool {handle.name}({handle.tool_call_id}): "
|
|
f"args_stream={joined!r} output={output!r}"
|
|
)
|
|
assert any(h.name == "search" for h in handles), (
|
|
"expected `search` tool call"
|
|
)
|
|
finally:
|
|
await raw.aclose()
|
|
|
|
|
|
def run_sync() -> None:
|
|
header("sync tools_agent tool_calls")
|
|
threads, raw = make_sync_client()
|
|
try:
|
|
with threads.stream(assistant_id=TOOLS_ASSISTANT_ID) as thread:
|
|
thread.run.start(
|
|
input={"messages": [{"role": "human", "content": "search for v3"}]}
|
|
)
|
|
|
|
handles = list(thread.tool_calls)
|
|
print(f" total handles: {len(handles)}")
|
|
for handle in handles:
|
|
deltas = list(handle.deltas)
|
|
output = handle.output
|
|
joined = "".join(deltas)
|
|
print(
|
|
f" tool {handle.name}({handle.tool_call_id}): "
|
|
f"args_stream={joined!r} output={output!r}"
|
|
)
|
|
assert any(h.name == "search" for h in handles), (
|
|
"expected `search` tool call"
|
|
)
|
|
finally:
|
|
raw.close()
|
|
|
|
|
|
def main() -> None:
|
|
check_api_reachable()
|
|
asyncio.run(run_async())
|
|
run_sync()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|