From c6208678549a88caa53748b4696ffc517483d656 Mon Sep 17 00:00:00 2001 From: Shane Date: Sat, 22 Aug 2026 16:13:06 -0400 Subject: [PATCH] fix: disable preemptive_generation for tool call compatibility; add get_time tool - preemptive_generation was incompatible with MCP tool calls: it starts generating before the turn finalizes, breaking the tool execution loop (agent said 'let me check' then hung forever) - Add get_time(location?) to weather_mcp.py for date/time queries - Update system prompt with Weather & Time section --- Dockerfile | 1 + agent/agent.py | 9 ++++++--- agent/weather_mcp.py | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 96baf56..0f10d54 100644 --- a/Dockerfile +++ b/Dockerfile @@ -41,6 +41,7 @@ RUN curl -sSL "https://github.com/livekit/livekit/releases/download/${LIVEKIT_VE COPY --from=build /app/agent/.venv /opt/voice-agent/.venv COPY agent/agent.py /opt/voice-agent/agent.py COPY agent/web_mcp.py /opt/voice-agent/web_mcp.py +COPY agent/weather_mcp.py /opt/voice-agent/weather_mcp.py # Copy web frontend + token endpoint COPY web/index.html /var/www/voice/ diff --git a/agent/agent.py b/agent/agent.py index 94c35b6..c9cc381 100644 --- a/agent/agent.py +++ b/agent/agent.py @@ -70,10 +70,12 @@ SYSTEM_PROMPT = textwrap.dedent("""\ source naturally ("according to..."). If a search comes up empty, say so briefly and move on. - # Weather + # Weather & Time You can check the weather for any location. Use get_weather when the user asks about current conditions, temperature, or forecasts. It returns a short summary — relay it naturally in your own words. + Use get_time when the user asks what time or day it is. If they mention + a city, pass it as the location argument. """) @@ -269,8 +271,9 @@ async def handle_job(ctx: JobContext) -> None: # VAD-based turn detection: agent waits for user to stop speaking. # ("adaptive" mode requires the LiveKit Cloud barge-in service.) interruption={"mode": "vad"}, - # Start generating the LLM response before the user fully stops - preemptive_generation={"enabled": True}, + # Preemptive generation is incompatible with tool calls — it starts + # generating before the turn finalizes, breaking the MCP execution loop. + preemptive_generation={"enabled": False}, ), ) diff --git a/agent/weather_mcp.py b/agent/weather_mcp.py index d82b7ee..5d3d248 100644 --- a/agent/weather_mcp.py +++ b/agent/weather_mcp.py @@ -14,6 +14,7 @@ The tool returns a short, conversational summary suitable for speech from __future__ import annotations import os +from datetime import datetime, timezone import httpx from mcp.server.fastmcp import FastMCP @@ -84,5 +85,43 @@ async def get_weather(location: str) -> str: return summary +@mcp.tool() +def get_time(location: str = "") -> str: + """Get the current date and time. + + If a location is provided, returns the local time there (approximated + via wttr.in's timezone data). Otherwise returns UTC time. + Use this when the user asks what time or day it is. + """ + if not location: + now = datetime.now(timezone.utc) + return f"It's {now.strftime('%I:%M %p')} on {now.strftime('%A, %B %d')}, UTC." + + try: + async def _fetch(): + async with httpx.AsyncClient(timeout=15) as client: + resp = await client.get( + f"{WTTR_BASE}/{location}?format=j1", + headers={"User-Agent": "curl/8.0"}, + ) + resp.raise_for_status() + return resp.json() + + import asyncio + data = asyncio.get_event_loop().run_until_complete(_fetch()) + current = data.get("current_condition", [{}])[0] or {} + tz_offset = current.get("localObsDateTime", "") + area = data.get("nearest_area", [{}])[0] or {} + city = (area.get("areaName", [{}])[0].get("value") or location).strip() + + if tz_offset: + return f"It's {tz_offset.split(' ')[1][:5]} on {tz_offset.split(' ')[0]} in {city}." + except Exception: # noqa: BLE001 + pass + + now = datetime.now(timezone.utc) + return f"It's {now.strftime('%I:%M %p')} on {now.strftime('%A, %B %d')}, UTC." + + if __name__ == "__main__": mcp.run(transport="stdio")