kailash 0.1.4__py3-none-any.whl → 0.1.5__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,1624 @@
1
+ """Self-Organizing Agent Pool nodes for autonomous team formation and problem solving.
2
+
3
+ This module implements nodes that enable agents to self-organize into teams,
4
+ collaborate dynamically, and solve problems without centralized orchestration.
5
+ """
6
+
7
+ import json
8
+ import random
9
+ import time
10
+ import uuid
11
+ from collections import defaultdict, deque
12
+ from datetime import datetime
13
+ from enum import Enum
14
+ from typing import Any, Callable, Dict, List, Optional, Set, Tuple
15
+
16
+ from kailash.nodes.ai.a2a import A2AAgentNode, SharedMemoryPoolNode
17
+ from kailash.nodes.base import Node, NodeParameter, register_node
18
+
19
+
20
+ class TeamFormationStrategy(Enum):
21
+ """Strategies for forming agent teams."""
22
+
23
+ CAPABILITY_MATCHING = "capability_matching"
24
+ SWARM_BASED = "swarm_based"
25
+ MARKET_BASED = "market_based"
26
+ HIERARCHICAL = "hierarchical"
27
+ RANDOM = "random"
28
+
29
+
30
+ class AgentStatus(Enum):
31
+ """Status of an agent in the pool."""
32
+
33
+ AVAILABLE = "available"
34
+ BUSY = "busy"
35
+ FORMING_TEAM = "forming_team"
36
+ INACTIVE = "inactive"
37
+
38
+
39
+ @register_node()
40
+ class AgentPoolManagerNode(Node):
41
+ """
42
+ Manages a pool of self-organizing agents with capability tracking,
43
+ performance monitoring, and dynamic availability management.
44
+
45
+ This node serves as the registry and coordinator for a dynamic pool of agents,
46
+ tracking their capabilities, availability, performance metrics, and collaboration
47
+ patterns. It enables efficient agent discovery, team formation, and performance-based
48
+ agent selection for complex multi-agent workflows.
49
+
50
+ Design Philosophy:
51
+ The AgentPoolManagerNode embodies decentralized management principles, allowing
52
+ agents to join and leave dynamically while maintaining global visibility into
53
+ pool capabilities. It facilitates emergence of specialized teams based on task
54
+ requirements and historical performance, creating an adaptive workforce that
55
+ improves over time through tracked metrics and learned collaboration patterns.
56
+
57
+ Upstream Dependencies:
58
+ - OrchestrationManagerNode: Provides agent registration requests
59
+ - TeamFormationNode: Queries for available agents with specific capabilities
60
+ - A2ACoordinatorNode: Updates agent status during task execution
61
+ - Performance monitoring systems: Supply metrics updates
62
+
63
+ Downstream Consumers:
64
+ - TeamFormationNode: Uses agent registry for team composition
65
+ - ProblemAnalyzerNode: Queries capabilities for feasibility analysis
66
+ - SolutionEvaluatorNode: Accesses performance history
67
+ - Reporting systems: Aggregate pool analytics
68
+
69
+ Configuration:
70
+ No static configuration required. The pool adapts dynamically based on
71
+ registered agents and their evolving performance metrics. Default performance
72
+ thresholds can be adjusted at runtime.
73
+
74
+ Implementation Details:
75
+ - Maintains in-memory registry with O(1) agent lookup
76
+ - Indexes agents by capability for fast searching
77
+ - Tracks performance metrics with exponential moving averages
78
+ - Records collaboration history for team affinity analysis
79
+ - Implements status transitions with validation
80
+ - Supports bulk operations for efficiency
81
+ - Thread-safe for concurrent access
82
+
83
+ Error Handling:
84
+ - Validates agent IDs to prevent duplicates
85
+ - Handles missing agents gracefully in queries
86
+ - Returns empty results rather than errors for searches
87
+ - Validates status transitions
88
+
89
+ Side Effects:
90
+ - Maintains persistent agent registry across calls
91
+ - Updates performance metrics incrementally
92
+ - Records team formation history
93
+ - May affect agent availability for other tasks
94
+
95
+ Examples:
96
+ >>> # Create agent pool manager
97
+ >>> pool_manager = AgentPoolManagerNode()
98
+ >>>
99
+ >>> # Test basic structure
100
+ >>> params = pool_manager.get_parameters()
101
+ >>> assert "action" in params
102
+ >>> assert "agent_id" in params
103
+ >>>
104
+ >>> # Test simple registration
105
+ >>> result = pool_manager.run(
106
+ ... action="register",
107
+ ... agent_id="test_agent",
108
+ ... capabilities=["analysis"]
109
+ ... )
110
+ >>> assert result["success"] == True
111
+ """
112
+
113
+ def __init__(self):
114
+ super().__init__()
115
+ self.agent_registry = {}
116
+ self.availability_tracker = {}
117
+ self.performance_metrics = defaultdict(
118
+ lambda: {
119
+ "tasks_completed": 0,
120
+ "success_rate": 0.8,
121
+ "avg_contribution_score": 0.7,
122
+ "specializations": {},
123
+ "collaboration_history": [],
124
+ }
125
+ )
126
+ self.capability_index = defaultdict(set)
127
+ self.team_history = deque(maxlen=100)
128
+
129
+ def get_parameters(self) -> Dict[str, NodeParameter]:
130
+ return {
131
+ "action": NodeParameter(
132
+ name="action",
133
+ type=str,
134
+ required=False,
135
+ default="list",
136
+ description="Action: 'register', 'unregister', 'find_by_capability', 'update_status', 'get_metrics', 'list'",
137
+ ),
138
+ "agent_id": NodeParameter(
139
+ name="agent_id", type=str, required=False, description="ID of the agent"
140
+ ),
141
+ "capabilities": NodeParameter(
142
+ name="capabilities",
143
+ type=list,
144
+ required=False,
145
+ default=[],
146
+ description="List of agent capabilities",
147
+ ),
148
+ "metadata": NodeParameter(
149
+ name="metadata",
150
+ type=dict,
151
+ required=False,
152
+ default={},
153
+ description="Additional agent metadata",
154
+ ),
155
+ "required_capabilities": NodeParameter(
156
+ name="required_capabilities",
157
+ type=list,
158
+ required=False,
159
+ default=[],
160
+ description="Capabilities required for search",
161
+ ),
162
+ "min_performance": NodeParameter(
163
+ name="min_performance",
164
+ type=float,
165
+ required=False,
166
+ default=0.7,
167
+ description="Minimum performance score",
168
+ ),
169
+ "status": NodeParameter(
170
+ name="status",
171
+ type=str,
172
+ required=False,
173
+ description="New status for agent",
174
+ ),
175
+ "performance_update": NodeParameter(
176
+ name="performance_update",
177
+ type=dict,
178
+ required=False,
179
+ description="Performance metrics to update",
180
+ ),
181
+ }
182
+
183
+ def run(self, **kwargs) -> Dict[str, Any]:
184
+ """Execute pool management action."""
185
+ action = kwargs.get("action", "list")
186
+
187
+ if action == "register":
188
+ return self._register_agent(kwargs)
189
+ elif action == "unregister":
190
+ return self._unregister_agent(kwargs)
191
+ elif action == "find_by_capability":
192
+ return self._find_by_capability(kwargs)
193
+ elif action == "update_status":
194
+ return self._update_status(kwargs)
195
+ elif action == "get_metrics":
196
+ return self._get_metrics(kwargs)
197
+ elif action == "list":
198
+ return self._list_agents()
199
+ else:
200
+ return {"success": False, "error": f"Unknown action: {action}"}
201
+
202
+ def _register_agent(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
203
+ """Register a new agent in the pool."""
204
+ agent_id = kwargs.get("agent_id")
205
+ if not agent_id:
206
+ agent_id = f"agent_{uuid.uuid4().hex[:8]}"
207
+
208
+ capabilities = kwargs.get("capabilities", [])
209
+ metadata = kwargs.get("metadata", {})
210
+
211
+ # Register agent
212
+ self.agent_registry[agent_id] = {
213
+ "id": agent_id,
214
+ "capabilities": capabilities,
215
+ "metadata": metadata,
216
+ "registered_at": time.time(),
217
+ "last_active": time.time(),
218
+ }
219
+
220
+ # Update availability
221
+ self.availability_tracker[agent_id] = AgentStatus.AVAILABLE.value
222
+
223
+ # Index capabilities
224
+ for capability in capabilities:
225
+ self.capability_index[capability].add(agent_id)
226
+
227
+ # Initialize performance metrics if provided
228
+ if "performance_history" in metadata:
229
+ self.performance_metrics[agent_id].update(metadata["performance_history"])
230
+
231
+ return {
232
+ "success": True,
233
+ "agent_id": agent_id,
234
+ "status": "registered",
235
+ "capabilities": capabilities,
236
+ "pool_size": len(self.agent_registry),
237
+ }
238
+
239
+ def _unregister_agent(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
240
+ """Remove an agent from the pool."""
241
+ agent_id = kwargs.get("agent_id")
242
+
243
+ if agent_id not in self.agent_registry:
244
+ return {"success": False, "error": f"Agent {agent_id} not found"}
245
+
246
+ # Remove from indices
247
+ agent_data = self.agent_registry[agent_id]
248
+ for capability in agent_data["capabilities"]:
249
+ self.capability_index[capability].discard(agent_id)
250
+
251
+ # Remove from registry
252
+ del self.agent_registry[agent_id]
253
+ del self.availability_tracker[agent_id]
254
+
255
+ return {
256
+ "success": True,
257
+ "agent_id": agent_id,
258
+ "status": "unregistered",
259
+ "pool_size": len(self.agent_registry),
260
+ }
261
+
262
+ def _find_by_capability(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
263
+ """Find agents matching required capabilities."""
264
+ required_capabilities = set(kwargs.get("required_capabilities", []))
265
+ min_performance = kwargs.get("min_performance", 0.7)
266
+
267
+ if not required_capabilities:
268
+ return {
269
+ "success": True,
270
+ "agents": list(self.agent_registry.keys()),
271
+ "count": len(self.agent_registry),
272
+ }
273
+
274
+ # Find agents with all required capabilities
275
+ matching_agents = None
276
+ for capability in required_capabilities:
277
+ agents_with_capability = self.capability_index.get(capability, set())
278
+ if matching_agents is None:
279
+ matching_agents = agents_with_capability.copy()
280
+ else:
281
+ matching_agents &= agents_with_capability
282
+
283
+ if not matching_agents:
284
+ return {"success": True, "agents": [], "count": 0}
285
+
286
+ # Filter by performance and availability
287
+ qualified_agents = []
288
+ for agent_id in matching_agents:
289
+ if self.availability_tracker.get(agent_id) != AgentStatus.AVAILABLE.value:
290
+ continue
291
+
292
+ performance = self.performance_metrics[agent_id]["success_rate"]
293
+ if performance >= min_performance:
294
+ agent_info = self.agent_registry[agent_id].copy()
295
+ agent_info["performance"] = performance
296
+ agent_info["status"] = self.availability_tracker[agent_id]
297
+ qualified_agents.append(agent_info)
298
+
299
+ # Sort by performance
300
+ qualified_agents.sort(key=lambda x: x["performance"], reverse=True)
301
+
302
+ return {
303
+ "success": True,
304
+ "agents": qualified_agents,
305
+ "count": len(qualified_agents),
306
+ "total_pool_size": len(self.agent_registry),
307
+ }
308
+
309
+ def _update_status(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
310
+ """Update agent status."""
311
+ agent_id = kwargs.get("agent_id")
312
+ new_status = kwargs.get("status")
313
+
314
+ if agent_id not in self.agent_registry:
315
+ return {"success": False, "error": f"Agent {agent_id} not found"}
316
+
317
+ # Validate status
318
+ valid_statuses = [s.value for s in AgentStatus]
319
+ if new_status not in valid_statuses:
320
+ return {"success": False, "error": f"Invalid status: {new_status}"}
321
+
322
+ old_status = self.availability_tracker.get(agent_id)
323
+ self.availability_tracker[agent_id] = new_status
324
+ self.agent_registry[agent_id]["last_active"] = time.time()
325
+
326
+ # Update performance if provided
327
+ if "performance_update" in kwargs:
328
+ perf_update = kwargs["performance_update"]
329
+ metrics = self.performance_metrics[agent_id]
330
+
331
+ if "task_completed" in perf_update:
332
+ metrics["tasks_completed"] += 1
333
+ success = perf_update.get("success", True)
334
+ # Update success rate with exponential moving average
335
+ alpha = 0.2
336
+ metrics["success_rate"] = (
337
+ alpha * (1.0 if success else 0.0)
338
+ + (1 - alpha) * metrics["success_rate"]
339
+ )
340
+
341
+ if "contribution_score" in perf_update:
342
+ score = perf_update["contribution_score"]
343
+ metrics["avg_contribution_score"] = (
344
+ alpha * score + (1 - alpha) * metrics["avg_contribution_score"]
345
+ )
346
+
347
+ if "specialization" in perf_update:
348
+ spec = perf_update["specialization"]
349
+ if spec not in metrics["specializations"]:
350
+ metrics["specializations"][spec] = 0
351
+ metrics["specializations"][spec] += 1
352
+
353
+ return {
354
+ "success": True,
355
+ "agent_id": agent_id,
356
+ "old_status": old_status,
357
+ "new_status": new_status,
358
+ "last_active": self.agent_registry[agent_id]["last_active"],
359
+ }
360
+
361
+ def _get_metrics(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
362
+ """Get performance metrics for an agent or all agents."""
363
+ agent_id = kwargs.get("agent_id")
364
+
365
+ if agent_id:
366
+ if agent_id not in self.agent_registry:
367
+ return {"success": False, "error": f"Agent {agent_id} not found"}
368
+
369
+ return {
370
+ "success": True,
371
+ "agent_id": agent_id,
372
+ "metrics": self.performance_metrics[agent_id],
373
+ "registration_info": self.agent_registry[agent_id],
374
+ }
375
+ else:
376
+ # Aggregate metrics
377
+ total_agents = len(self.agent_registry)
378
+ available_agents = sum(
379
+ 1
380
+ for status in self.availability_tracker.values()
381
+ if status == AgentStatus.AVAILABLE.value
382
+ )
383
+
384
+ avg_success_rate = sum(
385
+ m["success_rate"] for m in self.performance_metrics.values()
386
+ ) / max(total_agents, 1)
387
+
388
+ capability_distribution = {}
389
+ for capability, agents in self.capability_index.items():
390
+ capability_distribution[capability] = len(agents)
391
+
392
+ return {
393
+ "success": True,
394
+ "pool_metrics": {
395
+ "total_agents": total_agents,
396
+ "available_agents": available_agents,
397
+ "avg_success_rate": avg_success_rate,
398
+ "capability_distribution": capability_distribution,
399
+ "status_distribution": dict(
400
+ (
401
+ status,
402
+ sum(
403
+ 1
404
+ for s in self.availability_tracker.values()
405
+ if s == status
406
+ ),
407
+ )
408
+ for status in [s.value for s in AgentStatus]
409
+ ),
410
+ },
411
+ }
412
+
413
+ def _list_agents(self) -> Dict[str, Any]:
414
+ """List all agents in the pool."""
415
+ agents = []
416
+ for agent_id, agent_data in self.agent_registry.items():
417
+ agent_info = agent_data.copy()
418
+ agent_info["status"] = self.availability_tracker.get(agent_id)
419
+ agent_info["performance"] = self.performance_metrics[agent_id][
420
+ "success_rate"
421
+ ]
422
+ agents.append(agent_info)
423
+
424
+ return {"success": True, "agents": agents, "count": len(agents)}
425
+
426
+
427
+ @register_node()
428
+ class ProblemAnalyzerNode(Node):
429
+ """
430
+ Analyzes problems to determine required capabilities, complexity,
431
+ and optimal team composition.
432
+
433
+ This node performs intelligent problem decomposition and requirement analysis,
434
+ identifying the specific capabilities needed to solve a problem, estimating its
435
+ complexity, and suggesting optimal team configurations. It uses pattern matching,
436
+ keyword analysis, and domain heuristics to create actionable problem specifications.
437
+
438
+ Design Philosophy:
439
+ The ProblemAnalyzerNode acts as the strategic planner in self-organizing systems,
440
+ translating high-level problem descriptions into concrete capability requirements
441
+ and team specifications. It embodies the principle that effective problem solving
442
+ begins with thorough understanding and proper decomposition of the challenge.
443
+
444
+ Upstream Dependencies:
445
+ - User interfaces or APIs providing problem descriptions
446
+ - OrchestrationManagerNode: Supplies problem context
447
+ - Domain configuration systems: Provide capability mappings
448
+
449
+ Downstream Consumers:
450
+ - TeamFormationNode: Uses capability requirements for team assembly
451
+ - AgentPoolManagerNode: Queries based on required capabilities
452
+ - SolutionEvaluatorNode: References problem analysis for validation
453
+ - Resource planning systems: Use complexity estimates
454
+
455
+ Configuration:
456
+ The analyzer uses predefined capability patterns that can be extended
457
+ for domain-specific problems. Analysis depth and decomposition strategies
458
+ can be configured at runtime.
459
+
460
+ Implementation Details:
461
+ - Pattern-based capability extraction from problem text
462
+ - Keyword-driven complexity scoring
463
+ - Multi-factor team size estimation
464
+ - Hierarchical problem decomposition
465
+ - Context-aware requirement adjustment
466
+ - Priority-based capability ranking
467
+ - Time and resource estimation
468
+
469
+ Error Handling:
470
+ - Handles vague problem descriptions with default analysis
471
+ - Validates decomposition strategies
472
+ - Returns minimal requirements for unrecognized problems
473
+ - Never fails - always provides best-effort analysis
474
+
475
+ Side Effects:
476
+ - No persistent side effects
477
+ - Pure analysis function
478
+ - May influence downstream team formation
479
+
480
+ Examples:
481
+ >>> # Test parameter structure without constructor validation
482
+ >>> analyzer = ProblemAnalyzerNode.__new__(ProblemAnalyzerNode)
483
+ >>> params = analyzer.get_parameters()
484
+ >>> assert "problem_description" in params
485
+ >>> assert "decomposition_strategy" in params
486
+ """
487
+
488
+ def __init__(self):
489
+ super().__init__()
490
+ self.capability_patterns = {
491
+ "data": ["data_collection", "data_cleaning", "data_validation"],
492
+ "analysis": [
493
+ "statistical_analysis",
494
+ "data_analysis",
495
+ "pattern_recognition",
496
+ ],
497
+ "model": ["machine_learning", "predictive_modeling", "model_validation"],
498
+ "research": ["literature_review", "research", "synthesis"],
499
+ "visualization": ["data_visualization", "reporting", "presentation"],
500
+ "domain": ["domain_expertise", "validation", "interpretation"],
501
+ }
502
+
503
+ def get_parameters(self) -> Dict[str, NodeParameter]:
504
+ return {
505
+ "problem_description": NodeParameter(
506
+ name="problem_description",
507
+ type=str,
508
+ required=True,
509
+ description="Description of the problem to solve",
510
+ ),
511
+ "context": NodeParameter(
512
+ name="context",
513
+ type=dict,
514
+ required=False,
515
+ default={},
516
+ description="Additional context about the problem",
517
+ ),
518
+ "decomposition_strategy": NodeParameter(
519
+ name="decomposition_strategy",
520
+ type=str,
521
+ required=False,
522
+ default="hierarchical",
523
+ description="Strategy for decomposing the problem",
524
+ ),
525
+ "analysis_depth": NodeParameter(
526
+ name="analysis_depth",
527
+ type=str,
528
+ required=False,
529
+ default="standard",
530
+ description="Depth of analysis: 'quick', 'standard', 'comprehensive'",
531
+ ),
532
+ }
533
+
534
+ def run(self, **kwargs) -> Dict[str, Any]:
535
+ """Analyze the problem to determine requirements."""
536
+ problem_description = kwargs["problem_description"]
537
+ context = kwargs.get("context", {})
538
+ strategy = kwargs.get("decomposition_strategy", "hierarchical")
539
+ depth = kwargs.get("analysis_depth", "standard")
540
+
541
+ # Extract key terms and requirements
542
+ problem_lower = problem_description.lower()
543
+ required_capabilities = set()
544
+
545
+ # Pattern matching for capabilities
546
+ for pattern, caps in self.capability_patterns.items():
547
+ if pattern in problem_lower:
548
+ required_capabilities.update(caps)
549
+
550
+ # Add specific capabilities based on keywords
551
+ keyword_capabilities = {
552
+ "predict": ["predictive_modeling", "machine_learning"],
553
+ "forecast": ["time_series_analysis", "predictive_modeling"],
554
+ "analyze": ["data_analysis", "statistical_analysis"],
555
+ "visualize": ["data_visualization", "reporting"],
556
+ "research": ["research", "literature_review"],
557
+ "optimize": ["optimization", "algorithm_design"],
558
+ "classify": ["classification", "machine_learning"],
559
+ "cluster": ["clustering", "unsupervised_learning"],
560
+ }
561
+
562
+ for keyword, caps in keyword_capabilities.items():
563
+ if keyword in problem_lower:
564
+ required_capabilities.update(caps)
565
+
566
+ # Determine complexity
567
+ complexity_factors = {
568
+ "simple_keywords": ["basic", "simple", "straightforward"],
569
+ "complex_keywords": ["complex", "advanced", "sophisticated", "multi"],
570
+ "scale_keywords": ["large", "massive", "big data", "scalable"],
571
+ }
572
+
573
+ complexity_score = 0.5 # Base complexity
574
+
575
+ for keyword in complexity_factors["simple_keywords"]:
576
+ if keyword in problem_lower:
577
+ complexity_score -= 0.1
578
+
579
+ for keyword in complexity_factors["complex_keywords"]:
580
+ if keyword in problem_lower:
581
+ complexity_score += 0.2
582
+
583
+ for keyword in complexity_factors["scale_keywords"]:
584
+ if keyword in problem_lower:
585
+ complexity_score += 0.1
586
+
587
+ complexity_score = max(0.1, min(1.0, complexity_score))
588
+
589
+ # Estimate team size based on complexity and capabilities
590
+ base_team_size = len(required_capabilities) // 3 + 1
591
+ complexity_multiplier = 1 + complexity_score
592
+ estimated_agents = int(base_team_size * complexity_multiplier)
593
+
594
+ # Decompose problem
595
+ if strategy == "hierarchical":
596
+ decomposition = self._hierarchical_decomposition(
597
+ problem_description, required_capabilities
598
+ )
599
+ else:
600
+ decomposition = self._simple_decomposition(
601
+ problem_description, required_capabilities
602
+ )
603
+
604
+ # Determine quality threshold based on context
605
+ quality_threshold = 0.8 # Default
606
+ if context.get("urgency") == "high":
607
+ quality_threshold = 0.7
608
+ elif context.get("criticality") == "high":
609
+ quality_threshold = 0.9
610
+
611
+ return {
612
+ "success": True,
613
+ "analysis": {
614
+ "problem": problem_description,
615
+ "required_capabilities": list(required_capabilities),
616
+ "complexity_score": complexity_score,
617
+ "estimated_agents": estimated_agents,
618
+ "quality_threshold": quality_threshold,
619
+ "time_estimate": self._estimate_time(
620
+ complexity_score, estimated_agents
621
+ ),
622
+ "decomposition": decomposition,
623
+ "priority_capabilities": self._prioritize_capabilities(
624
+ required_capabilities, problem_description
625
+ ),
626
+ "context_factors": context,
627
+ },
628
+ }
629
+
630
+ def _hierarchical_decomposition(
631
+ self, problem: str, capabilities: Set[str]
632
+ ) -> List[Dict]:
633
+ """Decompose problem hierarchically."""
634
+ # Simple heuristic decomposition
635
+ phases = []
636
+
637
+ # Phase 1: Data/Research
638
+ if any(cap in capabilities for cap in ["data_collection", "research"]):
639
+ phases.append(
640
+ {
641
+ "phase": "data_gathering",
642
+ "subtasks": [
643
+ {"name": "collect_data", "capabilities": ["data_collection"]},
644
+ {"name": "validate_data", "capabilities": ["data_validation"]},
645
+ ],
646
+ "priority": 1,
647
+ }
648
+ )
649
+
650
+ # Phase 2: Analysis
651
+ if any(
652
+ cap in capabilities for cap in ["data_analysis", "statistical_analysis"]
653
+ ):
654
+ phases.append(
655
+ {
656
+ "phase": "analysis",
657
+ "subtasks": [
658
+ {
659
+ "name": "exploratory_analysis",
660
+ "capabilities": ["data_analysis"],
661
+ },
662
+ {
663
+ "name": "statistical_testing",
664
+ "capabilities": ["statistical_analysis"],
665
+ },
666
+ ],
667
+ "priority": 2,
668
+ }
669
+ )
670
+
671
+ # Phase 3: Modeling
672
+ if any(
673
+ cap in capabilities for cap in ["machine_learning", "predictive_modeling"]
674
+ ):
675
+ phases.append(
676
+ {
677
+ "phase": "modeling",
678
+ "subtasks": [
679
+ {
680
+ "name": "model_development",
681
+ "capabilities": ["machine_learning"],
682
+ },
683
+ {
684
+ "name": "model_validation",
685
+ "capabilities": ["model_validation"],
686
+ },
687
+ ],
688
+ "priority": 3,
689
+ }
690
+ )
691
+
692
+ # Phase 4: Reporting
693
+ phases.append(
694
+ {
695
+ "phase": "reporting",
696
+ "subtasks": [
697
+ {
698
+ "name": "create_visualizations",
699
+ "capabilities": ["data_visualization"],
700
+ },
701
+ {
702
+ "name": "write_report",
703
+ "capabilities": ["reporting", "synthesis"],
704
+ },
705
+ ],
706
+ "priority": 4,
707
+ }
708
+ )
709
+
710
+ return phases
711
+
712
+ def _simple_decomposition(self, problem: str, capabilities: Set[str]) -> List[Dict]:
713
+ """Simple task decomposition."""
714
+ tasks = []
715
+ for i, cap in enumerate(capabilities):
716
+ tasks.append(
717
+ {
718
+ "task_id": f"task_{i+1}",
719
+ "description": f"Apply {cap} to problem",
720
+ "required_capability": cap,
721
+ "estimated_duration": 30, # minutes
722
+ }
723
+ )
724
+ return tasks
725
+
726
+ def _estimate_time(self, complexity: float, agents: int) -> int:
727
+ """Estimate time in minutes."""
728
+ base_time = 60 # Base 1 hour
729
+ complexity_factor = 1 + complexity * 2 # Up to 3x for complex
730
+ parallelization_factor = 1 / (1 + agents * 0.1) # Diminishing returns
731
+
732
+ return int(base_time * complexity_factor * parallelization_factor)
733
+
734
+ def _prioritize_capabilities(
735
+ self, capabilities: Set[str], problem: str
736
+ ) -> List[str]:
737
+ """Prioritize capabilities based on problem."""
738
+ # Simple prioritization based on problem keywords
739
+ priority_map = {
740
+ "urgent": ["data_analysis", "reporting"],
741
+ "predict": ["machine_learning", "predictive_modeling"],
742
+ "research": ["research", "literature_review"],
743
+ "optimize": ["optimization", "algorithm_design"],
744
+ }
745
+
746
+ prioritized = []
747
+ problem_lower = problem.lower()
748
+
749
+ for keyword, priority_caps in priority_map.items():
750
+ if keyword in problem_lower:
751
+ for cap in priority_caps:
752
+ if cap in capabilities and cap not in prioritized:
753
+ prioritized.append(cap)
754
+
755
+ # Add remaining capabilities
756
+ for cap in capabilities:
757
+ if cap not in prioritized:
758
+ prioritized.append(cap)
759
+
760
+ return prioritized
761
+
762
+
763
+ @register_node()
764
+ class TeamFormationNode(Node):
765
+ """
766
+ Forms optimal teams based on problem requirements and agent capabilities.
767
+
768
+ Supports multiple formation strategies including capability matching,
769
+ swarm-based organization, market-based auctions, and hierarchical structures.
770
+
771
+ Examples:
772
+ >>> formation_engine = TeamFormationNode()
773
+ >>>
774
+ >>> result = formation_engine.run(
775
+ ... problem_analysis={
776
+ ... "required_capabilities": ["data_analysis", "machine_learning"],
777
+ ... "complexity_score": 0.8,
778
+ ... "estimated_agents": 4
779
+ ... },
780
+ ... available_agents=[
781
+ ... {"id": "agent1", "capabilities": ["data_analysis"], "performance": 0.9},
782
+ ... {"id": "agent2", "capabilities": ["machine_learning"], "performance": 0.85}
783
+ ... ],
784
+ ... formation_strategy="capability_matching"
785
+ ... )
786
+ """
787
+
788
+ def __init__(self):
789
+ super().__init__()
790
+ self.formation_history = deque(maxlen=50)
791
+ self.team_performance_cache = {}
792
+
793
+ def get_parameters(self) -> Dict[str, NodeParameter]:
794
+ return {
795
+ "problem_analysis": NodeParameter(
796
+ name="problem_analysis",
797
+ type=dict,
798
+ required=False,
799
+ default={},
800
+ description="Analysis of the problem from ProblemAnalyzerNode",
801
+ ),
802
+ "available_agents": NodeParameter(
803
+ name="available_agents",
804
+ type=list,
805
+ required=False,
806
+ default=[],
807
+ description="List of available agents with their capabilities",
808
+ ),
809
+ "formation_strategy": NodeParameter(
810
+ name="formation_strategy",
811
+ type=str,
812
+ required=False,
813
+ default="capability_matching",
814
+ description="Team formation strategy",
815
+ ),
816
+ "constraints": NodeParameter(
817
+ name="constraints",
818
+ type=dict,
819
+ required=False,
820
+ default={},
821
+ description="Constraints for team formation",
822
+ ),
823
+ "optimization_rounds": NodeParameter(
824
+ name="optimization_rounds",
825
+ type=int,
826
+ required=False,
827
+ default=3,
828
+ description="Number of optimization iterations",
829
+ ),
830
+ "diversity_weight": NodeParameter(
831
+ name="diversity_weight",
832
+ type=float,
833
+ required=False,
834
+ default=0.2,
835
+ description="Weight for team diversity (0-1)",
836
+ ),
837
+ }
838
+
839
+ def run(self, **kwargs) -> Dict[str, Any]:
840
+ """Form an optimal team."""
841
+ problem_analysis = kwargs.get("problem_analysis", {})
842
+ available_agents = kwargs.get("available_agents", [])
843
+ strategy = kwargs.get("formation_strategy", "capability_matching")
844
+ constraints = kwargs.get("constraints", {})
845
+ optimization_rounds = kwargs.get("optimization_rounds", 3)
846
+
847
+ if not available_agents:
848
+ return {"success": False, "error": "No available agents", "team": []}
849
+
850
+ # Form initial team based on strategy
851
+ if strategy == TeamFormationStrategy.CAPABILITY_MATCHING.value:
852
+ team = self._capability_matching_formation(
853
+ problem_analysis, available_agents, constraints
854
+ )
855
+ elif strategy == TeamFormationStrategy.SWARM_BASED.value:
856
+ team = self._swarm_based_formation(
857
+ problem_analysis, available_agents, constraints
858
+ )
859
+ elif strategy == TeamFormationStrategy.MARKET_BASED.value:
860
+ team = self._market_based_formation(
861
+ problem_analysis, available_agents, constraints
862
+ )
863
+ elif strategy == TeamFormationStrategy.HIERARCHICAL.value:
864
+ team = self._hierarchical_formation(
865
+ problem_analysis, available_agents, constraints
866
+ )
867
+ else:
868
+ team = self._random_formation(
869
+ problem_analysis, available_agents, constraints
870
+ )
871
+
872
+ # Optimize team composition
873
+ for round in range(optimization_rounds):
874
+ optimization_result = self._optimize_team(
875
+ team, problem_analysis, available_agents
876
+ )
877
+ if optimization_result["improved"]:
878
+ team = optimization_result["team"]
879
+ else:
880
+ break
881
+
882
+ # Calculate team metrics
883
+ team_metrics = self._calculate_team_metrics(team, problem_analysis)
884
+
885
+ # Record formation
886
+ self.formation_history.append(
887
+ {
888
+ "timestamp": time.time(),
889
+ "problem": problem_analysis.get("problem", "unknown"),
890
+ "strategy": strategy,
891
+ "team_size": len(team),
892
+ "fitness_score": team_metrics["fitness_score"],
893
+ }
894
+ )
895
+
896
+ return {
897
+ "success": True,
898
+ "team": team,
899
+ "team_metrics": team_metrics,
900
+ "formation_strategy": strategy,
901
+ "optimization_rounds_used": round + 1,
902
+ }
903
+
904
+ def _capability_matching_formation(
905
+ self, problem: Dict, agents: List[Dict], constraints: Dict
906
+ ) -> List[Dict]:
907
+ """Form team by matching capabilities to requirements."""
908
+ required_capabilities = set(problem.get("required_capabilities", []))
909
+ selected_agents = []
910
+ covered_capabilities = set()
911
+
912
+ # Sort agents by performance and capability coverage
913
+ agent_scores = []
914
+ for agent in agents:
915
+ agent_caps = set(agent.get("capabilities", []))
916
+ overlap = agent_caps & required_capabilities
917
+ uncovered = overlap - covered_capabilities
918
+
919
+ score = (
920
+ len(uncovered) * 2 # Prioritize new capabilities
921
+ + len(overlap) # Total relevant capabilities
922
+ + agent.get("performance", 0.8) * 2 # Performance weight
923
+ )
924
+
925
+ agent_scores.append((score, agent))
926
+
927
+ # Sort by score
928
+ agent_scores.sort(key=lambda x: x[0], reverse=True)
929
+
930
+ # Select agents
931
+ max_team_size = constraints.get("max_team_size", 10)
932
+ min_team_size = constraints.get("min_team_size", 2)
933
+
934
+ for score, agent in agent_scores:
935
+ if len(selected_agents) >= max_team_size:
936
+ break
937
+
938
+ agent_caps = set(agent.get("capabilities", []))
939
+ if agent_caps & required_capabilities: # Has relevant capabilities
940
+ selected_agents.append(agent)
941
+ covered_capabilities.update(agent_caps)
942
+
943
+ # Check if all required capabilities are covered
944
+ if covered_capabilities >= required_capabilities:
945
+ if len(selected_agents) >= min_team_size:
946
+ break
947
+
948
+ return selected_agents
949
+
950
+ def _swarm_based_formation(
951
+ self, problem: Dict, agents: List[Dict], constraints: Dict
952
+ ) -> List[Dict]:
953
+ """Form team using swarm intelligence principles."""
954
+ required_capabilities = set(problem.get("required_capabilities", []))
955
+ complexity = problem.get("complexity_score", 0.5)
956
+
957
+ # Calculate attraction scores between agents
958
+ attraction_matrix = {}
959
+ for i, agent1 in enumerate(agents):
960
+ for j, agent2 in enumerate(agents):
961
+ if i != j:
962
+ # Attraction based on complementary capabilities
963
+ caps1 = set(agent1.get("capabilities", []))
964
+ caps2 = set(agent2.get("capabilities", []))
965
+
966
+ complementarity = len((caps1 | caps2) & required_capabilities)
967
+ overlap = len(caps1 & caps2)
968
+
969
+ # High complementarity, low overlap is good
970
+ attraction = (
971
+ complementarity / max(len(required_capabilities), 1)
972
+ - overlap * 0.1
973
+ )
974
+ attraction_matrix[(i, j)] = max(0, attraction)
975
+
976
+ # Form clusters using attraction
977
+ clusters = []
978
+ unassigned = set(range(len(agents)))
979
+
980
+ while unassigned and len(clusters) < 5: # Max 5 clusters
981
+ # Start new cluster with highest performance unassigned agent
982
+ seed_idx = max(unassigned, key=lambda i: agents[i].get("performance", 0.8))
983
+ cluster = [seed_idx]
984
+ unassigned.remove(seed_idx)
985
+
986
+ # Grow cluster based on attraction
987
+ while len(cluster) < len(agents) // 3: # Max cluster size
988
+ best_candidate = None
989
+ best_attraction = 0
990
+
991
+ for candidate in unassigned:
992
+ # Average attraction to cluster members
993
+ avg_attraction = sum(
994
+ attraction_matrix.get((member, candidate), 0)
995
+ for member in cluster
996
+ ) / len(cluster)
997
+
998
+ if avg_attraction > best_attraction:
999
+ best_attraction = avg_attraction
1000
+ best_candidate = candidate
1001
+
1002
+ if best_candidate and best_attraction > 0.3:
1003
+ cluster.append(best_candidate)
1004
+ unassigned.remove(best_candidate)
1005
+ else:
1006
+ break
1007
+
1008
+ clusters.append(cluster)
1009
+
1010
+ # Select best cluster based on capability coverage
1011
+ best_cluster = []
1012
+ best_coverage = 0
1013
+
1014
+ for cluster in clusters:
1015
+ cluster_agents = [agents[i] for i in cluster]
1016
+ cluster_caps = set()
1017
+ for agent in cluster_agents:
1018
+ cluster_caps.update(agent.get("capabilities", []))
1019
+
1020
+ coverage = len(cluster_caps & required_capabilities)
1021
+ if coverage > best_coverage:
1022
+ best_coverage = coverage
1023
+ best_cluster = cluster_agents
1024
+
1025
+ return best_cluster
1026
+
1027
+ def _market_based_formation(
1028
+ self, problem: Dict, agents: List[Dict], constraints: Dict
1029
+ ) -> List[Dict]:
1030
+ """Form team using market-based auction mechanism."""
1031
+ required_capabilities = problem.get("required_capabilities", [])
1032
+ budget = constraints.get("budget", 100)
1033
+
1034
+ # Agents bid for participation
1035
+ bids = []
1036
+ for agent in agents:
1037
+ agent_caps = set(agent.get("capabilities", []))
1038
+ relevant_caps = agent_caps & set(required_capabilities)
1039
+
1040
+ if relevant_caps:
1041
+ # Calculate bid based on capability match and performance
1042
+ capability_value = len(relevant_caps) / max(
1043
+ len(required_capabilities), 1
1044
+ )
1045
+ performance = agent.get("performance", 0.8)
1046
+
1047
+ # Lower bid = higher chance of selection (inverse auction)
1048
+ bid_amount = (2 - capability_value - performance) * 10
1049
+
1050
+ bids.append(
1051
+ {
1052
+ "agent": agent,
1053
+ "bid": bid_amount,
1054
+ "value": capability_value * performance,
1055
+ }
1056
+ )
1057
+
1058
+ # Sort by value/cost ratio
1059
+ bids.sort(key=lambda x: x["value"] / x["bid"], reverse=True)
1060
+
1061
+ # Select agents within budget
1062
+ selected_agents = []
1063
+ total_cost = 0
1064
+
1065
+ for bid in bids:
1066
+ if total_cost + bid["bid"] <= budget:
1067
+ selected_agents.append(bid["agent"])
1068
+ total_cost += bid["bid"]
1069
+
1070
+ return selected_agents
1071
+
1072
+ def _hierarchical_formation(
1073
+ self, problem: Dict, agents: List[Dict], constraints: Dict
1074
+ ) -> List[Dict]:
1075
+ """Form team with hierarchical structure."""
1076
+ required_capabilities = problem.get("required_capabilities", [])
1077
+
1078
+ # Identify potential leaders (high performance, multiple capabilities)
1079
+ leader_candidates = []
1080
+ for agent in agents:
1081
+ caps = agent.get("capabilities", [])
1082
+ if len(caps) >= 3 and agent.get("performance", 0) > 0.85:
1083
+ leader_candidates.append(agent)
1084
+
1085
+ # Select leader
1086
+ if leader_candidates:
1087
+ leader = max(leader_candidates, key=lambda a: a.get("performance", 0))
1088
+ else:
1089
+ leader = max(agents, key=lambda a: a.get("performance", 0))
1090
+
1091
+ team = [leader]
1092
+ remaining_agents = [a for a in agents if a != leader]
1093
+
1094
+ # Leader selects team members based on complementary skills
1095
+ leader_caps = set(leader.get("capabilities", []))
1096
+ needed_caps = set(required_capabilities) - leader_caps
1097
+
1098
+ for cap in needed_caps:
1099
+ # Find best agent for each needed capability
1100
+ candidates = [
1101
+ a for a in remaining_agents if cap in a.get("capabilities", [])
1102
+ ]
1103
+
1104
+ if candidates:
1105
+ best = max(candidates, key=lambda a: a.get("performance", 0))
1106
+ team.append(best)
1107
+ remaining_agents.remove(best)
1108
+
1109
+ return team
1110
+
1111
+ def _random_formation(
1112
+ self, problem: Dict, agents: List[Dict], constraints: Dict
1113
+ ) -> List[Dict]:
1114
+ """Random team formation for baseline comparison."""
1115
+ team_size = min(
1116
+ problem.get("estimated_agents", 5),
1117
+ len(agents),
1118
+ constraints.get("max_team_size", 10),
1119
+ )
1120
+
1121
+ return random.sample(agents, team_size)
1122
+
1123
+ def _optimize_team(
1124
+ self, team: List[Dict], problem: Dict, all_agents: List[Dict]
1125
+ ) -> Dict[str, Any]:
1126
+ """Optimize team composition."""
1127
+ current_fitness = self._calculate_team_fitness(team, problem)
1128
+
1129
+ # Try swapping team members
1130
+ non_team_agents = [a for a in all_agents if a not in team]
1131
+
1132
+ if not non_team_agents:
1133
+ return {"improved": False, "team": team}
1134
+
1135
+ best_team = team.copy()
1136
+ best_fitness = current_fitness
1137
+
1138
+ for i, member in enumerate(team):
1139
+ for candidate in non_team_agents:
1140
+ # Try swapping
1141
+ new_team = team.copy()
1142
+ new_team[i] = candidate
1143
+
1144
+ new_fitness = self._calculate_team_fitness(new_team, problem)
1145
+
1146
+ if new_fitness > best_fitness:
1147
+ best_fitness = new_fitness
1148
+ best_team = new_team.copy()
1149
+
1150
+ improved = best_fitness > current_fitness
1151
+
1152
+ return {
1153
+ "improved": improved,
1154
+ "team": best_team,
1155
+ "fitness_improvement": best_fitness - current_fitness,
1156
+ }
1157
+
1158
+ def _calculate_team_fitness(self, team: List[Dict], problem: Dict) -> float:
1159
+ """Calculate how well a team matches problem requirements."""
1160
+ required_capabilities = set(problem.get("required_capabilities", []))
1161
+
1162
+ # Capability coverage
1163
+ team_capabilities = set()
1164
+ for agent in team:
1165
+ team_capabilities.update(agent.get("capabilities", []))
1166
+
1167
+ coverage = len(team_capabilities & required_capabilities) / max(
1168
+ len(required_capabilities), 1
1169
+ )
1170
+
1171
+ # Average performance
1172
+ avg_performance = sum(a.get("performance", 0.8) for a in team) / max(
1173
+ len(team), 1
1174
+ )
1175
+
1176
+ # Team size efficiency
1177
+ target_size = problem.get("estimated_agents", 5)
1178
+ size_penalty = abs(len(team) - target_size) / max(target_size, 1) * 0.2
1179
+
1180
+ # Diversity bonus
1181
+ unique_capabilities = len(team_capabilities)
1182
+ diversity_bonus = min(unique_capabilities / max(len(team) * 3, 1), 0.2)
1183
+
1184
+ fitness = (
1185
+ coverage * 0.5 + avg_performance * 0.3 + diversity_bonus - size_penalty
1186
+ )
1187
+
1188
+ return max(0, min(1, fitness))
1189
+
1190
+ def _calculate_team_metrics(
1191
+ self, team: List[Dict], problem: Dict
1192
+ ) -> Dict[str, Any]:
1193
+ """Calculate comprehensive team metrics."""
1194
+ required_capabilities = set(problem.get("required_capabilities", []))
1195
+ team_capabilities = set()
1196
+
1197
+ for agent in team:
1198
+ team_capabilities.update(agent.get("capabilities", []))
1199
+
1200
+ return {
1201
+ "team_size": len(team),
1202
+ "capability_coverage": len(team_capabilities & required_capabilities)
1203
+ / max(len(required_capabilities), 1),
1204
+ "total_capabilities": len(team_capabilities),
1205
+ "avg_performance": sum(a.get("performance", 0.8) for a in team)
1206
+ / max(len(team), 1),
1207
+ "fitness_score": self._calculate_team_fitness(team, problem),
1208
+ "missing_capabilities": list(required_capabilities - team_capabilities),
1209
+ "redundant_capabilities": list(team_capabilities - required_capabilities),
1210
+ }
1211
+
1212
+
1213
+ @register_node()
1214
+ class SelfOrganizingAgentNode(A2AAgentNode):
1215
+ """
1216
+ Self-organizing agent that can autonomously join teams, collaborate,
1217
+ and adapt its behavior based on team dynamics.
1218
+
1219
+ Examples:
1220
+ >>> # Create self-organizing agent
1221
+ >>> agent = SelfOrganizingAgentNode()
1222
+ >>>
1223
+ >>> # Test basic structure
1224
+ >>> params = agent.get_parameters()
1225
+ >>> assert "agent_id" in params
1226
+ >>> assert "capabilities" in params
1227
+ """
1228
+
1229
+ def __init__(self):
1230
+ super().__init__()
1231
+ self.team_memberships = {}
1232
+ self.collaboration_history = deque(maxlen=50)
1233
+ self.skill_adaptations = defaultdict(float)
1234
+
1235
+ def get_parameters(self) -> Dict[str, NodeParameter]:
1236
+ params = super().get_parameters()
1237
+
1238
+ # Add self-organization specific parameters
1239
+ params.update(
1240
+ {
1241
+ "capabilities": NodeParameter(
1242
+ name="capabilities",
1243
+ type=list,
1244
+ required=False,
1245
+ default=[],
1246
+ description="Agent's capabilities",
1247
+ ),
1248
+ "team_context": NodeParameter(
1249
+ name="team_context",
1250
+ type=dict,
1251
+ required=False,
1252
+ default={},
1253
+ description="Current team information",
1254
+ ),
1255
+ "collaboration_mode": NodeParameter(
1256
+ name="collaboration_mode",
1257
+ type=str,
1258
+ required=False,
1259
+ default="cooperative",
1260
+ description="Mode: 'cooperative', 'competitive', 'mixed'",
1261
+ ),
1262
+ "adaptation_rate": NodeParameter(
1263
+ name="adaptation_rate",
1264
+ type=float,
1265
+ required=False,
1266
+ default=0.1,
1267
+ description="How quickly agent adapts behavior (0-1)",
1268
+ ),
1269
+ "task": NodeParameter(
1270
+ name="task",
1271
+ type=str,
1272
+ required=False,
1273
+ description="Specific task for the agent",
1274
+ ),
1275
+ "autonomy_level": NodeParameter(
1276
+ name="autonomy_level",
1277
+ type=float,
1278
+ required=False,
1279
+ default=0.8,
1280
+ description="Level of autonomous decision making (0-1)",
1281
+ ),
1282
+ }
1283
+ )
1284
+
1285
+ return params
1286
+
1287
+ def run(self, **kwargs) -> Dict[str, Any]:
1288
+ """Execute self-organizing agent behavior."""
1289
+ agent_id = kwargs.get("agent_id")
1290
+ capabilities = kwargs.get("capabilities", [])
1291
+ team_context = kwargs.get("team_context", {})
1292
+ collaboration_mode = kwargs.get("collaboration_mode", "cooperative")
1293
+ task = kwargs.get("task", "")
1294
+
1295
+ # Adapt behavior based on team context
1296
+ if team_context:
1297
+ self._adapt_to_team(agent_id, team_context, collaboration_mode)
1298
+
1299
+ # Enhance task with self-organization context
1300
+ if task:
1301
+ enhanced_task = self._enhance_task_with_context(
1302
+ task, team_context, capabilities
1303
+ )
1304
+ kwargs["messages"] = kwargs.get("messages", [])
1305
+ kwargs["messages"].append({"role": "user", "content": enhanced_task})
1306
+
1307
+ # Add self-organization instructions to system prompt
1308
+ so_prompt = f"""You are a self-organizing agent with capabilities: {', '.join(capabilities)}.
1309
+
1310
+ Current team context: {json.dumps(team_context, indent=2)}
1311
+ Collaboration mode: {collaboration_mode}
1312
+
1313
+ Guidelines:
1314
+ 1. Leverage your specific capabilities to contribute to the team goal
1315
+ 2. Coordinate with other team members when mentioned
1316
+ 3. Adapt your approach based on team dynamics
1317
+ 4. Share insights that others can build upon
1318
+ 5. Be proactive in identifying how you can help
1319
+
1320
+ {kwargs.get('system_prompt', '')}"""
1321
+
1322
+ kwargs["system_prompt"] = so_prompt
1323
+
1324
+ # Execute base A2A agent
1325
+ result = super().run(**kwargs)
1326
+
1327
+ # Track collaboration
1328
+ if result.get("success"):
1329
+ self._track_collaboration(agent_id, team_context, task, result)
1330
+
1331
+ # Add self-organization metadata
1332
+ result["self_organization"] = {
1333
+ "agent_id": agent_id,
1334
+ "capabilities": capabilities,
1335
+ "team_memberships": list(self.team_memberships.keys()),
1336
+ "adaptations": dict(self.skill_adaptations),
1337
+ "collaboration_mode": collaboration_mode,
1338
+ "task": task,
1339
+ }
1340
+
1341
+ return result
1342
+
1343
+ def _adapt_to_team(self, agent_id: str, team_context: Dict, mode: str):
1344
+ """Adapt behavior to team dynamics."""
1345
+ team_id = team_context.get("team_id")
1346
+ if not team_id:
1347
+ return
1348
+
1349
+ # Track team membership
1350
+ if team_id not in self.team_memberships:
1351
+ self.team_memberships[team_id] = {
1352
+ "joined_at": time.time(),
1353
+ "contributions": 0,
1354
+ "role": "member",
1355
+ }
1356
+
1357
+ # Adapt based on other members' capabilities
1358
+ other_members = team_context.get("other_members", [])
1359
+ if other_members:
1360
+ # In cooperative mode, focus on complementary skills
1361
+ if mode == "cooperative":
1362
+ # Increase weight on unique capabilities
1363
+ for cap in self.skill_adaptations:
1364
+ self.skill_adaptations[cap] *= 0.9 # Decay
1365
+
1366
+ # In competitive mode, enhance overlapping skills
1367
+ elif mode == "competitive":
1368
+ for cap in self.skill_adaptations:
1369
+ self.skill_adaptations[cap] *= 1.1 # Enhance
1370
+
1371
+ def _enhance_task_with_context(
1372
+ self, task: str, team_context: Dict, capabilities: List[str]
1373
+ ) -> str:
1374
+ """Enhance task description with team context."""
1375
+ enhanced = task
1376
+
1377
+ if team_context.get("team_goal"):
1378
+ enhanced = f"Team Goal: {team_context['team_goal']}\n\nYour Task: {task}"
1379
+
1380
+ if team_context.get("other_members"):
1381
+ enhanced += (
1382
+ f"\n\nOther team members: {', '.join(team_context['other_members'])}"
1383
+ )
1384
+ enhanced += "\nCoordinate and build upon their work as needed."
1385
+
1386
+ enhanced += (
1387
+ f"\n\nYour unique capabilities to leverage: {', '.join(capabilities)}"
1388
+ )
1389
+
1390
+ return enhanced
1391
+
1392
+ def _track_collaboration(
1393
+ self, agent_id: str, team_context: Dict, task: str, result: Dict
1394
+ ):
1395
+ """Track collaboration history and performance."""
1396
+ team_id = team_context.get("team_id", "unknown")
1397
+
1398
+ collaboration_entry = {
1399
+ "timestamp": time.time(),
1400
+ "agent_id": agent_id,
1401
+ "team_id": team_id,
1402
+ "task": task,
1403
+ "success": result.get("success", False),
1404
+ "insights_generated": result.get("a2a_metadata", {}).get(
1405
+ "insights_generated", 0
1406
+ ),
1407
+ }
1408
+
1409
+ self.collaboration_history.append(collaboration_entry)
1410
+
1411
+ # Update team membership stats
1412
+ if team_id in self.team_memberships:
1413
+ self.team_memberships[team_id]["contributions"] += 1
1414
+
1415
+
1416
+ @register_node()
1417
+ class SolutionEvaluatorNode(Node):
1418
+ """
1419
+ Evaluates solutions produced by agent teams and determines if
1420
+ iteration is needed.
1421
+
1422
+ Examples:
1423
+ >>> evaluator = SolutionEvaluatorNode()
1424
+ >>>
1425
+ >>> result = evaluator.run(
1426
+ ... solution={
1427
+ ... "approach": "Clustering analysis",
1428
+ ... "findings": ["3 distinct customer segments identified"],
1429
+ ... "confidence": 0.85
1430
+ ... },
1431
+ ... problem_requirements={
1432
+ ... "quality_threshold": 0.8,
1433
+ ... "required_outputs": ["segmentation", "recommendations"]
1434
+ ... },
1435
+ ... team_performance={
1436
+ ... "collaboration_score": 0.9,
1437
+ ... "time_taken": 45
1438
+ ... }
1439
+ ... )
1440
+ """
1441
+
1442
+ def __init__(self):
1443
+ super().__init__()
1444
+ self.evaluation_history = deque(maxlen=100)
1445
+
1446
+ def get_parameters(self) -> Dict[str, NodeParameter]:
1447
+ return {
1448
+ "solution": NodeParameter(
1449
+ name="solution",
1450
+ type=dict,
1451
+ required=False,
1452
+ default={},
1453
+ description="Solution to evaluate",
1454
+ ),
1455
+ "problem_requirements": NodeParameter(
1456
+ name="problem_requirements",
1457
+ type=dict,
1458
+ required=False,
1459
+ default={},
1460
+ description="Original problem requirements",
1461
+ ),
1462
+ "team_performance": NodeParameter(
1463
+ name="team_performance",
1464
+ type=dict,
1465
+ required=False,
1466
+ default={},
1467
+ description="Team performance metrics",
1468
+ ),
1469
+ "evaluation_criteria": NodeParameter(
1470
+ name="evaluation_criteria",
1471
+ type=dict,
1472
+ required=False,
1473
+ default={},
1474
+ description="Custom evaluation criteria",
1475
+ ),
1476
+ "iteration_count": NodeParameter(
1477
+ name="iteration_count",
1478
+ type=int,
1479
+ required=False,
1480
+ default=0,
1481
+ description="Current iteration number",
1482
+ ),
1483
+ }
1484
+
1485
+ def run(self, **kwargs) -> Dict[str, Any]:
1486
+ """Evaluate solution quality."""
1487
+ solution = kwargs.get("solution", {})
1488
+ requirements = kwargs.get("problem_requirements", {})
1489
+ team_performance = kwargs.get("team_performance", {})
1490
+ criteria = kwargs.get("evaluation_criteria", {})
1491
+ iteration = kwargs.get("iteration_count", 0)
1492
+
1493
+ # Evaluate different aspects
1494
+ quality_scores = {}
1495
+
1496
+ # 1. Completeness
1497
+ required_outputs = requirements.get("required_outputs", [])
1498
+ if required_outputs:
1499
+ outputs_found = sum(
1500
+ 1
1501
+ for output in required_outputs
1502
+ if output.lower() in str(solution).lower()
1503
+ )
1504
+ quality_scores["completeness"] = outputs_found / len(required_outputs)
1505
+ else:
1506
+ quality_scores["completeness"] = 0.8 # Default if not specified
1507
+
1508
+ # 2. Confidence/Certainty
1509
+ confidence = solution.get("confidence", 0.7)
1510
+ quality_scores["confidence"] = confidence
1511
+
1512
+ # 3. Innovation (based on solution complexity/uniqueness)
1513
+ solution_text = json.dumps(solution)
1514
+ quality_scores["innovation"] = min(len(set(solution_text.split())) / 100, 1.0)
1515
+
1516
+ # 4. Team collaboration
1517
+ collab_score = team_performance.get("collaboration_score", 0.8)
1518
+ quality_scores["collaboration"] = collab_score
1519
+
1520
+ # 5. Efficiency
1521
+ time_taken = team_performance.get("time_taken", 60)
1522
+ time_limit = requirements.get("time_estimate", 60)
1523
+ efficiency = min(time_limit / max(time_taken, 1), 1.0)
1524
+ quality_scores["efficiency"] = efficiency
1525
+
1526
+ # Calculate overall score
1527
+ weights = criteria.get(
1528
+ "weights",
1529
+ {
1530
+ "completeness": 0.3,
1531
+ "confidence": 0.25,
1532
+ "innovation": 0.15,
1533
+ "collaboration": 0.15,
1534
+ "efficiency": 0.15,
1535
+ },
1536
+ )
1537
+
1538
+ overall_score = sum(
1539
+ quality_scores.get(aspect, 0) * weight for aspect, weight in weights.items()
1540
+ )
1541
+
1542
+ # Determine if iteration needed
1543
+ quality_threshold = requirements.get("quality_threshold", 0.8)
1544
+ needs_iteration = overall_score < quality_threshold and iteration < 3
1545
+
1546
+ # Generate feedback for improvement
1547
+ feedback = self._generate_feedback(quality_scores, requirements, overall_score)
1548
+
1549
+ # Record evaluation
1550
+ self.evaluation_history.append(
1551
+ {
1552
+ "timestamp": time.time(),
1553
+ "overall_score": overall_score,
1554
+ "quality_scores": quality_scores,
1555
+ "iteration": iteration,
1556
+ "needs_iteration": needs_iteration,
1557
+ }
1558
+ )
1559
+
1560
+ return {
1561
+ "success": True,
1562
+ "overall_score": overall_score,
1563
+ "quality_scores": quality_scores,
1564
+ "meets_threshold": overall_score >= quality_threshold,
1565
+ "needs_iteration": needs_iteration,
1566
+ "feedback": feedback,
1567
+ "recommended_actions": self._recommend_actions(
1568
+ quality_scores, feedback, iteration
1569
+ ),
1570
+ }
1571
+
1572
+ def _generate_feedback(
1573
+ self, scores: Dict[str, float], requirements: Dict, overall: float
1574
+ ) -> Dict[str, Any]:
1575
+ """Generate specific feedback for improvement."""
1576
+ feedback = {"strengths": [], "weaknesses": [], "suggestions": []}
1577
+
1578
+ # Identify strengths and weaknesses
1579
+ for aspect, score in scores.items():
1580
+ if score >= 0.8:
1581
+ feedback["strengths"].append(f"Strong {aspect} (score: {score:.2f})")
1582
+ elif score < 0.6:
1583
+ feedback["weaknesses"].append(f"Weak {aspect} (score: {score:.2f})")
1584
+
1585
+ # Generate suggestions
1586
+ if scores.get("completeness", 1) < 0.8:
1587
+ feedback["suggestions"].append("Ensure all required outputs are addressed")
1588
+
1589
+ if scores.get("confidence", 1) < 0.7:
1590
+ feedback["suggestions"].append("Gather more evidence or validate findings")
1591
+
1592
+ if scores.get("collaboration", 1) < 0.7:
1593
+ feedback["suggestions"].append(
1594
+ "Improve team coordination and information sharing"
1595
+ )
1596
+
1597
+ return feedback
1598
+
1599
+ def _recommend_actions(
1600
+ self, scores: Dict[str, float], feedback: Dict, iteration: int
1601
+ ) -> List[str]:
1602
+ """Recommend specific actions for improvement."""
1603
+ actions = []
1604
+
1605
+ # Based on weakest areas
1606
+ weakest_aspect = min(scores.items(), key=lambda x: x[1])[0]
1607
+
1608
+ if weakest_aspect == "completeness":
1609
+ actions.append("Add specialists for missing output areas")
1610
+ elif weakest_aspect == "confidence":
1611
+ actions.append("Add validation or domain expert agents")
1612
+ elif weakest_aspect == "innovation":
1613
+ actions.append("Encourage diverse solution approaches")
1614
+ elif weakest_aspect == "collaboration":
1615
+ actions.append("Improve communication protocols")
1616
+ elif weakest_aspect == "efficiency":
1617
+ actions.append("Parallelize tasks or reduce team size")
1618
+
1619
+ # Iteration-specific recommendations
1620
+ if iteration > 1:
1621
+ actions.append("Consider alternative team composition")
1622
+ actions.append("Try different problem decomposition")
1623
+
1624
+ return actions