This commit is contained in:
1 parent
321421e37f
commit
120ab192f1
7 files changed
+558
-79
No files matched your search
+29
-16
@@ -80,7 +80,8 @@ SYSTEM_PROMPT = textwrap.dedent("""\
|
||||
no markdown, and no formatting except the Azure delivery markers below.
|
||||
- Spell out numbers when it sounds more natural ("twenty twenty-six"
|
||||
instead of "2026").
|
||||
- If you need to ask a question, ask exactly one.
|
||||
- Make reasonable assumptions and answer directly. Ask a follow-up only
|
||||
when it is genuinely impossible to proceed without one.
|
||||
- Be warm and direct. Don't be sycophantic or overly formal.
|
||||
- If you don't know something, say so briefly and move on.
|
||||
|
||||
@@ -144,7 +145,7 @@ SYSTEM_PROMPT = textwrap.dedent("""\
|
||||
- User says "look up the price of a PS5" → CALL dispatch_task with
|
||||
description="Find the current retail price of a PlayStation 5"
|
||||
After calling dispatch_task, tell the user "I'll get on that for you"
|
||||
and keep the conversation going. The result comes back automatically — when
|
||||
without asking for more permission or details. The result comes back automatically — when
|
||||
you receive it, share the findings naturally in your conversational style.
|
||||
You can have multiple tasks running at once.
|
||||
|
||||
@@ -612,29 +613,41 @@ async def handle_job(ctx: JobContext) -> None:
|
||||
),
|
||||
)
|
||||
|
||||
# Listen for data messages (voice switching) from the web UI.
|
||||
# Listen for conversation controls (voice selection and voice/text mode).
|
||||
# Defined here so it captures this job's session via closure.
|
||||
def _on_room_data(packet) -> None:
|
||||
topic = getattr(packet, "topic", None)
|
||||
if topic and topic != "voice-control":
|
||||
if topic and topic not in ("voice-control", "conversation-control"):
|
||||
return
|
||||
try:
|
||||
msg = json.loads(packet.data.decode("utf-8"))
|
||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||
return
|
||||
if msg.get("type") != "set_voice":
|
||||
|
||||
if msg.get("type") == "set_mode":
|
||||
mode = msg.get("mode")
|
||||
if mode not in ("voice", "text"):
|
||||
return
|
||||
voice_enabled = mode == "voice"
|
||||
logger.info("Switching conversation mode to %s", mode)
|
||||
if not voice_enabled:
|
||||
asyncio.create_task(session.interrupt())
|
||||
session.input.set_audio_enabled(voice_enabled)
|
||||
session.output.set_audio_enabled(voice_enabled)
|
||||
return
|
||||
voice = msg.get("voice", "")
|
||||
if not voice:
|
||||
return
|
||||
logger.info("Switching TTS voice to %s", voice)
|
||||
try:
|
||||
tts = session.tts
|
||||
if hasattr(tts, "update_options"):
|
||||
tts.update_options(voice=voice)
|
||||
logger.info("Voice updated to %s", voice)
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning("Failed to update voice: %s", e)
|
||||
|
||||
if msg.get("type") == "set_voice":
|
||||
voice = msg.get("voice", "")
|
||||
if not voice:
|
||||
return
|
||||
logger.info("Switching TTS voice to %s", voice)
|
||||
try:
|
||||
active_tts = session.tts
|
||||
if hasattr(active_tts, "update_options"):
|
||||
active_tts.update_options(voice=voice)
|
||||
logger.info("Voice updated to %s", voice)
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning("Failed to update voice: %s", e)
|
||||
|
||||
ctx.room.on("data_received", _on_room_data)
|
||||
|
||||
|
||||
+106
-12
@@ -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}"
|
||||
|
||||
|
||||
Reference in new issue
Block a user