Stream conversational acknowledgment in parallel with tool execution

When the intent router detects a tool call, the acknowledgment sentence
and the tool now execute concurrently via asyncio.gather. The acknowledgment
uses the small intent model (already in VRAM) with max_tokens=40, so it
completes in ~200-400ms — the user sees text almost immediately instead of
staring at a status label for the full main-model TTFT (~22s).

The acknowledgment text is:
- Streamed to the client as a chunk event (clears the status spinner)
- Included in the assistant message for round 1 so the main LLM continues
  coherently from where the acknowledgment left off
- Recorded in TTFT timing (acknowledgment counts as first token)

Varied phrasing is enforced in the system prompt so responses feel natural
rather than formulaic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-18 20:29:58 -05:00
parent 7b02cc5cfd
commit 1b63371bb3
@@ -50,6 +50,59 @@ _TOOL_LABELS: dict[str, str] = {
"delete_todo": "Removing todo",
}
# Action phrases used in the acknowledgment prompt — "You are about to: {action}"
_TOOL_ACTIONS: dict[str, str] = {
"create_task": "create a task",
"create_note": "create a new note",
"update_note": "update an existing note",
"list_tasks": "look up tasks",
"search_notes": "search through notes",
"create_event": "schedule a calendar event",
"list_events": "check the calendar",
"search_events": "search calendar events",
"update_event": "update a calendar event",
"delete_event": "remove a calendar event",
"list_calendars": "list available calendars",
"create_todo": "create a calendar todo",
"list_todos": "check calendar todos",
"update_todo": "update a calendar todo",
"complete_todo": "mark a todo complete",
"delete_todo": "remove a calendar todo",
}
async def _generate_acknowledgment(user_content: str, tool_name: str, model: str) -> str:
"""Generate a brief conversational acknowledgment that runs in parallel with tool execution.
Uses the intent model (small, fast, already in VRAM) so the sentence is ready
within ~200-400ms. Returned string includes a trailing double-newline so the
main LLM response starts on a new paragraph.
"""
action = _TOOL_ACTIONS.get(tool_name, "work on that")
messages = [
{
"role": "system",
"content": (
"You are a helpful assistant. Write ONE short, natural sentence acknowledging "
"what you are about to do. Vary your phrasing — do not always start with "
"'Let me'. Be warm and conversational. Do not answer the question yet. "
"Output only the sentence, nothing else."
),
},
{
"role": "user",
"content": f"User said: {user_content}\nYou are about to: {action}",
},
]
try:
ack = await generate_completion(messages, model, max_tokens=40)
ack = ack.strip()
if ack:
return ack + "\n\n"
except Exception:
logger.warning("Failed to generate acknowledgment", exc_info=True)
return ""
async def _generate_title(messages: list[dict], model: str) -> str:
"""Ask the LLM for a concise conversation title."""
@@ -186,11 +239,26 @@ async def run_generation(
)
if intent.should_execute:
buf.append_event("status", {"status": f"{_TOOL_LABELS.get(intent.tool_name, 'Working')}..."})
# Run tool execution and acknowledgment generation in parallel.
# The acknowledgment uses the fast intent model (already in VRAM),
# so the user sees text within ~200-400ms instead of waiting for
# the full main-model TTFT (~22s).
t_tool = time.monotonic()
result = await execute_tool(user_id, intent.tool_name, intent.arguments)
result, ack_text = await asyncio.gather(
execute_tool(user_id, intent.tool_name, intent.arguments),
_generate_acknowledgment(user_content, intent.tool_name, intent_model),
)
timing["tools"].append({"name": intent.tool_name, "ms": int((time.monotonic() - t_tool) * 1000)})
logger.info("Intent-routed tool %s result: success=%s", intent.tool_name, result.get("success"))
# Stream acknowledgment immediately — user sees text before main LLM starts
if ack_text:
buf.append_event("chunk", {"chunk": ack_text})
buf.content_so_far += ack_text
if timing["ttft_ms"] is None:
timing["ttft_ms"] = int((time.monotonic() - t_start) * 1000)
tool_record = {
"function": intent.tool_name,
"arguments": intent.arguments,
@@ -200,10 +268,11 @@ async def run_generation(
all_tool_calls.append(tool_record)
buf.append_event("tool_call", {"tool_call": tool_record})
# Inject into messages so LLM can write a natural response
# Include ack as the assistant's partial response so round 1
# continues coherently from where the acknowledgment left off
messages.append({
"role": "assistant",
"content": "",
"content": ack_text,
"tool_calls": [
{"function": {"name": intent.tool_name, "arguments": intent.arguments}}
],