feat: add seamless text and voice chat
Rebuild & Deploy / rebuild (push) Successful in 49s

This commit is contained in:
snowspeeder committed 2026-08-29 07:49:29 -04:00
1 parent 321421e37f
commit 120ab192f1
7 files changed
+558 -79

No files matched your search

+106 -12
View File
@@ -65,8 +65,8 @@ class TaskWorker:
model=model,
messages=messages,
tools=tools or None,
max_tokens=500,
temperature=0.7,
max_tokens=700,
temperature=0.3,
extra_body={"chat_template_kwargs": {"enable_thinking": False}},
)
except Exception as e:
@@ -102,18 +102,72 @@ class TaskWorker:
if not text:
reg.fail(self.task_id, "LLM returned empty response")
return
if self._is_deferred_answer(text):
reg.add_step(
self.task_id,
"thinking",
"Draft deferred the work; continuing automatically.",
)
if iteration == max_iterations - 1:
reg.fail(self.task_id, "Could not produce a complete answer")
return
messages.extend(
[
{"role": "assistant", "content": text},
{
"role": "user",
"content": (
"Do not ask me a follow-up or offer to continue later. "
"Continue the task now, run any more targeted tool calls "
"you need, make reasonable assumptions, and only finish "
"when you have directly answered the original request."
),
},
]
)
continue
reg.add_step(self.task_id, "text", text)
reg.complete(self.task_id, text)
return
reg.fail(self.task_id, f"Reached max iterations ({max_iterations})")
@staticmethod
def _is_deferred_answer(text: str) -> bool:
"""Reject drafts that ask permission or defer the requested work."""
lowered = text.lower()
deferral_phrases = (
"would you like me",
"do you want me",
"should i ",
"shall i ",
"if you'd like",
"if you would like",
"let me know if",
"i need to perform",
"i need more information",
"i can refine",
"i can search",
"i can provide",
"need a more targeted search",
)
return "?" in text or any(phrase in lowered for phrase in deferral_phrases)
def _system_prompt(self) -> str:
today = datetime.now(timezone.utc).strftime("%B %d, %Y")
return (
"You are Hope's research assistant working on a background task. "
"Use your tools to gather information thoroughly. When you have "
"enough, provide a final answer in 1-3 sentences suitable for "
"speaking aloud. Plain text only, no markdown or formatting."
"You are Hope's autonomous research assistant working on a background task. "
f"Today is {today}. Use your tools until you have directly completed the request. "
"Never ask the user a follow-up question, ask permission to search again, or offer "
"to finish later; make reasonable assumptions and continue autonomously. If a web "
"search is broad or inconclusive, immediately run a more targeted query and scrape "
"the most promising result pages, including today's date and specific reputable "
"sources when freshness matters. For headline "
"requests, return concrete named events rather than links to news homepages, and "
"respect any requested item count exactly. Only "
"produce a final answer once it fulfills the original request. The final answer must "
"be 1-3 concise sentences suitable for speaking aloud, with plain text only and no "
"markdown or formatting."
)
def _load_tools(self) -> list[dict]:
@@ -157,7 +211,7 @@ class TaskWorker:
"type": "function",
"function": {
"name": "web_search",
"description": "Search the web for current information using Firecrawl.",
"description": "Search the web for current information. Returns titles, descriptions, and URLs.",
"parameters": {
"type": "object",
"properties": {"query": {"type": "string"}},
@@ -165,6 +219,18 @@ class TaskWorker:
},
},
},
{
"type": "function",
"function": {
"name": "web_scrape",
"description": "Read the full content of a promising URL returned by web_search.",
"parameters": {
"type": "object",
"properties": {"url": {"type": "string"}},
"required": ["url"],
},
},
},
]
async def _execute_tool(self, name: str, args: dict) -> str:
@@ -227,18 +293,46 @@ class TaskWorker:
async with httpx.AsyncClient(timeout=30) as client:
resp = await client.post(
f"{firecrawl_base}/v1/search",
json={"query": query, "limit": 3},
json={"query": query, "limit": 5},
)
if resp.status_code == 200:
data = resp.json()
items = data.get("data", [])
lines = [
f"{r.get('title', '')}: {r.get('description', '')}"
for r in items[:3]
]
lines = []
for item in items[:5]:
title = (item.get("title") or "Untitled").strip()
description = (item.get("description") or "").strip()
url = (item.get("url") or "").strip()
lines.append(f"{title}: {description}")
if url:
lines.append(f"URL: {url}")
return "\n".join(lines) or "No results found."
return f"Search failed (HTTP {resp.status_code})."
elif name == "web_scrape":
url = args.get("url", "")
if not url:
return "A URL is required."
firecrawl_base = os.environ.get("FIRECRAWL_BASE", "")
if not firecrawl_base:
return "Web scraping is not configured."
async with httpx.AsyncClient(timeout=90) as client:
resp = await client.post(
f"{firecrawl_base}/v1/scrape",
json={"url": url, "formats": ["markdown"], "timeout": 60000},
)
if resp.status_code != 200:
return f"Scrape failed (HTTP {resp.status_code})."
data = resp.json()
if not data.get("success"):
return f"Scrape failed: {data.get('error', 'unknown error')}"
result = data.get("data", {})
markdown = (result.get("markdown") or "").strip()
if not markdown:
return "No page content was extracted."
title = (result.get("metadata", {}).get("title") or "").strip()
return f"Page: {title}\n\n{markdown[:8000]}"
else:
return f"Unknown tool: {name}"