empathy-framework 3.11.0__py3-none-any.whl → 4.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,632 @@
1
+ """Execution strategies for agent composition patterns.
2
+
3
+ This module implements the 6 grammar rules for composing agents:
4
+ 1. Sequential (A → B → C)
5
+ 2. Parallel (A || B || C)
6
+ 3. Debate (A ⇄ B ⇄ C → Synthesis)
7
+ 4. Teaching (Junior → Expert validation)
8
+ 5. Refinement (Draft → Review → Polish)
9
+ 6. Adaptive (Classifier → Specialist)
10
+
11
+ Security:
12
+ - All agent outputs validated before passing to next agent
13
+ - No eval() or exec() usage
14
+ - Timeout enforcement at strategy level
15
+
16
+ Example:
17
+ >>> strategy = SequentialStrategy()
18
+ >>> agents = [agent1, agent2, agent3]
19
+ >>> result = await strategy.execute(agents, context)
20
+ """
21
+
22
+ import asyncio
23
+ import logging
24
+ from abc import ABC, abstractmethod
25
+ from dataclasses import dataclass
26
+ from typing import Any
27
+
28
+ from .agent_templates import AgentTemplate
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ @dataclass
34
+ class AgentResult:
35
+ """Result from agent execution.
36
+
37
+ Attributes:
38
+ agent_id: ID of agent that produced result
39
+ success: Whether execution succeeded
40
+ output: Agent output data
41
+ confidence: Confidence score (0-1)
42
+ duration_seconds: Execution time
43
+ error: Error message if failed
44
+ """
45
+
46
+ agent_id: str
47
+ success: bool
48
+ output: dict[str, Any]
49
+ confidence: float = 0.0
50
+ duration_seconds: float = 0.0
51
+ error: str = ""
52
+
53
+
54
+ @dataclass
55
+ class StrategyResult:
56
+ """Aggregated result from strategy execution.
57
+
58
+ Attributes:
59
+ success: Whether overall execution succeeded
60
+ outputs: List of individual agent results
61
+ aggregated_output: Combined/synthesized output
62
+ total_duration: Total execution time
63
+ errors: List of errors encountered
64
+ """
65
+
66
+ success: bool
67
+ outputs: list[AgentResult]
68
+ aggregated_output: dict[str, Any]
69
+ total_duration: float = 0.0
70
+ errors: list[str] = None
71
+
72
+ def __post_init__(self):
73
+ """Initialize errors list if None."""
74
+ if self.errors is None:
75
+ self.errors = []
76
+
77
+
78
+ class ExecutionStrategy(ABC):
79
+ """Base class for agent composition strategies.
80
+
81
+ All strategies must implement execute() method to define
82
+ how agents are coordinated and results aggregated.
83
+ """
84
+
85
+ @abstractmethod
86
+ async def execute(self, agents: list[AgentTemplate], context: dict[str, Any]) -> StrategyResult:
87
+ """Execute agents using this strategy.
88
+
89
+ Args:
90
+ agents: List of agent templates to execute
91
+ context: Initial context for execution
92
+
93
+ Returns:
94
+ StrategyResult with aggregated outputs
95
+
96
+ Raises:
97
+ ValueError: If agents list is empty
98
+ TimeoutError: If execution exceeds timeout
99
+ """
100
+ pass
101
+
102
+ async def _execute_agent(self, agent: AgentTemplate, context: dict[str, Any]) -> AgentResult:
103
+ """Execute a single agent (simulated for now).
104
+
105
+ This is a placeholder that simulates agent execution.
106
+ In production, this would invoke the actual agent runtime.
107
+
108
+ Args:
109
+ agent: Agent template to execute
110
+ context: Execution context
111
+
112
+ Returns:
113
+ AgentResult with execution outcome
114
+ """
115
+ logger.info(f"Executing agent: {agent.id}")
116
+
117
+ # Simulate execution (placeholder)
118
+ # In production: would call agent runtime/LLM
119
+ await asyncio.sleep(0.1) # Simulate work
120
+
121
+ # Simulated success result
122
+ return AgentResult(
123
+ agent_id=agent.id,
124
+ success=True,
125
+ output={
126
+ "agent_role": agent.role,
127
+ "context_received": list(context.keys()),
128
+ "simulated": True,
129
+ },
130
+ confidence=0.85,
131
+ duration_seconds=0.1,
132
+ )
133
+
134
+ def _aggregate_results(self, results: list[AgentResult]) -> dict[str, Any]:
135
+ """Aggregate results from multiple agents.
136
+
137
+ Args:
138
+ results: List of agent results
139
+
140
+ Returns:
141
+ Aggregated output dictionary
142
+ """
143
+ return {
144
+ "num_agents": len(results),
145
+ "all_succeeded": all(r.success for r in results),
146
+ "avg_confidence": (
147
+ sum(r.confidence for r in results) / len(results) if results else 0.0
148
+ ),
149
+ "outputs": [r.output for r in results],
150
+ }
151
+
152
+
153
+ class SequentialStrategy(ExecutionStrategy):
154
+ """Sequential composition (A → B → C).
155
+
156
+ Executes agents one after another, passing results forward.
157
+ Each agent receives output from previous agent in context.
158
+
159
+ Use when:
160
+ - Tasks must be done in order
161
+ - Each step depends on previous results
162
+ - Pipeline processing needed
163
+
164
+ Example:
165
+ Coverage Analyzer → Test Generator → Quality Validator
166
+ """
167
+
168
+ async def execute(self, agents: list[AgentTemplate], context: dict[str, Any]) -> StrategyResult:
169
+ """Execute agents sequentially.
170
+
171
+ Args:
172
+ agents: List of agents to execute in order
173
+ context: Initial context
174
+
175
+ Returns:
176
+ StrategyResult with sequential execution results
177
+ """
178
+ if not agents:
179
+ raise ValueError("agents list cannot be empty")
180
+
181
+ logger.info(f"Sequential execution of {len(agents)} agents")
182
+
183
+ results: list[AgentResult] = []
184
+ current_context = context.copy()
185
+ total_duration = 0.0
186
+
187
+ for agent in agents:
188
+ try:
189
+ result = await self._execute_agent(agent, current_context)
190
+ results.append(result)
191
+ total_duration += result.duration_seconds
192
+
193
+ # Pass output to next agent's context
194
+ if result.success:
195
+ current_context[f"{agent.id}_output"] = result.output
196
+ else:
197
+ logger.error(f"Agent {agent.id} failed: {result.error}")
198
+ # Continue or stop based on error handling policy
199
+ # For now: continue to next agent
200
+
201
+ except Exception as e:
202
+ logger.exception(f"Error executing agent {agent.id}: {e}")
203
+ results.append(
204
+ AgentResult(
205
+ agent_id=agent.id,
206
+ success=False,
207
+ output={},
208
+ error=str(e),
209
+ )
210
+ )
211
+
212
+ return StrategyResult(
213
+ success=all(r.success for r in results),
214
+ outputs=results,
215
+ aggregated_output=self._aggregate_results(results),
216
+ total_duration=total_duration,
217
+ errors=[r.error for r in results if not r.success],
218
+ )
219
+
220
+
221
+ class ParallelStrategy(ExecutionStrategy):
222
+ """Parallel composition (A || B || C).
223
+
224
+ Executes all agents simultaneously, aggregates results.
225
+ Each agent receives same initial context.
226
+
227
+ Use when:
228
+ - Independent validations needed
229
+ - Multi-perspective review desired
230
+ - Time optimization important
231
+
232
+ Example:
233
+ Security Audit || Performance Check || Code Quality || Docs Check
234
+ """
235
+
236
+ async def execute(self, agents: list[AgentTemplate], context: dict[str, Any]) -> StrategyResult:
237
+ """Execute agents in parallel.
238
+
239
+ Args:
240
+ agents: List of agents to execute concurrently
241
+ context: Initial context for all agents
242
+
243
+ Returns:
244
+ StrategyResult with parallel execution results
245
+ """
246
+ if not agents:
247
+ raise ValueError("agents list cannot be empty")
248
+
249
+ logger.info(f"Parallel execution of {len(agents)} agents")
250
+
251
+ # Execute all agents concurrently
252
+ tasks = [self._execute_agent(agent, context) for agent in agents]
253
+
254
+ try:
255
+ results = await asyncio.gather(*tasks, return_exceptions=True)
256
+ except Exception as e:
257
+ logger.exception(f"Error in parallel execution: {e}")
258
+ raise
259
+
260
+ # Process results (handle exceptions)
261
+ processed_results: list[AgentResult] = []
262
+ for i, result in enumerate(results):
263
+ if isinstance(result, Exception):
264
+ logger.error(f"Agent {agents[i].id} raised exception: {result}")
265
+ processed_results.append(
266
+ AgentResult(
267
+ agent_id=agents[i].id,
268
+ success=False,
269
+ output={},
270
+ error=str(result),
271
+ )
272
+ )
273
+ else:
274
+ processed_results.append(result)
275
+
276
+ total_duration = max((r.duration_seconds for r in processed_results), default=0.0)
277
+
278
+ return StrategyResult(
279
+ success=all(r.success for r in processed_results),
280
+ outputs=processed_results,
281
+ aggregated_output=self._aggregate_results(processed_results),
282
+ total_duration=total_duration,
283
+ errors=[r.error for r in processed_results if not r.success],
284
+ )
285
+
286
+
287
+ class DebateStrategy(ExecutionStrategy):
288
+ """Debate/Consensus composition (A ⇄ B ⇄ C → Synthesis).
289
+
290
+ Agents provide independent opinions, then a synthesizer
291
+ aggregates and resolves conflicts.
292
+
293
+ Use when:
294
+ - Multiple expert opinions needed
295
+ - Architecture decisions require debate
296
+ - Tradeoff analysis needed
297
+
298
+ Example:
299
+ Architect(scale) || Architect(cost) || Architect(simplicity) → Synthesizer
300
+ """
301
+
302
+ async def execute(self, agents: list[AgentTemplate], context: dict[str, Any]) -> StrategyResult:
303
+ """Execute debate pattern.
304
+
305
+ Args:
306
+ agents: List of agents to debate (recommend 2-4)
307
+ context: Initial context
308
+
309
+ Returns:
310
+ StrategyResult with synthesized consensus
311
+ """
312
+ if not agents:
313
+ raise ValueError("agents list cannot be empty")
314
+
315
+ if len(agents) < 2:
316
+ logger.warning("Debate pattern works best with 2+ agents")
317
+
318
+ logger.info(f"Debate execution with {len(agents)} agents")
319
+
320
+ # Phase 1: Parallel execution for independent opinions
321
+ parallel_strategy = ParallelStrategy()
322
+ phase1_result = await parallel_strategy.execute(agents, context)
323
+
324
+ # Phase 2: Synthesis (simplified - no actual synthesizer agent)
325
+ # In production: would use dedicated synthesizer agent
326
+ synthesis = {
327
+ "debate_participants": [r.agent_id for r in phase1_result.outputs],
328
+ "opinions": [r.output for r in phase1_result.outputs],
329
+ "consensus": self._synthesize_opinions(phase1_result.outputs),
330
+ }
331
+
332
+ return StrategyResult(
333
+ success=phase1_result.success,
334
+ outputs=phase1_result.outputs,
335
+ aggregated_output=synthesis,
336
+ total_duration=phase1_result.total_duration,
337
+ errors=phase1_result.errors,
338
+ )
339
+
340
+ def _synthesize_opinions(self, results: list[AgentResult]) -> dict[str, Any]:
341
+ """Synthesize multiple agent opinions into consensus.
342
+
343
+ Args:
344
+ results: Agent results to synthesize
345
+
346
+ Returns:
347
+ Synthesized consensus
348
+ """
349
+ # Simplified synthesis: majority vote on success
350
+ success_votes = sum(1 for r in results if r.success)
351
+ consensus_reached = success_votes > len(results) / 2
352
+
353
+ return {
354
+ "consensus_reached": consensus_reached,
355
+ "success_votes": success_votes,
356
+ "total_votes": len(results),
357
+ "avg_confidence": (
358
+ sum(r.confidence for r in results) / len(results) if results else 0.0
359
+ ),
360
+ }
361
+
362
+
363
+ class TeachingStrategy(ExecutionStrategy):
364
+ """Teaching/Validation (Junior → Expert Review).
365
+
366
+ Junior agent attempts task (cheap tier), expert validates.
367
+ If validation fails, expert takes over.
368
+
369
+ Use when:
370
+ - Cost-effective generation desired
371
+ - Quality assurance critical
372
+ - Simple tasks with review needed
373
+
374
+ Example:
375
+ Junior Writer(CHEAP) → Quality Gate → (pass ? done : Expert Review(CAPABLE))
376
+ """
377
+
378
+ def __init__(self, quality_threshold: float = 0.7):
379
+ """Initialize teaching strategy.
380
+
381
+ Args:
382
+ quality_threshold: Minimum confidence for junior to pass (0-1)
383
+ """
384
+ self.quality_threshold = quality_threshold
385
+
386
+ async def execute(self, agents: list[AgentTemplate], context: dict[str, Any]) -> StrategyResult:
387
+ """Execute teaching pattern.
388
+
389
+ Args:
390
+ agents: [junior_agent, expert_agent] (exactly 2)
391
+ context: Initial context
392
+
393
+ Returns:
394
+ StrategyResult with teaching outcome
395
+ """
396
+ if len(agents) != 2:
397
+ raise ValueError("Teaching strategy requires exactly 2 agents")
398
+
399
+ junior, expert = agents
400
+ logger.info(f"Teaching: {junior.id} → {expert.id} validation")
401
+
402
+ results: list[AgentResult] = []
403
+ total_duration = 0.0
404
+
405
+ # Phase 1: Junior attempt
406
+ junior_result = await self._execute_agent(junior, context)
407
+ results.append(junior_result)
408
+ total_duration += junior_result.duration_seconds
409
+
410
+ # Phase 2: Quality gate
411
+ if junior_result.success and junior_result.confidence >= self.quality_threshold:
412
+ logger.info(f"Junior passed quality gate (confidence={junior_result.confidence:.2f})")
413
+ aggregated = {"outcome": "junior_success", "junior_output": junior_result.output}
414
+ else:
415
+ logger.info(
416
+ f"Junior failed quality gate, expert taking over "
417
+ f"(confidence={junior_result.confidence:.2f})"
418
+ )
419
+
420
+ # Phase 3: Expert takeover
421
+ expert_context = context.copy()
422
+ expert_context["junior_attempt"] = junior_result.output
423
+ expert_result = await self._execute_agent(expert, expert_context)
424
+ results.append(expert_result)
425
+ total_duration += expert_result.duration_seconds
426
+
427
+ aggregated = {
428
+ "outcome": "expert_takeover",
429
+ "junior_output": junior_result.output,
430
+ "expert_output": expert_result.output,
431
+ }
432
+
433
+ return StrategyResult(
434
+ success=all(r.success for r in results),
435
+ outputs=results,
436
+ aggregated_output=aggregated,
437
+ total_duration=total_duration,
438
+ errors=[r.error for r in results if not r.success],
439
+ )
440
+
441
+
442
+ class RefinementStrategy(ExecutionStrategy):
443
+ """Progressive Refinement (Draft → Review → Polish).
444
+
445
+ Iterative improvement through multiple quality levels.
446
+ Each agent refines output from previous stage.
447
+
448
+ Use when:
449
+ - Iterative improvement needed
450
+ - Quality ladder desired
451
+ - Multi-stage refinement beneficial
452
+
453
+ Example:
454
+ Drafter(CHEAP) → Reviewer(CAPABLE) → Polisher(PREMIUM)
455
+ """
456
+
457
+ async def execute(self, agents: list[AgentTemplate], context: dict[str, Any]) -> StrategyResult:
458
+ """Execute refinement pattern.
459
+
460
+ Args:
461
+ agents: [drafter, reviewer, polisher] (3+ agents)
462
+ context: Initial context
463
+
464
+ Returns:
465
+ StrategyResult with refined output
466
+ """
467
+ if len(agents) < 2:
468
+ raise ValueError("Refinement strategy requires at least 2 agents")
469
+
470
+ logger.info(f"Refinement with {len(agents)} stages")
471
+
472
+ results: list[AgentResult] = []
473
+ current_context = context.copy()
474
+ total_duration = 0.0
475
+
476
+ for i, agent in enumerate(agents):
477
+ stage_name = f"stage_{i+1}"
478
+ logger.info(f"Refinement {stage_name}: {agent.id}")
479
+
480
+ result = await self._execute_agent(agent, current_context)
481
+ results.append(result)
482
+ total_duration += result.duration_seconds
483
+
484
+ if result.success:
485
+ # Pass refined output to next stage
486
+ current_context[f"{stage_name}_output"] = result.output
487
+ current_context["previous_output"] = result.output
488
+ else:
489
+ logger.error(f"Refinement stage {i+1} failed: {result.error}")
490
+ break # Stop refinement on failure
491
+
492
+ # Final output is from last successful stage
493
+ final_output = results[-1].output if results[-1].success else {}
494
+
495
+ return StrategyResult(
496
+ success=all(r.success for r in results),
497
+ outputs=results,
498
+ aggregated_output={
499
+ "refinement_stages": len(results),
500
+ "final_output": final_output,
501
+ "stage_outputs": [r.output for r in results],
502
+ },
503
+ total_duration=total_duration,
504
+ errors=[r.error for r in results if not r.success],
505
+ )
506
+
507
+
508
+ class AdaptiveStrategy(ExecutionStrategy):
509
+ """Adaptive Routing (Classifier → Specialist).
510
+
511
+ Classifier assesses task complexity, routes to appropriate specialist.
512
+ Right-sizing: match agent tier to task needs.
513
+
514
+ Use when:
515
+ - Variable task complexity
516
+ - Cost optimization desired
517
+ - Right-sizing important
518
+
519
+ Example:
520
+ Classifier(CHEAP) → route(simple|moderate|complex) → Specialist(tier)
521
+ """
522
+
523
+ async def execute(self, agents: list[AgentTemplate], context: dict[str, Any]) -> StrategyResult:
524
+ """Execute adaptive routing pattern.
525
+
526
+ Args:
527
+ agents: [classifier, *specialists] (2+ agents)
528
+ context: Initial context
529
+
530
+ Returns:
531
+ StrategyResult with routed execution
532
+ """
533
+ if len(agents) < 2:
534
+ raise ValueError("Adaptive strategy requires at least 2 agents")
535
+
536
+ classifier = agents[0]
537
+ specialists = agents[1:]
538
+
539
+ logger.info(f"Adaptive: {classifier.id} → {len(specialists)} specialists")
540
+
541
+ results: list[AgentResult] = []
542
+ total_duration = 0.0
543
+
544
+ # Phase 1: Classification
545
+ classifier_result = await self._execute_agent(classifier, context)
546
+ results.append(classifier_result)
547
+ total_duration += classifier_result.duration_seconds
548
+
549
+ if not classifier_result.success:
550
+ logger.error("Classifier failed, defaulting to first specialist")
551
+ selected_specialist = specialists[0]
552
+ else:
553
+ # Phase 2: Route to specialist based on classification
554
+ # Simplified: select based on confidence score
555
+ if classifier_result.confidence > 0.8:
556
+ # High confidence → simple task → cheap specialist
557
+ selected_specialist = min(
558
+ specialists,
559
+ key=lambda s: {
560
+ "CHEAP": 0,
561
+ "CAPABLE": 1,
562
+ "PREMIUM": 2,
563
+ }.get(s.tier_preference, 1),
564
+ )
565
+ else:
566
+ # Low confidence → complex task → premium specialist
567
+ selected_specialist = max(
568
+ specialists,
569
+ key=lambda s: {
570
+ "CHEAP": 0,
571
+ "CAPABLE": 1,
572
+ "PREMIUM": 2,
573
+ }.get(s.tier_preference, 1),
574
+ )
575
+
576
+ logger.info(f"Routed to specialist: {selected_specialist.id}")
577
+
578
+ # Phase 3: Execute selected specialist
579
+ specialist_context = context.copy()
580
+ specialist_context["classification"] = classifier_result.output
581
+ specialist_result = await self._execute_agent(selected_specialist, specialist_context)
582
+ results.append(specialist_result)
583
+ total_duration += specialist_result.duration_seconds
584
+
585
+ return StrategyResult(
586
+ success=all(r.success for r in results),
587
+ outputs=results,
588
+ aggregated_output={
589
+ "classification": classifier_result.output,
590
+ "selected_specialist": selected_specialist.id,
591
+ "specialist_output": specialist_result.output,
592
+ },
593
+ total_duration=total_duration,
594
+ errors=[r.error for r in results if not r.success],
595
+ )
596
+
597
+
598
+ # Strategy registry for lookup by name
599
+ STRATEGY_REGISTRY: dict[str, type[ExecutionStrategy]] = {
600
+ "sequential": SequentialStrategy,
601
+ "parallel": ParallelStrategy,
602
+ "debate": DebateStrategy,
603
+ "teaching": TeachingStrategy,
604
+ "refinement": RefinementStrategy,
605
+ "adaptive": AdaptiveStrategy,
606
+ }
607
+
608
+
609
+ def get_strategy(strategy_name: str) -> ExecutionStrategy:
610
+ """Get strategy instance by name.
611
+
612
+ Args:
613
+ strategy_name: Strategy name (e.g., "sequential", "parallel")
614
+
615
+ Returns:
616
+ ExecutionStrategy instance
617
+
618
+ Raises:
619
+ ValueError: If strategy name is invalid
620
+
621
+ Example:
622
+ >>> strategy = get_strategy("sequential")
623
+ >>> isinstance(strategy, SequentialStrategy)
624
+ True
625
+ """
626
+ if strategy_name not in STRATEGY_REGISTRY:
627
+ raise ValueError(
628
+ f"Unknown strategy: {strategy_name}. " f"Available: {list(STRATEGY_REGISTRY.keys())}"
629
+ )
630
+
631
+ strategy_class = STRATEGY_REGISTRY[strategy_name]
632
+ return strategy_class()