kailash 0.1.5__py3-none-any.whl → 0.2.1__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 +2 -0
- kailash/nodes/ai/a2a.py +714 -67
- kailash/nodes/ai/intelligent_agent_orchestrator.py +31 -37
- kailash/nodes/ai/iterative_llm_agent.py +1280 -0
- kailash/nodes/ai/llm_agent.py +324 -1
- kailash/nodes/ai/self_organizing.py +5 -6
- 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/__init__.py +1 -2
- kailash/nodes/data/readers.py +16 -6
- kailash/nodes/data/sql.py +699 -256
- kailash/nodes/data/writers.py +16 -6
- kailash/nodes/logic/__init__.py +8 -0
- kailash/nodes/logic/convergence.py +642 -0
- kailash/nodes/logic/loop.py +153 -0
- kailash/nodes/logic/operations.py +187 -27
- kailash/nodes/mixins/__init__.py +11 -0
- kailash/nodes/mixins/mcp.py +228 -0
- kailash/nodes/mixins.py +387 -0
- 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 +889 -0
- kailash/workflow/cycle_builder.py +579 -0
- kailash/workflow/cycle_config.py +725 -0
- kailash/workflow/cycle_debugger.py +860 -0
- kailash/workflow/cycle_exceptions.py +615 -0
- kailash/workflow/cycle_profiler.py +741 -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 +809 -0
- kailash/workflow/safety.py +365 -0
- kailash/workflow/templates.py +763 -0
- kailash/workflow/validation.py +751 -0
- {kailash-0.1.5.dist-info → kailash-0.2.1.dist-info}/METADATA +259 -12
- kailash-0.2.1.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.5.dist-info/RECORD +0 -88
- {kailash-0.1.5.dist-info → kailash-0.2.1.dist-info}/WHEEL +0 -0
- {kailash-0.1.5.dist-info → kailash-0.2.1.dist-info}/entry_points.txt +0 -0
- {kailash-0.1.5.dist-info → kailash-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.1.5.dist-info → kailash-0.2.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1280 @@
|
|
1
|
+
"""Iterative LLM Agent with progressive MCP discovery and execution capabilities."""
|
2
|
+
|
3
|
+
import time
|
4
|
+
from dataclasses import dataclass, field
|
5
|
+
from datetime import datetime
|
6
|
+
from typing import Any, Dict, List, Optional
|
7
|
+
|
8
|
+
from kailash.nodes.ai.llm_agent import LLMAgentNode
|
9
|
+
from kailash.nodes.base import NodeParameter, register_node
|
10
|
+
|
11
|
+
|
12
|
+
@dataclass
|
13
|
+
class IterationState:
|
14
|
+
"""State tracking for a single iteration."""
|
15
|
+
|
16
|
+
iteration: int
|
17
|
+
phase: str # discovery, planning, execution, reflection, convergence, synthesis
|
18
|
+
start_time: float
|
19
|
+
end_time: Optional[float] = None
|
20
|
+
discoveries: Dict[str, Any] = field(default_factory=dict)
|
21
|
+
plan: Dict[str, Any] = field(default_factory=dict)
|
22
|
+
execution_results: Dict[str, Any] = field(default_factory=dict)
|
23
|
+
reflection: Dict[str, Any] = field(default_factory=dict)
|
24
|
+
convergence_decision: Dict[str, Any] = field(default_factory=dict)
|
25
|
+
success: bool = False
|
26
|
+
error: Optional[str] = None
|
27
|
+
|
28
|
+
def to_dict(self) -> Dict[str, Any]:
|
29
|
+
"""Convert to dictionary for serialization."""
|
30
|
+
return {
|
31
|
+
"iteration": self.iteration,
|
32
|
+
"phase": self.phase,
|
33
|
+
"start_time": self.start_time,
|
34
|
+
"end_time": self.end_time,
|
35
|
+
"duration": (self.end_time - self.start_time) if self.end_time else None,
|
36
|
+
"discoveries": self.discoveries,
|
37
|
+
"plan": self.plan,
|
38
|
+
"execution_results": self.execution_results,
|
39
|
+
"reflection": self.reflection,
|
40
|
+
"convergence_decision": self.convergence_decision,
|
41
|
+
"success": self.success,
|
42
|
+
"error": self.error,
|
43
|
+
}
|
44
|
+
|
45
|
+
|
46
|
+
@dataclass
|
47
|
+
class MCPToolCapability:
|
48
|
+
"""Semantic understanding of an MCP tool's capabilities."""
|
49
|
+
|
50
|
+
name: str
|
51
|
+
description: str
|
52
|
+
primary_function: str
|
53
|
+
input_requirements: List[str]
|
54
|
+
output_format: str
|
55
|
+
domain: str
|
56
|
+
complexity: str # simple, medium, complex
|
57
|
+
dependencies: List[str]
|
58
|
+
confidence: float
|
59
|
+
server_source: str
|
60
|
+
|
61
|
+
def to_dict(self) -> Dict[str, Any]:
|
62
|
+
return {
|
63
|
+
"name": self.name,
|
64
|
+
"description": self.description,
|
65
|
+
"primary_function": self.primary_function,
|
66
|
+
"input_requirements": self.input_requirements,
|
67
|
+
"output_format": self.output_format,
|
68
|
+
"domain": self.domain,
|
69
|
+
"complexity": self.complexity,
|
70
|
+
"dependencies": self.dependencies,
|
71
|
+
"confidence": self.confidence,
|
72
|
+
"server_source": self.server_source,
|
73
|
+
}
|
74
|
+
|
75
|
+
|
76
|
+
@register_node()
|
77
|
+
class IterativeLLMAgentNode(LLMAgentNode):
|
78
|
+
"""
|
79
|
+
Iterative LLM Agent with progressive MCP discovery and execution.
|
80
|
+
|
81
|
+
This agent can discover MCP tools and resources dynamically, plan and execute
|
82
|
+
multi-step processes, reflect on results, and converge when goals are met
|
83
|
+
or iteration limits are reached.
|
84
|
+
|
85
|
+
Key Features:
|
86
|
+
- Progressive MCP discovery without pre-configuration
|
87
|
+
- 6-phase iterative process (Discovery → Planning → Execution → Reflection → Convergence → Synthesis)
|
88
|
+
- Semantic tool understanding and capability mapping
|
89
|
+
- Adaptive strategy based on iteration results
|
90
|
+
- Smart convergence criteria and resource management
|
91
|
+
|
92
|
+
Examples:
|
93
|
+
>>> # Basic iterative agent
|
94
|
+
>>> agent = IterativeLLMAgentNode()
|
95
|
+
>>> result = agent.run(
|
96
|
+
... messages=[{"role": "user", "content": "Find and analyze healthcare AI trends"}],
|
97
|
+
... mcp_servers=["http://localhost:8080"],
|
98
|
+
... max_iterations=3
|
99
|
+
... )
|
100
|
+
|
101
|
+
>>> # Advanced iterative agent with custom convergence
|
102
|
+
>>> result = agent.run(
|
103
|
+
... messages=[{"role": "user", "content": "Research and recommend AI implementation strategy"}],
|
104
|
+
... mcp_servers=["http://ai-registry:8080", "http://knowledge-base:8081"],
|
105
|
+
... max_iterations=5,
|
106
|
+
... discovery_mode="semantic",
|
107
|
+
... convergence_criteria={
|
108
|
+
... "goal_satisfaction": {"threshold": 0.9},
|
109
|
+
... "diminishing_returns": {"min_improvement": 0.1}
|
110
|
+
... }
|
111
|
+
... )
|
112
|
+
"""
|
113
|
+
|
114
|
+
def get_parameters(self) -> Dict[str, NodeParameter]:
|
115
|
+
"""Get parameters for iterative LLM agent configuration."""
|
116
|
+
base_params = super().get_parameters()
|
117
|
+
|
118
|
+
iterative_params = {
|
119
|
+
# Iteration Control
|
120
|
+
"max_iterations": NodeParameter(
|
121
|
+
name="max_iterations",
|
122
|
+
type=int,
|
123
|
+
required=False,
|
124
|
+
default=5,
|
125
|
+
description="Maximum number of discovery-execution cycles",
|
126
|
+
),
|
127
|
+
"convergence_criteria": NodeParameter(
|
128
|
+
name="convergence_criteria",
|
129
|
+
type=dict,
|
130
|
+
required=False,
|
131
|
+
default={},
|
132
|
+
description="""Criteria for determining when to stop iterating. Supports:
|
133
|
+
- goal_satisfaction: {"threshold": 0.8} - Stop when confidence >= threshold
|
134
|
+
- early_satisfaction: {"enabled": True, "threshold": 0.85, "custom_check": callable} - Early stopping with optional custom function
|
135
|
+
- diminishing_returns: {"enabled": True, "min_improvement": 0.05} - Stop when improvement < threshold
|
136
|
+
- quality_gates: {"min_confidence": 0.7, "custom_validator": callable} - Quality checks with optional custom validator
|
137
|
+
- resource_limits: {"max_cost": 1.0, "max_time": 300} - Hard resource limits
|
138
|
+
- custom_criteria: [{"name": "my_check", "function": callable, "weight": 0.5}] - User-defined criteria
|
139
|
+
""",
|
140
|
+
),
|
141
|
+
# Discovery Configuration
|
142
|
+
"discovery_mode": NodeParameter(
|
143
|
+
name="discovery_mode",
|
144
|
+
type=str,
|
145
|
+
required=False,
|
146
|
+
default="progressive",
|
147
|
+
description="Discovery strategy: progressive, exhaustive, semantic",
|
148
|
+
),
|
149
|
+
"discovery_budget": NodeParameter(
|
150
|
+
name="discovery_budget",
|
151
|
+
type=dict,
|
152
|
+
required=False,
|
153
|
+
default={"max_servers": 5, "max_tools": 20, "max_resources": 50},
|
154
|
+
description="Limits for discovery process",
|
155
|
+
),
|
156
|
+
# Iterative Configuration
|
157
|
+
"reflection_enabled": NodeParameter(
|
158
|
+
name="reflection_enabled",
|
159
|
+
type=bool,
|
160
|
+
required=False,
|
161
|
+
default=True,
|
162
|
+
description="Enable reflection phase between iterations",
|
163
|
+
),
|
164
|
+
"adaptation_strategy": NodeParameter(
|
165
|
+
name="adaptation_strategy",
|
166
|
+
type=str,
|
167
|
+
required=False,
|
168
|
+
default="dynamic",
|
169
|
+
description="How to adapt strategy: static, dynamic, ml_guided",
|
170
|
+
),
|
171
|
+
# Performance and Monitoring
|
172
|
+
"enable_detailed_logging": NodeParameter(
|
173
|
+
name="enable_detailed_logging",
|
174
|
+
type=bool,
|
175
|
+
required=False,
|
176
|
+
default=True,
|
177
|
+
description="Enable detailed iteration logging for debugging",
|
178
|
+
),
|
179
|
+
"iteration_timeout": NodeParameter(
|
180
|
+
name="iteration_timeout",
|
181
|
+
type=int,
|
182
|
+
required=False,
|
183
|
+
default=300,
|
184
|
+
description="Timeout for each iteration in seconds",
|
185
|
+
),
|
186
|
+
}
|
187
|
+
|
188
|
+
# Merge base parameters with iterative parameters
|
189
|
+
base_params.update(iterative_params)
|
190
|
+
return base_params
|
191
|
+
|
192
|
+
def run(self, **kwargs) -> Dict[str, Any]:
|
193
|
+
"""
|
194
|
+
Execute iterative LLM agent with 6-phase process.
|
195
|
+
|
196
|
+
Args:
|
197
|
+
**kwargs: All parameters from get_parameters() plus inherited LLMAgentNode params
|
198
|
+
|
199
|
+
Returns:
|
200
|
+
Dict containing:
|
201
|
+
success (bool): Whether the iterative process completed successfully
|
202
|
+
final_response (str): Synthesized final response
|
203
|
+
iterations (List[Dict]): Detailed log of all iterations
|
204
|
+
discoveries (Dict): All discovered MCP capabilities
|
205
|
+
convergence_reason (str): Why the process stopped
|
206
|
+
total_duration (float): Total execution time
|
207
|
+
resource_usage (Dict): Resource consumption metrics
|
208
|
+
"""
|
209
|
+
# Extract iterative-specific parameters
|
210
|
+
max_iterations = kwargs.get("max_iterations", 5)
|
211
|
+
convergence_criteria = kwargs.get("convergence_criteria", {})
|
212
|
+
discovery_mode = kwargs.get("discovery_mode", "progressive")
|
213
|
+
discovery_budget = kwargs.get(
|
214
|
+
"discovery_budget", {"max_servers": 5, "max_tools": 20, "max_resources": 50}
|
215
|
+
)
|
216
|
+
reflection_enabled = kwargs.get("reflection_enabled", True)
|
217
|
+
adaptation_strategy = kwargs.get("adaptation_strategy", "dynamic")
|
218
|
+
enable_detailed_logging = kwargs.get("enable_detailed_logging", True)
|
219
|
+
kwargs.get("iteration_timeout", 300)
|
220
|
+
|
221
|
+
# Initialize iterative execution state
|
222
|
+
start_time = time.time()
|
223
|
+
iterations: List[IterationState] = []
|
224
|
+
global_discoveries = {
|
225
|
+
"servers": {},
|
226
|
+
"tools": {},
|
227
|
+
"resources": {},
|
228
|
+
"capabilities": {},
|
229
|
+
}
|
230
|
+
converged = False
|
231
|
+
convergence_reason = "max_iterations_reached"
|
232
|
+
|
233
|
+
try:
|
234
|
+
# Main iterative loop
|
235
|
+
for iteration_num in range(1, max_iterations + 1):
|
236
|
+
iteration_state = IterationState(
|
237
|
+
iteration=iteration_num, phase="discovery", start_time=time.time()
|
238
|
+
)
|
239
|
+
|
240
|
+
if enable_detailed_logging:
|
241
|
+
self.logger.info(
|
242
|
+
f"Starting iteration {iteration_num}/{max_iterations}"
|
243
|
+
)
|
244
|
+
|
245
|
+
try:
|
246
|
+
# Phase 1: Discovery
|
247
|
+
iteration_state.discoveries = self._phase_discovery(
|
248
|
+
kwargs, global_discoveries, discovery_mode, discovery_budget
|
249
|
+
)
|
250
|
+
|
251
|
+
# Phase 2: Planning
|
252
|
+
iteration_state.phase = "planning"
|
253
|
+
iteration_state.plan = self._phase_planning(
|
254
|
+
kwargs,
|
255
|
+
iteration_state.discoveries,
|
256
|
+
global_discoveries,
|
257
|
+
iterations,
|
258
|
+
)
|
259
|
+
|
260
|
+
# Phase 3: Execution
|
261
|
+
iteration_state.phase = "execution"
|
262
|
+
iteration_state.execution_results = self._phase_execution(
|
263
|
+
kwargs, iteration_state.plan, iteration_state.discoveries
|
264
|
+
)
|
265
|
+
|
266
|
+
# Phase 4: Reflection (if enabled)
|
267
|
+
if reflection_enabled:
|
268
|
+
iteration_state.phase = "reflection"
|
269
|
+
iteration_state.reflection = self._phase_reflection(
|
270
|
+
kwargs, iteration_state.execution_results, iterations
|
271
|
+
)
|
272
|
+
|
273
|
+
# Phase 5: Convergence
|
274
|
+
iteration_state.phase = "convergence"
|
275
|
+
convergence_result = self._phase_convergence(
|
276
|
+
kwargs,
|
277
|
+
iteration_state,
|
278
|
+
iterations,
|
279
|
+
convergence_criteria,
|
280
|
+
global_discoveries,
|
281
|
+
)
|
282
|
+
iteration_state.convergence_decision = convergence_result
|
283
|
+
|
284
|
+
if convergence_result["should_stop"]:
|
285
|
+
converged = True
|
286
|
+
convergence_reason = convergence_result["reason"]
|
287
|
+
|
288
|
+
# Update global discoveries
|
289
|
+
self._update_global_discoveries(
|
290
|
+
global_discoveries, iteration_state.discoveries
|
291
|
+
)
|
292
|
+
|
293
|
+
iteration_state.success = True
|
294
|
+
iteration_state.end_time = time.time()
|
295
|
+
|
296
|
+
except Exception as e:
|
297
|
+
iteration_state.error = str(e)
|
298
|
+
iteration_state.success = False
|
299
|
+
iteration_state.end_time = time.time()
|
300
|
+
|
301
|
+
if enable_detailed_logging:
|
302
|
+
self.logger.error(f"Iteration {iteration_num} failed: {e}")
|
303
|
+
|
304
|
+
iterations.append(iteration_state)
|
305
|
+
|
306
|
+
# Check if we should stop
|
307
|
+
if converged:
|
308
|
+
break
|
309
|
+
|
310
|
+
# Adapt strategy for next iteration if enabled
|
311
|
+
if adaptation_strategy == "dynamic" and iteration_state.success:
|
312
|
+
self._adapt_strategy(kwargs, iteration_state, iterations)
|
313
|
+
|
314
|
+
# Phase 6: Synthesis
|
315
|
+
final_response = self._phase_synthesis(
|
316
|
+
kwargs, iterations, global_discoveries
|
317
|
+
)
|
318
|
+
|
319
|
+
total_duration = time.time() - start_time
|
320
|
+
|
321
|
+
return {
|
322
|
+
"success": True,
|
323
|
+
"final_response": final_response,
|
324
|
+
"iterations": [iter_state.to_dict() for iter_state in iterations],
|
325
|
+
"discoveries": global_discoveries,
|
326
|
+
"convergence_reason": convergence_reason,
|
327
|
+
"total_iterations": len(iterations),
|
328
|
+
"total_duration": total_duration,
|
329
|
+
"resource_usage": self._calculate_resource_usage(iterations),
|
330
|
+
"metadata": {
|
331
|
+
"max_iterations": max_iterations,
|
332
|
+
"discovery_mode": discovery_mode,
|
333
|
+
"reflection_enabled": reflection_enabled,
|
334
|
+
"adaptation_strategy": adaptation_strategy,
|
335
|
+
},
|
336
|
+
}
|
337
|
+
|
338
|
+
except Exception as e:
|
339
|
+
return {
|
340
|
+
"success": False,
|
341
|
+
"error": str(e),
|
342
|
+
"error_type": type(e).__name__,
|
343
|
+
"iterations": [iter_state.to_dict() for iter_state in iterations],
|
344
|
+
"discoveries": global_discoveries,
|
345
|
+
"total_duration": time.time() - start_time,
|
346
|
+
"convergence_reason": "error_occurred",
|
347
|
+
"recovery_suggestions": [
|
348
|
+
"Check MCP server connectivity",
|
349
|
+
"Verify discovery budget limits",
|
350
|
+
"Review convergence criteria configuration",
|
351
|
+
"Check iteration timeout settings",
|
352
|
+
],
|
353
|
+
}
|
354
|
+
|
355
|
+
def _phase_discovery(
|
356
|
+
self,
|
357
|
+
kwargs: Dict[str, Any],
|
358
|
+
global_discoveries: Dict[str, Any],
|
359
|
+
discovery_mode: str,
|
360
|
+
discovery_budget: Dict[str, Any],
|
361
|
+
) -> Dict[str, Any]:
|
362
|
+
"""
|
363
|
+
Phase 1: Discover MCP servers, tools, and resources.
|
364
|
+
|
365
|
+
Args:
|
366
|
+
kwargs: Original run parameters
|
367
|
+
global_discoveries: Accumulated discoveries from previous iterations
|
368
|
+
discovery_mode: Discovery strategy
|
369
|
+
discovery_budget: Resource limits for discovery
|
370
|
+
|
371
|
+
Returns:
|
372
|
+
Dictionary containing new discoveries in this iteration
|
373
|
+
"""
|
374
|
+
discoveries = {
|
375
|
+
"new_servers": [],
|
376
|
+
"new_tools": [],
|
377
|
+
"new_resources": [],
|
378
|
+
"tool_capabilities": [],
|
379
|
+
}
|
380
|
+
|
381
|
+
mcp_servers = kwargs.get("mcp_servers", [])
|
382
|
+
|
383
|
+
# Discover from each MCP server
|
384
|
+
for server_config in mcp_servers:
|
385
|
+
server_id = (
|
386
|
+
server_config
|
387
|
+
if isinstance(server_config, str)
|
388
|
+
else server_config.get("url", "unknown")
|
389
|
+
)
|
390
|
+
|
391
|
+
# Skip if already discovered and not in exhaustive mode
|
392
|
+
if (
|
393
|
+
discovery_mode != "exhaustive"
|
394
|
+
and server_id in global_discoveries["servers"]
|
395
|
+
):
|
396
|
+
continue
|
397
|
+
|
398
|
+
try:
|
399
|
+
# Discover tools from this server
|
400
|
+
server_tools = self._discover_server_tools(
|
401
|
+
server_config, discovery_budget
|
402
|
+
)
|
403
|
+
self.logger.info(
|
404
|
+
f"Discovered {len(server_tools)} tools from server {server_id}"
|
405
|
+
)
|
406
|
+
discoveries["new_tools"].extend(server_tools)
|
407
|
+
|
408
|
+
# Discover resources from this server
|
409
|
+
server_resources = self._discover_server_resources(
|
410
|
+
server_config, discovery_budget
|
411
|
+
)
|
412
|
+
self.logger.info(
|
413
|
+
f"Discovered {len(server_resources)} resources from server {server_id}"
|
414
|
+
)
|
415
|
+
discoveries["new_resources"].extend(server_resources)
|
416
|
+
|
417
|
+
# Analyze tool capabilities if in semantic mode
|
418
|
+
if discovery_mode == "semantic":
|
419
|
+
for tool in server_tools:
|
420
|
+
capability = self._analyze_tool_capability(tool, server_id)
|
421
|
+
discoveries["tool_capabilities"].append(capability.to_dict())
|
422
|
+
|
423
|
+
discoveries["new_servers"].append(
|
424
|
+
{
|
425
|
+
"id": server_id,
|
426
|
+
"config": server_config,
|
427
|
+
"discovered_at": datetime.now().isoformat(),
|
428
|
+
"tools_count": len(server_tools),
|
429
|
+
"resources_count": len(server_resources),
|
430
|
+
}
|
431
|
+
)
|
432
|
+
|
433
|
+
self.logger.info(
|
434
|
+
f"Server {server_id} discovery complete: {len(server_tools)} tools, {len(server_resources)} resources"
|
435
|
+
)
|
436
|
+
|
437
|
+
except Exception as e:
|
438
|
+
self.logger.debug(f"Discovery failed for server {server_id}: {e}")
|
439
|
+
discoveries["new_servers"].append(
|
440
|
+
{
|
441
|
+
"id": server_id,
|
442
|
+
"config": server_config,
|
443
|
+
"discovered_at": datetime.now().isoformat(),
|
444
|
+
"error": str(e),
|
445
|
+
"tools_count": 0,
|
446
|
+
"resources_count": 0,
|
447
|
+
}
|
448
|
+
)
|
449
|
+
|
450
|
+
return discoveries
|
451
|
+
|
452
|
+
def _discover_server_tools(
|
453
|
+
self, server_config: Any, budget: Dict[str, Any]
|
454
|
+
) -> List[Dict[str, Any]]:
|
455
|
+
"""Discover tools from a specific MCP server."""
|
456
|
+
# Use existing MCP tool discovery from parent class
|
457
|
+
try:
|
458
|
+
# Ensure MCP client is initialized
|
459
|
+
if not hasattr(self, "_mcp_client"):
|
460
|
+
from kailash.mcp import MCPClient
|
461
|
+
|
462
|
+
self._mcp_client = MCPClient()
|
463
|
+
|
464
|
+
# Call parent class method which returns OpenAI function format
|
465
|
+
discovered_tools_openai_format = self._discover_mcp_tools(
|
466
|
+
[server_config]
|
467
|
+
if not isinstance(server_config, list)
|
468
|
+
else server_config
|
469
|
+
)
|
470
|
+
|
471
|
+
# Convert from OpenAI function format to simple format for iterative agent
|
472
|
+
discovered_tools = []
|
473
|
+
for tool in discovered_tools_openai_format:
|
474
|
+
if isinstance(tool, dict) and "function" in tool:
|
475
|
+
func = tool["function"]
|
476
|
+
discovered_tools.append(
|
477
|
+
{
|
478
|
+
"name": func.get("name", "unknown"),
|
479
|
+
"description": func.get("description", ""),
|
480
|
+
"parameters": func.get("parameters", {}),
|
481
|
+
"mcp_server": func.get("mcp_server", "unknown"),
|
482
|
+
"mcp_server_config": func.get(
|
483
|
+
"mcp_server_config", server_config
|
484
|
+
),
|
485
|
+
}
|
486
|
+
)
|
487
|
+
else:
|
488
|
+
# Handle direct format
|
489
|
+
discovered_tools.append(tool)
|
490
|
+
|
491
|
+
# Apply budget limits
|
492
|
+
max_tools = budget.get("max_tools", 20)
|
493
|
+
return discovered_tools[:max_tools]
|
494
|
+
|
495
|
+
except Exception as e:
|
496
|
+
self.logger.debug(f"Tool discovery failed: {e}")
|
497
|
+
return []
|
498
|
+
|
499
|
+
def _discover_server_resources(
|
500
|
+
self, server_config: Any, budget: Dict[str, Any]
|
501
|
+
) -> List[Dict[str, Any]]:
|
502
|
+
"""Discover resources from a specific MCP server."""
|
503
|
+
# Mock implementation - in real version would use MCP resource discovery
|
504
|
+
try:
|
505
|
+
server_id = (
|
506
|
+
server_config
|
507
|
+
if isinstance(server_config, str)
|
508
|
+
else server_config.get("url", "unknown")
|
509
|
+
)
|
510
|
+
max_resources = budget.get("max_resources", 50)
|
511
|
+
|
512
|
+
# Mock discovered resources
|
513
|
+
mock_resources = [
|
514
|
+
{
|
515
|
+
"uri": f"{server_id}/resource/data/overview",
|
516
|
+
"name": "Data Overview",
|
517
|
+
"type": "data",
|
518
|
+
"description": "Overview of available data sources",
|
519
|
+
},
|
520
|
+
{
|
521
|
+
"uri": f"{server_id}/resource/templates/analysis",
|
522
|
+
"name": "Analysis Templates",
|
523
|
+
"type": "template",
|
524
|
+
"description": "Pre-built analysis templates",
|
525
|
+
},
|
526
|
+
]
|
527
|
+
|
528
|
+
return mock_resources[:max_resources]
|
529
|
+
|
530
|
+
except Exception as e:
|
531
|
+
self.logger.debug(f"Resource discovery failed: {e}")
|
532
|
+
return []
|
533
|
+
|
534
|
+
def _analyze_tool_capability(
|
535
|
+
self, tool: Dict[str, Any], server_id: str
|
536
|
+
) -> MCPToolCapability:
|
537
|
+
"""Analyze tool description to understand semantic capabilities."""
|
538
|
+
# Extract tool information
|
539
|
+
if isinstance(tool, dict) and "function" in tool:
|
540
|
+
func = tool["function"]
|
541
|
+
name = func.get("name", "unknown")
|
542
|
+
description = func.get("description", "")
|
543
|
+
else:
|
544
|
+
name = tool.get("name", "unknown")
|
545
|
+
description = tool.get("description", "")
|
546
|
+
|
547
|
+
# Simple semantic analysis (in real implementation, use LLM for analysis)
|
548
|
+
primary_function = "data_processing"
|
549
|
+
if "search" in description.lower():
|
550
|
+
primary_function = "search"
|
551
|
+
elif "analyze" in description.lower():
|
552
|
+
primary_function = "analysis"
|
553
|
+
elif "create" in description.lower() or "generate" in description.lower():
|
554
|
+
primary_function = "generation"
|
555
|
+
|
556
|
+
# Determine domain
|
557
|
+
domain = "general"
|
558
|
+
if any(
|
559
|
+
keyword in description.lower()
|
560
|
+
for keyword in ["health", "medical", "clinical"]
|
561
|
+
):
|
562
|
+
domain = "healthcare"
|
563
|
+
elif any(
|
564
|
+
keyword in description.lower()
|
565
|
+
for keyword in ["finance", "banking", "investment"]
|
566
|
+
):
|
567
|
+
domain = "finance"
|
568
|
+
elif any(
|
569
|
+
keyword in description.lower()
|
570
|
+
for keyword in ["data", "analytics", "statistics"]
|
571
|
+
):
|
572
|
+
domain = "data_science"
|
573
|
+
|
574
|
+
# Determine complexity
|
575
|
+
complexity = "simple"
|
576
|
+
if len(description) > 100 or "complex" in description.lower():
|
577
|
+
complexity = "complex"
|
578
|
+
elif len(description) > 50:
|
579
|
+
complexity = "medium"
|
580
|
+
|
581
|
+
return MCPToolCapability(
|
582
|
+
name=name,
|
583
|
+
description=description,
|
584
|
+
primary_function=primary_function,
|
585
|
+
input_requirements=["query"] if "search" in primary_function else ["data"],
|
586
|
+
output_format="text",
|
587
|
+
domain=domain,
|
588
|
+
complexity=complexity,
|
589
|
+
dependencies=[],
|
590
|
+
confidence=0.8, # Mock confidence
|
591
|
+
server_source=server_id,
|
592
|
+
)
|
593
|
+
|
594
|
+
def _phase_planning(
|
595
|
+
self,
|
596
|
+
kwargs: Dict[str, Any],
|
597
|
+
discoveries: Dict[str, Any],
|
598
|
+
global_discoveries: Dict[str, Any],
|
599
|
+
previous_iterations: List[IterationState],
|
600
|
+
) -> Dict[str, Any]:
|
601
|
+
"""Phase 2: Create execution plan based on discoveries."""
|
602
|
+
messages = kwargs.get("messages", [])
|
603
|
+
user_query = ""
|
604
|
+
|
605
|
+
# Extract user intent
|
606
|
+
for msg in reversed(messages):
|
607
|
+
if msg.get("role") == "user":
|
608
|
+
user_query = msg.get("content", "")
|
609
|
+
break
|
610
|
+
|
611
|
+
# Analyze available tools and create plan
|
612
|
+
available_tools = discoveries.get("new_tools", []) + list(
|
613
|
+
global_discoveries.get("tools", {}).values()
|
614
|
+
)
|
615
|
+
|
616
|
+
# Simple planning logic (in real implementation, use LLM for planning)
|
617
|
+
plan = {
|
618
|
+
"user_query": user_query,
|
619
|
+
"selected_tools": [],
|
620
|
+
"execution_steps": [],
|
621
|
+
"expected_outcomes": [],
|
622
|
+
"resource_requirements": {},
|
623
|
+
"success_criteria": {},
|
624
|
+
}
|
625
|
+
|
626
|
+
# Select relevant tools
|
627
|
+
for tool in available_tools[:3]: # Limit to top 3 tools
|
628
|
+
if isinstance(tool, dict) and "function" in tool:
|
629
|
+
plan["selected_tools"].append(tool["function"]["name"])
|
630
|
+
elif isinstance(tool, dict):
|
631
|
+
plan["selected_tools"].append(tool.get("name", "unknown"))
|
632
|
+
|
633
|
+
# Create execution steps
|
634
|
+
if "analyze" in user_query.lower():
|
635
|
+
plan["execution_steps"] = [
|
636
|
+
{
|
637
|
+
"step": 1,
|
638
|
+
"action": "gather_data",
|
639
|
+
"tools": plan["selected_tools"][:1],
|
640
|
+
},
|
641
|
+
{
|
642
|
+
"step": 2,
|
643
|
+
"action": "perform_analysis",
|
644
|
+
"tools": plan["selected_tools"][1:2],
|
645
|
+
},
|
646
|
+
{
|
647
|
+
"step": 3,
|
648
|
+
"action": "generate_insights",
|
649
|
+
"tools": plan["selected_tools"][2:3],
|
650
|
+
},
|
651
|
+
]
|
652
|
+
else:
|
653
|
+
plan["execution_steps"] = [
|
654
|
+
{"step": 1, "action": "execute_query", "tools": plan["selected_tools"]}
|
655
|
+
]
|
656
|
+
|
657
|
+
plan["expected_outcomes"] = ["analysis_results", "insights", "recommendations"]
|
658
|
+
|
659
|
+
return plan
|
660
|
+
|
661
|
+
def _phase_execution(
|
662
|
+
self, kwargs: Dict[str, Any], plan: Dict[str, Any], discoveries: Dict[str, Any]
|
663
|
+
) -> Dict[str, Any]:
|
664
|
+
"""Phase 3: Execute the planned actions."""
|
665
|
+
execution_results = {
|
666
|
+
"steps_completed": [],
|
667
|
+
"tool_outputs": {},
|
668
|
+
"intermediate_results": [],
|
669
|
+
"success": True,
|
670
|
+
"errors": [],
|
671
|
+
}
|
672
|
+
|
673
|
+
# Execute each step in the plan
|
674
|
+
for step in plan.get("execution_steps", []):
|
675
|
+
step_num = step.get("step", 0)
|
676
|
+
action = step.get("action", "unknown")
|
677
|
+
tools = step.get("tools", [])
|
678
|
+
|
679
|
+
try:
|
680
|
+
# Mock tool execution (in real implementation, call actual tools)
|
681
|
+
step_result = {
|
682
|
+
"step": step_num,
|
683
|
+
"action": action,
|
684
|
+
"tools_used": tools,
|
685
|
+
"output": f"Mock execution result for {action} using tools: {', '.join(tools)}",
|
686
|
+
"success": True,
|
687
|
+
"duration": 1.5,
|
688
|
+
}
|
689
|
+
|
690
|
+
execution_results["steps_completed"].append(step_result)
|
691
|
+
execution_results["intermediate_results"].append(step_result["output"])
|
692
|
+
|
693
|
+
# Store tool outputs
|
694
|
+
for tool in tools:
|
695
|
+
execution_results["tool_outputs"][tool] = f"Output from {tool}"
|
696
|
+
|
697
|
+
except Exception as e:
|
698
|
+
error_result = {
|
699
|
+
"step": step_num,
|
700
|
+
"action": action,
|
701
|
+
"tools_used": tools,
|
702
|
+
"error": str(e),
|
703
|
+
"success": False,
|
704
|
+
}
|
705
|
+
execution_results["steps_completed"].append(error_result)
|
706
|
+
execution_results["errors"].append(str(e))
|
707
|
+
execution_results["success"] = False
|
708
|
+
|
709
|
+
return execution_results
|
710
|
+
|
711
|
+
def _phase_reflection(
|
712
|
+
self,
|
713
|
+
kwargs: Dict[str, Any],
|
714
|
+
execution_results: Dict[str, Any],
|
715
|
+
previous_iterations: List[IterationState],
|
716
|
+
) -> Dict[str, Any]:
|
717
|
+
"""Phase 4: Reflect on execution results and assess progress."""
|
718
|
+
reflection = {
|
719
|
+
"quality_assessment": {},
|
720
|
+
"goal_progress": {},
|
721
|
+
"areas_for_improvement": [],
|
722
|
+
"next_iteration_suggestions": [],
|
723
|
+
"confidence_score": 0.0,
|
724
|
+
}
|
725
|
+
|
726
|
+
# Assess execution quality
|
727
|
+
total_steps = len(execution_results.get("steps_completed", []))
|
728
|
+
successful_steps = sum(
|
729
|
+
1
|
730
|
+
for step in execution_results.get("steps_completed", [])
|
731
|
+
if step.get("success", False)
|
732
|
+
)
|
733
|
+
|
734
|
+
reflection["quality_assessment"] = {
|
735
|
+
"execution_success_rate": successful_steps / max(total_steps, 1),
|
736
|
+
"errors_encountered": len(execution_results.get("errors", [])),
|
737
|
+
"tools_utilized": len(execution_results.get("tool_outputs", {})),
|
738
|
+
"output_quality": (
|
739
|
+
"good" if successful_steps > total_steps * 0.7 else "poor"
|
740
|
+
),
|
741
|
+
}
|
742
|
+
|
743
|
+
# Assess progress toward goal
|
744
|
+
messages = kwargs.get("messages", [])
|
745
|
+
user_query = ""
|
746
|
+
for msg in reversed(messages):
|
747
|
+
if msg.get("role") == "user":
|
748
|
+
user_query = msg.get("content", "")
|
749
|
+
break
|
750
|
+
|
751
|
+
# Simple goal progress assessment
|
752
|
+
has_analysis = any(
|
753
|
+
"analyze" in result.lower()
|
754
|
+
for result in execution_results.get("intermediate_results", [])
|
755
|
+
)
|
756
|
+
has_data = len(execution_results.get("tool_outputs", {})) > 0
|
757
|
+
|
758
|
+
progress_score = 0.5 # Base score
|
759
|
+
if has_analysis:
|
760
|
+
progress_score += 0.3
|
761
|
+
if has_data:
|
762
|
+
progress_score += 0.2
|
763
|
+
|
764
|
+
reflection["goal_progress"] = {
|
765
|
+
"estimated_completion": min(progress_score, 1.0),
|
766
|
+
"goals_achieved": ["data_gathering"] if has_data else [],
|
767
|
+
"goals_remaining": ["analysis"] if not has_analysis else [],
|
768
|
+
"quality_threshold_met": progress_score > 0.7,
|
769
|
+
}
|
770
|
+
|
771
|
+
# Suggest improvements
|
772
|
+
if execution_results.get("errors"):
|
773
|
+
reflection["areas_for_improvement"].append(
|
774
|
+
"Error handling and tool reliability"
|
775
|
+
)
|
776
|
+
if successful_steps < total_steps:
|
777
|
+
reflection["areas_for_improvement"].append(
|
778
|
+
"Tool selection and configuration"
|
779
|
+
)
|
780
|
+
|
781
|
+
# Suggestions for next iteration
|
782
|
+
if progress_score < 0.8:
|
783
|
+
reflection["next_iteration_suggestions"].append(
|
784
|
+
"Explore additional tools or data sources"
|
785
|
+
)
|
786
|
+
if not has_analysis and "analyze" in user_query.lower():
|
787
|
+
reflection["next_iteration_suggestions"].append(
|
788
|
+
"Focus on analysis and insight generation"
|
789
|
+
)
|
790
|
+
|
791
|
+
reflection["confidence_score"] = progress_score
|
792
|
+
|
793
|
+
return reflection
|
794
|
+
|
795
|
+
def _phase_convergence(
|
796
|
+
self,
|
797
|
+
kwargs: Dict[str, Any],
|
798
|
+
iteration_state: IterationState,
|
799
|
+
previous_iterations: List[IterationState],
|
800
|
+
convergence_criteria: Dict[str, Any],
|
801
|
+
global_discoveries: Dict[str, Any],
|
802
|
+
) -> Dict[str, Any]:
|
803
|
+
"""Phase 5: Decide whether to continue iterating or stop."""
|
804
|
+
convergence_result = {
|
805
|
+
"should_stop": False,
|
806
|
+
"reason": "",
|
807
|
+
"confidence": 0.0,
|
808
|
+
"criteria_met": {},
|
809
|
+
"recommendations": [],
|
810
|
+
}
|
811
|
+
|
812
|
+
# Default convergence criteria
|
813
|
+
default_criteria = {
|
814
|
+
"goal_satisfaction": {"threshold": 0.8},
|
815
|
+
"diminishing_returns": {
|
816
|
+
"enabled": True,
|
817
|
+
"min_improvement": 0.05,
|
818
|
+
"lookback_window": 2,
|
819
|
+
},
|
820
|
+
"resource_limits": {"max_cost": 1.0, "max_time": 300},
|
821
|
+
"quality_gates": {"min_confidence": 0.7},
|
822
|
+
"early_satisfaction": {
|
823
|
+
"enabled": True,
|
824
|
+
"threshold": 0.85,
|
825
|
+
}, # Stop early if very confident
|
826
|
+
}
|
827
|
+
|
828
|
+
# Merge with provided criteria
|
829
|
+
criteria = {**default_criteria, **convergence_criteria}
|
830
|
+
|
831
|
+
# Extract user query for context
|
832
|
+
messages = kwargs.get("messages", [])
|
833
|
+
user_query = ""
|
834
|
+
for msg in reversed(messages):
|
835
|
+
if msg.get("role") == "user":
|
836
|
+
user_query = msg.get("content", "")
|
837
|
+
break
|
838
|
+
|
839
|
+
# Analyze current execution results for satisfaction
|
840
|
+
execution_results = iteration_state.execution_results
|
841
|
+
reflection = iteration_state.reflection
|
842
|
+
|
843
|
+
# Enhanced goal satisfaction analysis
|
844
|
+
if reflection and "confidence_score" in reflection:
|
845
|
+
confidence_score = reflection["confidence_score"]
|
846
|
+
goal_threshold = criteria.get("goal_satisfaction", {}).get("threshold", 0.8)
|
847
|
+
|
848
|
+
# Check if we have sufficient data and analysis
|
849
|
+
has_sufficient_data = len(execution_results.get("tool_outputs", {})) >= 1
|
850
|
+
has_analysis_content = any(
|
851
|
+
len(result) > 50
|
852
|
+
for result in execution_results.get("intermediate_results", [])
|
853
|
+
)
|
854
|
+
execution_success_rate = reflection.get("quality_assessment", {}).get(
|
855
|
+
"execution_success_rate", 0
|
856
|
+
)
|
857
|
+
|
858
|
+
# Enhanced satisfaction calculation
|
859
|
+
satisfaction_score = confidence_score
|
860
|
+
|
861
|
+
# Boost score if we have good data and analysis
|
862
|
+
if (
|
863
|
+
has_sufficient_data
|
864
|
+
and has_analysis_content
|
865
|
+
and execution_success_rate > 0.8
|
866
|
+
):
|
867
|
+
satisfaction_score += 0.1
|
868
|
+
|
869
|
+
# Boost score if user query seems simple and we have a good response
|
870
|
+
simple_query_indicators = ["what", "how", "analyze", "explain"]
|
871
|
+
if any(
|
872
|
+
indicator in user_query.lower() for indicator in simple_query_indicators
|
873
|
+
):
|
874
|
+
if has_analysis_content:
|
875
|
+
satisfaction_score += 0.1
|
876
|
+
|
877
|
+
# Apply early satisfaction check
|
878
|
+
early_config = criteria.get("early_satisfaction", {})
|
879
|
+
early_threshold = early_config.get("threshold", 0.85)
|
880
|
+
if early_config.get("enabled", True):
|
881
|
+
# Check built-in early satisfaction criteria
|
882
|
+
meets_builtin_criteria = (
|
883
|
+
satisfaction_score >= early_threshold and has_sufficient_data
|
884
|
+
)
|
885
|
+
|
886
|
+
# Check user-defined custom early stopping function
|
887
|
+
custom_check_func = early_config.get("custom_check")
|
888
|
+
meets_custom_criteria = True
|
889
|
+
|
890
|
+
if custom_check_func and callable(custom_check_func):
|
891
|
+
try:
|
892
|
+
# Call user-defined function with current state
|
893
|
+
custom_result = custom_check_func(
|
894
|
+
{
|
895
|
+
"iteration_state": iteration_state,
|
896
|
+
"satisfaction_score": satisfaction_score,
|
897
|
+
"execution_results": execution_results,
|
898
|
+
"reflection": reflection,
|
899
|
+
"user_query": user_query,
|
900
|
+
"previous_iterations": previous_iterations,
|
901
|
+
"global_discoveries": kwargs.get(
|
902
|
+
"_global_discoveries", {}
|
903
|
+
),
|
904
|
+
}
|
905
|
+
)
|
906
|
+
meets_custom_criteria = bool(custom_result)
|
907
|
+
|
908
|
+
if isinstance(custom_result, dict):
|
909
|
+
# Custom function can return detailed result
|
910
|
+
meets_custom_criteria = custom_result.get(
|
911
|
+
"should_stop", False
|
912
|
+
)
|
913
|
+
if "reason" in custom_result:
|
914
|
+
convergence_result["reason"] = (
|
915
|
+
f"custom_early_stop: {custom_result['reason']}"
|
916
|
+
)
|
917
|
+
if "confidence" in custom_result:
|
918
|
+
convergence_result["confidence"] = custom_result[
|
919
|
+
"confidence"
|
920
|
+
]
|
921
|
+
except Exception as e:
|
922
|
+
self.logger.warning(
|
923
|
+
f"Custom early stopping function failed: {e}"
|
924
|
+
)
|
925
|
+
meets_custom_criteria = True # Fall back to built-in criteria
|
926
|
+
|
927
|
+
if meets_builtin_criteria and meets_custom_criteria:
|
928
|
+
convergence_result["should_stop"] = True
|
929
|
+
if not convergence_result[
|
930
|
+
"reason"
|
931
|
+
]: # Only set if custom didn't provide one
|
932
|
+
convergence_result["reason"] = "early_satisfaction_achieved"
|
933
|
+
if not convergence_result[
|
934
|
+
"confidence"
|
935
|
+
]: # Only set if custom didn't provide one
|
936
|
+
convergence_result["confidence"] = satisfaction_score
|
937
|
+
convergence_result["criteria_met"]["early_satisfaction"] = True
|
938
|
+
return convergence_result
|
939
|
+
|
940
|
+
# Standard goal satisfaction check
|
941
|
+
goal_satisfaction = satisfaction_score >= goal_threshold
|
942
|
+
convergence_result["criteria_met"]["goal_satisfaction"] = goal_satisfaction
|
943
|
+
|
944
|
+
if goal_satisfaction:
|
945
|
+
convergence_result["should_stop"] = True
|
946
|
+
convergence_result["reason"] = "goal_satisfaction_achieved"
|
947
|
+
convergence_result["confidence"] = satisfaction_score
|
948
|
+
|
949
|
+
# Check if first iteration was already very successful
|
950
|
+
if len(previous_iterations) == 0: # This is iteration 1
|
951
|
+
if execution_results.get("success", False):
|
952
|
+
success_rate = reflection.get("quality_assessment", {}).get(
|
953
|
+
"execution_success_rate", 0
|
954
|
+
)
|
955
|
+
confidence = reflection.get("confidence_score", 0)
|
956
|
+
|
957
|
+
# If first iteration was highly successful and confident, consider stopping
|
958
|
+
if success_rate >= 0.9 and confidence >= 0.8:
|
959
|
+
convergence_result["should_stop"] = True
|
960
|
+
convergence_result["reason"] = "first_iteration_highly_successful"
|
961
|
+
convergence_result["confidence"] = confidence
|
962
|
+
convergence_result["criteria_met"]["first_iteration_success"] = True
|
963
|
+
return convergence_result
|
964
|
+
|
965
|
+
# Check diminishing returns (only if we've had multiple iterations)
|
966
|
+
if len(previous_iterations) >= 1 and criteria.get(
|
967
|
+
"diminishing_returns", {}
|
968
|
+
).get("enabled", True):
|
969
|
+
lookback = criteria.get("diminishing_returns", {}).get("lookback_window", 2)
|
970
|
+
min_improvement = criteria.get("diminishing_returns", {}).get(
|
971
|
+
"min_improvement", 0.05
|
972
|
+
)
|
973
|
+
|
974
|
+
# Get recent confidence scores
|
975
|
+
recent_scores = []
|
976
|
+
if reflection and "confidence_score" in reflection:
|
977
|
+
recent_scores.append(reflection["confidence_score"])
|
978
|
+
|
979
|
+
for prev_iter in previous_iterations[-lookback:]:
|
980
|
+
if prev_iter.reflection and "confidence_score" in prev_iter.reflection:
|
981
|
+
recent_scores.append(prev_iter.reflection["confidence_score"])
|
982
|
+
|
983
|
+
if len(recent_scores) >= 2:
|
984
|
+
# Compare current with previous iteration
|
985
|
+
current_score = recent_scores[0]
|
986
|
+
previous_score = recent_scores[1]
|
987
|
+
improvement = current_score - previous_score
|
988
|
+
|
989
|
+
diminishing_returns = improvement < min_improvement
|
990
|
+
convergence_result["criteria_met"][
|
991
|
+
"diminishing_returns"
|
992
|
+
] = diminishing_returns
|
993
|
+
|
994
|
+
# Only stop for diminishing returns if we already have decent confidence
|
995
|
+
if (
|
996
|
+
diminishing_returns
|
997
|
+
and current_score >= 0.7
|
998
|
+
and not convergence_result["should_stop"]
|
999
|
+
):
|
1000
|
+
convergence_result["should_stop"] = True
|
1001
|
+
convergence_result["reason"] = "diminishing_returns_detected"
|
1002
|
+
convergence_result["confidence"] = current_score
|
1003
|
+
|
1004
|
+
# Check quality gates
|
1005
|
+
quality_threshold = criteria.get("quality_gates", {}).get("min_confidence", 0.7)
|
1006
|
+
if reflection and reflection.get("confidence_score", 0) >= quality_threshold:
|
1007
|
+
convergence_result["criteria_met"]["quality_gates"] = True
|
1008
|
+
|
1009
|
+
# If we meet quality gates and have good execution, consider stopping
|
1010
|
+
execution_quality = reflection.get("quality_assessment", {}).get(
|
1011
|
+
"execution_success_rate", 0
|
1012
|
+
)
|
1013
|
+
|
1014
|
+
# Only stop for quality gates if we've actually discovered and used tools
|
1015
|
+
has_real_discoveries = len(global_discoveries.get("tools", {})) > 0
|
1016
|
+
tools_actually_used = len(execution_results.get("tool_outputs", {})) > 0
|
1017
|
+
|
1018
|
+
if (
|
1019
|
+
execution_quality >= 0.8
|
1020
|
+
and not convergence_result["should_stop"]
|
1021
|
+
and has_real_discoveries
|
1022
|
+
and tools_actually_used
|
1023
|
+
):
|
1024
|
+
convergence_result["should_stop"] = True
|
1025
|
+
convergence_result["reason"] = "quality_gates_satisfied"
|
1026
|
+
convergence_result["confidence"] = reflection["confidence_score"]
|
1027
|
+
|
1028
|
+
# Resource limits check
|
1029
|
+
resource_limits = criteria.get("resource_limits", {})
|
1030
|
+
total_time = sum(
|
1031
|
+
(iter_state.end_time - iter_state.start_time)
|
1032
|
+
for iter_state in previous_iterations + [iteration_state]
|
1033
|
+
if iter_state.end_time
|
1034
|
+
)
|
1035
|
+
|
1036
|
+
if total_time > resource_limits.get("max_time", 300):
|
1037
|
+
convergence_result["should_stop"] = True
|
1038
|
+
convergence_result["reason"] = "time_limit_exceeded"
|
1039
|
+
convergence_result["confidence"] = (
|
1040
|
+
reflection.get("confidence_score", 0.5) if reflection else 0.5
|
1041
|
+
)
|
1042
|
+
|
1043
|
+
# Check custom convergence criteria
|
1044
|
+
custom_criteria = criteria.get("custom_criteria", [])
|
1045
|
+
if custom_criteria and not convergence_result["should_stop"]:
|
1046
|
+
total_custom_weight = 0
|
1047
|
+
custom_stop_score = 0
|
1048
|
+
|
1049
|
+
for custom_criterion in custom_criteria:
|
1050
|
+
if not isinstance(custom_criterion, dict):
|
1051
|
+
continue
|
1052
|
+
|
1053
|
+
criterion_func = custom_criterion.get("function")
|
1054
|
+
criterion_weight = custom_criterion.get("weight", 1.0)
|
1055
|
+
criterion_name = custom_criterion.get("name", "unnamed_custom")
|
1056
|
+
|
1057
|
+
if criterion_func and callable(criterion_func):
|
1058
|
+
try:
|
1059
|
+
# Call custom convergence function
|
1060
|
+
custom_result = criterion_func(
|
1061
|
+
{
|
1062
|
+
"iteration_state": iteration_state,
|
1063
|
+
"previous_iterations": previous_iterations,
|
1064
|
+
"execution_results": execution_results,
|
1065
|
+
"reflection": reflection,
|
1066
|
+
"user_query": user_query,
|
1067
|
+
"global_discoveries": kwargs.get(
|
1068
|
+
"_global_discoveries", {}
|
1069
|
+
),
|
1070
|
+
"total_duration": sum(
|
1071
|
+
(iter_state.end_time - iter_state.start_time)
|
1072
|
+
for iter_state in previous_iterations
|
1073
|
+
+ [iteration_state]
|
1074
|
+
if iter_state.end_time
|
1075
|
+
),
|
1076
|
+
}
|
1077
|
+
)
|
1078
|
+
|
1079
|
+
# Handle different return types
|
1080
|
+
if isinstance(custom_result, bool):
|
1081
|
+
criterion_score = 1.0 if custom_result else 0.0
|
1082
|
+
elif isinstance(custom_result, (int, float)):
|
1083
|
+
criterion_score = float(custom_result)
|
1084
|
+
elif isinstance(custom_result, dict):
|
1085
|
+
criterion_score = custom_result.get("score", 0.0)
|
1086
|
+
# If custom function says stop immediately
|
1087
|
+
if custom_result.get("stop_immediately", False):
|
1088
|
+
convergence_result["should_stop"] = True
|
1089
|
+
convergence_result["reason"] = (
|
1090
|
+
f"custom_criterion_{criterion_name}_immediate_stop"
|
1091
|
+
)
|
1092
|
+
convergence_result["confidence"] = custom_result.get(
|
1093
|
+
"confidence", 0.8
|
1094
|
+
)
|
1095
|
+
convergence_result["criteria_met"][
|
1096
|
+
f"custom_{criterion_name}"
|
1097
|
+
] = True
|
1098
|
+
return convergence_result
|
1099
|
+
else:
|
1100
|
+
criterion_score = 0.0
|
1101
|
+
|
1102
|
+
# Accumulate weighted score
|
1103
|
+
custom_stop_score += criterion_score * criterion_weight
|
1104
|
+
total_custom_weight += criterion_weight
|
1105
|
+
convergence_result["criteria_met"][
|
1106
|
+
f"custom_{criterion_name}"
|
1107
|
+
] = (criterion_score > 0.5)
|
1108
|
+
|
1109
|
+
except Exception as e:
|
1110
|
+
self.logger.warning(
|
1111
|
+
f"Custom convergence criterion '{criterion_name}' failed: {e}"
|
1112
|
+
)
|
1113
|
+
|
1114
|
+
# Check if weighted custom criteria suggest stopping
|
1115
|
+
if total_custom_weight > 0:
|
1116
|
+
avg_custom_score = custom_stop_score / total_custom_weight
|
1117
|
+
if avg_custom_score >= 0.8: # High confidence from custom criteria
|
1118
|
+
convergence_result["should_stop"] = True
|
1119
|
+
convergence_result["reason"] = "custom_criteria_consensus"
|
1120
|
+
convergence_result["confidence"] = avg_custom_score
|
1121
|
+
return convergence_result
|
1122
|
+
|
1123
|
+
# Add recommendations for next iteration if not stopping
|
1124
|
+
if not convergence_result["should_stop"] and reflection:
|
1125
|
+
confidence = reflection.get("confidence_score", 0)
|
1126
|
+
if confidence < 0.6:
|
1127
|
+
convergence_result["recommendations"].append(
|
1128
|
+
"Focus on gathering more comprehensive data"
|
1129
|
+
)
|
1130
|
+
if execution_results.get("errors"):
|
1131
|
+
convergence_result["recommendations"].append(
|
1132
|
+
"Improve tool selection and error handling"
|
1133
|
+
)
|
1134
|
+
if len(execution_results.get("intermediate_results", [])) < 2:
|
1135
|
+
convergence_result["recommendations"].append(
|
1136
|
+
"Execute more analysis steps for thoroughness"
|
1137
|
+
)
|
1138
|
+
|
1139
|
+
return convergence_result
|
1140
|
+
|
1141
|
+
def _phase_synthesis(
|
1142
|
+
self,
|
1143
|
+
kwargs: Dict[str, Any],
|
1144
|
+
iterations: List[IterationState],
|
1145
|
+
global_discoveries: Dict[str, Any],
|
1146
|
+
) -> str:
|
1147
|
+
"""Phase 6: Synthesize results from all iterations into final response."""
|
1148
|
+
messages = kwargs.get("messages", [])
|
1149
|
+
user_query = ""
|
1150
|
+
for msg in reversed(messages):
|
1151
|
+
if msg.get("role") == "user":
|
1152
|
+
user_query = msg.get("content", "")
|
1153
|
+
break
|
1154
|
+
|
1155
|
+
# Collect all execution results
|
1156
|
+
all_results = []
|
1157
|
+
all_insights = []
|
1158
|
+
|
1159
|
+
for iteration in iterations:
|
1160
|
+
if iteration.success and iteration.execution_results:
|
1161
|
+
results = iteration.execution_results.get("intermediate_results", [])
|
1162
|
+
all_results.extend(results)
|
1163
|
+
|
1164
|
+
if iteration.reflection:
|
1165
|
+
goals_achieved = iteration.reflection.get("goal_progress", {}).get(
|
1166
|
+
"goals_achieved", []
|
1167
|
+
)
|
1168
|
+
all_insights.extend(goals_achieved)
|
1169
|
+
|
1170
|
+
# Create synthesized response
|
1171
|
+
synthesis = f"## Analysis Results for: {user_query}\n\n"
|
1172
|
+
|
1173
|
+
if all_results:
|
1174
|
+
synthesis += "### Key Findings:\n"
|
1175
|
+
for i, result in enumerate(all_results[:5], 1): # Limit to top 5 results
|
1176
|
+
synthesis += f"{i}. {result}\n"
|
1177
|
+
synthesis += "\n"
|
1178
|
+
|
1179
|
+
# Add iteration summary
|
1180
|
+
synthesis += "### Process Summary:\n"
|
1181
|
+
synthesis += f"- Completed {len(iterations)} iterations\n"
|
1182
|
+
synthesis += f"- Discovered {len(global_discoveries.get('tools', {}))} tools and {len(global_discoveries.get('resources', {}))} resources\n"
|
1183
|
+
|
1184
|
+
successful_iterations = sum(1 for it in iterations if it.success)
|
1185
|
+
synthesis += (
|
1186
|
+
f"- {successful_iterations}/{len(iterations)} iterations successful\n\n"
|
1187
|
+
)
|
1188
|
+
|
1189
|
+
# Add confidence and evidence
|
1190
|
+
final_confidence = 0.8 # Mock final confidence
|
1191
|
+
synthesis += f"### Confidence: {final_confidence:.1%}\n"
|
1192
|
+
synthesis += f"Based on analysis using {len(global_discoveries.get('tools', {}))} MCP tools and comprehensive iterative processing.\n\n"
|
1193
|
+
|
1194
|
+
# Add recommendations if analysis-focused
|
1195
|
+
if "analyze" in user_query.lower() or "recommend" in user_query.lower():
|
1196
|
+
synthesis += "### Recommendations:\n"
|
1197
|
+
synthesis += (
|
1198
|
+
"1. Continue monitoring key metrics identified in this analysis\n"
|
1199
|
+
)
|
1200
|
+
synthesis += "2. Consider implementing suggested improvements\n"
|
1201
|
+
synthesis += "3. Review findings with stakeholders for validation\n"
|
1202
|
+
|
1203
|
+
return synthesis
|
1204
|
+
|
1205
|
+
def _update_global_discoveries(
|
1206
|
+
self, global_discoveries: Dict[str, Any], new_discoveries: Dict[str, Any]
|
1207
|
+
) -> None:
|
1208
|
+
"""Update global discoveries with new findings."""
|
1209
|
+
# Update servers
|
1210
|
+
for server in new_discoveries.get("new_servers", []):
|
1211
|
+
global_discoveries["servers"][server["id"]] = server
|
1212
|
+
|
1213
|
+
# Update tools
|
1214
|
+
for tool in new_discoveries.get("new_tools", []):
|
1215
|
+
tool_name = tool.get("name") if isinstance(tool, dict) else str(tool)
|
1216
|
+
if isinstance(tool, dict) and "function" in tool:
|
1217
|
+
tool_name = tool["function"].get("name", tool_name)
|
1218
|
+
global_discoveries["tools"][tool_name] = tool
|
1219
|
+
|
1220
|
+
# Update resources
|
1221
|
+
for resource in new_discoveries.get("new_resources", []):
|
1222
|
+
resource_uri = resource.get("uri", str(resource))
|
1223
|
+
global_discoveries["resources"][resource_uri] = resource
|
1224
|
+
|
1225
|
+
# Update capabilities
|
1226
|
+
for capability in new_discoveries.get("tool_capabilities", []):
|
1227
|
+
cap_name = capability.get("name", "unknown")
|
1228
|
+
global_discoveries["capabilities"][cap_name] = capability
|
1229
|
+
|
1230
|
+
def _adapt_strategy(
|
1231
|
+
self,
|
1232
|
+
kwargs: Dict[str, Any],
|
1233
|
+
iteration_state: IterationState,
|
1234
|
+
previous_iterations: List[IterationState],
|
1235
|
+
) -> None:
|
1236
|
+
"""Adapt strategy for next iteration based on results."""
|
1237
|
+
# Simple adaptation logic (in real implementation, use more sophisticated ML)
|
1238
|
+
if iteration_state.reflection:
|
1239
|
+
confidence = iteration_state.reflection.get("confidence_score", 0.5)
|
1240
|
+
|
1241
|
+
# If confidence is low, suggest more thorough discovery
|
1242
|
+
if confidence < 0.6:
|
1243
|
+
kwargs["discovery_mode"] = "exhaustive"
|
1244
|
+
|
1245
|
+
# If errors occurred, reduce timeout for faster iteration
|
1246
|
+
if iteration_state.execution_results.get("errors"):
|
1247
|
+
kwargs["iteration_timeout"] = min(
|
1248
|
+
kwargs.get("iteration_timeout", 300), 180
|
1249
|
+
)
|
1250
|
+
|
1251
|
+
def _calculate_resource_usage(
|
1252
|
+
self, iterations: List[IterationState]
|
1253
|
+
) -> Dict[str, Any]:
|
1254
|
+
"""Calculate resource usage across all iterations."""
|
1255
|
+
total_duration = sum(
|
1256
|
+
(iter_state.end_time - iter_state.start_time)
|
1257
|
+
for iter_state in iterations
|
1258
|
+
if iter_state.end_time
|
1259
|
+
)
|
1260
|
+
|
1261
|
+
total_tools_used = 0
|
1262
|
+
total_api_calls = 0
|
1263
|
+
|
1264
|
+
for iteration in iterations:
|
1265
|
+
if iteration.execution_results:
|
1266
|
+
total_tools_used += len(
|
1267
|
+
iteration.execution_results.get("tool_outputs", {})
|
1268
|
+
)
|
1269
|
+
total_api_calls += len(
|
1270
|
+
iteration.execution_results.get("steps_completed", [])
|
1271
|
+
)
|
1272
|
+
|
1273
|
+
return {
|
1274
|
+
"total_duration_seconds": total_duration,
|
1275
|
+
"total_iterations": len(iterations),
|
1276
|
+
"total_tools_used": total_tools_used,
|
1277
|
+
"total_api_calls": total_api_calls,
|
1278
|
+
"average_iteration_time": total_duration / max(len(iterations), 1),
|
1279
|
+
"estimated_cost_usd": total_api_calls * 0.01, # Mock cost calculation
|
1280
|
+
}
|