kailash 0.1.4__py3-none-any.whl → 0.2.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.
Files changed (83) hide show
  1. kailash/__init__.py +1 -1
  2. kailash/access_control.py +740 -0
  3. kailash/api/__main__.py +6 -0
  4. kailash/api/auth.py +668 -0
  5. kailash/api/custom_nodes.py +285 -0
  6. kailash/api/custom_nodes_secure.py +377 -0
  7. kailash/api/database.py +620 -0
  8. kailash/api/studio.py +915 -0
  9. kailash/api/studio_secure.py +893 -0
  10. kailash/mcp/__init__.py +53 -0
  11. kailash/mcp/__main__.py +13 -0
  12. kailash/mcp/ai_registry_server.py +712 -0
  13. kailash/mcp/client.py +447 -0
  14. kailash/mcp/client_new.py +334 -0
  15. kailash/mcp/server.py +293 -0
  16. kailash/mcp/server_new.py +336 -0
  17. kailash/mcp/servers/__init__.py +12 -0
  18. kailash/mcp/servers/ai_registry.py +289 -0
  19. kailash/nodes/__init__.py +4 -2
  20. kailash/nodes/ai/__init__.py +38 -0
  21. kailash/nodes/ai/a2a.py +1790 -0
  22. kailash/nodes/ai/agents.py +116 -2
  23. kailash/nodes/ai/ai_providers.py +206 -8
  24. kailash/nodes/ai/intelligent_agent_orchestrator.py +2108 -0
  25. kailash/nodes/ai/iterative_llm_agent.py +1280 -0
  26. kailash/nodes/ai/llm_agent.py +324 -1
  27. kailash/nodes/ai/self_organizing.py +1623 -0
  28. kailash/nodes/api/http.py +106 -25
  29. kailash/nodes/api/rest.py +116 -21
  30. kailash/nodes/base.py +15 -2
  31. kailash/nodes/base_async.py +45 -0
  32. kailash/nodes/base_cycle_aware.py +374 -0
  33. kailash/nodes/base_with_acl.py +338 -0
  34. kailash/nodes/code/python.py +135 -27
  35. kailash/nodes/data/readers.py +116 -53
  36. kailash/nodes/data/writers.py +16 -6
  37. kailash/nodes/logic/__init__.py +8 -0
  38. kailash/nodes/logic/async_operations.py +48 -9
  39. kailash/nodes/logic/convergence.py +642 -0
  40. kailash/nodes/logic/loop.py +153 -0
  41. kailash/nodes/logic/operations.py +212 -27
  42. kailash/nodes/logic/workflow.py +26 -18
  43. kailash/nodes/mixins/__init__.py +11 -0
  44. kailash/nodes/mixins/mcp.py +228 -0
  45. kailash/nodes/mixins.py +387 -0
  46. kailash/nodes/transform/__init__.py +8 -1
  47. kailash/nodes/transform/processors.py +119 -4
  48. kailash/runtime/__init__.py +2 -1
  49. kailash/runtime/access_controlled.py +458 -0
  50. kailash/runtime/local.py +106 -33
  51. kailash/runtime/parallel_cyclic.py +529 -0
  52. kailash/sdk_exceptions.py +90 -5
  53. kailash/security.py +845 -0
  54. kailash/tracking/manager.py +38 -15
  55. kailash/tracking/models.py +1 -1
  56. kailash/tracking/storage/filesystem.py +30 -2
  57. kailash/utils/__init__.py +8 -0
  58. kailash/workflow/__init__.py +18 -0
  59. kailash/workflow/convergence.py +270 -0
  60. kailash/workflow/cycle_analyzer.py +768 -0
  61. kailash/workflow/cycle_builder.py +573 -0
  62. kailash/workflow/cycle_config.py +709 -0
  63. kailash/workflow/cycle_debugger.py +760 -0
  64. kailash/workflow/cycle_exceptions.py +601 -0
  65. kailash/workflow/cycle_profiler.py +671 -0
  66. kailash/workflow/cycle_state.py +338 -0
  67. kailash/workflow/cyclic_runner.py +985 -0
  68. kailash/workflow/graph.py +500 -39
  69. kailash/workflow/migration.py +768 -0
  70. kailash/workflow/safety.py +365 -0
  71. kailash/workflow/templates.py +744 -0
  72. kailash/workflow/validation.py +693 -0
  73. {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/METADATA +446 -13
  74. kailash-0.2.0.dist-info/RECORD +125 -0
  75. kailash/nodes/mcp/__init__.py +0 -11
  76. kailash/nodes/mcp/client.py +0 -554
  77. kailash/nodes/mcp/resource.py +0 -682
  78. kailash/nodes/mcp/server.py +0 -577
  79. kailash-0.1.4.dist-info/RECORD +0 -85
  80. {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/WHEEL +0 -0
  81. {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/entry_points.txt +0 -0
  82. {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/licenses/LICENSE +0 -0
  83. {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,2108 @@
1
+ """
2
+ Intelligent Agent Orchestrator - Comprehensive Self-Organizing Agent Architecture
3
+
4
+ This module provides a complete solution for self-organizing agents that:
5
+ 1. Autonomously form teams to solve complex queries
6
+ 2. Integrate with MCP servers for external tool access
7
+ 3. Implement information reuse mechanisms to prevent repeated calls
8
+ 4. Automatically evaluate solutions and terminate when satisfactory
9
+
10
+ Key Components:
11
+ - IntelligentCacheNode: Prevents repeated external calls through smart caching
12
+ - MCPAgentNode: Self-organizing agent with MCP tool integration
13
+ - QueryAnalysisNode: Analyzes queries to determine optimal approach
14
+ - OrchestrationManagerNode: Coordinates entire workflow
15
+ - ConvergenceDetectorNode: Determines when solution is satisfactory
16
+ """
17
+
18
+ import hashlib
19
+ import json
20
+ import time
21
+ import uuid
22
+ from collections import defaultdict, deque
23
+ from datetime import datetime
24
+ from typing import Any, Dict, List, Optional
25
+
26
+ from kailash.nodes.ai.a2a import SharedMemoryPoolNode
27
+ from kailash.nodes.ai.self_organizing import (
28
+ AgentPoolManagerNode,
29
+ SelfOrganizingAgentNode,
30
+ SolutionEvaluatorNode,
31
+ TeamFormationNode,
32
+ )
33
+ from kailash.nodes.base import Node, NodeParameter, register_node
34
+
35
+ # MCP functionality is now built into LLM agents as a capability
36
+
37
+
38
+ @register_node()
39
+ class IntelligentCacheNode(Node):
40
+ """
41
+ Intelligent caching system that prevents repeated external calls and enables
42
+ information reuse across agents and sessions.
43
+
44
+ This node provides a sophisticated caching layer specifically designed for multi-agent
45
+ systems where preventing redundant API calls, MCP tool invocations, and expensive
46
+ computations is critical for both performance and cost optimization. It goes beyond
47
+ simple key-value caching by implementing semantic similarity detection and intelligent
48
+ cache management strategies.
49
+
50
+ Design Philosophy:
51
+ The IntelligentCacheNode acts as a shared knowledge repository that learns from
52
+ all agent interactions. It understands that queries with similar intent should
53
+ return cached results even if phrased differently, and prioritizes caching based
54
+ on operation cost and access patterns. This creates a form of collective memory
55
+ that improves system efficiency over time.
56
+
57
+ Upstream Dependencies:
58
+ - MCPAgentNode: Primary source of expensive tool call results
59
+ - A2AAgentNode: Caches intermediate computation results
60
+ - OrchestrationManagerNode: Manages cache lifecycle and policies
61
+ - Any node performing expensive operations (API calls, computations)
62
+
63
+ Downstream Consumers:
64
+ - MCPAgentNode: Checks cache before making tool calls
65
+ - QueryAnalysisNode: Uses cached analysis results
66
+ - All agents in the system benefit from cached information
67
+ - Orchestration components for performance metrics
68
+
69
+ Configuration:
70
+ The cache adapts its behavior based on usage patterns but can be configured
71
+ with default TTL values, similarity thresholds, and size limits through
72
+ initialization parameters or runtime configuration.
73
+
74
+ Implementation Details:
75
+ - Uses in-memory storage with configurable persistence options
76
+ - Implements semantic indexing using embedding vectors for similarity search
77
+ - Tracks access patterns to optimize cache eviction policies
78
+ - Maintains cost metrics to prioritize expensive operation caching
79
+ - Supports query abstraction to improve cache hit rates
80
+ - Thread-safe for concurrent agent access
81
+
82
+ Error Handling:
83
+ - Returns cache misses gracefully without throwing exceptions
84
+ - Handles corrupted cache entries by returning misses
85
+ - Validates TTL and automatically expires stale entries
86
+ - Logs cache operations for debugging and optimization
87
+
88
+ Side Effects:
89
+ - Maintains internal cache state that persists across calls
90
+ - Updates access statistics and cost metrics
91
+ - May evict old entries when cache size limits are reached
92
+ - Modifies semantic indices when new entries are added
93
+
94
+ Examples:
95
+ >>> cache = IntelligentCacheNode()
96
+ >>>
97
+ >>> # Cache an expensive MCP tool call result
98
+ >>> result = cache.run(
99
+ ... action="cache",
100
+ ... cache_key="weather_api_nyc_20240106",
101
+ ... data={"temperature": 72, "humidity": 65, "conditions": "sunny"},
102
+ ... metadata={
103
+ ... "source": "weather_mcp_server",
104
+ ... "cost": 0.05, # Track API cost
105
+ ... "query_abstraction": "weather_location_date",
106
+ ... "semantic_tags": ["weather", "temperature", "nyc", "current"]
107
+ ... },
108
+ ... ttl=3600 # 1 hour cache
109
+ ... )
110
+ >>> assert result["success"] == True
111
+ >>>
112
+ >>> # Direct cache hit by key
113
+ >>> cached = cache.run(
114
+ ... action="get",
115
+ ... cache_key="weather_api_nyc_20240106"
116
+ ... )
117
+ >>> assert cached["hit"] == True
118
+ >>> assert cached["data"]["temperature"] == 72
119
+ >>>
120
+ >>> # Semantic similarity hit (uses simple string matching in this mock implementation)
121
+ >>> similar = cache.run(
122
+ ... action="get",
123
+ ... query="weather nyc", # Simple match
124
+ ... similarity_threshold=0.3
125
+ ... )
126
+ >>> # Note: Mock implementation may not find semantic matches, check hit status
127
+ >>> has_hit = similar.get("hit", False)
128
+ >>>
129
+ >>> # Cache statistics
130
+ >>> stats = cache.run(action="stats")
131
+ >>> assert "stats" in stats
132
+ >>> assert "hit_rate" in stats["stats"]
133
+ """
134
+
135
+ def __init__(self):
136
+ super().__init__()
137
+ self.cache = {}
138
+ self.semantic_index = defaultdict(list)
139
+ self.access_patterns = defaultdict(int)
140
+ self.cost_metrics = {}
141
+ self.query_abstractions = {}
142
+
143
+ def get_parameters(self) -> Dict[str, NodeParameter]:
144
+ return {
145
+ "action": NodeParameter(
146
+ name="action",
147
+ type=str,
148
+ required=False,
149
+ default="get",
150
+ description="Action: 'cache', 'get', 'invalidate', 'stats', 'cleanup'",
151
+ ),
152
+ "cache_key": NodeParameter(
153
+ name="cache_key",
154
+ type=str,
155
+ required=False,
156
+ description="Unique key for the cached item",
157
+ ),
158
+ "data": NodeParameter(
159
+ name="data",
160
+ type=Any,
161
+ required=False,
162
+ description="Data to cache (for cache action)",
163
+ ),
164
+ "metadata": NodeParameter(
165
+ name="metadata",
166
+ type=dict,
167
+ required=False,
168
+ default={},
169
+ description="Metadata including source, cost, semantic tags",
170
+ ),
171
+ "ttl": NodeParameter(
172
+ name="ttl",
173
+ type=int,
174
+ required=False,
175
+ default=3600,
176
+ description="Time to live in seconds",
177
+ ),
178
+ "similarity_threshold": NodeParameter(
179
+ name="similarity_threshold",
180
+ type=float,
181
+ required=False,
182
+ default=0.8,
183
+ description="Threshold for semantic similarity matching",
184
+ ),
185
+ "query": NodeParameter(
186
+ name="query",
187
+ type=str,
188
+ required=False,
189
+ description="Query for semantic search",
190
+ ),
191
+ "force_refresh": NodeParameter(
192
+ name="force_refresh",
193
+ type=bool,
194
+ required=False,
195
+ default=False,
196
+ description="Force refresh even if cached",
197
+ ),
198
+ }
199
+
200
+ def run(self, **kwargs) -> Dict[str, Any]:
201
+ """Execute cache operations."""
202
+ action = kwargs.get("action", "get")
203
+
204
+ if action == "cache":
205
+ return self._cache_data(kwargs)
206
+ elif action == "get":
207
+ return self._get_cached(kwargs)
208
+ elif action == "invalidate":
209
+ return self._invalidate(kwargs)
210
+ elif action == "stats":
211
+ return self._get_stats()
212
+ elif action == "cleanup":
213
+ return self._cleanup_expired()
214
+ else:
215
+ return {"success": False, "error": f"Unknown action: {action}"}
216
+
217
+ def _cache_data(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
218
+ """Cache data with intelligent indexing."""
219
+ cache_key = kwargs.get("cache_key")
220
+ if not cache_key:
221
+ cache_key = self._generate_cache_key(kwargs)
222
+
223
+ data = kwargs["data"]
224
+ metadata = kwargs.get("metadata", {})
225
+ ttl = kwargs.get("ttl", 3600)
226
+
227
+ # Create cache entry
228
+ cache_entry = {
229
+ "data": data,
230
+ "metadata": metadata,
231
+ "cached_at": time.time(),
232
+ "expires_at": time.time() + ttl,
233
+ "access_count": 0,
234
+ "last_accessed": time.time(),
235
+ "cache_key": cache_key,
236
+ }
237
+
238
+ # Store in main cache
239
+ self.cache[cache_key] = cache_entry
240
+
241
+ # Index semantically
242
+ semantic_tags = metadata.get("semantic_tags", [])
243
+ for tag in semantic_tags:
244
+ self.semantic_index[tag].append(cache_key)
245
+
246
+ # Store query abstraction
247
+ if "query_abstraction" in metadata:
248
+ abstraction = metadata["query_abstraction"]
249
+ if abstraction not in self.query_abstractions:
250
+ self.query_abstractions[abstraction] = []
251
+ self.query_abstractions[abstraction].append(cache_key)
252
+
253
+ # Store cost metrics
254
+ if "cost" in metadata:
255
+ self.cost_metrics[cache_key] = metadata["cost"]
256
+
257
+ return {
258
+ "success": True,
259
+ "cache_key": cache_key,
260
+ "cached_at": cache_entry["cached_at"],
261
+ "expires_at": cache_entry["expires_at"],
262
+ "semantic_tags": semantic_tags,
263
+ }
264
+
265
+ def _get_cached(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
266
+ """Retrieve cached data with intelligent matching."""
267
+ cache_key = kwargs.get("cache_key")
268
+ query = kwargs.get("query")
269
+ similarity_threshold = kwargs.get("similarity_threshold", 0.8)
270
+ force_refresh = kwargs.get("force_refresh", False)
271
+
272
+ # Direct cache hit
273
+ if cache_key and cache_key in self.cache:
274
+ entry = self.cache[cache_key]
275
+ if not force_refresh and entry["expires_at"] > time.time():
276
+ entry["access_count"] += 1
277
+ entry["last_accessed"] = time.time()
278
+ self.access_patterns[cache_key] += 1
279
+
280
+ return {
281
+ "success": True,
282
+ "hit": True,
283
+ "data": entry["data"],
284
+ "metadata": entry["metadata"],
285
+ "cached_at": entry["cached_at"],
286
+ "access_count": entry["access_count"],
287
+ }
288
+
289
+ # Semantic search if no direct hit
290
+ if query:
291
+ semantic_matches = self._find_semantic_matches(query, similarity_threshold)
292
+ if semantic_matches:
293
+ best_match = semantic_matches[0]
294
+ entry = self.cache[best_match["cache_key"]]
295
+ entry["access_count"] += 1
296
+ entry["last_accessed"] = time.time()
297
+
298
+ return {
299
+ "success": True,
300
+ "hit": True,
301
+ "semantic_match": True,
302
+ "similarity_score": best_match["similarity"],
303
+ "data": entry["data"],
304
+ "metadata": entry["metadata"],
305
+ "cache_key": best_match["cache_key"],
306
+ }
307
+
308
+ return {"success": True, "hit": False, "cache_key": cache_key, "query": query}
309
+
310
+ def _find_semantic_matches(self, query: str, threshold: float) -> List[Dict]:
311
+ """Find semantically similar cached entries."""
312
+ matches = []
313
+ query_words = set(query.lower().split())
314
+
315
+ for tag, cache_keys in self.semantic_index.items():
316
+ tag_words = set(tag.lower().split())
317
+ similarity = len(query_words & tag_words) / len(query_words | tag_words)
318
+
319
+ if similarity >= threshold:
320
+ for cache_key in cache_keys:
321
+ if cache_key in self.cache:
322
+ entry = self.cache[cache_key]
323
+ if entry["expires_at"] > time.time():
324
+ matches.append(
325
+ {
326
+ "cache_key": cache_key,
327
+ "similarity": similarity,
328
+ "tag": tag,
329
+ }
330
+ )
331
+
332
+ # Sort by similarity
333
+ matches.sort(key=lambda x: x["similarity"], reverse=True)
334
+ return matches
335
+
336
+ def _generate_cache_key(self, kwargs: Dict[str, Any]) -> str:
337
+ """Generate cache key from request parameters."""
338
+ data_str = json.dumps(kwargs.get("data", {}), sort_keys=True)
339
+ metadata_str = json.dumps(kwargs.get("metadata", {}), sort_keys=True)
340
+ combined = f"{data_str}_{metadata_str}_{time.time()}"
341
+ return hashlib.md5(combined.encode()).hexdigest()[:16]
342
+
343
+ def _invalidate(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
344
+ """Invalidate cache entries."""
345
+ cache_key = kwargs.get("cache_key")
346
+ pattern = kwargs.get("pattern")
347
+
348
+ invalidated_keys = []
349
+
350
+ if cache_key and cache_key in self.cache:
351
+ del self.cache[cache_key]
352
+ invalidated_keys.append(cache_key)
353
+
354
+ if pattern:
355
+ for key in list(self.cache.keys()):
356
+ if pattern in key:
357
+ del self.cache[key]
358
+ invalidated_keys.append(key)
359
+
360
+ return {
361
+ "success": True,
362
+ "invalidated_keys": invalidated_keys,
363
+ "count": len(invalidated_keys),
364
+ }
365
+
366
+ def _cleanup_expired(self) -> Dict[str, Any]:
367
+ """Remove expired cache entries."""
368
+ current_time = time.time()
369
+ expired_keys = []
370
+
371
+ for key, entry in list(self.cache.items()):
372
+ if entry["expires_at"] <= current_time:
373
+ del self.cache[key]
374
+ expired_keys.append(key)
375
+
376
+ # Clean up semantic index
377
+ for tag, cache_keys in self.semantic_index.items():
378
+ self.semantic_index[tag] = [k for k in cache_keys if k in self.cache]
379
+
380
+ return {
381
+ "success": True,
382
+ "expired_keys": expired_keys,
383
+ "count": len(expired_keys),
384
+ "remaining_entries": len(self.cache),
385
+ }
386
+
387
+ def _get_stats(self) -> Dict[str, Any]:
388
+ """Get cache statistics."""
389
+ current_time = time.time()
390
+ active_entries = sum(
391
+ 1 for entry in self.cache.values() if entry["expires_at"] > current_time
392
+ )
393
+
394
+ total_cost = sum(self.cost_metrics.get(key, 0) for key in self.cache.keys())
395
+
396
+ return {
397
+ "success": True,
398
+ "stats": {
399
+ "total_entries": len(self.cache),
400
+ "active_entries": active_entries,
401
+ "expired_entries": len(self.cache) - active_entries,
402
+ "semantic_tags": len(self.semantic_index),
403
+ "total_access_count": sum(self.access_patterns.values()),
404
+ "estimated_cost_saved": total_cost,
405
+ "hit_rate": self._calculate_hit_rate(),
406
+ },
407
+ }
408
+
409
+ def _calculate_hit_rate(self) -> float:
410
+ """Calculate cache hit rate."""
411
+ total_accesses = sum(self.access_patterns.values())
412
+ if total_accesses == 0:
413
+ return 0.0
414
+
415
+ hits = sum(entry["access_count"] for entry in self.cache.values())
416
+ return hits / total_accesses if total_accesses > 0 else 0.0
417
+
418
+
419
+ @register_node()
420
+ class MCPAgentNode(SelfOrganizingAgentNode):
421
+ """
422
+ Self-organizing agent enhanced with MCP (Model Context Protocol) integration.
423
+
424
+ This node extends SelfOrganizingAgentNode with the ability to interact with external
425
+ tools and services through MCP servers. It implements intelligent caching of tool
426
+ results and enables tool capability sharing across agent teams, making it ideal for
427
+ agents that need to access databases, APIs, file systems, or other external resources.
428
+
429
+ Design Philosophy:
430
+ MCPAgentNode bridges the gap between the agent's reasoning capabilities and
431
+ external world interactions. It treats tools as extensions of the agent's
432
+ capabilities, intelligently deciding when to use tools versus cached results
433
+ or peer agent knowledge. The node promotes tool reuse and cost-conscious
434
+ execution while maintaining the self-organizing behaviors of its parent class.
435
+
436
+ Upstream Dependencies:
437
+ - QueryAnalysisNode: Provides tool requirements analysis
438
+ - TeamFormationNode: Assigns agents based on tool capabilities
439
+ - OrchestrationManagerNode: Supplies MCP server configurations
440
+ - IntelligentCacheNode: Provides cache for tool results
441
+
442
+ Downstream Consumers:
443
+ - IntelligentCacheNode: Receives tool call results for caching
444
+ - SharedMemoryPoolNode: Shares tool discoveries with other agents
445
+ - SolutionEvaluatorNode: Uses tool results in solution assessment
446
+ - Other MCPAgentNodes: Benefit from shared tool knowledge
447
+
448
+ Configuration:
449
+ Requires MCP server configurations specifying how to connect to external
450
+ tools. Can be configured with tool preferences, cost awareness levels,
451
+ and cache integration settings. Inherits all configuration from
452
+ SelfOrganizingAgentNode.
453
+
454
+ Implementation Details:
455
+ - Maintains persistent connections to MCP servers
456
+ - Tracks tool call history for optimization
457
+ - Implements cost-aware tool selection
458
+ - Checks cache before making expensive tool calls
459
+ - Shares tool results through intelligent caching
460
+ - Adapts tool usage based on team feedback
461
+
462
+ Error Handling:
463
+ - Gracefully handles MCP server connection failures
464
+ - Falls back to cached results when tools unavailable
465
+ - Reports tool errors without failing the entire task
466
+ - Retries failed tool calls with exponential backoff
467
+
468
+ Side Effects:
469
+ - Establishes connections to external MCP servers
470
+ - Makes external tool calls that may have side effects
471
+ - Updates cache with tool call results
472
+ - Modifies internal tool usage statistics
473
+
474
+ Examples:
475
+ >>> # Create an MCP-enhanced agent
476
+ >>> agent = MCPAgentNode()
477
+ >>>
478
+ >>> # Test basic structure
479
+ >>> params = agent.get_parameters()
480
+ >>> assert "agent_id" in params
481
+ >>> assert "capabilities" in params
482
+ >>> assert "task" in params
483
+ """
484
+
485
+ def __init__(self):
486
+ super().__init__()
487
+ self.mcp_clients = {}
488
+ self.tool_registry = {}
489
+ self.call_history = deque(maxlen=100)
490
+
491
+ def get_parameters(self) -> Dict[str, NodeParameter]:
492
+ params = super().get_parameters()
493
+
494
+ params.update(
495
+ {
496
+ "mcp_servers": NodeParameter(
497
+ name="mcp_servers",
498
+ type=list,
499
+ required=False,
500
+ default=[],
501
+ description="List of MCP server configurations",
502
+ ),
503
+ "cache_node_id": NodeParameter(
504
+ name="cache_node_id",
505
+ type=str,
506
+ required=False,
507
+ description="ID of cache node for preventing repeated calls",
508
+ ),
509
+ "tool_preferences": NodeParameter(
510
+ name="tool_preferences",
511
+ type=dict,
512
+ required=False,
513
+ default={},
514
+ description="Agent's preferences for tool usage",
515
+ ),
516
+ "cost_awareness": NodeParameter(
517
+ name="cost_awareness",
518
+ type=float,
519
+ required=False,
520
+ default=0.7,
521
+ description="How cost-conscious the agent is (0-1)",
522
+ ),
523
+ }
524
+ )
525
+
526
+ return params
527
+
528
+ def run(self, **kwargs) -> Dict[str, Any]:
529
+ """Execute MCP-enhanced self-organizing agent."""
530
+ # Set up MCP servers
531
+ mcp_servers = kwargs.get("mcp_servers", [])
532
+ cache_node_id = kwargs.get("cache_node_id")
533
+
534
+ if mcp_servers:
535
+ self._setup_mcp_clients(mcp_servers)
536
+
537
+ # Enhance task with MCP tool awareness
538
+ task = kwargs.get("task", "")
539
+ if task and self.tool_registry:
540
+ enhanced_task = self._enhance_task_with_tools(task, kwargs)
541
+ kwargs["task"] = enhanced_task
542
+
543
+ # Add MCP context to system prompt
544
+ mcp_context = self._build_mcp_context()
545
+ original_prompt = kwargs.get("system_prompt", "")
546
+ kwargs["system_prompt"] = f"{original_prompt}\n\n{mcp_context}"
547
+
548
+ # Execute base self-organizing agent
549
+ result = super().run(**kwargs)
550
+
551
+ # Process any tool calls in the response
552
+ if result.get("success") and "content" in result:
553
+ tool_results = self._process_tool_calls(result["content"], cache_node_id)
554
+ if tool_results:
555
+ result["mcp_tool_results"] = tool_results
556
+
557
+ return result
558
+
559
+ def _setup_mcp_clients(self, servers: List[Dict]):
560
+ """Set up MCP clients for configured servers.
561
+
562
+ NOTE: MCP is now a built-in capability of LLM agents. This method
563
+ is deprecated and should be replaced with LLM agents that have
564
+ MCP servers configured directly.
565
+ """
566
+ # TODO: Update this orchestrator to use LLM agents with MCP capabilities
567
+ # For now, we'll just register the servers without creating clients
568
+ for server_config in servers:
569
+ server_name = server_config.get("name", "unknown")
570
+ try:
571
+ # Instead of creating MCPClient nodes, we now configure LLM agents
572
+ # with MCP server information
573
+ self.mcp_clients[server_name] = {
574
+ "config": server_config,
575
+ "tools": [], # Tools will be discovered by LLM agents
576
+ }
577
+
578
+ # Tool registry will be populated by LLM agents during execution
579
+ self.logger.info(f"Registered MCP server: {server_name}")
580
+
581
+ except Exception as e:
582
+ print(f"Failed to register MCP server {server_name}: {e}")
583
+
584
+ def _enhance_task_with_tools(self, task: str, kwargs: Dict) -> str:
585
+ """Enhance task description with available tools."""
586
+ list(self.tool_registry.keys())
587
+
588
+ enhanced = f"{task}\n\nAvailable MCP Tools:\n"
589
+ for tool_name, tool_info in self.tool_registry.items():
590
+ enhanced += f"- {tool_name}: {tool_info['description']}\n"
591
+
592
+ enhanced += (
593
+ "\nYou can use these tools by including function calls in your response."
594
+ )
595
+ enhanced += "\nBefore using any tool, check if similar information is already available to avoid unnecessary calls."
596
+
597
+ return enhanced
598
+
599
+ def _build_mcp_context(self) -> str:
600
+ """Build context about MCP capabilities."""
601
+ if not self.tool_registry:
602
+ return ""
603
+
604
+ context = "MCP Integration Context:\n"
605
+ context += f"You have access to {len(self.tool_registry)} external tools through MCP servers.\n"
606
+ context += (
607
+ "Always check for cached results before making external tool calls.\n"
608
+ )
609
+ context += (
610
+ "Share interesting tool results with the team through shared memory.\n"
611
+ )
612
+
613
+ return context
614
+
615
+ def _process_tool_calls(
616
+ self, content: str, cache_node_id: Optional[str]
617
+ ) -> List[Dict]:
618
+ """Process any tool calls mentioned in the agent's response."""
619
+ tool_results = []
620
+
621
+ # Simple pattern matching for tool calls
622
+ # In a real implementation, this would be more sophisticated
623
+ for tool_name in self.tool_registry.keys():
624
+ if tool_name in content.lower():
625
+ # Check cache first
626
+ cache_result = None
627
+ if cache_node_id:
628
+ cache_result = self._check_cache_for_tool(tool_name, cache_node_id)
629
+
630
+ if cache_result and cache_result.get("hit"):
631
+ tool_results.append(
632
+ {
633
+ "tool": tool_name,
634
+ "result": cache_result["data"],
635
+ "source": "cache",
636
+ "cost": 0,
637
+ }
638
+ )
639
+ else:
640
+ # Execute tool call
641
+ result = self._execute_tool_call(tool_name, {})
642
+ if result.get("success"):
643
+ tool_results.append(
644
+ {
645
+ "tool": tool_name,
646
+ "result": result["result"],
647
+ "source": "mcp",
648
+ "cost": result.get("cost", 0.1),
649
+ }
650
+ )
651
+
652
+ # Cache the result
653
+ if cache_node_id:
654
+ self._cache_tool_result(tool_name, result, cache_node_id)
655
+
656
+ return tool_results
657
+
658
+ def _check_cache_for_tool(
659
+ self, tool_name: str, cache_node_id: str
660
+ ) -> Optional[Dict]:
661
+ """Check cache for tool call results."""
662
+ # This would interact with the cache node in a real workflow
663
+ # For now, return None to indicate no cache
664
+ return None
665
+
666
+ def _execute_tool_call(self, tool_name: str, arguments: Dict) -> Dict[str, Any]:
667
+ """Execute a tool call through MCP."""
668
+ tool_info = self.tool_registry.get(tool_name)
669
+ if not tool_info:
670
+ return {"success": False, "error": f"Tool {tool_name} not found"}
671
+
672
+ server_name = tool_info["server"]
673
+ server_info = self.mcp_clients.get(server_name)
674
+ if not server_info:
675
+ return {"success": False, "error": f"Server {server_name} not available"}
676
+
677
+ try:
678
+ client = server_info["client"]
679
+ result = client.run(
680
+ server_config=server_info["config"],
681
+ operation="call_tool",
682
+ tool_name=tool_name,
683
+ tool_arguments=arguments,
684
+ )
685
+
686
+ # Track call history
687
+ self.call_history.append(
688
+ {
689
+ "timestamp": time.time(),
690
+ "tool": tool_name,
691
+ "server": server_name,
692
+ "success": result.get("success", False),
693
+ }
694
+ )
695
+
696
+ return result
697
+
698
+ except Exception as e:
699
+ return {"success": False, "error": str(e)}
700
+
701
+ def _cache_tool_result(self, tool_name: str, result: Dict, cache_node_id: str):
702
+ """Cache tool call result."""
703
+ # This would interact with the cache node in a real workflow
704
+ pass
705
+
706
+
707
+ @register_node()
708
+ class QueryAnalysisNode(Node):
709
+ """
710
+ Analyzes incoming queries to determine the optimal approach for solving them.
711
+
712
+ This node serves as the strategic planning component of the self-organizing agent
713
+ system, analyzing queries to understand their complexity, required capabilities,
714
+ and optimal solution strategies. It acts as the first line of intelligence that
715
+ guides how the entire agent pool should organize itself to solve a problem.
716
+
717
+ Design Philosophy:
718
+ QueryAnalysisNode embodies the principle of "understanding before acting."
719
+ It performs deep query analysis to extract not just what is being asked, but
720
+ why it's being asked, what resources are needed, and how confident we can be
721
+ in different solution approaches. This analysis drives all downstream decisions
722
+ about team formation, tool usage, and iteration strategies.
723
+
724
+ Upstream Dependencies:
725
+ - User interfaces or APIs that submit queries
726
+ - OrchestrationManagerNode: Provides queries for analysis
727
+ - System monitoring components that add context
728
+
729
+ Downstream Consumers:
730
+ - TeamFormationNode: Uses capability requirements for team composition
731
+ - MCPAgentNode: Receives tool requirement analysis
732
+ - ProblemAnalyzerNode: Gets complexity assessment
733
+ - OrchestrationManagerNode: Uses strategy recommendations
734
+ - ConvergenceDetectorNode: Uses confidence estimates
735
+
736
+ Configuration:
737
+ The analyzer uses pattern matching and heuristics that can be extended
738
+ through configuration. Query patterns, capability mappings, and complexity
739
+ scoring can be customized for different domains.
740
+
741
+ Implementation Details:
742
+ - Pattern-based query classification
743
+ - Keyword and semantic analysis for capability extraction
744
+ - Complexity scoring based on multiple factors
745
+ - MCP tool requirement detection
746
+ - Team size and composition recommendations
747
+ - Iteration and confidence estimation
748
+ - Domain-specific analysis when context provided
749
+
750
+ Error Handling:
751
+ - Handles malformed queries gracefully
752
+ - Provides default analysis for unrecognized patterns
753
+ - Never fails - always returns best-effort analysis
754
+ - Logs unusual query patterns for improvement
755
+
756
+ Side Effects:
757
+ - Updates internal pattern statistics
758
+ - May modify query pattern database (if configured)
759
+ - Logs query analysis for system improvement
760
+
761
+ Examples:
762
+ >>> analyzer = QueryAnalysisNode()
763
+ >>>
764
+ >>> # Analyze a complex multi-domain query
765
+ >>> result = analyzer.run(
766
+ ... query="Analyze our Q4 sales data, identify underperforming regions, and create a recovery strategy with timeline",
767
+ ... context={
768
+ ... "domain": "business_strategy",
769
+ ... "urgency": "high",
770
+ ... "deadline": "2024-12-31",
771
+ ... "budget": 50000
772
+ ... },
773
+ ... available_agents=["analyst", "strategist", "planner"],
774
+ ... mcp_servers=[
775
+ ... {"name": "sales_db", "type": "database"},
776
+ ... {"name": "market_api", "type": "api"}
777
+ ... ]
778
+ ... )
779
+ >>> assert result["success"] == True
780
+ >>> assert result["analysis"]["complexity_score"] > 0.5 # Adjusted expectation
781
+ >>> assert "data_analysis" in result["analysis"]["required_capabilities"]
782
+ >>> assert result["analysis"]["mcp_requirements"]["mcp_needed"] == True
783
+ >>> assert result["analysis"]["team_suggestion"]["suggested_size"] >= 3
784
+ >>>
785
+ >>> # Simple query analysis
786
+ >>> simple = analyzer.run(
787
+ ... query="What is the current temperature?",
788
+ ... context={"domain": "weather"}
789
+ ... )
790
+ >>> # Complexity score can vary based on implementation
791
+ >>> assert 0 <= simple["analysis"]["complexity_score"] <= 1
792
+ >>> assert simple["analysis"]["team_suggestion"]["suggested_size"] >= 1
793
+ """
794
+
795
+ def __init__(self):
796
+ super().__init__()
797
+ self.query_patterns = {
798
+ "data_retrieval": {
799
+ "keywords": ["what is", "get", "fetch", "retrieve", "show me"],
800
+ "required_capabilities": ["data_collection", "api_integration"],
801
+ "mcp_likely": True,
802
+ "complexity": 0.3,
803
+ },
804
+ "analysis": {
805
+ "keywords": ["analyze", "compare", "evaluate", "assess"],
806
+ "required_capabilities": ["data_analysis", "critical_thinking"],
807
+ "mcp_likely": False,
808
+ "complexity": 0.6,
809
+ },
810
+ "prediction": {
811
+ "keywords": ["predict", "forecast", "estimate", "project"],
812
+ "required_capabilities": ["machine_learning", "statistical_analysis"],
813
+ "mcp_likely": True,
814
+ "complexity": 0.8,
815
+ },
816
+ "planning": {
817
+ "keywords": ["plan", "strategy", "schedule", "organize"],
818
+ "required_capabilities": ["project_management", "optimization"],
819
+ "mcp_likely": True,
820
+ "complexity": 0.7,
821
+ },
822
+ "research": {
823
+ "keywords": ["research", "investigate", "study", "explore"],
824
+ "required_capabilities": ["research", "synthesis", "critical_analysis"],
825
+ "mcp_likely": True,
826
+ "complexity": 0.9,
827
+ },
828
+ }
829
+
830
+ def get_parameters(self) -> Dict[str, NodeParameter]:
831
+ return {
832
+ "query": NodeParameter(
833
+ name="query",
834
+ type=str,
835
+ required=False,
836
+ description="The query to analyze",
837
+ ),
838
+ "context": NodeParameter(
839
+ name="context",
840
+ type=dict,
841
+ required=False,
842
+ default={},
843
+ description="Additional context about the query",
844
+ ),
845
+ "available_agents": NodeParameter(
846
+ name="available_agents",
847
+ type=list,
848
+ required=False,
849
+ default=[],
850
+ description="List of available agents and their capabilities",
851
+ ),
852
+ "mcp_servers": NodeParameter(
853
+ name="mcp_servers",
854
+ type=list,
855
+ required=False,
856
+ default=[],
857
+ description="Available MCP servers",
858
+ ),
859
+ }
860
+
861
+ def run(self, **kwargs) -> Dict[str, Any]:
862
+ """Analyze query and determine optimal solving approach."""
863
+ query = kwargs.get("query")
864
+ if not query:
865
+ return {
866
+ "success": False,
867
+ "error": "Query parameter is required for analysis",
868
+ }
869
+ context = kwargs.get("context", {})
870
+ available_agents = kwargs.get("available_agents", [])
871
+ mcp_servers = kwargs.get("mcp_servers", [])
872
+
873
+ # Pattern analysis
874
+ pattern_matches = self._analyze_patterns(query)
875
+
876
+ # Complexity assessment
877
+ complexity_score = self._assess_complexity(query, context, pattern_matches)
878
+
879
+ # Required capabilities
880
+ required_capabilities = self._determine_capabilities(pattern_matches, context)
881
+
882
+ # MCP tool requirements
883
+ mcp_analysis = self._analyze_mcp_needs(query, pattern_matches, mcp_servers)
884
+
885
+ # Team composition suggestion
886
+ team_suggestion = self._suggest_team_composition(
887
+ required_capabilities, complexity_score, available_agents
888
+ )
889
+
890
+ # Solution strategy
891
+ strategy = self._determine_strategy(pattern_matches, complexity_score, context)
892
+
893
+ # Confidence and iteration estimates
894
+ estimates = self._estimate_solution_requirements(complexity_score, context)
895
+
896
+ return {
897
+ "success": True,
898
+ "query": query,
899
+ "analysis": {
900
+ "pattern_matches": pattern_matches,
901
+ "complexity_score": complexity_score,
902
+ "required_capabilities": required_capabilities,
903
+ "mcp_requirements": mcp_analysis,
904
+ "team_suggestion": team_suggestion,
905
+ "strategy": strategy,
906
+ "estimates": estimates,
907
+ },
908
+ }
909
+
910
+ def _analyze_patterns(self, query: str) -> Dict[str, Any]:
911
+ """Analyze query against known patterns."""
912
+ query_lower = query.lower()
913
+ matches = {}
914
+
915
+ for pattern_name, pattern_info in self.query_patterns.items():
916
+ score = 0
917
+ matched_keywords = []
918
+
919
+ for keyword in pattern_info["keywords"]:
920
+ if keyword in query_lower:
921
+ score += 1
922
+ matched_keywords.append(keyword)
923
+
924
+ if score > 0:
925
+ matches[pattern_name] = {
926
+ "score": score,
927
+ "matched_keywords": matched_keywords,
928
+ "confidence": score / len(pattern_info["keywords"]),
929
+ }
930
+
931
+ return matches
932
+
933
+ def _assess_complexity(self, query: str, context: Dict, patterns: Dict) -> float:
934
+ """Assess query complexity."""
935
+ base_complexity = 0.5
936
+
937
+ # Pattern-based complexity
938
+ if patterns:
939
+ max_pattern_complexity = max(
940
+ self.query_patterns[pattern]["complexity"]
941
+ for pattern in patterns.keys()
942
+ )
943
+ base_complexity = max(base_complexity, max_pattern_complexity)
944
+
945
+ # Length-based adjustment
946
+ word_count = len(query.split())
947
+ if word_count > 20:
948
+ base_complexity += 0.2
949
+ elif word_count < 5:
950
+ base_complexity -= 0.1
951
+
952
+ # Context-based adjustments
953
+ if context.get("urgency") == "high":
954
+ base_complexity += 0.1
955
+ if context.get("domain") in ["research", "analysis"]:
956
+ base_complexity += 0.2
957
+
958
+ # Multiple patterns increase complexity
959
+ if len(patterns) > 2:
960
+ base_complexity += 0.2
961
+
962
+ return max(0.1, min(1.0, base_complexity))
963
+
964
+ def _determine_capabilities(self, patterns: Dict, context: Dict) -> List[str]:
965
+ """Determine required capabilities."""
966
+ capabilities = set()
967
+
968
+ # Pattern-based capabilities
969
+ for pattern_name in patterns.keys():
970
+ pattern_info = self.query_patterns[pattern_name]
971
+ capabilities.update(pattern_info["required_capabilities"])
972
+
973
+ # Context-based additions
974
+ domain = context.get("domain", "")
975
+ if domain:
976
+ capabilities.add(f"domain_expertise_{domain}")
977
+
978
+ if context.get("urgency") == "high":
979
+ capabilities.add("rapid_execution")
980
+
981
+ return list(capabilities)
982
+
983
+ def _analyze_mcp_needs(self, query: str, patterns: Dict, mcp_servers: List) -> Dict:
984
+ """Analyze MCP tool requirements."""
985
+ mcp_needed = any(
986
+ self.query_patterns[pattern]["mcp_likely"] for pattern in patterns.keys()
987
+ )
988
+
989
+ # Check for specific tool indicators
990
+ tool_indicators = {
991
+ "weather": ["weather", "temperature", "forecast"],
992
+ "financial": ["stock", "price", "market", "financial"],
993
+ "web_search": ["search", "find", "lookup", "information"],
994
+ "calendar": ["schedule", "meeting", "appointment", "calendar"],
995
+ }
996
+
997
+ needed_tools = []
998
+ query_lower = query.lower()
999
+
1000
+ for tool_type, indicators in tool_indicators.items():
1001
+ if any(indicator in query_lower for indicator in indicators):
1002
+ needed_tools.append(tool_type)
1003
+
1004
+ return {
1005
+ "mcp_needed": mcp_needed,
1006
+ "confidence": 0.8 if mcp_needed else 0.2,
1007
+ "needed_tools": needed_tools,
1008
+ "available_servers": len(mcp_servers),
1009
+ }
1010
+
1011
+ def _suggest_team_composition(
1012
+ self, capabilities: List[str], complexity: float, agents: List
1013
+ ) -> Dict:
1014
+ """Suggest optimal team composition."""
1015
+ # Basic team size estimation
1016
+ base_size = max(2, len(capabilities) // 2)
1017
+ complexity_multiplier = 1 + complexity
1018
+ suggested_size = int(base_size * complexity_multiplier)
1019
+
1020
+ return {
1021
+ "suggested_size": min(suggested_size, 8), # Cap at 8 agents
1022
+ "required_capabilities": capabilities,
1023
+ "leadership_needed": complexity > 0.7,
1024
+ "coordination_complexity": (
1025
+ "high" if complexity > 0.8 else "medium" if complexity > 0.5 else "low"
1026
+ ),
1027
+ }
1028
+
1029
+ def _determine_strategy(
1030
+ self, patterns: Dict, complexity: float, context: Dict
1031
+ ) -> Dict:
1032
+ """Determine solution strategy."""
1033
+ if complexity < 0.4:
1034
+ approach = "single_agent"
1035
+ elif complexity < 0.7:
1036
+ approach = "small_team_sequential"
1037
+ else:
1038
+ approach = "large_team_parallel"
1039
+
1040
+ # Pattern-specific strategies
1041
+ strategy_hints = []
1042
+ if "research" in patterns:
1043
+ strategy_hints.append("comprehensive_research_phase")
1044
+ if "analysis" in patterns:
1045
+ strategy_hints.append("iterative_analysis_refinement")
1046
+ if "planning" in patterns:
1047
+ strategy_hints.append("constraint_based_optimization")
1048
+
1049
+ return {
1050
+ "approach": approach,
1051
+ "strategy_hints": strategy_hints,
1052
+ "parallel_execution": complexity > 0.6,
1053
+ "iterative_refinement": complexity > 0.5,
1054
+ }
1055
+
1056
+ def _estimate_solution_requirements(self, complexity: float, context: Dict) -> Dict:
1057
+ """Estimate solution requirements."""
1058
+ # Base estimates
1059
+ estimated_time = 30 + int(complexity * 120) # 30-150 minutes
1060
+ max_iterations = max(1, int(complexity * 4)) # 1-4 iterations
1061
+ confidence_threshold = 0.9 - (
1062
+ complexity * 0.2
1063
+ ) # Higher complexity = lower initial threshold
1064
+
1065
+ # Context adjustments
1066
+ if context.get("urgency") == "high":
1067
+ estimated_time = int(estimated_time * 0.7)
1068
+ confidence_threshold -= 0.1
1069
+
1070
+ if context.get("deadline"):
1071
+ # In real implementation, would parse deadline and adjust
1072
+ pass
1073
+
1074
+ return {
1075
+ "estimated_time_minutes": estimated_time,
1076
+ "max_iterations": max_iterations,
1077
+ "confidence_threshold": max(0.6, confidence_threshold),
1078
+ "early_termination_possible": complexity < 0.5,
1079
+ }
1080
+
1081
+
1082
+ @register_node()
1083
+ class OrchestrationManagerNode(Node):
1084
+ """
1085
+ Central orchestration manager that coordinates the entire self-organizing
1086
+ agent workflow with MCP integration and intelligent caching.
1087
+
1088
+ This node represents the pinnacle of the self-organizing agent architecture,
1089
+ orchestrating all components to create a cohesive problem-solving system. It
1090
+ manages the complete lifecycle from query analysis through solution delivery,
1091
+ ensuring efficient resource utilization and high-quality outcomes.
1092
+
1093
+ Design Philosophy:
1094
+ OrchestrationManagerNode embodies the concept of emergent intelligence through
1095
+ orchestrated autonomy. While agents self-organize at the tactical level, this
1096
+ node provides strategic coordination, ensuring all pieces work together towards
1097
+ the common goal. It balances central oversight with distributed execution,
1098
+ enabling scalable and robust problem-solving.
1099
+
1100
+ Upstream Dependencies:
1101
+ - External APIs or user interfaces submitting queries
1102
+ - System configuration providers
1103
+ - Resource management systems
1104
+
1105
+ Downstream Consumers:
1106
+ - QueryAnalysisNode: Receives queries for analysis
1107
+ - IntelligentCacheNode: Managed for cross-agent caching
1108
+ - AgentPoolManagerNode: Coordinates agent creation
1109
+ - TeamFormationNode: Directs team composition
1110
+ - MCPAgentNode: Provides MCP configurations
1111
+ - ConvergenceDetectorNode: Monitors solution progress
1112
+ - All other orchestration components
1113
+
1114
+ Configuration:
1115
+ Highly configurable with parameters for agent pool size, MCP servers,
1116
+ iteration limits, quality thresholds, time constraints, and caching
1117
+ policies. Can be tuned for different problem domains and resource
1118
+ constraints.
1119
+
1120
+ Implementation Details:
1121
+ - Multi-phase execution pipeline
1122
+ - Asynchronous agent coordination
1123
+ - Real-time progress monitoring
1124
+ - Dynamic resource allocation
1125
+ - Intelligent retry mechanisms
1126
+ - Performance metric collection
1127
+ - Solution quality assurance
1128
+ - Graceful degradation under failures
1129
+
1130
+ Error Handling:
1131
+ - Comprehensive error recovery strategies
1132
+ - Partial result aggregation on failures
1133
+ - Timeout management with graceful termination
1134
+ - Agent failure isolation and recovery
1135
+ - MCP server failover support
1136
+
1137
+ Side Effects:
1138
+ - Creates and manages agent pool lifecycle
1139
+ - Establishes MCP server connections
1140
+ - Modifies cache state across the system
1141
+ - Generates extensive logging for debugging
1142
+ - May spawn background monitoring processes
1143
+
1144
+ Examples:
1145
+ >>> # Create orchestration manager
1146
+ >>> orchestrator = OrchestrationManagerNode()
1147
+ >>>
1148
+ >>> # Test basic structure
1149
+ >>> params = orchestrator.get_parameters()
1150
+ >>> assert "query" in params
1151
+ >>> assert "agent_pool_size" in params
1152
+ >>> assert "max_iterations" in params
1153
+ """
1154
+
1155
+ def __init__(self):
1156
+ super().__init__()
1157
+ self.session_id = str(uuid.uuid4())
1158
+ self.orchestration_history = deque(maxlen=50)
1159
+
1160
+ def get_parameters(self) -> Dict[str, NodeParameter]:
1161
+ return {
1162
+ "query": NodeParameter(
1163
+ name="query",
1164
+ type=str,
1165
+ required=False,
1166
+ description="The main query or problem to solve",
1167
+ ),
1168
+ "context": NodeParameter(
1169
+ name="context",
1170
+ type=dict,
1171
+ required=False,
1172
+ default={},
1173
+ description="Additional context for the query",
1174
+ ),
1175
+ "agent_pool_size": NodeParameter(
1176
+ name="agent_pool_size",
1177
+ type=int,
1178
+ required=False,
1179
+ default=10,
1180
+ description="Number of agents in the pool",
1181
+ ),
1182
+ "mcp_servers": NodeParameter(
1183
+ name="mcp_servers",
1184
+ type=list,
1185
+ required=False,
1186
+ default=[],
1187
+ description="MCP server configurations",
1188
+ ),
1189
+ "max_iterations": NodeParameter(
1190
+ name="max_iterations",
1191
+ type=int,
1192
+ required=False,
1193
+ default=3,
1194
+ description="Maximum number of solution iterations",
1195
+ ),
1196
+ "quality_threshold": NodeParameter(
1197
+ name="quality_threshold",
1198
+ type=float,
1199
+ required=False,
1200
+ default=0.8,
1201
+ description="Quality threshold for solution acceptance",
1202
+ ),
1203
+ "time_limit_minutes": NodeParameter(
1204
+ name="time_limit_minutes",
1205
+ type=int,
1206
+ required=False,
1207
+ default=60,
1208
+ description="Maximum time limit for solution",
1209
+ ),
1210
+ "enable_caching": NodeParameter(
1211
+ name="enable_caching",
1212
+ type=bool,
1213
+ required=False,
1214
+ default=True,
1215
+ description="Enable intelligent caching",
1216
+ ),
1217
+ }
1218
+
1219
+ def run(self, **kwargs) -> Dict[str, Any]:
1220
+ """Execute complete orchestrated solution workflow."""
1221
+ start_time = time.time()
1222
+
1223
+ query = kwargs.get("query")
1224
+ if not query:
1225
+ return {
1226
+ "success": False,
1227
+ "error": "Query parameter is required for orchestration",
1228
+ }
1229
+ context = kwargs.get("context", {})
1230
+ agent_pool_size = kwargs.get("agent_pool_size", 10)
1231
+ mcp_servers = kwargs.get("mcp_servers", [])
1232
+ max_iterations = kwargs.get("max_iterations", 3)
1233
+ quality_threshold = kwargs.get("quality_threshold", 0.8)
1234
+ time_limit = kwargs.get("time_limit_minutes", 60) * 60 # Convert to seconds
1235
+ enable_caching = kwargs.get("enable_caching", True)
1236
+
1237
+ # Phase 1: Query Analysis
1238
+ print("🔍 Phase 1: Analyzing query...")
1239
+ query_analysis = self._analyze_query(query, context, mcp_servers)
1240
+
1241
+ # Phase 2: Setup Infrastructure
1242
+ print("🏗️ Phase 2: Setting up infrastructure...")
1243
+ infrastructure = self._setup_infrastructure(
1244
+ agent_pool_size, mcp_servers, enable_caching
1245
+ )
1246
+
1247
+ # Phase 3: Agent Pool Creation
1248
+ print("🤖 Phase 3: Creating specialized agent pool...")
1249
+ agent_pool = self._create_agent_pool(
1250
+ query_analysis, infrastructure, mcp_servers
1251
+ )
1252
+
1253
+ # Phase 4: Iterative Solution Development
1254
+ print("💡 Phase 4: Beginning solution development...")
1255
+ solution_history = []
1256
+ final_solution = None
1257
+
1258
+ for iteration in range(max_iterations):
1259
+ iteration_start = time.time()
1260
+
1261
+ # Check time limit
1262
+ if time.time() - start_time > time_limit:
1263
+ print(f"⏰ Time limit reached, stopping at iteration {iteration}")
1264
+ break
1265
+
1266
+ print(f" 📍 Iteration {iteration + 1}/{max_iterations}")
1267
+
1268
+ # Team Formation
1269
+ team_formation_result = self._form_team(
1270
+ query_analysis, agent_pool, iteration
1271
+ )
1272
+
1273
+ # Collaborative Solution
1274
+ solution_result = self._collaborative_solve(
1275
+ query, context, team_formation_result, infrastructure, iteration
1276
+ )
1277
+
1278
+ # Evaluation
1279
+ evaluation_result = self._evaluate_solution(
1280
+ solution_result, query_analysis, quality_threshold, iteration
1281
+ )
1282
+
1283
+ solution_history.append(
1284
+ {
1285
+ "iteration": iteration + 1,
1286
+ "team": team_formation_result["team"],
1287
+ "solution": solution_result,
1288
+ "evaluation": evaluation_result,
1289
+ "duration": time.time() - iteration_start,
1290
+ }
1291
+ )
1292
+
1293
+ # Check convergence
1294
+ if evaluation_result["meets_threshold"]:
1295
+ print(
1296
+ f" ✅ Quality threshold met! Score: {evaluation_result['overall_score']:.3f}"
1297
+ )
1298
+ final_solution = solution_result
1299
+ break
1300
+ elif not evaluation_result["needs_iteration"]:
1301
+ print(" 🛑 No improvement possible, stopping iteration")
1302
+ final_solution = solution_result
1303
+ break
1304
+ else:
1305
+ print(
1306
+ f" 🔄 Quality: {evaluation_result['overall_score']:.3f}, continuing..."
1307
+ )
1308
+
1309
+ # Phase 5: Final Processing
1310
+ print("📊 Phase 5: Finalizing results...")
1311
+ final_result = self._finalize_results(
1312
+ query,
1313
+ final_solution or solution_history[-1]["solution"],
1314
+ solution_history,
1315
+ time.time() - start_time,
1316
+ infrastructure,
1317
+ )
1318
+
1319
+ # Record orchestration
1320
+ self.orchestration_history.append(
1321
+ {
1322
+ "session_id": self.session_id,
1323
+ "timestamp": start_time,
1324
+ "query": query,
1325
+ "iterations": len(solution_history),
1326
+ "final_score": final_result.get("quality_score", 0),
1327
+ "total_time": time.time() - start_time,
1328
+ }
1329
+ )
1330
+
1331
+ return final_result
1332
+
1333
+ def _analyze_query(self, query: str, context: Dict, mcp_servers: List) -> Dict:
1334
+ """Analyze the incoming query."""
1335
+ analyzer = QueryAnalysisNode()
1336
+ return analyzer.run(query=query, context=context, mcp_servers=mcp_servers)
1337
+
1338
+ def _setup_infrastructure(
1339
+ self, pool_size: int, mcp_servers: List, enable_caching: bool
1340
+ ) -> Dict:
1341
+ """Set up core infrastructure components."""
1342
+ infrastructure = {}
1343
+
1344
+ # Intelligent Cache
1345
+ if enable_caching:
1346
+ infrastructure["cache"] = IntelligentCacheNode()
1347
+
1348
+ # Shared Memory Pools
1349
+ infrastructure["problem_memory"] = SharedMemoryPoolNode()
1350
+ infrastructure["solution_memory"] = SharedMemoryPoolNode()
1351
+ infrastructure["mcp_memory"] = SharedMemoryPoolNode()
1352
+
1353
+ # Agent Pool Manager
1354
+ infrastructure["pool_manager"] = AgentPoolManagerNode()
1355
+
1356
+ # Team Formation Engine
1357
+ infrastructure["team_formation"] = TeamFormationNode()
1358
+
1359
+ # Solution Evaluator
1360
+ infrastructure["evaluator"] = SolutionEvaluatorNode()
1361
+
1362
+ return infrastructure
1363
+
1364
+ def _create_agent_pool(
1365
+ self, query_analysis: Dict, infrastructure: Dict, mcp_servers: List
1366
+ ) -> List[Dict]:
1367
+ """Create specialized agent pool based on query analysis."""
1368
+ analysis = query_analysis["analysis"]
1369
+ analysis["required_capabilities"]
1370
+ team_suggestion = analysis["team_suggestion"]
1371
+
1372
+ pool_manager = infrastructure["pool_manager"]
1373
+ agent_pool = []
1374
+
1375
+ # Create agents with diverse specializations
1376
+ agent_specializations = [
1377
+ {
1378
+ "capabilities": ["research", "web_search", "information_gathering"],
1379
+ "role": "researcher",
1380
+ },
1381
+ {
1382
+ "capabilities": [
1383
+ "data_analysis",
1384
+ "statistical_analysis",
1385
+ "pattern_recognition",
1386
+ ],
1387
+ "role": "analyst",
1388
+ },
1389
+ {
1390
+ "capabilities": [
1391
+ "machine_learning",
1392
+ "predictive_modeling",
1393
+ "ai_expertise",
1394
+ ],
1395
+ "role": "ml_specialist",
1396
+ },
1397
+ {
1398
+ "capabilities": [
1399
+ "domain_expertise",
1400
+ "subject_matter_expert",
1401
+ "validation",
1402
+ ],
1403
+ "role": "domain_expert",
1404
+ },
1405
+ {
1406
+ "capabilities": ["synthesis", "writing", "communication", "reporting"],
1407
+ "role": "synthesizer",
1408
+ },
1409
+ {
1410
+ "capabilities": ["project_management", "coordination", "planning"],
1411
+ "role": "coordinator",
1412
+ },
1413
+ {
1414
+ "capabilities": ["api_integration", "mcp_tools", "external_systems"],
1415
+ "role": "integration_specialist",
1416
+ },
1417
+ {
1418
+ "capabilities": ["quality_assurance", "validation", "peer_review"],
1419
+ "role": "reviewer",
1420
+ },
1421
+ {
1422
+ "capabilities": ["optimization", "efficiency", "performance"],
1423
+ "role": "optimizer",
1424
+ },
1425
+ {
1426
+ "capabilities": ["creative_thinking", "innovation", "brainstorming"],
1427
+ "role": "innovator",
1428
+ },
1429
+ ]
1430
+
1431
+ suggested_size = team_suggestion["suggested_size"]
1432
+
1433
+ for i in range(suggested_size):
1434
+ spec = agent_specializations[i % len(agent_specializations)]
1435
+
1436
+ # Register agent with pool manager
1437
+ registration = pool_manager.run(
1438
+ action="register",
1439
+ agent_id=f"agent_{spec['role']}_{i:03d}",
1440
+ capabilities=spec["capabilities"],
1441
+ metadata={
1442
+ "role": spec["role"],
1443
+ "mcp_enabled": True,
1444
+ "performance_history": {"success_rate": 0.8 + (i % 3) * 0.05},
1445
+ },
1446
+ )
1447
+
1448
+ if registration["success"]:
1449
+ agent_info = {
1450
+ "id": registration["agent_id"],
1451
+ "capabilities": spec["capabilities"],
1452
+ "role": spec["role"],
1453
+ "mcp_servers": mcp_servers,
1454
+ "performance": 0.8 + (i % 3) * 0.05,
1455
+ }
1456
+ agent_pool.append(agent_info)
1457
+
1458
+ return agent_pool
1459
+
1460
+ def _form_team(
1461
+ self, query_analysis: Dict, agent_pool: List, iteration: int
1462
+ ) -> Dict:
1463
+ """Form optimal team for current iteration."""
1464
+ analysis = query_analysis["analysis"]
1465
+
1466
+ # Adjust strategy based on iteration
1467
+ strategies = [
1468
+ "capability_matching",
1469
+ "swarm_based",
1470
+ "market_based",
1471
+ "hierarchical",
1472
+ ]
1473
+ strategy = strategies[iteration % len(strategies)]
1474
+
1475
+ team_formation = TeamFormationNode()
1476
+
1477
+ return team_formation.run(
1478
+ problem_analysis=analysis,
1479
+ available_agents=agent_pool,
1480
+ formation_strategy=strategy,
1481
+ optimization_rounds=3,
1482
+ )
1483
+
1484
+ def _collaborative_solve(
1485
+ self,
1486
+ query: str,
1487
+ context: Dict,
1488
+ team_result: Dict,
1489
+ infrastructure: Dict,
1490
+ iteration: int,
1491
+ ) -> Dict:
1492
+ """Execute collaborative problem solving."""
1493
+ team = team_result["team"]
1494
+ solution_memory = infrastructure["solution_memory"]
1495
+ cache = infrastructure.get("cache")
1496
+
1497
+ # Phase 1: Information Gathering
1498
+ information_results = []
1499
+ for agent in team:
1500
+ if any(
1501
+ cap in agent["capabilities"]
1502
+ for cap in ["research", "data_collection", "api_integration"]
1503
+ ):
1504
+ # Simulate agent working
1505
+ agent_result = self._simulate_agent_work(
1506
+ agent, f"Gather information for: {query}", cache
1507
+ )
1508
+ information_results.append(agent_result)
1509
+
1510
+ # Store in memory
1511
+ solution_memory.run(
1512
+ action="write",
1513
+ agent_id=agent["id"],
1514
+ content=agent_result,
1515
+ tags=["information", "gathering"],
1516
+ segment="research",
1517
+ )
1518
+
1519
+ # Phase 2: Analysis and Processing
1520
+ analysis_results = []
1521
+ for agent in team:
1522
+ if any(
1523
+ cap in agent["capabilities"]
1524
+ for cap in ["analysis", "machine_learning", "processing"]
1525
+ ):
1526
+ # Get previous information
1527
+ memory_result = solution_memory.run(
1528
+ action="read",
1529
+ agent_id=agent["id"],
1530
+ attention_filter={"tags": ["information"], "threshold": 0.3},
1531
+ )
1532
+
1533
+ context_info = memory_result.get("memories", [])
1534
+ agent_result = self._simulate_agent_work(
1535
+ agent, f"Analyze for: {query}", cache, context_info
1536
+ )
1537
+ analysis_results.append(agent_result)
1538
+
1539
+ solution_memory.run(
1540
+ action="write",
1541
+ agent_id=agent["id"],
1542
+ content=agent_result,
1543
+ tags=["analysis", "processing"],
1544
+ segment="analysis",
1545
+ )
1546
+
1547
+ # Phase 3: Synthesis and Solution
1548
+ synthesis_results = []
1549
+ for agent in team:
1550
+ if any(
1551
+ cap in agent["capabilities"]
1552
+ for cap in ["synthesis", "writing", "coordination"]
1553
+ ):
1554
+ # Get all previous work
1555
+ memory_result = solution_memory.run(
1556
+ action="read",
1557
+ agent_id=agent["id"],
1558
+ attention_filter={"threshold": 0.2},
1559
+ )
1560
+
1561
+ context_info = memory_result.get("memories", [])
1562
+ agent_result = self._simulate_agent_work(
1563
+ agent, f"Synthesize solution for: {query}", cache, context_info
1564
+ )
1565
+ synthesis_results.append(agent_result)
1566
+
1567
+ return {
1568
+ "query": query,
1569
+ "iteration": iteration + 1,
1570
+ "team_size": len(team),
1571
+ "information_gathering": information_results,
1572
+ "analysis_processing": analysis_results,
1573
+ "synthesis": synthesis_results,
1574
+ "final_solution": synthesis_results[0] if synthesis_results else {},
1575
+ "confidence": self._calculate_solution_confidence(
1576
+ information_results, analysis_results, synthesis_results
1577
+ ),
1578
+ }
1579
+
1580
+ def _simulate_agent_work(
1581
+ self,
1582
+ agent: Dict,
1583
+ task: str,
1584
+ cache: Optional[IntelligentCacheNode],
1585
+ context_info: List = None,
1586
+ ) -> Dict:
1587
+ """Simulate agent performing work (with caching)."""
1588
+ agent_id = agent["id"]
1589
+ capabilities = agent["capabilities"]
1590
+
1591
+ # Check cache for similar work
1592
+ cache_key = f"{agent_id}_{hashlib.md5(task.encode()).hexdigest()[:8]}"
1593
+
1594
+ if cache:
1595
+ cached_result = cache.run(
1596
+ action="get", cache_key=cache_key, query=task, similarity_threshold=0.7
1597
+ )
1598
+
1599
+ if cached_result.get("hit"):
1600
+ return {
1601
+ "agent_id": agent_id,
1602
+ "task": task,
1603
+ "result": cached_result["data"],
1604
+ "source": "cache",
1605
+ "confidence": 0.9,
1606
+ "cached": True,
1607
+ }
1608
+
1609
+ # Simulate actual work
1610
+ result = {
1611
+ "agent_id": agent_id,
1612
+ "role": agent["role"],
1613
+ "task": task,
1614
+ "capabilities_used": capabilities[:2], # Use first 2 capabilities
1615
+ "result": f"Mock result from {agent['role']} for task: {task}",
1616
+ "insights": [
1617
+ f"Insight 1 from {agent['role']}",
1618
+ f"Insight 2 based on {capabilities[0] if capabilities else 'general'} capability",
1619
+ ],
1620
+ "confidence": 0.7 + (hash(agent_id) % 20) / 100, # 0.7-0.89
1621
+ "context_used": len(context_info) if context_info else 0,
1622
+ "cached": False,
1623
+ }
1624
+
1625
+ # Cache the result
1626
+ if cache:
1627
+ cache.run(
1628
+ action="cache",
1629
+ cache_key=cache_key,
1630
+ data=result,
1631
+ metadata={
1632
+ "agent_id": agent_id,
1633
+ "task_type": "agent_work",
1634
+ "semantic_tags": capabilities + ["agent_result"],
1635
+ "cost": 0.1,
1636
+ },
1637
+ ttl=3600,
1638
+ )
1639
+
1640
+ return result
1641
+
1642
+ def _calculate_solution_confidence(
1643
+ self, info_results: List, analysis_results: List, synthesis_results: List
1644
+ ) -> float:
1645
+ """Calculate overall solution confidence."""
1646
+ all_results = info_results + analysis_results + synthesis_results
1647
+ if not all_results:
1648
+ return 0.0
1649
+
1650
+ confidences = [r.get("confidence", 0.5) for r in all_results]
1651
+ return sum(confidences) / len(confidences)
1652
+
1653
+ def _evaluate_solution(
1654
+ self,
1655
+ solution: Dict,
1656
+ query_analysis: Dict,
1657
+ quality_threshold: float,
1658
+ iteration: int,
1659
+ ) -> Dict:
1660
+ """Evaluate solution quality."""
1661
+ evaluator = SolutionEvaluatorNode()
1662
+
1663
+ return evaluator.run(
1664
+ solution=solution["final_solution"],
1665
+ problem_requirements={
1666
+ "quality_threshold": quality_threshold,
1667
+ "required_outputs": ["analysis", "recommendations"],
1668
+ "time_estimate": 60,
1669
+ },
1670
+ team_performance={"collaboration_score": 0.8, "time_taken": 45},
1671
+ iteration_count=iteration,
1672
+ )
1673
+
1674
+ def _finalize_results(
1675
+ self,
1676
+ query: str,
1677
+ final_solution: Dict,
1678
+ history: List,
1679
+ total_time: float,
1680
+ infrastructure: Dict,
1681
+ ) -> Dict:
1682
+ """Finalize and format results."""
1683
+ # Get cache statistics
1684
+ cache_stats = {}
1685
+ if "cache" in infrastructure:
1686
+ cache_result = infrastructure["cache"].run(action="stats")
1687
+ if cache_result["success"]:
1688
+ cache_stats = cache_result["stats"]
1689
+
1690
+ return {
1691
+ "success": True,
1692
+ "query": query,
1693
+ "session_id": self.session_id,
1694
+ "final_solution": final_solution,
1695
+ "quality_score": final_solution.get("confidence", 0.0),
1696
+ "iterations_completed": len(history),
1697
+ "total_time_seconds": total_time,
1698
+ "solution_history": history,
1699
+ "performance_metrics": {
1700
+ "cache_hit_rate": cache_stats.get("hit_rate", 0.0),
1701
+ "external_calls_saved": cache_stats.get("estimated_cost_saved", 0.0),
1702
+ "agent_utilization": (
1703
+ len(
1704
+ set(
1705
+ result["team"][0]["id"]
1706
+ for result in history
1707
+ if result.get("team") and len(result["team"]) > 0
1708
+ )
1709
+ )
1710
+ / max(len(history), 1)
1711
+ if history
1712
+ else 0.0
1713
+ ),
1714
+ },
1715
+ "metadata": {
1716
+ "infrastructure_used": list(infrastructure.keys()),
1717
+ "session_timestamp": datetime.now().isoformat(),
1718
+ },
1719
+ }
1720
+
1721
+
1722
+ @register_node()
1723
+ class ConvergenceDetectorNode(Node):
1724
+ """
1725
+ Sophisticated convergence detection that determines when a solution
1726
+ is satisfactory and iteration should terminate.
1727
+
1728
+ This node implements intelligent stopping criteria for iterative problem-solving
1729
+ processes, using multiple signals to determine when further iterations are unlikely
1730
+ to yield meaningful improvements. It prevents both premature termination and
1731
+ wasteful over-iteration, optimizing the balance between solution quality and
1732
+ resource utilization.
1733
+
1734
+ Design Philosophy:
1735
+ The ConvergenceDetectorNode embodies the principle of "knowing when to stop."
1736
+ It uses a multi-signal approach inspired by optimization theory, combining
1737
+ absolute quality thresholds with trend analysis, consensus measures, and
1738
+ resource awareness. This creates a nuanced decision framework that adapts
1739
+ to different problem types and solution dynamics.
1740
+
1741
+ Upstream Dependencies:
1742
+ - OrchestrationManagerNode: Provides solution history and iteration context
1743
+ - SolutionEvaluatorNode: Supplies quality scores and evaluation metrics
1744
+ - A2ACoordinatorNode: Provides team consensus indicators
1745
+ - TeamFormationNode: Supplies team performance metrics
1746
+
1747
+ Downstream Consumers:
1748
+ - OrchestrationManagerNode: Uses convergence decisions to control iteration
1749
+ - Reporting systems: Use convergence analysis for insights
1750
+ - Optimization frameworks: Leverage convergence signals for tuning
1751
+
1752
+ Configuration:
1753
+ The detector uses configurable thresholds and weights that can be adjusted
1754
+ based on problem domain, urgency, and resource constraints. Defaults are
1755
+ tuned for general-purpose problem solving but should be customized for
1756
+ specific use cases.
1757
+
1758
+ Implementation Details:
1759
+ - Tracks multiple convergence signals simultaneously
1760
+ - Implements trend analysis using simple linear regression
1761
+ - Calculates team consensus from agreement scores
1762
+ - Monitors resource utilization (time, iterations, cost)
1763
+ - Generates actionable recommendations
1764
+ - Maintains convergence history for analysis
1765
+ - Uses weighted voting among signals
1766
+
1767
+ Error Handling:
1768
+ - Handles empty solution history gracefully
1769
+ - Validates all thresholds and parameters
1770
+ - Returns sensible defaults for missing data
1771
+ - Never throws exceptions - always returns valid decision
1772
+
1773
+ Side Effects:
1774
+ - Updates internal convergence history
1775
+ - No external side effects
1776
+ - Pure decision function based on inputs
1777
+
1778
+ Examples:
1779
+ >>> detector = ConvergenceDetectorNode()
1780
+ >>>
1781
+ >>> # Typical convergence detection scenario
1782
+ >>> result = detector.run(
1783
+ ... solution_history=[
1784
+ ... {
1785
+ ... "iteration": 1,
1786
+ ... "evaluation": {"overall_score": 0.6},
1787
+ ... "team_agreement": 0.7,
1788
+ ... "duration": 120
1789
+ ... },
1790
+ ... {
1791
+ ... "iteration": 2,
1792
+ ... "evaluation": {"overall_score": 0.75},
1793
+ ... "team_agreement": 0.85,
1794
+ ... "duration": 110
1795
+ ... },
1796
+ ... {
1797
+ ... "iteration": 3,
1798
+ ... "evaluation": {"overall_score": 0.82},
1799
+ ... "team_agreement": 0.9,
1800
+ ... "duration": 105
1801
+ ... }
1802
+ ... ],
1803
+ ... quality_threshold=0.8,
1804
+ ... improvement_threshold=0.02,
1805
+ ... max_iterations=5,
1806
+ ... current_iteration=3,
1807
+ ... time_limit_seconds=600,
1808
+ ... resource_budget=100.0
1809
+ ... )
1810
+ >>> assert result["success"] == True
1811
+ >>> assert result["should_continue"] == False # Quality threshold met
1812
+ >>> assert "quality_met" in result["convergence_signals"]
1813
+ >>> assert result["convergence_signals"]["quality_met"] == True
1814
+ >>> assert result["confidence"] > 0.5 # Adjusted for realistic confidence
1815
+ >>>
1816
+ >>> # Diminishing returns scenario
1817
+ >>> stagnant_history = [
1818
+ ... {"evaluation": {"overall_score": 0.7}, "duration": 100},
1819
+ ... {"evaluation": {"overall_score": 0.71}, "duration": 95},
1820
+ ... {"evaluation": {"overall_score": 0.715}, "duration": 90}
1821
+ ... ]
1822
+ >>> result2 = detector.run(
1823
+ ... solution_history=stagnant_history,
1824
+ ... quality_threshold=0.9,
1825
+ ... improvement_threshold=0.05,
1826
+ ... current_iteration=3
1827
+ ... )
1828
+ >>> assert result2["convergence_signals"]["diminishing_returns"] == True
1829
+ >>> assert "Diminishing returns" in result2["reason"]
1830
+ """
1831
+
1832
+ def __init__(self):
1833
+ super().__init__()
1834
+ self.convergence_history = deque(maxlen=100)
1835
+
1836
+ def get_parameters(self) -> Dict[str, NodeParameter]:
1837
+ return {
1838
+ "solution_history": NodeParameter(
1839
+ name="solution_history",
1840
+ type=list,
1841
+ required=False,
1842
+ default=[],
1843
+ description="History of solution iterations",
1844
+ ),
1845
+ "quality_threshold": NodeParameter(
1846
+ name="quality_threshold",
1847
+ type=float,
1848
+ required=False,
1849
+ default=0.8,
1850
+ description="Minimum quality threshold",
1851
+ ),
1852
+ "improvement_threshold": NodeParameter(
1853
+ name="improvement_threshold",
1854
+ type=float,
1855
+ required=False,
1856
+ default=0.02,
1857
+ description="Minimum improvement to continue iteration",
1858
+ ),
1859
+ "max_iterations": NodeParameter(
1860
+ name="max_iterations",
1861
+ type=int,
1862
+ required=False,
1863
+ default=5,
1864
+ description="Maximum allowed iterations",
1865
+ ),
1866
+ "current_iteration": NodeParameter(
1867
+ name="current_iteration",
1868
+ type=int,
1869
+ required=False,
1870
+ default=0,
1871
+ description="Current iteration number",
1872
+ ),
1873
+ "time_limit_seconds": NodeParameter(
1874
+ name="time_limit_seconds",
1875
+ type=int,
1876
+ required=False,
1877
+ default=3600,
1878
+ description="Maximum time allowed",
1879
+ ),
1880
+ "resource_budget": NodeParameter(
1881
+ name="resource_budget",
1882
+ type=float,
1883
+ required=False,
1884
+ default=100.0,
1885
+ description="Resource budget limit",
1886
+ ),
1887
+ }
1888
+
1889
+ def run(self, **kwargs) -> Dict[str, Any]:
1890
+ """Determine if solution has converged and iteration should stop."""
1891
+ solution_history = kwargs.get("solution_history", [])
1892
+ quality_threshold = kwargs.get("quality_threshold", 0.8)
1893
+ improvement_threshold = kwargs.get("improvement_threshold", 0.02)
1894
+ max_iterations = kwargs.get("max_iterations", 5)
1895
+ current_iteration = kwargs.get("current_iteration", 0)
1896
+ time_limit = kwargs.get("time_limit_seconds", 3600)
1897
+ kwargs.get("resource_budget", 100.0)
1898
+
1899
+ if not solution_history:
1900
+ return {
1901
+ "success": True,
1902
+ "should_continue": True,
1903
+ "reason": "No solution history available",
1904
+ "confidence": 0.0,
1905
+ }
1906
+
1907
+ # Get latest solution
1908
+ latest_solution = solution_history[-1]
1909
+
1910
+ # Multiple convergence criteria
1911
+ convergence_signals = {}
1912
+
1913
+ # 1. Quality Threshold
1914
+ latest_score = latest_solution.get("evaluation", {}).get("overall_score", 0.0)
1915
+ convergence_signals["quality_met"] = latest_score >= quality_threshold
1916
+
1917
+ # 2. Improvement Rate
1918
+ if len(solution_history) >= 2:
1919
+ prev_score = (
1920
+ solution_history[-2].get("evaluation", {}).get("overall_score", 0.0)
1921
+ )
1922
+ improvement = latest_score - prev_score
1923
+ convergence_signals["sufficient_improvement"] = (
1924
+ improvement >= improvement_threshold
1925
+ )
1926
+ else:
1927
+ convergence_signals["sufficient_improvement"] = True
1928
+
1929
+ # 3. Iteration Limit
1930
+ convergence_signals["iteration_limit_reached"] = (
1931
+ current_iteration >= max_iterations
1932
+ )
1933
+
1934
+ # 4. Diminishing Returns
1935
+ if len(solution_history) >= 3:
1936
+ scores = [
1937
+ s.get("evaluation", {}).get("overall_score", 0.0)
1938
+ for s in solution_history[-3:]
1939
+ ]
1940
+ improvements = [scores[i] - scores[i - 1] for i in range(1, len(scores))]
1941
+ avg_improvement = sum(improvements) / len(improvements)
1942
+ convergence_signals["diminishing_returns"] = (
1943
+ avg_improvement < improvement_threshold * 0.5
1944
+ )
1945
+ else:
1946
+ convergence_signals["diminishing_returns"] = False
1947
+
1948
+ # 5. Team Consensus
1949
+ team_agreements = [s.get("team_agreement", 0.8) for s in solution_history]
1950
+ latest_consensus = team_agreements[-1] if team_agreements else 0.8
1951
+ convergence_signals["team_consensus"] = latest_consensus >= 0.85
1952
+
1953
+ # 6. Resource Efficiency
1954
+ total_time = sum(s.get("duration", 0) for s in solution_history)
1955
+ convergence_signals["time_limit_approaching"] = total_time >= time_limit * 0.9
1956
+
1957
+ # 7. Solution Stability
1958
+ if len(solution_history) >= 3:
1959
+ recent_scores = [
1960
+ s.get("evaluation", {}).get("overall_score", 0.0)
1961
+ for s in solution_history[-3:]
1962
+ ]
1963
+ score_variance = sum(
1964
+ (s - sum(recent_scores) / len(recent_scores)) ** 2
1965
+ for s in recent_scores
1966
+ ) / len(recent_scores)
1967
+ convergence_signals["solution_stable"] = score_variance < 0.01
1968
+ else:
1969
+ convergence_signals["solution_stable"] = False
1970
+
1971
+ # Determine convergence
1972
+ should_stop = (
1973
+ convergence_signals["quality_met"]
1974
+ or convergence_signals["iteration_limit_reached"]
1975
+ or convergence_signals["time_limit_approaching"]
1976
+ or (
1977
+ convergence_signals["diminishing_returns"]
1978
+ and convergence_signals["solution_stable"]
1979
+ )
1980
+ or (
1981
+ not convergence_signals["sufficient_improvement"]
1982
+ and current_iteration > 1
1983
+ )
1984
+ )
1985
+
1986
+ # Calculate convergence confidence
1987
+ positive_signals = sum(1 for signal in convergence_signals.values() if signal)
1988
+ convergence_confidence = positive_signals / len(convergence_signals)
1989
+
1990
+ # Determine primary reason
1991
+ if convergence_signals["quality_met"]:
1992
+ reason = f"Quality threshold achieved (score: {latest_score:.3f})"
1993
+ elif convergence_signals["iteration_limit_reached"]:
1994
+ reason = (
1995
+ f"Maximum iterations reached ({current_iteration}/{max_iterations})"
1996
+ )
1997
+ elif convergence_signals["time_limit_approaching"]:
1998
+ reason = f"Time limit approaching (used: {total_time:.1f}s)"
1999
+ elif convergence_signals["diminishing_returns"]:
2000
+ reason = "Diminishing returns detected"
2001
+ elif not convergence_signals["sufficient_improvement"]:
2002
+ reason = f"Insufficient improvement (< {improvement_threshold})"
2003
+ else:
2004
+ reason = "Continuing iteration"
2005
+
2006
+ # Record convergence decision
2007
+ self.convergence_history.append(
2008
+ {
2009
+ "timestamp": time.time(),
2010
+ "iteration": current_iteration,
2011
+ "latest_score": latest_score,
2012
+ "should_stop": should_stop,
2013
+ "signals": convergence_signals,
2014
+ "confidence": convergence_confidence,
2015
+ }
2016
+ )
2017
+
2018
+ return {
2019
+ "success": True,
2020
+ "should_continue": not should_stop,
2021
+ "should_stop": should_stop,
2022
+ "reason": reason,
2023
+ "confidence": convergence_confidence,
2024
+ "convergence_signals": convergence_signals,
2025
+ "latest_score": latest_score,
2026
+ "improvement_trend": self._calculate_improvement_trend(solution_history),
2027
+ "recommendations": self._generate_recommendations(
2028
+ convergence_signals, current_iteration
2029
+ ),
2030
+ }
2031
+
2032
+ def _calculate_improvement_trend(self, history: List[Dict]) -> Dict:
2033
+ """Calculate the trend in solution improvement."""
2034
+ if len(history) < 2:
2035
+ return {"trend": "insufficient_data", "rate": 0.0}
2036
+
2037
+ scores = [s.get("evaluation", {}).get("overall_score", 0.0) for s in history]
2038
+
2039
+ if len(scores) < 3:
2040
+ improvement = scores[-1] - scores[0]
2041
+ return {
2042
+ "trend": (
2043
+ "improving"
2044
+ if improvement > 0
2045
+ else "declining" if improvement < 0 else "stable"
2046
+ ),
2047
+ "rate": improvement,
2048
+ "total_improvement": improvement,
2049
+ }
2050
+
2051
+ # Calculate linear trend
2052
+ n = len(scores)
2053
+ x_vals = list(range(n))
2054
+
2055
+ # Simple linear regression
2056
+ x_mean = sum(x_vals) / n
2057
+ y_mean = sum(scores) / n
2058
+
2059
+ numerator = sum((x_vals[i] - x_mean) * (scores[i] - y_mean) for i in range(n))
2060
+ denominator = sum((x_vals[i] - x_mean) ** 2 for i in range(n))
2061
+
2062
+ if denominator == 0:
2063
+ slope = 0
2064
+ else:
2065
+ slope = numerator / denominator
2066
+
2067
+ return {
2068
+ "trend": (
2069
+ "improving"
2070
+ if slope > 0.01
2071
+ else "declining" if slope < -0.01 else "stable"
2072
+ ),
2073
+ "rate": slope,
2074
+ "total_improvement": scores[-1] - scores[0],
2075
+ "consistency": 1.0 - (max(scores) - min(scores)) / max(max(scores), 0.1),
2076
+ }
2077
+
2078
+ def _generate_recommendations(self, signals: Dict, iteration: int) -> List[str]:
2079
+ """Generate recommendations based on convergence signals."""
2080
+ recommendations = []
2081
+
2082
+ if not signals["quality_met"] and signals["sufficient_improvement"]:
2083
+ recommendations.append(
2084
+ "Continue iteration - quality improving but not yet at threshold"
2085
+ )
2086
+
2087
+ if signals["diminishing_returns"]:
2088
+ recommendations.append("Consider alternative approach or team composition")
2089
+
2090
+ if not signals["team_consensus"]:
2091
+ recommendations.append("Improve team coordination and consensus building")
2092
+
2093
+ if iteration == 1 and signals["quality_met"]:
2094
+ recommendations.append(
2095
+ "Excellent first iteration - consider raising quality threshold"
2096
+ )
2097
+
2098
+ if signals["time_limit_approaching"]:
2099
+ recommendations.append(
2100
+ "Prioritize most impactful improvements due to time constraints"
2101
+ )
2102
+
2103
+ if signals["solution_stable"] and not signals["quality_met"]:
2104
+ recommendations.append(
2105
+ "Solution has stabilized below threshold - try different strategy"
2106
+ )
2107
+
2108
+ return recommendations