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 files

Previously 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>

Files changed (1) hide show
  1. 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
- """Resolve all tool calls, then stream the final response token by token."""
 
 
 
 
 
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
- resp = await _get_client().chat.completions.create(
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
- # Streaming final-answer phase
106
- stream = await _get_client().chat.completions.create(
107
- model=m,
108
- messages=history,
109
- tools=TOOLS,
110
- tool_choice="none",
111
- stream=True,
112
- store=True,
113
- )
114
- async for chunk in stream:
115
- token = chunk.choices[0].delta.content
116
- if token:
117
- yield token
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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})