Mayank2027 commited on
Commit
cedf413
·
verified ·
1 Parent(s): 3654b61

Update supervisor.py

Browse files
Files changed (1) hide show
  1. supervisor.py +91 -319
supervisor.py CHANGED
@@ -1,24 +1,15 @@
1
- # supervisor.py – Fixed
2
  import json
3
  import logging
4
  import asyncio
5
  from typing import Dict, List, Any, Optional
6
  from datetime import datetime
7
- from llm_client import LLMClient, LLMRouter
8
- from models import PlanStep, TaskStatus, AgentMetrics, SessionMetrics
9
 
10
  logger = logging.getLogger("MultiAgentSystem")
11
 
12
-
13
  class SupervisorAgent:
14
- """
15
- Intelligent task supervisor with:
16
- - Dependency-aware parallel execution
17
- - Dynamic replanning on failure
18
- - Cost-optimized LLM selection
19
- - Real-time progress tracking
20
- """
21
-
22
  def __init__(self, tools, state, metrics_collector=None):
23
  self.tools = tools
24
  self.state = state
@@ -28,326 +19,107 @@ class SupervisorAgent:
28
  self.done = False
29
  self.result = None
30
  self.metrics_collector = metrics_collector
31
- self.session_metrics: Optional[SessionMetrics] = None
32
-
33
- # Execution control
34
- self._semaphore: Optional[asyncio.Semaphore] = None
35
- self._running_tasks: Dict[int, asyncio.Task] = {}
36
  self._cancelled = False
37
-
38
  def register_specialist(self, name, agent):
39
  self.specialists[name] = agent
40
-
41
- async def plan_task(self, task: str) -> List[PlanStep]:
42
- """Generate execution plan with LLM."""
43
- # Retrieve similar past episodes for context
44
- try:
45
- from memory import memory_manager
46
- similar = await memory_manager.retrieve_similar_episodes(task, n=2)
47
- lessons = "\n".join([ep.get("lessons", "") for ep in similar if ep.get("lessons")])
48
- except:
49
- lessons = ""
50
-
51
- prompt = f"""You are the Supervisor. Decompose the user request into sub-tasks.
52
-
53
- Available agents: {list(self.specialists.keys())}
54
 
55
- Past lessons learned:
56
- {lessons}
57
-
58
- User request: {task}
 
59
 
60
- Return a JSON array of steps. Each step must have:
61
- - step: integer (1, 2, 3...)
62
- - agent: one of the available agent names
63
- - task: detailed description of what this agent should do
64
- - depends_on: array of step numbers this step depends on (can be empty)
 
 
 
 
 
 
 
65
 
66
- Rules:
67
- - Minimize dependencies to maximize parallelism
68
- - Break complex tasks into 3-8 steps
69
- - Each step should be independently verifiable
70
- - Include a final synthesis step if needed
71
 
72
- Example:
73
- [
74
- {{"step": 1, "agent": "Research", "task": "Search Wikipedia for AI history", "depends_on": []}},
75
- {{"step": 2, "agent": "Research", "task": "Search for latest AI breakthroughs 2024", "depends_on": []}},
76
- {{"step": 3, "agent": "Content", "task": "Summarize findings into report", "depends_on": [1, 2]}}
77
- ]"""
78
-
79
- messages = [
80
- {"role": "system", "content": "You are a task decomposition expert. Output valid JSON only."},
81
- {"role": "user", "content": prompt}
82
- ]
83
-
84
- try:
85
- raw, metadata = await LLMClient.chat(
86
- messages,
87
- temperature=0.2,
88
- response_format="json",
89
- task_type="planning"
90
- )
91
- plan_data = json.loads(raw)
92
- if isinstance(plan_data, list):
93
- plan = [PlanStep(**step) for step in plan_data]
94
- # Validate agent names
95
- for step in plan:
96
- if step.agent not in self.specialists and step.agent != "Supervisor":
97
- logger.warning(f"Unknown agent '{step.agent}', defaulting to Research")
98
- step.agent = "Research"
99
- return plan
100
- except Exception as e:
101
- logger.error(f"Plan parsing failed: {e}. Raw: {raw[:200] if 'raw' in locals() else ''}")
102
-
103
- # Fallback: single-step plan
104
- return [PlanStep(step=1, agent="Research", task=task, depends_on=[])]
105
-
106
- async def execute_plan_parallel(self, plan: List[PlanStep]) -> Dict[int, Dict]:
107
- """
108
- Execute plan with true parallelism respecting dependencies.
109
- Uses semaphore to limit concurrent agents.
110
- """
111
- from config import MAX_PARALLEL_AGENTS
112
- self._semaphore = asyncio.Semaphore(MAX_PARALLEL_AGENTS)
113
- self._cancelled = False
114
-
115
- results: Dict[int, Dict] = {}
116
- completed = set()
117
- failed = set()
118
-
119
- # Create a dummy metrics object if none exists
120
- dummy_metrics = AgentMetrics(session_id="dummy", agent_name="dummy")
121
-
122
- async def execute_step(step: PlanStep) -> tuple:
123
- """Execute a single plan step."""
124
- async with self._semaphore:
125
- if self._cancelled:
126
- return step.step, {"error": "Execution cancelled", "status": "cancelled"}
127
-
128
- step.status = TaskStatus.RUNNING
129
- step.started_at = datetime.now()
130
-
131
- agent = self.specialists.get(step.agent)
132
- if not agent:
133
- step.status = TaskStatus.ERROR
134
- return step.step, {"error": f"Agent {step.agent} not found"}
135
-
136
- # Build context from completed dependencies
137
- dep_results = {
138
- dep: results[dep]
139
- for dep in step.depends_on
140
- if dep in completed
141
  }
142
-
143
- context = self._build_context(step, dep_results)
144
-
145
- # Track metrics (safely)
146
- metrics = dummy_metrics
147
- if self.session_metrics:
148
- metrics = self.session_metrics.get_or_create_agent_metrics(step.agent)
149
- metrics.start()
150
-
151
- try:
152
- result = await agent.run(context)
153
- metrics.stop()
154
- metrics.add_success()
155
-
156
- step.status = TaskStatus.COMPLETED
157
- step.completed_at = datetime.now()
158
- step.result = result
159
-
160
- return step.step, {
161
- "agent": step.agent,
162
- "task": step.task,
163
- "result": result,
164
- "status": "completed"
165
- }
166
-
167
- except Exception as e:
168
- metrics.stop()
169
- metrics.add_error()
170
- logger.error(f"Step {step.step} failed: {e}")
171
-
172
- # Retry logic
173
- if step.retries < step.max_retries:
174
- step.retries += 1
175
- step.status = TaskStatus.PENDING
176
- logger.info(f"Retrying step {step.step} (attempt {step.retries}/{step.max_retries})")
177
- return await execute_step(step)
178
-
179
- step.status = TaskStatus.ERROR
180
- step.result = {"error": str(e)}
181
- return step.step, {
182
- "agent": step.agent,
183
- "task": step.task,
184
- "error": str(e),
185
- "status": "error"
186
- }
187
-
188
- # Main execution loop
189
- pending = set(range(len(plan)))
190
-
191
- while pending and not self._cancelled:
192
- # Find all ready steps (dependencies satisfied)
193
- ready = []
194
- for i in pending:
195
- step = plan[i]
196
- deps_satisfied = all(
197
- (d - 1) in completed or (d - 1) in failed # step numbers are 1-based
198
- for d in step.depends_on
199
- )
200
- if deps_satisfied:
201
- ready.append(i)
202
-
203
- if not ready:
204
- if pending:
205
- logger.error("Deadlock detected in plan execution")
206
- for i in pending:
207
- plan[i].status = TaskStatus.ERROR
208
- failed.add(i) # Mark as failed to break the loop
209
- pending.clear()
210
- break
211
-
212
- # Execute ready steps in parallel
213
- tasks = [execute_step(plan[i]) for i in ready]
214
- batch_results = await asyncio.gather(*tasks, return_exceptions=True)
215
-
216
- # Process results
217
- for i, res in zip(ready, batch_results):
218
- if isinstance(res, Exception):
219
- logger.error(f"Step {plan[i].step} execution error: {res}")
220
- # Mark as failed
221
- failed.add(i)
222
- else:
223
- step_idx, result = res
224
- results[step_idx - 1] = result
225
- if "error" in result and result.get("status") != "retry":
226
- failed.add(i)
227
- else:
228
- completed.add(i)
229
- pending.discard(i)
230
-
231
  # Update progress
232
- self.state.steps = len(completed)
233
- progress = int((len(completed) / len(plan)) * 100)
234
- # (Progress is read by orchestrator.get_status() which calls this method's status)
235
-
236
  return results
237
-
238
- def _build_context(self, step: PlanStep, dep_results: Dict) -> str:
239
- """Build execution context for an agent."""
240
- context_parts = [f"Your task: {step.task}"]
241
-
242
- if dep_results:
243
- context_parts.append("\nResults from previous steps:")
244
- for dep_idx, dep_result in dep_results.items():
245
- context_parts.append(f"\nStep {dep_idx + 1}: {json.dumps(dep_result, default=str)[:500]}")
246
-
247
- context_parts.append(f"\nOverall goal: {self.state.task}")
248
- context_parts.append(f"\nYou are the {step.agent} Agent. Use your specialist skills.")
249
-
250
- return "\n".join(context_parts)
251
-
252
- async def synthesize(self, task: str, results: Dict[int, Dict]) -> str:
253
- """Synthesize final answer from all agent results."""
254
- # Filter out failed steps
255
- successful = {k: v for k, v in results.items() if "error" not in v}
256
-
257
- if not successful:
258
- return "All steps failed. Unable to complete task."
259
-
260
- prompt = f"""Synthesize a comprehensive final answer.
261
 
262
- Original task: {task}
263
-
264
- Agent results:
265
- {json.dumps(successful, indent=2, default=str)[:4000]}
 
 
 
 
 
 
 
 
 
 
266
 
267
- Instructions:
268
- - Combine all agent outputs into a coherent response
269
- - Highlight key findings
270
- - Note any failures or gaps
271
- - Provide actionable next steps if applicable"""
272
-
273
- messages = [
274
- {"role": "system", "content": "You are a synthesis expert. Create clear, actionable summaries."},
275
- {"role": "user", "content": prompt}
276
- ]
277
-
278
- result, metadata = await LLMClient.chat(
279
- messages,
280
- temperature=0.3,
281
- task_type="synthesis"
282
- )
283
- return result
284
-
285
  async def run(self, task: str) -> Dict:
286
- """Main execution flow."""
287
  self.state.task = task
288
  self.done = False
289
-
290
- # Initialize metrics if collector is present
291
- if self.metrics_collector:
292
- self.session_metrics = self.metrics_collector.start_session(self.state.session_id or "unknown")
293
- self.session_metrics.plan_steps = 0
294
-
295
- try:
296
- # Phase 1: Planning
297
- self.state.plan = await self.plan_task(task)
298
- if self.session_metrics:
299
- self.session_metrics.plan_steps = len(self.state.plan)
300
-
301
- # Phase 2: Execution (parallel)
302
- results = await self.execute_plan_parallel(self.state.plan)
303
- self.agent_results = results
304
-
305
- # Phase 3: Synthesis
306
- solution = await self.synthesize(task, results)
307
- self.result = solution
308
- self.done = True
309
-
310
- # Store episode for learning
311
- try:
312
- from memory import memory_manager
313
- await memory_manager.store_episode(
314
- self.state.session_id or "unknown",
315
- task,
316
- [p.model_dump() for p in self.state.plan],
317
- results,
318
- True,
319
- "Task completed successfully"
320
- )
321
- except:
322
- pass
323
-
324
- # Finalize metrics
325
- if self.session_metrics:
326
- await self.session_metrics.finalize()
327
- await self.session_metrics.save()
328
-
329
- return {
330
- "plan": [p.model_dump() for p in self.state.plan],
331
- "agent_results": results,
332
- "solution": solution,
333
- "metrics": self.session_metrics.to_dict() if self.session_metrics else None
334
- }
335
-
336
- except Exception as e:
337
- logger.error(f"Supervisor run failed: {e}")
338
- self.done = True
339
- if self.session_metrics:
340
- await self.session_metrics.finalize()
341
-
342
- return {
343
- "error": str(e),
344
- "plan": [p.model_dump() for p in self.state.plan],
345
- "agent_results": self.agent_results,
346
- "metrics": self.session_metrics.to_dict() if self.session_metrics else None
347
- }
348
-
349
  def cancel(self):
350
- """Cancel ongoing execution."""
351
- self._cancelled = True
352
- for task in self._running_tasks.values():
353
- task.cancel()
 
1
+ # supervisor.py – Simple Serial Execution with Timeouts
2
  import json
3
  import logging
4
  import asyncio
5
  from typing import Dict, List, Any, Optional
6
  from datetime import datetime
7
+ from llm_client import LLMClient
8
+ from models import PlanStep, TaskStatus
9
 
10
  logger = logging.getLogger("MultiAgentSystem")
11
 
 
12
  class SupervisorAgent:
 
 
 
 
 
 
 
 
13
  def __init__(self, tools, state, metrics_collector=None):
14
  self.tools = tools
15
  self.state = state
 
19
  self.done = False
20
  self.result = None
21
  self.metrics_collector = metrics_collector
 
 
 
 
 
22
  self._cancelled = False
23
+
24
  def register_specialist(self, name, agent):
25
  self.specialists[name] = agent
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
+ async def plan_task(self, task: str) -> List[PlanStep]:
28
+ """Generate a simple plan – always one step for reliability."""
29
+ # Fallback: always use Research agent for the whole task
30
+ logger.info(f"📋 Supervisor creating plan for task: {task}")
31
+ return [PlanStep(step=1, agent="Research", task=task, depends_on=[])]
32
 
33
+ async def execute_plan_serial(self, plan: List[PlanStep]) -> Dict[int, Dict]:
34
+ """Execute steps one by one with timeout."""
35
+ results = {}
36
+ for step in plan:
37
+ if self._cancelled:
38
+ break
39
+ logger.info(f"▶️ Executing step {step.step}: {step.task} (agent: {step.agent})")
40
+ self.state.steps = step.step - 1 # progress
41
+ agent = self.specialists.get(step.agent)
42
+ if not agent:
43
+ results[step.step] = {"error": f"Agent {step.agent} not found"}
44
+ continue
45
 
46
+ # Build context
47
+ context = f"Task: {self.state.task}\nYour sub-task: {step.task}"
48
+ # Add previous results
49
+ if results:
50
+ context += f"\nPrevious results: {json.dumps(results, default=str)[:500]}"
51
 
52
+ try:
53
+ # Run agent with timeout (60 seconds)
54
+ result = await asyncio.wait_for(agent.run(context), timeout=60.0)
55
+ step.status = TaskStatus.COMPLETED
56
+ results[step.step] = {
57
+ "agent": step.agent,
58
+ "task": step.task,
59
+ "result": result,
60
+ "status": "completed"
61
+ }
62
+ logger.info(f"✅ Step {step.step} completed successfully.")
63
+ except asyncio.TimeoutError:
64
+ logger.error(f"⏰ Step {step.step} timed out after 60s")
65
+ step.status = TaskStatus.ERROR
66
+ results[step.step] = {
67
+ "agent": step.agent,
68
+ "task": step.task,
69
+ "error": "Timeout",
70
+ "status": "error"
71
+ }
72
+ except Exception as e:
73
+ logger.error(f"❌ Step {step.step} failed: {e}")
74
+ step.status = TaskStatus.ERROR
75
+ results[step.step] = {
76
+ "agent": step.agent,
77
+ "task": step.task,
78
+ "error": str(e),
79
+ "status": "error"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  # Update progress
82
+ self.state.steps = len(results)
 
 
 
83
  return results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
+ async def synthesize(self, task: str, results: Dict[int, Dict]) -> str:
86
+ """Simple synthesis: combine results."""
87
+ if not results:
88
+ return "No results from any step."
89
+ # Combine all step results into a single message
90
+ parts = []
91
+ for step_num, res in results.items():
92
+ if "error" in res:
93
+ parts.append(f"Step {step_num} failed: {res['error']}")
94
+ else:
95
+ result_data = res.get("result", {})
96
+ agent_result = result_data.get("result", "No result")
97
+ parts.append(f"Step {step_num} ({res['agent']}): {agent_result}")
98
+ return "\n".join(parts)
99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  async def run(self, task: str) -> Dict:
101
+ """Main flow: plan -> execute -> synthesize."""
102
  self.state.task = task
103
  self.done = False
104
+
105
+ # Plan (always single step for reliability)
106
+ plan = await self.plan_task(task)
107
+ self.plan = plan
108
+
109
+ # Execute
110
+ results = await self.execute_plan_serial(plan)
111
+ self.agent_results = results
112
+
113
+ # Synthesize
114
+ solution = await self.synthesize(task, results)
115
+ self.result = solution
116
+ self.done = True
117
+
118
+ return {
119
+ "plan": [p.model_dump() for p in self.plan],
120
+ "agent_results": results,
121
+ "solution": solution
122
+ }
123
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  def cancel(self):
125
+ self._cancelled = True