Vaibhav Kathait Claude Sonnet 5 commited on
Commit ·
57f672f
1
Parent(s): 7f73287
Stream agent replies from the first LLM call to cut response latency
Browse filesPreviously every turn made a non-streaming call to check for tool calls,
then re-generated the same answer with a second streaming call even when
no tool was needed — doubling time-to-first-token before TTS/animation
could start. Now the first call streams directly; tool calls are
reassembled from the stream and only trigger a follow-up call when
actually requested.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- agent/agent.py +46 -26
agent/agent.py
CHANGED
|
@@ -78,40 +78,60 @@ async def run_agent(messages: list, model: str | None = None) -> str:
|
|
| 78 |
|
| 79 |
|
| 80 |
async def stream_agent(messages: list, model: str | None = None) -> AsyncGenerator[str, None]:
|
| 81 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
history = _build_history(messages)
|
| 83 |
m = model or LLM_MODEL
|
| 84 |
|
| 85 |
-
# Tool-resolution phase — non-streaming until no more tool calls
|
| 86 |
while True:
|
| 87 |
-
|
| 88 |
model=m,
|
| 89 |
messages=history,
|
| 90 |
tools=TOOLS,
|
| 91 |
tool_choice="auto",
|
|
|
|
| 92 |
store=True,
|
| 93 |
)
|
| 94 |
-
msg = resp.choices[0].message
|
| 95 |
-
|
| 96 |
-
if not msg.tool_calls:
|
| 97 |
-
break # hand off to streaming pass
|
| 98 |
-
|
| 99 |
-
history.append(msg.model_dump(exclude_none=True))
|
| 100 |
-
for tc in msg.tool_calls:
|
| 101 |
-
args = json.loads(tc.function.arguments or "{}")
|
| 102 |
-
result = await run_tool(tc.function.name, args)
|
| 103 |
-
history.append({"role": "tool", "tool_call_id": tc.id, "content": result})
|
| 104 |
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
|
| 80 |
async def stream_agent(messages: list, model: str | None = None) -> AsyncGenerator[str, None]:
|
| 81 |
+
"""
|
| 82 |
+
Stream tokens as soon as the model produces them. Every pass is a single
|
| 83 |
+
streaming call — if the model asks for tool calls we resolve them and
|
| 84 |
+
loop with another streaming call; if it answers directly, those tokens
|
| 85 |
+
are yielded immediately with no extra non-streamed round trip first.
|
| 86 |
+
"""
|
| 87 |
history = _build_history(messages)
|
| 88 |
m = model or LLM_MODEL
|
| 89 |
|
|
|
|
| 90 |
while True:
|
| 91 |
+
stream = await _get_client().chat.completions.create(
|
| 92 |
model=m,
|
| 93 |
messages=history,
|
| 94 |
tools=TOOLS,
|
| 95 |
tool_choice="auto",
|
| 96 |
+
stream=True,
|
| 97 |
store=True,
|
| 98 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
+
tool_calls: dict[int, dict] = {}
|
| 101 |
+
async for chunk in stream:
|
| 102 |
+
delta = chunk.choices[0].delta
|
| 103 |
+
if delta.content:
|
| 104 |
+
yield delta.content
|
| 105 |
+
for tc_delta in (delta.tool_calls or []):
|
| 106 |
+
slot = tool_calls.setdefault(
|
| 107 |
+
tc_delta.index, {"id": "", "name": "", "arguments": ""}
|
| 108 |
+
)
|
| 109 |
+
if tc_delta.id:
|
| 110 |
+
slot["id"] = tc_delta.id
|
| 111 |
+
if tc_delta.function:
|
| 112 |
+
if tc_delta.function.name:
|
| 113 |
+
slot["name"] += tc_delta.function.name
|
| 114 |
+
if tc_delta.function.arguments:
|
| 115 |
+
slot["arguments"] += tc_delta.function.arguments
|
| 116 |
+
|
| 117 |
+
if not tool_calls:
|
| 118 |
+
return # final answer was already streamed above
|
| 119 |
+
|
| 120 |
+
ordered = [tool_calls[i] for i in sorted(tool_calls)]
|
| 121 |
+
history.append({
|
| 122 |
+
"role": "assistant",
|
| 123 |
+
"content": None,
|
| 124 |
+
"tool_calls": [
|
| 125 |
+
{
|
| 126 |
+
"id": tc["id"],
|
| 127 |
+
"type": "function",
|
| 128 |
+
"function": {"name": tc["name"], "arguments": tc["arguments"]},
|
| 129 |
+
}
|
| 130 |
+
for tc in ordered
|
| 131 |
+
],
|
| 132 |
+
})
|
| 133 |
+
for tc in ordered:
|
| 134 |
+
args = json.loads(tc["arguments"] or "{}")
|
| 135 |
+
print(f"[AGENT] tool call: {tc['name']}({args})", flush=True)
|
| 136 |
+
result = await run_tool(tc["name"], args)
|
| 137 |
+
history.append({"role": "tool", "tool_call_id": tc["id"], "content": result})
|