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.
- kailash/__init__.py +1 -1
- kailash/access_control.py +740 -0
- kailash/api/__main__.py +6 -0
- kailash/api/auth.py +668 -0
- kailash/api/custom_nodes.py +285 -0
- kailash/api/custom_nodes_secure.py +377 -0
- kailash/api/database.py +620 -0
- kailash/api/studio.py +915 -0
- kailash/api/studio_secure.py +893 -0
- kailash/mcp/__init__.py +53 -0
- kailash/mcp/__main__.py +13 -0
- kailash/mcp/ai_registry_server.py +712 -0
- kailash/mcp/client.py +447 -0
- kailash/mcp/client_new.py +334 -0
- kailash/mcp/server.py +293 -0
- kailash/mcp/server_new.py +336 -0
- kailash/mcp/servers/__init__.py +12 -0
- kailash/mcp/servers/ai_registry.py +289 -0
- kailash/nodes/__init__.py +4 -2
- kailash/nodes/ai/__init__.py +38 -0
- kailash/nodes/ai/a2a.py +1790 -0
- kailash/nodes/ai/agents.py +116 -2
- kailash/nodes/ai/ai_providers.py +206 -8
- kailash/nodes/ai/intelligent_agent_orchestrator.py +2108 -0
- kailash/nodes/ai/iterative_llm_agent.py +1280 -0
- kailash/nodes/ai/llm_agent.py +324 -1
- kailash/nodes/ai/self_organizing.py +1623 -0
- kailash/nodes/api/http.py +106 -25
- kailash/nodes/api/rest.py +116 -21
- kailash/nodes/base.py +15 -2
- kailash/nodes/base_async.py +45 -0
- kailash/nodes/base_cycle_aware.py +374 -0
- kailash/nodes/base_with_acl.py +338 -0
- kailash/nodes/code/python.py +135 -27
- kailash/nodes/data/readers.py +116 -53
- kailash/nodes/data/writers.py +16 -6
- kailash/nodes/logic/__init__.py +8 -0
- kailash/nodes/logic/async_operations.py +48 -9
- kailash/nodes/logic/convergence.py +642 -0
- kailash/nodes/logic/loop.py +153 -0
- kailash/nodes/logic/operations.py +212 -27
- kailash/nodes/logic/workflow.py +26 -18
- kailash/nodes/mixins/__init__.py +11 -0
- kailash/nodes/mixins/mcp.py +228 -0
- kailash/nodes/mixins.py +387 -0
- kailash/nodes/transform/__init__.py +8 -1
- kailash/nodes/transform/processors.py +119 -4
- kailash/runtime/__init__.py +2 -1
- kailash/runtime/access_controlled.py +458 -0
- kailash/runtime/local.py +106 -33
- kailash/runtime/parallel_cyclic.py +529 -0
- kailash/sdk_exceptions.py +90 -5
- kailash/security.py +845 -0
- kailash/tracking/manager.py +38 -15
- kailash/tracking/models.py +1 -1
- kailash/tracking/storage/filesystem.py +30 -2
- kailash/utils/__init__.py +8 -0
- kailash/workflow/__init__.py +18 -0
- kailash/workflow/convergence.py +270 -0
- kailash/workflow/cycle_analyzer.py +768 -0
- kailash/workflow/cycle_builder.py +573 -0
- kailash/workflow/cycle_config.py +709 -0
- kailash/workflow/cycle_debugger.py +760 -0
- kailash/workflow/cycle_exceptions.py +601 -0
- kailash/workflow/cycle_profiler.py +671 -0
- kailash/workflow/cycle_state.py +338 -0
- kailash/workflow/cyclic_runner.py +985 -0
- kailash/workflow/graph.py +500 -39
- kailash/workflow/migration.py +768 -0
- kailash/workflow/safety.py +365 -0
- kailash/workflow/templates.py +744 -0
- kailash/workflow/validation.py +693 -0
- {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/METADATA +446 -13
- kailash-0.2.0.dist-info/RECORD +125 -0
- kailash/nodes/mcp/__init__.py +0 -11
- kailash/nodes/mcp/client.py +0 -554
- kailash/nodes/mcp/resource.py +0 -682
- kailash/nodes/mcp/server.py +0 -577
- kailash-0.1.4.dist-info/RECORD +0 -85
- {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/WHEEL +0 -0
- {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {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
|