Sanyam400 commited on
Commit
6145277
Β·
verified Β·
1 Parent(s): 33c192a

Update app/agent_system.py

Browse files
Files changed (1) hide show
  1. app/agent_system.py +306 -444
app/agent_system.py CHANGED
@@ -1,548 +1,410 @@
1
- import os
2
- import json
3
- import asyncio
4
- import datetime
5
- import traceback
6
- import subprocess
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
- LONGCAT_BASE_URL = "https://api.longcat.chat/openai/v1"
15
  MODEL_MAP = {
16
- "LongCat-Flash-Lite": "LongCat-Flash-Lite",
17
- "LongCat-Flash-Chat": "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
- - run_python_code(code: str) -> str
99
- Executes Python code in a sandbox. Returns stdout + stderr.
100
- Use for data processing, file operations, complex calculations.
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
- - search_information(query: str) -> str
107
- Queries knowledge base. Use for research tasks.
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
- {BUILTIN_TOOLS_DOC}
131
-
132
- ## Your Job
133
- When a user sends a task:
134
- 1. Analyze what kind of work is needed
135
- 2. Use built-in tools directly for simple things (datetime, math, voice, code)
136
- 3. For complex tasks, design sub-agents each focused on one responsibility
137
- 4. For each sub-agent, design exact Python tools they need
138
- 5. Return a structured JSON execution plan
139
-
140
- ## Response Format β€” valid JSON ONLY, no markdown fences:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  {{
142
- "task_analysis": "Clear explanation of what needs to be done",
143
- "needs_sub_agents": true/false,
144
- "builtin_tools_to_use": ["get_current_datetime", "calculate_math"],
 
 
145
  "sub_agents": [
146
  {{
147
- "name": "AgentName",
148
- "role": "Specific professional role",
149
- "goal": "What this agent achieves",
150
- "backstory": "Brief agent background",
 
151
  "tools": [
152
  {{
153
- "name": "tool_function_name",
154
- "description": "What this tool does",
155
- "parameters": "param1: str, param2: int = 10",
156
- "return_type": "str",
157
- "implementation": "# Python code body (4-space indent, use # comments not triple quotes)\\n result = do_something(param1)\\n return str(result)"
 
158
  }}
159
- ],
160
- "task_description": "Exact task for this agent to perform",
161
- "expected_output": "What format/content to expect back"
162
  }}
163
  ],
164
- "execution_order": ["AgentName1", "AgentName2"],
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
- ## Rules
172
- - Simple questions (time, math, quick facts) = no sub-agents, use builtin_tools_to_use
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 build_tool_function(spec: dict):
181
- name = spec.get("name", "unnamed_tool")
182
- params = spec.get("parameters", "input: str")
183
- rtype = spec.get("return_type", "str")
184
- impl = spec.get("implementation", "return str(input)")
185
-
186
- lines = impl.strip().splitlines()
187
- indented = "\n".join(" " + l if l.strip() else "" for l in lines)
188
-
189
- src = f"def {name}({params}) -> {rtype}:\n{indented}\n"
190
- ns = {}
191
- try:
192
- exec(src, ns)
193
- fn = ns[name]
194
- fn.__doc__ = spec.get("description", "")
195
- return fn, None
196
- except Exception as e:
197
- def fallback(**kwargs) -> str:
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 get_client(self, api_key: str, model: str = DEFAULT_MODEL) -> AsyncOpenAI:
229
- key = f"{api_key}:{model}"
230
- if key not in self._clients:
231
- self._clients[key] = AsyncOpenAI(
232
- api_key=api_key,
233
- base_url=LONGCAT_BASE_URL,
234
- )
235
- return self._clients[key]
236
 
237
- async def plan_task(self, client, user_message: str, history: list, model: str) -> dict:
238
- messages = [{"role": "system", "content": build_orchestrator_system()}]
239
  for m in history[-6:]:
240
- messages.append({"role": m["role"], "content": str(m.get("content", ""))[:2000]})
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
- parts = raw.split("```")
256
- for p in parts:
257
- p2 = p.strip()
258
- if p2.startswith("json"):
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
- "task_analysis": "Direct response",
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["task_description"]},
312
  ],
313
- max_tokens=12000,
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 = "\nIMPORTANT: The user wants a voice response. End your message with: [VOICE_RESPONSE: <the exact text to speak>]"
331
 
332
- system = f"""{inject_datetime_context()}
333
- You are the Main Orchestrator. Synthesize results from sub-agents into a final response.
334
- Synthesis instruction: {synthesis_instruction}
335
- Output format: {output_format}
336
  {voice_note}
337
 
338
- Sub-agent results:
339
- {combined}
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": user_message},
348
  ],
349
- max_tokens=16000,
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
- yield c
357
-
358
- async def direct_response(self, client, user_message: str, history: list,
359
- builtin_tools: list, output_format: str, model: str) -> AsyncGenerator:
360
- # Execute builtin tools first
361
- tool_results = {}
362
- for tool_name in (builtin_tools or []):
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": m["role"], "content": str(m.get("content",""))[:3000]})
389
- messages.append({"role": "user", "content": user_message})
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
- async def stream_response(self, user_message: str, history: list,
 
404
  api_key: str, model: str = DEFAULT_MODEL) -> AsyncGenerator:
405
- def emit(data: dict) -> str:
406
- return json.dumps(data)
407
 
408
  model = MODEL_MAP.get(model, DEFAULT_MODEL)
409
- client = self.get_client(api_key, model)
410
 
411
  try:
412
- # Step 1: Plan
413
- yield emit({"type": "thinking", "text": "Analyzing your request..."})
414
  await asyncio.sleep(0)
415
 
416
- plan = await self.plan_task(client, user_message, history, model)
417
-
418
- yield emit({"type": "thinking", "text": plan.get("task_analysis", "Planning...")})
419
  await asyncio.sleep(0)
420
 
421
- sub_agents = plan.get("sub_agents", [])
422
- needs_sub = plan.get("needs_sub_agents", bool(sub_agents))
423
- builtin_tools = plan.get("builtin_tools_to_use", [])
424
- output_format = plan.get("output_format", "text")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425
 
426
- # Emit builtin tool calls
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
- # Step 2: Sub-agents or direct
439
- if needs_sub and sub_agents:
440
- yield emit({"type": "step", "text": f"Spawning {len(sub_agents)} sub-agent(s)..."})
 
441
 
442
  for spec in sub_agents:
443
- tool_names = [t["name"] for t in spec.get("tools", [])]
444
- yield emit({
445
- "type": "agent_created",
446
- "name": spec["name"],
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
- agent_results = {}
456
- order = plan.get("execution_order", [s["name"] for s in sub_agents])
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
- # Emit tool builds
468
- for t in spec.get("tools", []):
469
- yield emit({"type": "tool_building", "agent": agent_name,
470
- "tool": t["name"], "description": t.get("description", "")})
471
- await asyncio.sleep(0.05)
472
-
473
- try:
474
- r = await self.run_sub_agent(client, spec, context_so_far, model)
475
- agent_results[agent_name] = r
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
476
 
477
- # Emit tool results
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
- context_so_far += f"\n\n{agent_name}: {r['result'][:600]}"
483
- preview = r["result"][:300] + ("..." if len(r["result"]) > 300 else "")
484
- yield emit({"type": "agent_done", "name": agent_name, "preview": preview})
 
 
 
 
 
 
485
  except Exception as e:
486
- yield emit({"type": "agent_error", "name": agent_name, "error": str(e)[:200]})
487
- agent_results[agent_name] = {"result": f"Error: {e}", "tools_built": [], "tool_errors": [str(e)]}
488
 
489
- yield emit({"type": "step", "text": "Synthesizing final response..."})
490
- yield emit({"type": "response_start", "output_format": output_format})
491
 
492
- full_text = ""
493
- async for token in self.synthesize(client, user_message, agent_results,
494
- plan.get("synthesis_instruction", ""),
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
- # Direct response
514
- if builtin_tools:
515
- yield emit({"type": "step", "text": f"Using built-in tools: {', '.join(builtin_tools)}"})
516
- else:
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": "token", "content": token})
525
-
526
- # Handle voice in direct response
527
- if output_format == "voice" or "[VOICE_RESPONSE:" in full_text:
528
- try:
529
- if "[VOICE_RESPONSE:" in full_text:
530
- voice_text = full_text.split("[VOICE_RESPONSE:")[1].rsplit("]", 1)[0].strip()
531
- else:
532
- voice_text = full_text[:1000]
533
- audio_result = create_voice_response(voice_text)
534
- if audio_result.startswith("AUDIO_B64:"):
535
- yield emit({"type": "audio_response", "audio_b64": audio_result[10:],
536
- "text": voice_text})
537
- else:
538
- yield emit({"type": "voice_fallback", "text": voice_text})
539
- except Exception:
540
- pass
541
-
542
- yield emit({"type": "done"})
 
 
543
 
544
  except Exception as e:
545
- yield emit({"type": "error", "message": str(e), "detail": traceback.format_exc()[:800]})
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()