Sanyam400 commited on
Commit
ec32184
·
verified ·
1 Parent(s): 8813d72

Update app/agent_system.py

Browse files
Files changed (1) hide show
  1. app/agent_system.py +400 -212
app/agent_system.py CHANGED
@@ -1,46 +1,160 @@
1
  import os
2
  import json
3
  import asyncio
 
4
  import traceback
 
 
 
 
5
  from openai import AsyncOpenAI
6
  from typing import AsyncGenerator
7
  from docs_context import PRAISONAI_DOCS
8
 
9
  LONGCAT_BASE_URL = "https://api.longcat.chat/openai/v1"
10
- MODEL = "LongCat-Flash-Lite"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
 
13
- def build_orchestrator_system():
14
- return f"""You are the Main Orchestrator Agent for PraisonChat — a powerful AI system that solves complex tasks by dynamically creating specialized sub-agents, each with custom-built tools.
 
 
15
 
16
  {PRAISONAI_DOCS}
17
 
 
 
18
  ## Your Job
19
  When a user sends a task:
20
  1. Analyze what kind of work is needed
21
- 2. Design specialized sub-agents, each focused on one responsibility
22
- 3. For each sub-agent, design the exact Python tools they need
23
- 4. Return a structured execution plan as JSON
 
24
 
25
- ## Response Format
26
- Always respond with valid JSON in this exact structure:
27
  {{
28
- "task_analysis": "Clear explanation of what needs to be done and why",
29
- "needs_sub_agents": true,
 
30
  "sub_agents": [
31
  {{
32
  "name": "AgentName",
33
  "role": "Specific professional role",
34
  "goal": "What this agent achieves",
35
- "backstory": "Brief agent background/expertise",
36
  "tools": [
37
  {{
38
  "name": "tool_function_name",
39
  "description": "What this tool does",
40
  "parameters": "param1: str, param2: int = 10",
41
  "return_type": "str",
42
- "docstring": "Detailed docstring explaining the tool",
43
- "implementation": "Python code body (indented with 4 spaces, no def line)"
44
  }}
45
  ],
46
  "task_description": "Exact task for this agent to perform",
@@ -48,313 +162,387 @@ Always respond with valid JSON in this exact structure:
48
  }}
49
  ],
50
  "execution_order": ["AgentName1", "AgentName2"],
51
- "synthesis_instruction": "How to combine all agent results into the final answer"
 
52
  }}
53
 
 
 
54
  ## Rules
55
- - Create sub-agents ONLY when needed. Simple questions = no sub-agents (set needs_sub_agents: false)
56
- - Tools must be real, executable Python code
57
- - Each tool implementation must be complete and working
58
- - Maximum 4 sub-agents per task
59
- - Tool implementations must not import anything not in Python stdlib
60
- - Keep tool implementations under 30 lines each
61
  """
62
 
63
 
64
- def build_tool_function(tool_spec: dict) -> callable:
65
- """Dynamically create a Python function from a tool spec."""
66
- name = tool_spec["name"]
67
- params = tool_spec.get("parameters", "input: str")
68
- return_type = tool_spec.get("return_type", "str")
69
- docstring = tool_spec.get("docstring", "Auto-generated tool")
70
- implementation = tool_spec.get("implementation", "return str(input)")
71
 
72
- # Build the function source
73
- func_source = f"""
74
- def {name}({params}) -> {return_type}:
75
- \"\"\"{docstring}\"\"\"
76
- {chr(10).join(' ' + line if line.strip() else '' for line in implementation.strip().splitlines())}
77
- """
78
- namespace = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  try:
80
- exec(func_source, namespace)
81
- return namespace[name]
 
 
 
 
 
 
 
 
 
 
82
  except Exception as e:
83
- # Return a safe fallback function
84
- def fallback_tool(**kwargs) -> str:
85
- return f"Tool '{name}' could not be created: {e}. Using fallback."
86
- fallback_tool.__name__ = name
87
- fallback_tool.__doc__ = docstring
88
- return fallback_tool
89
 
90
 
91
  class AgentOrchestrator:
92
  def __init__(self):
93
- self._client_cache = {}
94
 
95
- def get_client(self, api_key: str) -> AsyncOpenAI:
96
- if api_key not in self._client_cache:
97
- self._client_cache[api_key] = AsyncOpenAI(
 
98
  api_key=api_key,
99
- base_url=LONGCAT_BASE_URL
100
  )
101
- return self._client_cache[api_key]
102
 
103
- async def plan_task(self, client: AsyncOpenAI, user_message: str, history: list) -> dict:
104
- """Ask the orchestrator to plan the task."""
105
  messages = [{"role": "system", "content": build_orchestrator_system()}]
106
-
107
- # Add recent history for context
108
- for msg in history[-6:]:
109
- messages.append({"role": msg["role"], "content": str(msg["content"])[:2000]})
110
-
111
  messages.append({
112
  "role": "user",
113
- "content": f"Plan the execution for this task: {user_message}"
114
  })
115
 
116
- response = await client.chat.completions.create(
117
- model=MODEL,
118
  messages=messages,
119
  max_tokens=6000,
120
- temperature=0.2,
121
  )
122
-
123
- raw = response.choices[0].message.content.strip()
124
- # Strip markdown code fences if present
125
- if raw.startswith("```"):
126
- raw = raw.split("```")[1]
127
- if raw.startswith("json"):
128
- raw = raw[4:]
129
- raw = raw.strip()
130
-
 
 
131
  try:
132
  return json.loads(raw)
133
  except Exception:
134
  return {
135
  "task_analysis": "Direct response",
136
  "needs_sub_agents": False,
 
137
  "sub_agents": [],
138
  "execution_order": [],
139
- "synthesis_instruction": "Respond directly"
 
140
  }
141
 
142
- async def run_sub_agent(
143
- self,
144
- client: AsyncOpenAI,
145
- agent_spec: dict,
146
- context_so_far: str
147
- ) -> str:
148
- """Run a single sub-agent with its tools."""
149
- tool_descriptions = ""
150
- tools_created = []
151
  tool_errors = []
 
 
 
 
 
 
 
 
 
152
 
153
- for tool_spec in agent_spec.get("tools", []):
154
- fn = build_tool_function(tool_spec)
155
- tools_created.append(fn.__name__)
156
- tool_descriptions += f"\n- {fn.__name__}: {tool_spec.get('description', '')}"
 
157
 
158
- system_prompt = f"""You are {agent_spec['name']}.
159
- Role: {agent_spec['role']}
160
- Goal: {agent_spec['goal']}
161
- Backstory: {agent_spec.get('backstory', '')}
162
 
163
- You have access to these custom tools (simulate their usage in your reasoning):
164
- {tool_descriptions if tool_descriptions else "No specialized tools - use your knowledge directly."}
165
 
166
  Context from previous agents:
167
- {context_so_far if context_so_far else "You are the first agent running."}
168
 
169
- Execute your task thoroughly. Show your reasoning and tool usage step by step.
170
- Expected output: {agent_spec.get('expected_output', 'Detailed results')}"""
171
 
172
- response = await client.chat.completions.create(
173
- model=MODEL,
174
  messages=[
175
- {"role": "system", "content": system_prompt},
176
- {"role": "user", "content": agent_spec["task_description"]}
177
  ],
178
  max_tokens=12000,
179
  temperature=0.7,
180
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
 
182
- return response.choices[0].message.content
183
-
184
- async def synthesize(
185
- self,
186
- client: AsyncOpenAI,
187
- user_message: str,
188
- agent_results: dict,
189
- synthesis_instruction: str
190
- ) -> AsyncGenerator[str, None]:
191
- """Stream the final synthesized response."""
192
- results_text = "\n\n".join([
193
- f"=== {name} ===\n{result}"
194
- for name, result in agent_results.items()
195
- ])
196
-
197
- system_prompt = f"""You are the Main Orchestrator synthesizing results from specialized sub-agents.
198
-
199
  Synthesis instruction: {synthesis_instruction}
 
 
200
 
201
  Sub-agent results:
202
- {results_text}
203
 
204
- Provide a comprehensive, well-structured final response to the user.
205
- Use markdown formatting. Be thorough but concise."""
206
 
207
  stream = await client.chat.completions.create(
208
- model=MODEL,
209
  messages=[
210
- {"role": "system", "content": system_prompt},
211
- {"role": "user", "content": user_message}
212
  ],
213
  max_tokens=16000,
214
  temperature=0.7,
215
- stream=True
216
  )
217
-
218
  async for chunk in stream:
219
- delta = chunk.choices[0].delta
220
- if delta.content:
221
- yield delta.content
222
-
223
- async def direct_response(
224
- self,
225
- client: AsyncOpenAI,
226
- user_message: str,
227
- history: list
228
- ) -> AsyncGenerator[str, None]:
229
- """Stream a direct response without sub-agents."""
 
 
 
 
 
 
 
 
 
 
 
230
  messages = [{
231
  "role": "system",
232
- "content": "You are PraisonChat, a powerful AI assistant. Respond helpfully using markdown formatting."
 
 
 
 
 
 
 
233
  }]
234
- for msg in history[-10:]:
235
- messages.append({"role": msg["role"], "content": str(msg["content"])[:3000]})
236
  messages.append({"role": "user", "content": user_message})
237
 
238
  stream = await client.chat.completions.create(
239
- model=MODEL,
240
  messages=messages,
241
  max_tokens=16000,
242
  temperature=0.7,
243
- stream=True
244
  )
245
-
246
  async for chunk in stream:
247
- delta = chunk.choices[0].delta
248
- if delta.content:
249
- yield delta.content
250
-
251
- async def stream_response(
252
- self,
253
- user_message: str,
254
- history: list,
255
- api_key: str
256
- ) -> AsyncGenerator[str, None]:
257
- """Main entry point — streams SSE-formatted events."""
258
 
 
 
259
  def emit(data: dict) -> str:
260
  return json.dumps(data)
261
 
262
- client = self.get_client(api_key)
 
263
 
264
  try:
265
- # ── Step 1: Plan ─────────────────────────────────────────────────
266
- yield emit({"type": "step", "text": "🧠 Main Agent analyzing your task..."})
267
  await asyncio.sleep(0)
268
 
269
- plan = await self.plan_task(client, user_message, history)
270
 
271
- yield emit({
272
- "type": "step",
273
- "text": f"📋 {plan.get('task_analysis', 'Planning execution...')}"
274
- })
275
  await asyncio.sleep(0)
276
 
277
  sub_agents = plan.get("sub_agents", [])
278
- needs_sub_agents = plan.get("needs_sub_agents", bool(sub_agents))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
 
280
- # ── Step 2: Sub-agents or direct ─────────────────────────────────
281
- if needs_sub_agents and sub_agents:
282
- yield emit({
283
- "type": "step",
284
- "text": f"🤖 Spawning {len(sub_agents)} specialized sub-agent(s)..."
285
- })
286
 
287
- for agent_spec in sub_agents:
288
- tool_names = [t["name"] for t in agent_spec.get("tools", [])]
289
  yield emit({
290
  "type": "agent_created",
291
- "name": agent_spec["name"],
292
- "role": agent_spec["role"],
293
- "tools": tool_names
 
 
294
  })
295
  await asyncio.sleep(0.05)
296
 
297
- # Execute each sub-agent
298
  context_so_far = ""
299
  agent_results = {}
300
- execution_order = plan.get("execution_order", [a["name"] for a in sub_agents])
301
 
302
- for agent_name in execution_order:
303
- agent_spec = next(
304
- (a for a in sub_agents if a["name"] == agent_name), None
305
- )
306
- if not agent_spec:
307
  continue
308
 
309
- yield emit({
310
- "type": "step",
311
- "text": f"⚡ {agent_name} working on: {agent_spec['task_description'][:100]}..."
312
- })
313
  await asyncio.sleep(0)
314
 
 
 
 
 
 
 
315
  try:
316
- result = await self.run_sub_agent(client, agent_spec, context_so_far)
317
- agent_results[agent_name] = result
318
- context_so_far += f"\n\n{agent_name} completed: {result[:600]}"
319
-
320
- yield emit({
321
- "type": "agent_result",
322
- "name": agent_name,
323
- "preview": result[:300] + ("..." if len(result) > 300 else "")
324
- })
 
 
325
  except Exception as e:
326
- yield emit({
327
- "type": "step",
328
- "text": f"⚠️ {agent_name} encountered an issue: {str(e)[:100]}"
329
- })
330
- agent_results[agent_name] = f"Error: {e}"
331
-
332
- # Synthesize
333
- yield emit({"type": "step", "text": "✨ Synthesizing final response..."})
334
- yield emit({"type": "response_start"})
335
- await asyncio.sleep(0)
336
-
337
- async for token in self.synthesize(
338
- client, user_message, agent_results,
339
- plan.get("synthesis_instruction", "Combine all results into a clear response")
340
- ):
341
  yield emit({"type": "token", "content": token})
342
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  else:
344
  # Direct response
345
- yield emit({"type": "step", "text": "💬 Generating response..."})
346
- yield emit({"type": "response_start"})
347
- await asyncio.sleep(0)
348
-
349
- async for token in self.direct_response(client, user_message, history):
 
 
 
 
 
350
  yield emit({"type": "token", "content": token})
351
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
  yield emit({"type": "done"})
353
 
354
  except Exception as e:
355
- tb = traceback.format_exc()
356
- yield emit({"type": "error", "message": str(e), "detail": tb[:500]})
357
 
358
 
359
- # Singleton
360
- orchestrator = AgentOrchestrator()
 
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",
 
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()