Update app/agent_system.py
Browse files- app/agent_system.py +306 -444
app/agent_system.py
CHANGED
|
@@ -1,548 +1,410 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
import
|
| 7 |
-
import tempfile
|
| 8 |
-
import base64
|
| 9 |
-
import io
|
| 10 |
from openai import AsyncOpenAI
|
| 11 |
from typing import AsyncGenerator
|
| 12 |
from docs_context import PRAISONAI_DOCS
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
-
|
| 15 |
MODEL_MAP = {
|
| 16 |
-
"LongCat-Flash-Lite":
|
| 17 |
-
"LongCat-Flash-Chat":
|
| 18 |
-
"LongCat-Flash-Thinking-2601":"LongCat-Flash-Thinking-2601",
|
| 19 |
}
|
| 20 |
DEFAULT_MODEL = "LongCat-Flash-Lite"
|
| 21 |
|
| 22 |
-
# ββ Built-in tools (always available to every agent) ββββββββββββββββββββββββ
|
| 23 |
-
|
| 24 |
-
def get_current_datetime() -> str:
|
| 25 |
-
now = datetime.datetime.now()
|
| 26 |
-
utc = datetime.datetime.utcnow()
|
| 27 |
-
return (f"Local: {now.strftime('%A, %B %d, %Y at %I:%M:%S %p')}\n"
|
| 28 |
-
f"UTC: {utc.strftime('%Y-%m-%d %H:%M:%S')} UTC\n"
|
| 29 |
-
f"Unix: {int(now.timestamp())}")
|
| 30 |
-
|
| 31 |
-
def calculate_math(expression: str) -> str:
|
| 32 |
-
try:
|
| 33 |
-
safe_chars = set("0123456789+-*/.() %**^")
|
| 34 |
-
clean = expression.replace("^", "**")
|
| 35 |
-
if not all(c in safe_chars or c.isspace() for c in clean):
|
| 36 |
-
return "Error: unsafe characters in expression"
|
| 37 |
-
result = eval(clean, {"__builtins__": {}}, {})
|
| 38 |
-
return f"Result: {result}"
|
| 39 |
-
except Exception as e:
|
| 40 |
-
return f"Error: {e}"
|
| 41 |
-
|
| 42 |
-
def run_python_code(code: str) -> str:
|
| 43 |
-
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
|
| 44 |
-
f.write(code)
|
| 45 |
-
tmp = f.name
|
| 46 |
-
try:
|
| 47 |
-
result = subprocess.run(
|
| 48 |
-
['python3', tmp], capture_output=True, text=True, timeout=15
|
| 49 |
-
)
|
| 50 |
-
out = (result.stdout + result.stderr).strip()
|
| 51 |
-
return out or "(no output)"
|
| 52 |
-
except subprocess.TimeoutExpired:
|
| 53 |
-
return "Error: execution timed out after 15s"
|
| 54 |
-
except Exception as e:
|
| 55 |
-
return f"Error: {e}"
|
| 56 |
-
finally:
|
| 57 |
-
try:
|
| 58 |
-
os.unlink(tmp)
|
| 59 |
-
except Exception:
|
| 60 |
-
pass
|
| 61 |
-
|
| 62 |
-
def create_voice_response(text: str) -> str:
|
| 63 |
-
try:
|
| 64 |
-
from gtts import gTTS
|
| 65 |
-
tts = gTTS(text=text, lang='en', slow=False)
|
| 66 |
-
buf = io.BytesIO()
|
| 67 |
-
tts.write_to_fp(buf)
|
| 68 |
-
buf.seek(0)
|
| 69 |
-
b64 = base64.b64encode(buf.read()).decode('utf-8')
|
| 70 |
-
return f"AUDIO_B64:{b64}"
|
| 71 |
-
except Exception as e:
|
| 72 |
-
return f"VOICE_FALLBACK:{text[:2000]}"
|
| 73 |
-
|
| 74 |
-
def search_information(query: str) -> str:
|
| 75 |
-
# Simple stub - returns a helpful message since we don't have a search API key
|
| 76 |
-
# The agent can use its training knowledge to answer
|
| 77 |
-
return f"Searching for: {query}\n[Search tool: returning from internal knowledge - agent should answer from training data]"
|
| 78 |
-
|
| 79 |
-
BUILTIN_TOOLS = {
|
| 80 |
-
"get_current_datetime": get_current_datetime,
|
| 81 |
-
"calculate_math": calculate_math,
|
| 82 |
-
"run_python_code": run_python_code,
|
| 83 |
-
"create_voice_response":create_voice_response,
|
| 84 |
-
"search_information": search_information,
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
BUILTIN_TOOLS_DOC = """
|
| 88 |
-
## Always-Available Built-in Tools
|
| 89 |
-
These tools exist in every agent β no need to create them:
|
| 90 |
-
|
| 91 |
-
- get_current_datetime() -> str
|
| 92 |
-
Returns the exact current date and time (local + UTC + unix timestamp).
|
| 93 |
-
USE THIS whenever user asks about date, time, day, etc.
|
| 94 |
-
|
| 95 |
-
- calculate_math(expression: str) -> str
|
| 96 |
-
Evaluates math: "2 + 2", "100 * 3.14", "2**10", etc.
|
| 97 |
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
|
| 102 |
-
- create_voice_response(text: str) -> str
|
| 103 |
-
Converts text to MP3 audio via gTTS. Returns AUDIO_B64:<base64>.
|
| 104 |
-
USE THIS when user explicitly asks for voice/audio/spoken response.
|
| 105 |
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
CRITICAL RULES:
|
| 110 |
-
1. If user asks "what time is it" / "what date" / "what day" -> use get_current_datetime
|
| 111 |
-
2. If user asks for "voice" / "speak" / "audio" / "say it" -> use create_voice_response
|
| 112 |
-
3. NEVER say "I cannot" for tasks these tools handle
|
| 113 |
-
4. Always prefer tools over saying you lack capability
|
| 114 |
-
"""
|
| 115 |
-
|
| 116 |
-
def inject_datetime_context() -> str:
|
| 117 |
-
now = datetime.datetime.now()
|
| 118 |
-
return (f"[System context: Current datetime = "
|
| 119 |
-
f"{now.strftime('%A, %B %d, %Y %I:%M:%S %p')} local time | "
|
| 120 |
-
f"{datetime.datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC]\n")
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
def build_orchestrator_system() -> str:
|
| 124 |
-
return f"""{inject_datetime_context()}
|
| 125 |
-
You are the Main Orchestrator Agent for PraisonChat β a powerful AI that solves tasks by
|
| 126 |
-
dynamically creating specialized sub-agents with custom-built Python tools.
|
| 127 |
|
| 128 |
{PRAISONAI_DOCS}
|
| 129 |
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
{{
|
| 142 |
-
"task_analysis": "
|
| 143 |
-
"
|
| 144 |
-
|
|
|
|
|
|
|
| 145 |
"sub_agents": [
|
| 146 |
{{
|
| 147 |
-
"name": "
|
| 148 |
-
"role": "
|
| 149 |
-
"goal": "
|
| 150 |
-
"
|
|
|
|
| 151 |
"tools": [
|
| 152 |
{{
|
| 153 |
-
"name": "
|
| 154 |
-
"description": "
|
| 155 |
-
"
|
| 156 |
-
"
|
| 157 |
-
"
|
|
|
|
| 158 |
}}
|
| 159 |
-
]
|
| 160 |
-
"task_description": "Exact task for this agent to perform",
|
| 161 |
-
"expected_output": "What format/content to expect back"
|
| 162 |
}}
|
| 163 |
],
|
| 164 |
-
"
|
| 165 |
-
"synthesis_instruction": "How to combine all agent results into the final answer",
|
| 166 |
"output_format": "text"
|
| 167 |
}}
|
|
|
|
|
|
|
| 168 |
|
| 169 |
-
output_format options: "text", "voice", "code", "table", "json"
|
| 170 |
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
- Tool implementations: use # comments ONLY, never triple-quoted strings inside code
|
| 174 |
-
- Max 4 sub-agents per task
|
| 175 |
-
- Tool code must be valid Python, no imports not in stdlib
|
| 176 |
-
- If voice requested: set output_format to "voice" AND use create_voice_response tool
|
| 177 |
-
"""
|
| 178 |
|
| 179 |
|
| 180 |
-
def
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
return f"[Tool '{name}' build error: {e}]"
|
| 199 |
-
fallback.__name__ = name
|
| 200 |
-
return fallback, str(e)
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
def call_builtin_tool(name: str, agent_task: str) -> str:
|
| 204 |
-
fn = BUILTIN_TOOLS.get(name)
|
| 205 |
-
if not fn:
|
| 206 |
-
return f"Unknown built-in tool: {name}"
|
| 207 |
-
try:
|
| 208 |
-
if name == "get_current_datetime":
|
| 209 |
-
return fn()
|
| 210 |
-
elif name == "calculate_math":
|
| 211 |
-
return fn(agent_task)
|
| 212 |
-
elif name == "run_python_code":
|
| 213 |
-
return fn(agent_task)
|
| 214 |
-
elif name == "create_voice_response":
|
| 215 |
-
return fn(agent_task)
|
| 216 |
-
elif name == "search_information":
|
| 217 |
-
return fn(agent_task)
|
| 218 |
-
else:
|
| 219 |
-
return fn()
|
| 220 |
-
except Exception as e:
|
| 221 |
-
return f"Tool error: {e}"
|
| 222 |
|
| 223 |
|
| 224 |
class AgentOrchestrator:
|
| 225 |
def __init__(self):
|
| 226 |
self._clients: dict = {}
|
| 227 |
|
| 228 |
-
def
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
api_key=api_key,
|
| 233 |
-
base_url=LONGCAT_BASE_URL,
|
| 234 |
-
)
|
| 235 |
-
return self._clients[key]
|
| 236 |
|
| 237 |
-
async def
|
| 238 |
-
messages = [{"role": "system", "content":
|
| 239 |
for m in history[-6:]:
|
| 240 |
-
messages.append({"role": m["role"], "content": str(m.get("content",
|
| 241 |
-
messages.append({
|
| 242 |
-
"role": "user",
|
| 243 |
-
"content": f"Plan execution for: {user_message}"
|
| 244 |
-
})
|
| 245 |
|
| 246 |
resp = await client.chat.completions.create(
|
| 247 |
-
model=model,
|
| 248 |
-
messages=messages,
|
| 249 |
-
max_tokens=6000,
|
| 250 |
-
temperature=0.1,
|
| 251 |
)
|
| 252 |
raw = resp.choices[0].message.content.strip()
|
| 253 |
-
# Strip possible markdown fences
|
| 254 |
if "```" in raw:
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
p2 = p2[4:].strip()
|
| 260 |
-
if p2.startswith("{"):
|
| 261 |
-
raw = p2
|
| 262 |
-
break
|
| 263 |
try:
|
| 264 |
return json.loads(raw)
|
| 265 |
except Exception:
|
| 266 |
-
return {
|
| 267 |
-
|
| 268 |
-
"needs_sub_agents": False,
|
| 269 |
-
"builtin_tools_to_use": [],
|
| 270 |
-
"sub_agents": [],
|
| 271 |
-
"execution_order": [],
|
| 272 |
-
"synthesis_instruction": "Respond directly",
|
| 273 |
-
"output_format": "text",
|
| 274 |
-
}
|
| 275 |
-
|
| 276 |
-
async def run_sub_agent(self, client, spec: dict, context: str, model: str) -> dict:
|
| 277 |
-
tools_built = []
|
| 278 |
-
tool_errors = []
|
| 279 |
-
tool_descriptions = "\n".join(
|
| 280 |
-
f"- {t['name']}: {t.get('description','')}" for t in spec.get("tools", [])
|
| 281 |
-
)
|
| 282 |
-
# Build custom tools
|
| 283 |
-
for t in spec.get("tools", []):
|
| 284 |
-
fn, err = build_tool_function(t)
|
| 285 |
-
if err:
|
| 286 |
-
tool_errors.append(f"{t['name']}: {err}")
|
| 287 |
-
tools_built.append({"name": t["name"], "fn": fn, "desc": t.get("description", ""), "error": err})
|
| 288 |
-
|
| 289 |
-
system = f"""{inject_datetime_context()}
|
| 290 |
-
You are {spec['name']}, a specialized AI agent.
|
| 291 |
-
Role: {spec['role']}
|
| 292 |
-
Goal: {spec['goal']}
|
| 293 |
-
Backstory: {spec.get('backstory', '')}
|
| 294 |
-
|
| 295 |
-
Built-in tools always available:
|
| 296 |
-
{BUILTIN_TOOLS_DOC}
|
| 297 |
-
|
| 298 |
-
Custom tools for this task:
|
| 299 |
-
{tool_descriptions if tool_descriptions else 'None β use built-in tools and your knowledge'}
|
| 300 |
-
|
| 301 |
-
Context from previous agents:
|
| 302 |
-
{context if context else 'You are the first agent.'}
|
| 303 |
-
|
| 304 |
-
Execute your task. Show reasoning and tool usage step by step.
|
| 305 |
-
Expected output: {spec.get('expected_output', 'Detailed results')}"""
|
| 306 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
resp = await client.chat.completions.create(
|
| 308 |
model=model,
|
| 309 |
messages=[
|
| 310 |
{"role": "system", "content": system},
|
| 311 |
-
{"role": "user", "content": spec[
|
| 312 |
],
|
| 313 |
-
max_tokens=
|
| 314 |
-
temperature=0.7,
|
| 315 |
-
)
|
| 316 |
-
result = resp.choices[0].message.content
|
| 317 |
-
return {
|
| 318 |
-
"result": result,
|
| 319 |
-
"tools_built": [{"name": t["name"], "desc": t["desc"], "error": t.get("error")} for t in tools_built],
|
| 320 |
-
"tool_errors": tool_errors,
|
| 321 |
-
}
|
| 322 |
-
|
| 323 |
-
async def synthesize(self, client, user_message: str, agent_results: dict,
|
| 324 |
-
synthesis_instruction: str, output_format: str, model: str) -> AsyncGenerator:
|
| 325 |
-
combined = "\n\n".join(
|
| 326 |
-
f"=== {name} ===\n{r['result']}" for name, r in agent_results.items()
|
| 327 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
voice_note = ""
|
| 329 |
if output_format == "voice":
|
| 330 |
-
voice_note = "\
|
| 331 |
|
| 332 |
-
system = f"""
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
Output format: {output_format}
|
| 336 |
{voice_note}
|
| 337 |
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
Provide a comprehensive, well-structured markdown response."""
|
| 342 |
|
| 343 |
stream = await client.chat.completions.create(
|
| 344 |
model=model,
|
| 345 |
messages=[
|
| 346 |
{"role": "system", "content": system},
|
| 347 |
-
{"role": "user", "content":
|
| 348 |
],
|
| 349 |
-
max_tokens=
|
| 350 |
-
temperature=0.7,
|
| 351 |
-
stream=True,
|
| 352 |
)
|
| 353 |
async for chunk in stream:
|
| 354 |
c = chunk.choices[0].delta.content
|
| 355 |
-
if c:
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
if tool_name in BUILTIN_TOOLS:
|
| 364 |
-
tool_results[tool_name] = call_builtin_tool(tool_name, user_message)
|
| 365 |
-
|
| 366 |
-
tool_context = ""
|
| 367 |
-
if tool_results:
|
| 368 |
-
tool_context = "\n\nTool results:\n" + "\n".join(
|
| 369 |
-
f"[{k}]: {v}" for k, v in tool_results.items()
|
| 370 |
-
)
|
| 371 |
-
|
| 372 |
-
voice_note = ""
|
| 373 |
-
if output_format == "voice":
|
| 374 |
-
voice_note = "\nThe user wants a voice response. End your reply with: [VOICE_RESPONSE: <text to speak>]"
|
| 375 |
-
|
| 376 |
-
messages = [{
|
| 377 |
-
"role": "system",
|
| 378 |
-
"content": (
|
| 379 |
-
f"{inject_datetime_context()}"
|
| 380 |
-
"You are PraisonChat, a powerful AI assistant. "
|
| 381 |
-
"Respond helpfully using markdown. "
|
| 382 |
-
"You have real-time access to date/time, code execution, and voice tools. "
|
| 383 |
-
"NEVER say you cannot check the time or date β you have it above."
|
| 384 |
-
f"{tool_context}{voice_note}"
|
| 385 |
)
|
| 386 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
for m in history[-10:]:
|
| 388 |
-
messages.append({"role":
|
| 389 |
-
messages.append({"role":
|
| 390 |
|
| 391 |
stream = await client.chat.completions.create(
|
| 392 |
-
model=model,
|
| 393 |
-
messages=messages,
|
| 394 |
-
max_tokens=16000,
|
| 395 |
-
temperature=0.7,
|
| 396 |
-
stream=True,
|
| 397 |
)
|
| 398 |
async for chunk in stream:
|
| 399 |
c = chunk.choices[0].delta.content
|
| 400 |
-
if c:
|
| 401 |
-
yield c
|
| 402 |
|
| 403 |
-
|
|
|
|
| 404 |
api_key: str, model: str = DEFAULT_MODEL) -> AsyncGenerator:
|
| 405 |
-
def emit(
|
| 406 |
-
return json.dumps(
|
| 407 |
|
| 408 |
model = MODEL_MAP.get(model, DEFAULT_MODEL)
|
| 409 |
-
|
| 410 |
|
| 411 |
try:
|
| 412 |
-
#
|
| 413 |
-
yield emit({"type":
|
| 414 |
await asyncio.sleep(0)
|
| 415 |
|
| 416 |
-
plan = await self.
|
| 417 |
-
|
| 418 |
-
yield emit({"type": "thinking", "text": plan.get("task_analysis", "Planning...")})
|
| 419 |
await asyncio.sleep(0)
|
| 420 |
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 425 |
|
| 426 |
-
|
| 427 |
-
for bt in builtin_tools:
|
| 428 |
-
if bt in BUILTIN_TOOLS:
|
| 429 |
-
yield emit({"type": "tool_call", "tool": bt, "builtin": True})
|
| 430 |
-
await asyncio.sleep(0)
|
| 431 |
-
result = call_builtin_tool(bt, user_message)
|
| 432 |
-
is_audio = result.startswith("AUDIO_B64:")
|
| 433 |
-
preview = "[audio data]" if is_audio else result[:200]
|
| 434 |
-
yield emit({"type": "tool_result", "tool": bt, "result": preview, "is_audio": is_audio,
|
| 435 |
-
"audio_b64": result[10:] if is_audio else None})
|
| 436 |
-
await asyncio.sleep(0)
|
| 437 |
|
| 438 |
-
#
|
| 439 |
-
if
|
| 440 |
-
|
|
|
|
| 441 |
|
| 442 |
for spec in sub_agents:
|
| 443 |
-
tool_names = [t["name"] for t in spec.get("tools",
|
| 444 |
-
yield emit({
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
"role": spec["role"],
|
| 448 |
-
"goal": spec.get("goal", ""),
|
| 449 |
-
"tools": tool_names,
|
| 450 |
-
"tool_specs": spec.get("tools", []),
|
| 451 |
-
})
|
| 452 |
await asyncio.sleep(0.05)
|
| 453 |
|
| 454 |
context_so_far = ""
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
for agent_name in order:
|
| 459 |
-
spec = next((s for s in sub_agents if s["name"] == agent_name), None)
|
| 460 |
-
if not spec:
|
| 461 |
-
continue
|
| 462 |
-
|
| 463 |
-
yield emit({"type": "agent_working", "name": agent_name,
|
| 464 |
-
"task": spec["task_description"][:120]})
|
| 465 |
await asyncio.sleep(0)
|
| 466 |
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 476 |
|
| 477 |
-
|
| 478 |
-
for tb in r.get("tools_built", []):
|
| 479 |
-
yield emit({"type": "tool_ready", "agent": agent_name,
|
| 480 |
-
"tool": tb["name"], "error": tb.get("error")})
|
| 481 |
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 485 |
except Exception as e:
|
| 486 |
-
|
| 487 |
-
|
| 488 |
|
| 489 |
-
|
| 490 |
-
yield emit({"type":
|
| 491 |
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
output_format, model):
|
| 496 |
-
full_text += token
|
| 497 |
-
yield emit({"type": "token", "content": token})
|
| 498 |
-
|
| 499 |
-
# Handle voice in synthesized response
|
| 500 |
-
if output_format == "voice" and "[VOICE_RESPONSE:" in full_text:
|
| 501 |
-
try:
|
| 502 |
-
voice_text = full_text.split("[VOICE_RESPONSE:")[1].rsplit("]", 1)[0].strip()
|
| 503 |
-
audio_result = create_voice_response(voice_text)
|
| 504 |
-
if audio_result.startswith("AUDIO_B64:"):
|
| 505 |
-
yield emit({"type": "audio_response", "audio_b64": audio_result[10:],
|
| 506 |
-
"text": voice_text})
|
| 507 |
-
else:
|
| 508 |
-
yield emit({"type": "voice_fallback", "text": voice_text})
|
| 509 |
-
except Exception:
|
| 510 |
-
pass
|
| 511 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 512 |
else:
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
yield emit({"type": "step", "text": "Generating response..."})
|
| 518 |
-
yield emit({"type": "response_start", "output_format": output_format})
|
| 519 |
-
|
| 520 |
-
full_text = ""
|
| 521 |
-
async for token in self.direct_response(client, user_message, history,
|
| 522 |
-
builtin_tools, output_format, model):
|
| 523 |
full_text += token
|
| 524 |
-
yield emit({"type":
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
|
|
|
|
|
|
| 543 |
|
| 544 |
except Exception as e:
|
| 545 |
-
yield emit({"type":
|
| 546 |
|
| 547 |
|
| 548 |
orchestrator = AgentOrchestrator()
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
PraisonChat Agent System β Real Execution Engine
|
| 3 |
+
================================================
|
| 4 |
+
Agents actually execute tools. No simulation.
|
| 5 |
+
"""
|
| 6 |
+
import os, json, asyncio, datetime, traceback
|
|
|
|
|
|
|
|
|
|
| 7 |
from openai import AsyncOpenAI
|
| 8 |
from typing import AsyncGenerator
|
| 9 |
from docs_context import PRAISONAI_DOCS
|
| 10 |
+
from tool_executor import (
|
| 11 |
+
execute_tool, run_builtin_tool, pip_install,
|
| 12 |
+
BUILTIN_TOOL_IMPLEMENTATIONS, PKG_DIR
|
| 13 |
+
)
|
| 14 |
|
| 15 |
+
LONGCAT_BASE = "https://api.longcat.chat/openai/v1"
|
| 16 |
MODEL_MAP = {
|
| 17 |
+
"LongCat-Flash-Lite": "LongCat-Flash-Lite",
|
| 18 |
+
"LongCat-Flash-Chat": "LongCat-Flash-Chat",
|
| 19 |
+
"LongCat-Flash-Thinking-2601": "LongCat-Flash-Thinking-2601",
|
| 20 |
}
|
| 21 |
DEFAULT_MODEL = "LongCat-Flash-Lite"
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
+
def now_str() -> str:
|
| 25 |
+
n = datetime.datetime.now()
|
| 26 |
+
return n.strftime("%A, %B %d, %Y at %I:%M:%S %p")
|
| 27 |
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
+
ORCHESTRATOR_SYSTEM = f"""You are the Main Orchestrator Agent for PraisonChat.
|
| 30 |
+
Current datetime: {{DATETIME}}
|
| 31 |
+
Python package dir: {PKG_DIR}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
{PRAISONAI_DOCS}
|
| 34 |
|
| 35 |
+
## Available Built-in Tools (always executable, no creation needed)
|
| 36 |
+
- get_current_datetime β real system time
|
| 37 |
+
- search_web(query) β real DuckDuckGo search results
|
| 38 |
+
- fetch_webpage(url) β real HTML content extraction
|
| 39 |
+
- run_python_code(code) β real Python execution with output
|
| 40 |
+
- create_voice(text) β real MP3 audio via gTTS
|
| 41 |
+
- calculate(expression) β real math evaluation
|
| 42 |
+
|
| 43 |
+
## Your Task
|
| 44 |
+
Analyze the user's request and respond with a JSON plan.
|
| 45 |
+
RULES:
|
| 46 |
+
1. For date/time β use builtin: get_current_datetime
|
| 47 |
+
2. For web search β use builtin: search_web
|
| 48 |
+
3. For voice/audio β use builtin: create_voice
|
| 49 |
+
4. For math β use builtin: calculate
|
| 50 |
+
5. For code execution β use builtin: run_python_code
|
| 51 |
+
6. For complex multi-step tasks β create sub_agents
|
| 52 |
+
7. Sub-agent tools MUST have real working Python code using real libraries
|
| 53 |
+
8. Specify pip packages needed β they will be ACTUALLY installed
|
| 54 |
+
|
| 55 |
+
## Tool Implementation Rules (CRITICAL)
|
| 56 |
+
- Use duckduckgo_search for searches β NOT requests to google.com
|
| 57 |
+
- Use requests + BeautifulSoup for web scraping
|
| 58 |
+
- Use real API calls for external services
|
| 59 |
+
- Tool code must be complete, working Python
|
| 60 |
+
- Add: import sys; sys.path.insert(0, '{PKG_DIR}') at top of tool code
|
| 61 |
+
- Use # comments only, NEVER triple-quoted strings inside tool code
|
| 62 |
+
|
| 63 |
+
## Response Format β JSON only, no markdown fences:
|
| 64 |
{{
|
| 65 |
+
"task_analysis": "what needs to be done",
|
| 66 |
+
"builtin_tools": [
|
| 67 |
+
{{"name": "search_web", "call_arg": "actual search query here"}}
|
| 68 |
+
],
|
| 69 |
+
"needs_agents": false,
|
| 70 |
"sub_agents": [
|
| 71 |
{{
|
| 72 |
+
"name": "ResearchAgent",
|
| 73 |
+
"role": "Web Research Specialist",
|
| 74 |
+
"goal": "Find real information",
|
| 75 |
+
"task": "Search for X and summarize",
|
| 76 |
+
"expected_output": "Summary of findings",
|
| 77 |
"tools": [
|
| 78 |
{{
|
| 79 |
+
"name": "search_news",
|
| 80 |
+
"description": "Search for latest news",
|
| 81 |
+
"packages": ["duckduckgo-search"],
|
| 82 |
+
"call_arg_key": "query",
|
| 83 |
+
"call_arg_value": "actual search terms",
|
| 84 |
+
"code": "import sys\\nsys.path.insert(0, '{PKG_DIR}')\\ndef search_news(query: str) -> str:\\n from duckduckgo_search import DDGS\\n results = []\\n with DDGS() as ddgs:\\n for r in ddgs.news(query, max_results=5):\\n results.append(f\\"{{r['title']}}: {{r['body']}}\\" )\\n return '\\\\n'.join(results)"
|
| 85 |
}}
|
| 86 |
+
]
|
|
|
|
|
|
|
| 87 |
}}
|
| 88 |
],
|
| 89 |
+
"synthesis": "how to combine results",
|
|
|
|
| 90 |
"output_format": "text"
|
| 91 |
}}
|
| 92 |
+
output_format: text | voice | code | markdown
|
| 93 |
+
"""
|
| 94 |
|
|
|
|
| 95 |
|
| 96 |
+
def get_orchestrator_system():
|
| 97 |
+
return ORCHESTRATOR_SYSTEM.replace("{DATETIME}", now_str())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
|
| 99 |
|
| 100 |
+
def get_agent_system(spec: dict, context: str) -> str:
|
| 101 |
+
tools_desc = "\n".join(
|
| 102 |
+
f"- {t['name']}: {t.get('description','')}" for t in spec.get("tools", [])
|
| 103 |
+
)
|
| 104 |
+
return f"""You are {spec['name']}, a specialized AI agent.
|
| 105 |
+
Current datetime: {now_str()}
|
| 106 |
+
Role: {spec['role']}
|
| 107 |
+
Goal: {spec['goal']}
|
| 108 |
+
|
| 109 |
+
Tools available (already executed with real results):
|
| 110 |
+
{tools_desc}
|
| 111 |
+
|
| 112 |
+
Context from previous agents:
|
| 113 |
+
{context or 'You are the first agent.'}
|
| 114 |
+
|
| 115 |
+
Synthesize the real tool results into a clear, accurate response.
|
| 116 |
+
Expected output: {spec.get('expected_output', 'Detailed results')}
|
| 117 |
+
IMPORTANT: Use the actual tool results provided β do NOT make up or estimate data."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
|
| 120 |
class AgentOrchestrator:
|
| 121 |
def __init__(self):
|
| 122 |
self._clients: dict = {}
|
| 123 |
|
| 124 |
+
def client(self, api_key: str) -> AsyncOpenAI:
|
| 125 |
+
if api_key not in self._clients:
|
| 126 |
+
self._clients[api_key] = AsyncOpenAI(api_key=api_key, base_url=LONGCAT_BASE)
|
| 127 |
+
return self._clients[api_key]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
+
async def plan(self, client, msg: str, history: list, model: str) -> dict:
|
| 130 |
+
messages = [{"role": "system", "content": get_orchestrator_system()}]
|
| 131 |
for m in history[-6:]:
|
| 132 |
+
messages.append({"role": m["role"], "content": str(m.get("content",""))[:1500]})
|
| 133 |
+
messages.append({"role": "user", "content": f"Plan execution for: {msg}"})
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
resp = await client.chat.completions.create(
|
| 136 |
+
model=model, messages=messages, max_tokens=5000, temperature=0.1
|
|
|
|
|
|
|
|
|
|
| 137 |
)
|
| 138 |
raw = resp.choices[0].message.content.strip()
|
|
|
|
| 139 |
if "```" in raw:
|
| 140 |
+
for part in raw.split("```"):
|
| 141 |
+
p = part.strip().lstrip("json").strip()
|
| 142 |
+
if p.startswith("{"):
|
| 143 |
+
raw = p; break
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
try:
|
| 145 |
return json.loads(raw)
|
| 146 |
except Exception:
|
| 147 |
+
return {"task_analysis":"Direct","builtin_tools":[],"needs_agents":False,
|
| 148 |
+
"sub_agents":[],"synthesis":"","output_format":"text"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
|
| 150 |
+
async def synthesize_agent(self, client, spec: dict, tool_results: dict,
|
| 151 |
+
context: str, model: str) -> str:
|
| 152 |
+
tool_results_text = "\n\n".join(
|
| 153 |
+
f"[Tool: {k}]\n{v}" for k, v in tool_results.items()
|
| 154 |
+
)
|
| 155 |
+
system = get_agent_system(spec, context)
|
| 156 |
resp = await client.chat.completions.create(
|
| 157 |
model=model,
|
| 158 |
messages=[
|
| 159 |
{"role": "system", "content": system},
|
| 160 |
+
{"role": "user", "content": f"Task: {spec['task']}\n\nReal tool results:\n{tool_results_text}"},
|
| 161 |
],
|
| 162 |
+
max_tokens=8000, temperature=0.7,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
)
|
| 164 |
+
return resp.choices[0].message.content
|
| 165 |
+
|
| 166 |
+
async def final_synthesis(self, client, user_msg: str, agent_results: dict,
|
| 167 |
+
builtin_results: dict, synthesis_note: str,
|
| 168 |
+
output_format: str, model: str) -> AsyncGenerator:
|
| 169 |
+
all_results = ""
|
| 170 |
+
if builtin_results:
|
| 171 |
+
all_results += "## Real Tool Results\n"
|
| 172 |
+
all_results += "\n\n".join(f"[{k}]:\n{v}" for k,v in builtin_results.items())
|
| 173 |
+
if agent_results:
|
| 174 |
+
all_results += "\n\n## Agent Results\n"
|
| 175 |
+
all_results += "\n\n".join(f"[{k}]:\n{v}" for k,v in agent_results.items())
|
| 176 |
+
|
| 177 |
voice_note = ""
|
| 178 |
if output_format == "voice":
|
| 179 |
+
voice_note = "\nEnd with: [VOICE: <the text to speak aloud>]"
|
| 180 |
|
| 181 |
+
system = f"""You are PraisonChat, synthesizing REAL tool results into a final response.
|
| 182 |
+
Current datetime: {now_str()}
|
| 183 |
+
{synthesis_note}
|
|
|
|
| 184 |
{voice_note}
|
| 185 |
|
| 186 |
+
CRITICAL: Use ONLY the actual data from tool results. Do NOT estimate or fabricate data.
|
| 187 |
+
If a search returned real results, quote and cite them.
|
| 188 |
+
Format response in clear markdown."""
|
|
|
|
| 189 |
|
| 190 |
stream = await client.chat.completions.create(
|
| 191 |
model=model,
|
| 192 |
messages=[
|
| 193 |
{"role": "system", "content": system},
|
| 194 |
+
{"role": "user", "content": f"User asked: {user_msg}\n\nActual results to use:\n{all_results}"},
|
| 195 |
],
|
| 196 |
+
max_tokens=12000, temperature=0.7, stream=True
|
|
|
|
|
|
|
| 197 |
)
|
| 198 |
async for chunk in stream:
|
| 199 |
c = chunk.choices[0].delta.content
|
| 200 |
+
if c: yield c
|
| 201 |
+
|
| 202 |
+
async def direct_stream(self, client, user_msg: str, history: list,
|
| 203 |
+
builtin_results: dict, output_format: str, model: str) -> AsyncGenerator:
|
| 204 |
+
tool_ctx = ""
|
| 205 |
+
if builtin_results:
|
| 206 |
+
tool_ctx = "\n\nReal tool results:\n" + "\n\n".join(
|
| 207 |
+
f"[{k}]: {v}" for k,v in builtin_results.items()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
)
|
| 209 |
+
voice_note = "\nEnd reply with: [VOICE: <text to speak>]" if output_format=="voice" else ""
|
| 210 |
+
|
| 211 |
+
messages = [{"role":"system","content":(
|
| 212 |
+
f"You are PraisonChat, a powerful AI assistant.\n"
|
| 213 |
+
f"Current datetime: {now_str()}\n"
|
| 214 |
+
f"Use the real tool results below to answer accurately.{tool_ctx}{voice_note}\n"
|
| 215 |
+
f"NEVER say you cannot check the time/date β it is shown above.\n"
|
| 216 |
+
f"Format in clear markdown."
|
| 217 |
+
)}]
|
| 218 |
for m in history[-10:]:
|
| 219 |
+
messages.append({"role":m["role"],"content":str(m.get("content",""))[:2500]})
|
| 220 |
+
messages.append({"role":"user","content":user_msg})
|
| 221 |
|
| 222 |
stream = await client.chat.completions.create(
|
| 223 |
+
model=model, messages=messages, max_tokens=12000, temperature=0.7, stream=True
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
)
|
| 225 |
async for chunk in stream:
|
| 226 |
c = chunk.choices[0].delta.content
|
| 227 |
+
if c: yield c
|
|
|
|
| 228 |
|
| 229 |
+
# ββ Main streaming entry point ββββββββββββββββββββββββββββββββββββββββββ
|
| 230 |
+
async def stream_response(self, user_msg: str, history: list,
|
| 231 |
api_key: str, model: str = DEFAULT_MODEL) -> AsyncGenerator:
|
| 232 |
+
def emit(d: dict) -> str:
|
| 233 |
+
return json.dumps(d)
|
| 234 |
|
| 235 |
model = MODEL_MAP.get(model, DEFAULT_MODEL)
|
| 236 |
+
cl = self.client(api_key)
|
| 237 |
|
| 238 |
try:
|
| 239 |
+
# 1. Plan
|
| 240 |
+
yield emit({"type":"thinking","text":"Analyzing your requestβ¦"})
|
| 241 |
await asyncio.sleep(0)
|
| 242 |
|
| 243 |
+
plan = await self.plan(cl, user_msg, history, model)
|
| 244 |
+
yield emit({"type":"thinking","text": plan.get("task_analysis","Planningβ¦")})
|
|
|
|
| 245 |
await asyncio.sleep(0)
|
| 246 |
|
| 247 |
+
builtin_results: dict = {}
|
| 248 |
+
agent_results: dict = {}
|
| 249 |
+
|
| 250 |
+
# 2. Execute built-in tools REALLY
|
| 251 |
+
for bt in plan.get("builtin_tools", []):
|
| 252 |
+
tool_name = bt.get("name","")
|
| 253 |
+
call_arg = bt.get("call_arg", user_msg)
|
| 254 |
+
if tool_name not in BUILTIN_TOOL_IMPLEMENTATIONS:
|
| 255 |
+
continue
|
| 256 |
+
|
| 257 |
+
yield emit({"type":"tool_call","tool":tool_name,"arg":call_arg[:80],"builtin":True})
|
| 258 |
+
await asyncio.sleep(0)
|
| 259 |
+
|
| 260 |
+
# Actual execution in thread pool
|
| 261 |
+
spec = BUILTIN_TOOL_IMPLEMENTATIONS[tool_name]
|
| 262 |
+
args_dict = {}
|
| 263 |
+
if spec["args"]:
|
| 264 |
+
first_key = list(spec["args"].keys())[0]
|
| 265 |
+
args_dict = {first_key: call_arg}
|
| 266 |
+
|
| 267 |
+
loop = asyncio.get_event_loop()
|
| 268 |
+
result = await loop.run_in_executor(None,
|
| 269 |
+
lambda s=spec, a=args_dict: execute_tool(
|
| 270 |
+
s["code"], tool_name, a, s["packages"], timeout=45
|
| 271 |
+
)
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
if result["ok"]:
|
| 275 |
+
builtin_results[tool_name] = result["result"]
|
| 276 |
+
preview = result["result"][:200]
|
| 277 |
+
yield emit({"type":"tool_result","tool":tool_name,"result":preview,"ok":True})
|
| 278 |
+
|
| 279 |
+
# Handle audio immediately
|
| 280 |
+
if tool_name == "create_voice" and result["result"].startswith("AUDIO_B64:"):
|
| 281 |
+
yield emit({"type":"audio_response","audio_b64":result["result"][10:]})
|
| 282 |
+
else:
|
| 283 |
+
err = result.get("error","unknown error")
|
| 284 |
+
builtin_results[tool_name] = f"Error: {err}"
|
| 285 |
+
yield emit({"type":"tool_result","tool":tool_name,"result":err,"ok":False})
|
| 286 |
|
| 287 |
+
await asyncio.sleep(0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
|
| 289 |
+
# 3. Sub-agents with real tool execution
|
| 290 |
+
if plan.get("needs_agents") and plan.get("sub_agents"):
|
| 291 |
+
sub_agents = plan["sub_agents"]
|
| 292 |
+
yield emit({"type":"step","text":f"Spawning {len(sub_agents)} real agent(s)β¦"})
|
| 293 |
|
| 294 |
for spec in sub_agents:
|
| 295 |
+
tool_names = [t["name"] for t in spec.get("tools",[])]
|
| 296 |
+
yield emit({"type":"agent_created","name":spec["name"],
|
| 297 |
+
"role":spec["role"],"goal":spec.get("goal",""),
|
| 298 |
+
"tools":tool_names,"tool_specs":spec.get("tools",[])})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 299 |
await asyncio.sleep(0.05)
|
| 300 |
|
| 301 |
context_so_far = ""
|
| 302 |
+
for spec in sub_agents:
|
| 303 |
+
yield emit({"type":"agent_working","name":spec["name"],"task":spec["task"][:100]})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
await asyncio.sleep(0)
|
| 305 |
|
| 306 |
+
agent_tool_results = {}
|
| 307 |
+
|
| 308 |
+
for tool in spec.get("tools", []):
|
| 309 |
+
t_name = tool["name"]
|
| 310 |
+
t_code = tool.get("code","")
|
| 311 |
+
t_pkgs = tool.get("packages",[])
|
| 312 |
+
t_arg_key = tool.get("call_arg_key","input")
|
| 313 |
+
t_arg_val = tool.get("call_arg_value", user_msg)
|
| 314 |
+
|
| 315 |
+
yield emit({"type":"tool_building","agent":spec["name"],
|
| 316 |
+
"tool":t_name,"packages":t_pkgs,
|
| 317 |
+
"description":tool.get("description","")})
|
| 318 |
+
await asyncio.sleep(0)
|
| 319 |
+
|
| 320 |
+
# Install packages if needed
|
| 321 |
+
if t_pkgs:
|
| 322 |
+
loop = asyncio.get_event_loop()
|
| 323 |
+
ok, msg = await loop.run_in_executor(None, pip_install, t_pkgs)
|
| 324 |
+
yield emit({"type":"pkg_install","packages":t_pkgs,"ok":ok,"msg":msg})
|
| 325 |
+
|
| 326 |
+
# Actually execute the tool
|
| 327 |
+
call_args = {t_arg_key: t_arg_val}
|
| 328 |
+
loop = asyncio.get_event_loop()
|
| 329 |
+
result = await loop.run_in_executor(None,
|
| 330 |
+
lambda c=t_code, n=t_name, a=call_args, p=t_pkgs:
|
| 331 |
+
execute_tool(c, n, a, p, timeout=45)
|
| 332 |
+
)
|
| 333 |
+
|
| 334 |
+
if result["ok"]:
|
| 335 |
+
agent_tool_results[t_name] = result["result"]
|
| 336 |
+
preview = result["result"][:250]
|
| 337 |
+
yield emit({"type":"tool_ready","agent":spec["name"],"tool":t_name,
|
| 338 |
+
"result":preview,"ok":True})
|
| 339 |
+
|
| 340 |
+
# Voice from agent tool
|
| 341 |
+
if result["result"].startswith("AUDIO_B64:"):
|
| 342 |
+
yield emit({"type":"audio_response","audio_b64":result["result"][10:]})
|
| 343 |
+
else:
|
| 344 |
+
err = result.get("error","unknown")
|
| 345 |
+
agent_tool_results[t_name] = f"Error: {err}"
|
| 346 |
+
yield emit({"type":"tool_ready","agent":spec["name"],"tool":t_name,
|
| 347 |
+
"result":err,"ok":False,"error":err})
|
| 348 |
|
| 349 |
+
await asyncio.sleep(0)
|
|
|
|
|
|
|
|
|
|
| 350 |
|
| 351 |
+
# Agent synthesizes its real results
|
| 352 |
+
try:
|
| 353 |
+
agent_summary = await self.synthesize_agent(
|
| 354 |
+
cl, spec, agent_tool_results, context_so_far, model
|
| 355 |
+
)
|
| 356 |
+
agent_results[spec["name"]] = agent_summary
|
| 357 |
+
context_so_far += f"\n\n{spec['name']}: {agent_summary[:500]}"
|
| 358 |
+
yield emit({"type":"agent_done","name":spec["name"],
|
| 359 |
+
"preview":agent_summary[:300]})
|
| 360 |
except Exception as e:
|
| 361 |
+
agent_results[spec["name"]] = f"Agent error: {e}"
|
| 362 |
+
yield emit({"type":"agent_error","name":spec["name"],"error":str(e)[:150]})
|
| 363 |
|
| 364 |
+
# Final synthesis
|
| 365 |
+
yield emit({"type":"step","text":"Synthesizing real results into final answerβ¦"})
|
| 366 |
|
| 367 |
+
# 4. Stream final response
|
| 368 |
+
yield emit({"type":"response_start","output_format":plan.get("output_format","text")})
|
| 369 |
+
full_text = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
|
| 371 |
+
if plan.get("needs_agents") and agent_results:
|
| 372 |
+
async for token in self.final_synthesis(
|
| 373 |
+
cl, user_msg, agent_results, builtin_results,
|
| 374 |
+
plan.get("synthesis",""), plan.get("output_format","text"), model
|
| 375 |
+
):
|
| 376 |
+
full_text += token
|
| 377 |
+
yield emit({"type":"token","content":token})
|
| 378 |
else:
|
| 379 |
+
async for token in self.direct_stream(
|
| 380 |
+
cl, user_msg, history, builtin_results,
|
| 381 |
+
plan.get("output_format","text"), model
|
| 382 |
+
):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
full_text += token
|
| 384 |
+
yield emit({"type":"token","content":token})
|
| 385 |
+
|
| 386 |
+
# Extract and deliver voice if requested
|
| 387 |
+
if "[VOICE:" in full_text:
|
| 388 |
+
try:
|
| 389 |
+
voice_text = full_text.split("[VOICE:")[1].rsplit("]",1)[0].strip()
|
| 390 |
+
loop = asyncio.get_event_loop()
|
| 391 |
+
spec = BUILTIN_TOOL_IMPLEMENTATIONS["create_voice"]
|
| 392 |
+
vresult = await loop.run_in_executor(None,
|
| 393 |
+
lambda: execute_tool(spec["code"],"create_voice",
|
| 394 |
+
{"text":voice_text},spec["packages"],45)
|
| 395 |
+
)
|
| 396 |
+
if vresult["ok"] and vresult["result"].startswith("AUDIO_B64:"):
|
| 397 |
+
yield emit({"type":"audio_response","audio_b64":vresult["result"][10:],
|
| 398 |
+
"text":voice_text})
|
| 399 |
+
else:
|
| 400 |
+
yield emit({"type":"voice_fallback","text":voice_text})
|
| 401 |
+
except Exception:
|
| 402 |
+
pass
|
| 403 |
+
|
| 404 |
+
yield emit({"type":"done"})
|
| 405 |
|
| 406 |
except Exception as e:
|
| 407 |
+
yield emit({"type":"error","message":str(e),"detail":traceback.format_exc()[-600:]})
|
| 408 |
|
| 409 |
|
| 410 |
orchestrator = AgentOrchestrator()
|