Files
hope-voice-api/agent/dispatch_mcp.py
T
snowspeeder d4ea4fc984
Rebuild & Deploy / rebuild (push) Successful in 58s
fix: background tasks now fetch real page content; add task-status tools
Background research tasks were producing garbage or failing because the
self-hosted Firecrawl /v1/scrape defaults to a ~30s timeout that's too short
for heavy news/research sites, and the small LLM won't reliably chain a
second scrape call after a search that only returns snippets.

task_worker.py:
- web_search now auto-fetches the top results directly from the container
  (which has working outbound internet) and returns their real text, so the
  model gets actual page content in one call instead of snippet echoes.
- web_scrape requests timeout: 90000 (Firecrawl renders heavy sites but needs
  >30s), and falls back to a direct container fetch with tag-stripping if
  Firecrawl times out or returns nothing.

task_registry.py / dispatch_mcp.py:
- Add task_status(id) and list_tasks() tools so Hope can check the live state
  of any dispatched task (pending / running / completed / failed / STALLED).
- Track last_activity per task and surface a stall heuristic.
- Move the status summary into task_registry (no mcp dependency) so it's
  unit-testable.

agent.py: prompt tells Hope about task_status / list_tasks.
tests: add task-status summary and last-activity regression tests.
2026-08-30 06:54:57 -04:00

103 lines
3.4 KiB
Python

"""Dispatch MCP server — lets Hope spawn background tasks.
The main agent calls dispatch_task(description) when the user asks for
something long-running. The tool returns immediately with a task ID; the
actual work runs in a background asyncio task via TaskWorker.
Communication with the main agent process happens via /tmp/tasks/events.jsonl
(the file-based registry).
"""
from __future__ import annotations
import asyncio
import logging
import os
import time
from mcp.server.fastmcp import FastMCP
logger = logging.getLogger("voice-agent.dispatch")
mcp = FastMCP("dispatch")
@mcp.tool()
async def dispatch_task(description: str) -> str:
"""Spawn a background task to research or look something up.
Use this when the user asks you to do something that will take more than
a few seconds (research, look up news, multi-step investigation). The task
runs in parallel while you continue the conversation. When it completes,
the result will be delivered back to you automatically — you can tell the
user "I'll let you know when I find out."
Returns a short confirmation with the task ID.
"""
try:
from agent.task_registry import registry
from agent.task_worker import TaskWorker
except ImportError:
from task_registry import registry
from task_worker import TaskWorker
task = registry.create(description)
worker = TaskWorker(task.id, description)
asyncio.create_task(_run_worker(worker, task.id))
logger.info("Dispatched task %s: %s", task.id, description)
return f"Task {task.id} started. I'll report back when it's done."
async def _run_worker(worker: "TaskWorker", task_id: str):
try:
from agent.task_registry import registry
except ImportError:
from task_registry import registry
timeout_seconds = float(os.environ.get("TASK_TIMEOUT_SECONDS", "180"))
try:
await asyncio.wait_for(worker.run(), timeout=timeout_seconds)
except asyncio.TimeoutError:
message = f"Timed out after {timeout_seconds:g} seconds"
logger.error("Task %s %s", task_id, message.lower())
registry.fail(task_id, message)
except Exception as e:
logger.exception("Task %s crashed: %s", task_id, e)
registry.fail(task_id, str(e))
def _status_summary(task_id: str, stalled_s: float = 45.0) -> str:
"""Human-readable live status for one task, plus a stall hint."""
try:
from agent.task_registry import status_summary
except ImportError:
from task_registry import status_summary
return status_summary(task_id, stalled_s)
@mcp.tool()
async def task_status(task_id: str) -> str:
"""Check the live status of a background task you dispatched earlier,
including whether it is pending, running, completed, failed, or looking
stalled (no activity for a while). Pass the task id from dispatch_task
(or from list_tasks)."""
return _status_summary(task_id)
@mcp.tool()
async def list_tasks() -> str:
"""List every background task Hope has dispatched in this conversation,
with their live status so you can report progress to the user."""
try:
from agent.task_registry import registry
except ImportError:
from task_registry import registry
tasks = registry.list_all()
if not tasks:
return "No background tasks have been dispatched yet."
return "\n".join(_status_summary(t["id"]) for t in tasks)
if __name__ == "__main__":
mcp.run(transport="stdio")