omnimancer-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. omnimancer/__init__.py +42 -0
  2. omnimancer/__main__.py +9 -0
  3. omnimancer/cli/__init__.py +8 -0
  4. omnimancer/cli/agent_manager.py +826 -0
  5. omnimancer/cli/agent_wizard.py +944 -0
  6. omnimancer/cli/approval_formatter.py +734 -0
  7. omnimancer/cli/approval_integration.py +734 -0
  8. omnimancer/cli/approval_prompt.py +1286 -0
  9. omnimancer/cli/batch_approval_display.py +617 -0
  10. omnimancer/cli/batch_approval_filters.py +729 -0
  11. omnimancer/cli/batch_operation_monitor.py +775 -0
  12. omnimancer/cli/commands/performance_cmd.py +624 -0
  13. omnimancer/cli/commands.py +757 -0
  14. omnimancer/cli/completion_handler.py +281 -0
  15. omnimancer/cli/config_setup_wizard.py +783 -0
  16. omnimancer/cli/dynamic_commands.py +371 -0
  17. omnimancer/cli/handlers/__init__.py +18 -0
  18. omnimancer/cli/handlers/agent_handler.py +865 -0
  19. omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
  20. omnimancer/cli/handlers/custom_agent_handler.py +352 -0
  21. omnimancer/cli/handlers/permissions_handler.py +1609 -0
  22. omnimancer/cli/interface.py +6695 -0
  23. omnimancer/cli/performance_dashboard.py +612 -0
  24. omnimancer/core/__init__.py +34 -0
  25. omnimancer/core/agent/__init__.py +256 -0
  26. omnimancer/core/agent/agent_config.py +715 -0
  27. omnimancer/core/agent/agent_switcher.py +668 -0
  28. omnimancer/core/agent/approval_context.py +460 -0
  29. omnimancer/core/agent/approval_dialog.py +902 -0
  30. omnimancer/core/agent/approval_interface.py +628 -0
  31. omnimancer/core/agent/approval_manager.py +760 -0
  32. omnimancer/core/agent/cli_integration.py +470 -0
  33. omnimancer/core/agent/config.py +845 -0
  34. omnimancer/core/agent/custom_agent_integration.py +654 -0
  35. omnimancer/core/agent/diff_renderer.py +706 -0
  36. omnimancer/core/agent/file_content_display.py +503 -0
  37. omnimancer/core/agent/file_modification_workflow.py +658 -0
  38. omnimancer/core/agent/file_system_manager.py +1790 -0
  39. omnimancer/core/agent/input_handler.py +614 -0
  40. omnimancer/core/agent/metrics_collector.py +771 -0
  41. omnimancer/core/agent/optimization_engine.py +729 -0
  42. omnimancer/core/agent/performance_monitor.py +717 -0
  43. omnimancer/core/agent/persona.py +870 -0
  44. omnimancer/core/agent/persona_validator.py +672 -0
  45. omnimancer/core/agent/program_executor.py +748 -0
  46. omnimancer/core/agent/proposed_changes_integration.py +663 -0
  47. omnimancer/core/agent/read_before_write_errors.py +492 -0
  48. omnimancer/core/agent/read_before_write_ui.py +647 -0
  49. omnimancer/core/agent/rich_renderer.py +763 -0
  50. omnimancer/core/agent/status_config.py +238 -0
  51. omnimancer/core/agent/status_core.py +446 -0
  52. omnimancer/core/agent/status_integrated_engine.py +540 -0
  53. omnimancer/core/agent/status_manager.py +878 -0
  54. omnimancer/core/agent/status_ui_components.py +535 -0
  55. omnimancer/core/agent/template_applicator.py +548 -0
  56. omnimancer/core/agent/terminal_status_listener.py +394 -0
  57. omnimancer/core/agent/token_tracker.py +679 -0
  58. omnimancer/core/agent/types.py +60 -0
  59. omnimancer/core/agent/web_client.py +745 -0
  60. omnimancer/core/agent/workflow_orchestrator.py +826 -0
  61. omnimancer/core/agent_engine.py +1625 -0
  62. omnimancer/core/agent_mode_manager.py +706 -0
  63. omnimancer/core/agent_progress_ui.py +545 -0
  64. omnimancer/core/chat_manager.py +141 -0
  65. omnimancer/core/config_manager.py +2199 -0
  66. omnimancer/core/config_migration.py +772 -0
  67. omnimancer/core/config_provider.py +1974 -0
  68. omnimancer/core/config_security_validator.py +393 -0
  69. omnimancer/core/config_validator.py +768 -0
  70. omnimancer/core/conversation_manager.py +352 -0
  71. omnimancer/core/engine.py +787 -0
  72. omnimancer/core/enhanced_input.py +523 -0
  73. omnimancer/core/env_loader.py +83 -0
  74. omnimancer/core/fallback_manager.py +568 -0
  75. omnimancer/core/health_monitor.py +462 -0
  76. omnimancer/core/history_manager.py +462 -0
  77. omnimancer/core/mcp_integration_layer.py +878 -0
  78. omnimancer/core/models.py +1889 -0
  79. omnimancer/core/provider_capabilities.py +403 -0
  80. omnimancer/core/provider_initializer.py +450 -0
  81. omnimancer/core/provider_registry.py +432 -0
  82. omnimancer/core/security/__init__.py +16 -0
  83. omnimancer/core/security/approval_workflow.py +406 -0
  84. omnimancer/core/security/audit_logger.py +511 -0
  85. omnimancer/core/security/permission_controller.py +511 -0
  86. omnimancer/core/security/sandbox_manager.py +379 -0
  87. omnimancer/core/security/security_manager.py +530 -0
  88. omnimancer/core/setup_wizard.py +345 -0
  89. omnimancer/core/setup_wizard_core.py +449 -0
  90. omnimancer/core/setup_wizard_guide_generator.py +376 -0
  91. omnimancer/core/setup_wizard_provider_setup.py +454 -0
  92. omnimancer/core/setup_wizard_ui.py +317 -0
  93. omnimancer/core/setup_wizard_validation.py +371 -0
  94. omnimancer/core/signal_handler.py +174 -0
  95. omnimancer/mcp/__init__.py +11 -0
  96. omnimancer/mcp/client.py +725 -0
  97. omnimancer/mcp/manager.py +649 -0
  98. omnimancer/providers/__init__.py +24 -0
  99. omnimancer/providers/azure.py +608 -0
  100. omnimancer/providers/base.py +262 -0
  101. omnimancer/providers/bedrock.py +711 -0
  102. omnimancer/providers/claude.py +426 -0
  103. omnimancer/providers/claude_code.py +406 -0
  104. omnimancer/providers/cohere.py +412 -0
  105. omnimancer/providers/factory.py +988 -0
  106. omnimancer/providers/gemini.py +708 -0
  107. omnimancer/providers/mistral.py +554 -0
  108. omnimancer/providers/ollama.py +510 -0
  109. omnimancer/providers/openai.py +362 -0
  110. omnimancer/providers/openrouter.py +709 -0
  111. omnimancer/providers/perplexity.py +476 -0
  112. omnimancer/providers/vertex.py +617 -0
  113. omnimancer/providers/xai.py +484 -0
  114. omnimancer/ui/__init__.py +6 -0
  115. omnimancer/ui/cancellation_handler.py +235 -0
  116. omnimancer/ui/progress_indicator.py +318 -0
  117. omnimancer/utils/__init__.py +22 -0
  118. omnimancer/utils/error_handler.py +589 -0
  119. omnimancer/utils/errors.py +667 -0
  120. omnimancer/utils/retry.py +238 -0
  121. omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
  122. omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
  123. omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
  124. omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
  125. omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  126. omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,878 @@
1
+ """
2
+ Enhanced MCP Server Integration Layer for Omnimancer Agent.
3
+
4
+ This module provides advanced MCP server integration with capability matching,
5
+ tool discovery, result processing, and context-aware execution.
6
+ """
7
+
8
+ import asyncio
9
+ import json
10
+ import logging
11
+ import time
12
+ from collections import defaultdict
13
+ from dataclasses import dataclass, field
14
+ from enum import Enum
15
+ from typing import Any, Dict, List, Optional, Set, Tuple
16
+
17
+ from ..core.models import ToolDefinition
18
+ from ..mcp.manager import MCPManager
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class ToolCapability(Enum):
24
+ """Tool capability categories for matching."""
25
+
26
+ FILE_OPERATIONS = "file_operations"
27
+ WEB_REQUESTS = "web_requests"
28
+ DATA_PROCESSING = "data_processing"
29
+ SYSTEM_INFO = "system_info"
30
+ CODE_EXECUTION = "code_execution"
31
+ DATABASE = "database"
32
+ API_INTEGRATION = "api_integration"
33
+ SEARCH = "search"
34
+ COMMUNICATION = "communication"
35
+ MULTIMEDIA = "multimedia"
36
+ UNKNOWN = "unknown"
37
+
38
+
39
+ class ExecutionPriority(Enum):
40
+ """Priority levels for tool execution."""
41
+
42
+ CRITICAL = 1
43
+ HIGH = 2
44
+ NORMAL = 3
45
+ LOW = 4
46
+ BACKGROUND = 5
47
+
48
+
49
+ @dataclass
50
+ class ToolExecutionContext:
51
+ """Context information for tool execution."""
52
+
53
+ user_id: Optional[str] = None
54
+ session_id: Optional[str] = None
55
+ conversation_history: List[Dict[str, Any]] = field(default_factory=list)
56
+ task_context: Optional[str] = None
57
+ execution_priority: ExecutionPriority = ExecutionPriority.NORMAL
58
+ timeout_seconds: float = 30.0
59
+ retry_attempts: int = 3
60
+ metadata: Dict[str, Any] = field(default_factory=dict)
61
+
62
+
63
+ @dataclass
64
+ class ToolExecutionResult:
65
+ """Enhanced result from tool execution."""
66
+
67
+ success: bool
68
+ data: Optional[Any] = None
69
+ error: Optional[str] = None
70
+ execution_time: float = 0.0
71
+ server_name: Optional[str] = None
72
+ tool_name: Optional[str] = None
73
+ attempt_count: int = 1
74
+ context_used: bool = False
75
+ metadata: Dict[str, Any] = field(default_factory=dict)
76
+
77
+
78
+ @dataclass
79
+ class ToolMetrics:
80
+ """Metrics for tool performance tracking."""
81
+
82
+ total_executions: int = 0
83
+ successful_executions: int = 0
84
+ failed_executions: int = 0
85
+ average_execution_time: float = 0.0
86
+ last_execution_time: Optional[float] = None
87
+ last_success_time: Optional[float] = None
88
+ last_failure_time: Optional[float] = None
89
+ consecutive_failures: int = 0
90
+ error_patterns: Dict[str, int] = field(default_factory=dict)
91
+
92
+ @property
93
+ def success_rate(self) -> float:
94
+ """Calculate success rate."""
95
+ if self.total_executions == 0:
96
+ return 1.0
97
+ return self.successful_executions / self.total_executions
98
+
99
+ @property
100
+ def reliability_score(self) -> float:
101
+ """Calculate reliability score (0.0 to 1.0)."""
102
+ base_score = self.success_rate
103
+
104
+ # Penalize consecutive failures
105
+ failure_penalty = min(self.consecutive_failures * 0.1, 0.5)
106
+
107
+ # Bonus for recent success
108
+ recency_bonus = 0.0
109
+ if self.last_success_time and time.time() - self.last_success_time < 300:
110
+ recency_bonus = 0.1
111
+
112
+ return max(0.0, min(1.0, base_score - failure_penalty + recency_bonus))
113
+
114
+
115
+ class EnhancedMCPIntegrator:
116
+ """
117
+ Enhanced MCP integration layer with advanced capabilities including
118
+ tool discovery, capability matching, context-aware execution, and performance monitoring.
119
+ """
120
+
121
+ def __init__(self, mcp_manager: Optional[MCPManager] = None):
122
+ """
123
+ Initialize the enhanced MCP integrator.
124
+
125
+ Args:
126
+ mcp_manager: Optional MCP manager instance
127
+ """
128
+ self.mcp_manager = mcp_manager
129
+
130
+ # Tool discovery and caching
131
+ self.discovered_tools: Dict[str, ToolDefinition] = {}
132
+ self.tools_by_capability: Dict[ToolCapability, List[str]] = defaultdict(list)
133
+ self.tools_by_server: Dict[str, List[str]] = defaultdict(list)
134
+ self.tool_capabilities: Dict[str, Set[ToolCapability]] = {}
135
+
136
+ # Performance monitoring
137
+ self.tool_metrics: Dict[str, ToolMetrics] = {}
138
+ self.server_metrics: Dict[str, ToolMetrics] = {}
139
+
140
+ # Execution management
141
+ self.execution_queue: Dict[ExecutionPriority, List[Dict[str, Any]]] = (
142
+ defaultdict(list)
143
+ )
144
+ self.concurrent_executions: Dict[str, asyncio.Task] = {}
145
+ self.max_concurrent_executions = 5
146
+
147
+ # Caching
148
+ self.result_cache: Dict[str, Tuple[ToolExecutionResult, float]] = {}
149
+ self.cache_ttl = 300 # 5 minutes
150
+ self.cache_enabled = True
151
+
152
+ # Last discovery time
153
+ self.last_discovery_time = 0
154
+ self.discovery_interval = 60 # 1 minute
155
+
156
+ async def initialize(self) -> bool:
157
+ """
158
+ Initialize the MCP integrator and discover available tools.
159
+
160
+ Returns:
161
+ True if initialization was successful
162
+ """
163
+ if not self.mcp_manager or not self.mcp_manager.initialized:
164
+ logger.warning("MCP manager not available or not initialized")
165
+ return False
166
+
167
+ try:
168
+ await self.discover_tools()
169
+ logger.info(
170
+ f"MCP integrator initialized with {len(self.discovered_tools)} tools"
171
+ )
172
+ return True
173
+ except Exception as e:
174
+ logger.error(f"Failed to initialize MCP integrator: {e}")
175
+ return False
176
+
177
+ async def discover_tools(
178
+ self, force_refresh: bool = False
179
+ ) -> Dict[str, ToolDefinition]:
180
+ """
181
+ Discover and categorize available tools from all MCP servers.
182
+
183
+ Args:
184
+ force_refresh: Force rediscovery even if recently done
185
+
186
+ Returns:
187
+ Dictionary of discovered tools
188
+ """
189
+ current_time = time.time()
190
+
191
+ # Check if discovery is needed
192
+ if (
193
+ not force_refresh
194
+ and current_time - self.last_discovery_time < self.discovery_interval
195
+ and self.discovered_tools
196
+ ):
197
+ return self.discovered_tools
198
+
199
+ if not self.mcp_manager or not self.mcp_manager.initialized:
200
+ logger.warning("Cannot discover tools: MCP manager not available")
201
+ return {}
202
+
203
+ try:
204
+ logger.debug("Discovering MCP tools...")
205
+
206
+ # Get all available tools
207
+ available_tools = await self.mcp_manager.get_available_tools()
208
+
209
+ # Clear previous discoveries
210
+ self.discovered_tools.clear()
211
+ self.tools_by_capability.clear()
212
+ self.tools_by_server.clear()
213
+ self.tool_capabilities.clear()
214
+
215
+ # Process each tool
216
+ for tool in available_tools:
217
+ # Store tool definition
218
+ self.discovered_tools[tool.name] = tool
219
+
220
+ # Determine capabilities
221
+ capabilities = self._analyze_tool_capabilities(tool)
222
+ self.tool_capabilities[tool.name] = capabilities
223
+
224
+ # Categorize by capability
225
+ for capability in capabilities:
226
+ self.tools_by_capability[capability].append(tool.name)
227
+
228
+ # Find server for this tool
229
+ server_name = self._find_tool_server(tool.name)
230
+ if server_name:
231
+ self.tools_by_server[server_name].append(tool.name)
232
+
233
+ # Initialize metrics if not exists
234
+ if tool.name not in self.tool_metrics:
235
+ self.tool_metrics[tool.name] = ToolMetrics()
236
+
237
+ self.last_discovery_time = current_time
238
+
239
+ logger.info(
240
+ f"Discovered {len(self.discovered_tools)} tools across "
241
+ f"{len(self.tools_by_server)} servers"
242
+ )
243
+
244
+ # Log capability distribution
245
+ for capability, tools in self.tools_by_capability.items():
246
+ if tools:
247
+ logger.debug(f"{capability.value}: {len(tools)} tools")
248
+
249
+ return self.discovered_tools
250
+
251
+ except Exception as e:
252
+ logger.error(f"Tool discovery failed: {e}")
253
+ return {}
254
+
255
+ def _analyze_tool_capabilities(self, tool: ToolDefinition) -> Set[ToolCapability]:
256
+ """
257
+ Analyze a tool's capabilities based on its name, description, and parameters.
258
+
259
+ Args:
260
+ tool: Tool definition to analyze
261
+
262
+ Returns:
263
+ Set of identified capabilities
264
+ """
265
+ capabilities = set()
266
+
267
+ # Analyze based on tool name and description
268
+ text_to_analyze = f"{tool.name} {getattr(tool, 'description', '')}"
269
+ text_lower = text_to_analyze.lower()
270
+
271
+ # File operations
272
+ if any(
273
+ keyword in text_lower
274
+ for keyword in [
275
+ "file",
276
+ "read",
277
+ "write",
278
+ "directory",
279
+ "folder",
280
+ "path",
281
+ "save",
282
+ "load",
283
+ ]
284
+ ):
285
+ capabilities.add(ToolCapability.FILE_OPERATIONS)
286
+
287
+ # Web requests
288
+ if any(
289
+ keyword in text_lower
290
+ for keyword in [
291
+ "http",
292
+ "web",
293
+ "request",
294
+ "api",
295
+ "url",
296
+ "fetch",
297
+ "download",
298
+ "upload",
299
+ ]
300
+ ):
301
+ capabilities.add(ToolCapability.WEB_REQUESTS)
302
+
303
+ # Data processing
304
+ if any(
305
+ keyword in text_lower
306
+ for keyword in [
307
+ "data",
308
+ "process",
309
+ "parse",
310
+ "transform",
311
+ "convert",
312
+ "analyze",
313
+ "filter",
314
+ ]
315
+ ):
316
+ capabilities.add(ToolCapability.DATA_PROCESSING)
317
+
318
+ # System info
319
+ if any(
320
+ keyword in text_lower
321
+ for keyword in [
322
+ "system",
323
+ "info",
324
+ "status",
325
+ "monitor",
326
+ "health",
327
+ "performance",
328
+ ]
329
+ ):
330
+ capabilities.add(ToolCapability.SYSTEM_INFO)
331
+
332
+ # Code execution
333
+ if any(
334
+ keyword in text_lower
335
+ for keyword in [
336
+ "execute",
337
+ "run",
338
+ "code",
339
+ "script",
340
+ "command",
341
+ "shell",
342
+ "bash",
343
+ ]
344
+ ):
345
+ capabilities.add(ToolCapability.CODE_EXECUTION)
346
+
347
+ # Database
348
+ if any(
349
+ keyword in text_lower
350
+ for keyword in [
351
+ "database",
352
+ "db",
353
+ "sql",
354
+ "query",
355
+ "table",
356
+ "record",
357
+ ]
358
+ ):
359
+ capabilities.add(ToolCapability.DATABASE)
360
+
361
+ # API integration
362
+ if any(
363
+ keyword in text_lower
364
+ for keyword in [
365
+ "api",
366
+ "integration",
367
+ "service",
368
+ "endpoint",
369
+ "rest",
370
+ "graphql",
371
+ ]
372
+ ):
373
+ capabilities.add(ToolCapability.API_INTEGRATION)
374
+
375
+ # Search
376
+ if any(
377
+ keyword in text_lower
378
+ for keyword in ["search", "find", "lookup", "query", "index"]
379
+ ):
380
+ capabilities.add(ToolCapability.SEARCH)
381
+
382
+ # Communication
383
+ if any(
384
+ keyword in text_lower
385
+ for keyword in [
386
+ "email",
387
+ "message",
388
+ "notification",
389
+ "send",
390
+ "communicate",
391
+ ]
392
+ ):
393
+ capabilities.add(ToolCapability.COMMUNICATION)
394
+
395
+ # Multimedia
396
+ if any(
397
+ keyword in text_lower
398
+ for keyword in [
399
+ "image",
400
+ "video",
401
+ "audio",
402
+ "media",
403
+ "picture",
404
+ "photo",
405
+ ]
406
+ ):
407
+ capabilities.add(ToolCapability.MULTIMEDIA)
408
+
409
+ # Default to unknown if no capabilities identified
410
+ if not capabilities:
411
+ capabilities.add(ToolCapability.UNKNOWN)
412
+
413
+ return capabilities
414
+
415
+ def _find_tool_server(self, tool_name: str) -> Optional[str]:
416
+ """Find which server provides a specific tool."""
417
+ if not self.mcp_manager:
418
+ return None
419
+
420
+ return self.mcp_manager._find_tool_server(tool_name)
421
+
422
+ async def execute_tool_with_context(
423
+ self,
424
+ tool_name: str,
425
+ arguments: Dict[str, Any],
426
+ context: Optional[ToolExecutionContext] = None,
427
+ ) -> ToolExecutionResult:
428
+ """
429
+ Execute a tool with context awareness and enhanced error handling.
430
+
431
+ Args:
432
+ tool_name: Name of the tool to execute
433
+ arguments: Arguments to pass to the tool
434
+ context: Execution context
435
+
436
+ Returns:
437
+ Enhanced execution result
438
+ """
439
+ context = context or ToolExecutionContext()
440
+ start_time = time.time()
441
+
442
+ # Check cache first
443
+ if self.cache_enabled:
444
+ cached_result = self._get_cached_result(tool_name, arguments)
445
+ if cached_result:
446
+ logger.debug(f"Using cached result for tool: {tool_name}")
447
+ return cached_result
448
+
449
+ # Ensure tool is discovered
450
+ if tool_name not in self.discovered_tools:
451
+ await self.discover_tools()
452
+
453
+ if tool_name not in self.discovered_tools:
454
+ return ToolExecutionResult(
455
+ success=False,
456
+ error=f"Tool '{tool_name}' not found",
457
+ execution_time=time.time() - start_time,
458
+ tool_name=tool_name,
459
+ )
460
+
461
+ # Get server name
462
+ server_name = self._find_tool_server(tool_name)
463
+
464
+ # Execute with retries
465
+ last_error = None
466
+ attempt_count = 0
467
+
468
+ for attempt in range(context.retry_attempts):
469
+ attempt_count += 1
470
+
471
+ try:
472
+ # Add context to arguments if applicable
473
+ enhanced_args = self._enhance_arguments_with_context(arguments, context)
474
+
475
+ # Execute the tool
476
+ execution_start = time.time()
477
+
478
+ # Use timeout
479
+ result = await asyncio.wait_for(
480
+ self.mcp_manager.execute_tool(tool_name, enhanced_args),
481
+ timeout=context.timeout_seconds,
482
+ )
483
+
484
+ execution_time = time.time() - execution_start
485
+
486
+ # Create success result
487
+ success_result = ToolExecutionResult(
488
+ success=True,
489
+ data=(result.content if hasattr(result, "content") else result),
490
+ execution_time=execution_time,
491
+ server_name=server_name,
492
+ tool_name=tool_name,
493
+ attempt_count=attempt_count,
494
+ context_used=bool(enhanced_args != arguments),
495
+ metadata={
496
+ "server_response_time": execution_time,
497
+ "context_priority": context.execution_priority.name,
498
+ "retry_attempt": attempt_count,
499
+ },
500
+ )
501
+
502
+ # Record success metrics
503
+ self._record_execution_metrics(
504
+ tool_name, server_name, True, execution_time
505
+ )
506
+
507
+ # Cache result if applicable
508
+ if self.cache_enabled and self._is_cacheable(tool_name, arguments):
509
+ self._cache_result(tool_name, arguments, success_result)
510
+
511
+ return success_result
512
+
513
+ except asyncio.TimeoutError:
514
+ last_error = (
515
+ f"Tool execution timed out after {context.timeout_seconds}s"
516
+ )
517
+ logger.warning(f"Tool {tool_name} timed out on attempt {attempt + 1}")
518
+
519
+ except Exception as e:
520
+ last_error = str(e)
521
+ logger.warning(f"Tool {tool_name} failed on attempt {attempt + 1}: {e}")
522
+
523
+ # Don't retry for certain errors
524
+ if self._is_non_retryable_error(e):
525
+ break
526
+
527
+ # Wait before retry with exponential backoff
528
+ if attempt < context.retry_attempts - 1:
529
+ wait_time = min(2**attempt, 10)
530
+ await asyncio.sleep(wait_time)
531
+
532
+ # All attempts failed
533
+ total_execution_time = time.time() - start_time
534
+
535
+ error_result = ToolExecutionResult(
536
+ success=False,
537
+ error=last_error,
538
+ execution_time=total_execution_time,
539
+ server_name=server_name,
540
+ tool_name=tool_name,
541
+ attempt_count=attempt_count,
542
+ metadata={
543
+ "max_retries_reached": True,
544
+ "context_priority": context.execution_priority.name,
545
+ "final_error": last_error,
546
+ },
547
+ )
548
+
549
+ # Record failure metrics
550
+ self._record_execution_metrics(
551
+ tool_name, server_name, False, total_execution_time, last_error
552
+ )
553
+
554
+ return error_result
555
+
556
+ def _enhance_arguments_with_context(
557
+ self, arguments: Dict[str, Any], context: ToolExecutionContext
558
+ ) -> Dict[str, Any]:
559
+ """
560
+ Enhance tool arguments with context information if applicable.
561
+
562
+ Args:
563
+ arguments: Original arguments
564
+ context: Execution context
565
+
566
+ Returns:
567
+ Enhanced arguments
568
+ """
569
+ enhanced_args = arguments.copy()
570
+
571
+ # Add context fields that tools might use
572
+ if context.user_id:
573
+ enhanced_args.setdefault("user_id", context.user_id)
574
+
575
+ if context.session_id:
576
+ enhanced_args.setdefault("session_id", context.session_id)
577
+
578
+ if context.task_context:
579
+ enhanced_args.setdefault("task_context", context.task_context)
580
+
581
+ # Add relevant conversation history for context-aware tools
582
+ if context.conversation_history and len(context.conversation_history) > 0:
583
+ # Only add recent history to avoid overwhelming the tool
584
+ recent_history = context.conversation_history[-5:]
585
+ enhanced_args.setdefault("conversation_context", recent_history)
586
+
587
+ return enhanced_args
588
+
589
+ def _is_non_retryable_error(self, error: Exception) -> bool:
590
+ """Check if an error should not be retried."""
591
+ error_str = str(error).lower()
592
+ non_retryable_patterns = [
593
+ "invalid arguments",
594
+ "permission denied",
595
+ "not found",
596
+ "authentication failed",
597
+ "tool not found",
598
+ ]
599
+
600
+ return any(pattern in error_str for pattern in non_retryable_patterns)
601
+
602
+ def _is_cacheable(self, tool_name: str, arguments: Dict[str, Any]) -> bool:
603
+ """Check if a tool result should be cached."""
604
+ # Don't cache tools that might have side effects
605
+ side_effect_tools = ["write", "delete", "send", "execute", "run"]
606
+ if any(keyword in tool_name.lower() for keyword in side_effect_tools):
607
+ return False
608
+
609
+ # Don't cache if arguments contain dynamic data
610
+ if any(key in arguments for key in ["timestamp", "random", "session_id"]):
611
+ return False
612
+
613
+ return True
614
+
615
+ def _get_cached_result(
616
+ self, tool_name: str, arguments: Dict[str, Any]
617
+ ) -> Optional[ToolExecutionResult]:
618
+ """Get cached result if available and not expired."""
619
+ cache_key = self._generate_cache_key(tool_name, arguments)
620
+
621
+ if cache_key in self.result_cache:
622
+ result, timestamp = self.result_cache[cache_key]
623
+ if time.time() - timestamp < self.cache_ttl:
624
+ return result
625
+ else:
626
+ # Remove expired cache entry
627
+ del self.result_cache[cache_key]
628
+
629
+ return None
630
+
631
+ def _cache_result(
632
+ self,
633
+ tool_name: str,
634
+ arguments: Dict[str, Any],
635
+ result: ToolExecutionResult,
636
+ ):
637
+ """Cache a tool execution result."""
638
+ cache_key = self._generate_cache_key(tool_name, arguments)
639
+ self.result_cache[cache_key] = (result, time.time())
640
+
641
+ # Limit cache size
642
+ if len(self.result_cache) > 100:
643
+ # Remove oldest entries
644
+ sorted_cache = sorted(self.result_cache.items(), key=lambda x: x[1][1])
645
+ for key, _ in sorted_cache[:20]: # Remove 20 oldest entries
646
+ del self.result_cache[key]
647
+
648
+ def _generate_cache_key(self, tool_name: str, arguments: Dict[str, Any]) -> str:
649
+ """Generate a cache key for tool execution."""
650
+ # Sort arguments for consistent key generation
651
+ sorted_args = json.dumps(arguments, sort_keys=True)
652
+ return f"{tool_name}:{hash(sorted_args)}"
653
+
654
+ def _record_execution_metrics(
655
+ self,
656
+ tool_name: str,
657
+ server_name: Optional[str],
658
+ success: bool,
659
+ execution_time: float,
660
+ error: Optional[str] = None,
661
+ ):
662
+ """Record execution metrics for monitoring."""
663
+ current_time = time.time()
664
+
665
+ # Update tool metrics
666
+ if tool_name not in self.tool_metrics:
667
+ self.tool_metrics[tool_name] = ToolMetrics()
668
+
669
+ tool_metrics = self.tool_metrics[tool_name]
670
+ tool_metrics.total_executions += 1
671
+ tool_metrics.last_execution_time = current_time
672
+
673
+ # Update average execution time
674
+ if tool_metrics.average_execution_time == 0:
675
+ tool_metrics.average_execution_time = execution_time
676
+ else:
677
+ tool_metrics.average_execution_time = (
678
+ tool_metrics.average_execution_time * 0.8 + execution_time * 0.2
679
+ )
680
+
681
+ if success:
682
+ tool_metrics.successful_executions += 1
683
+ tool_metrics.last_success_time = current_time
684
+ tool_metrics.consecutive_failures = 0
685
+ else:
686
+ tool_metrics.failed_executions += 1
687
+ tool_metrics.last_failure_time = current_time
688
+ tool_metrics.consecutive_failures += 1
689
+
690
+ # Track error patterns
691
+ if error:
692
+ error_pattern = error[:50] # First 50 chars
693
+ tool_metrics.error_patterns[error_pattern] = (
694
+ tool_metrics.error_patterns.get(error_pattern, 0) + 1
695
+ )
696
+
697
+ # Update server metrics
698
+ if server_name:
699
+ if server_name not in self.server_metrics:
700
+ self.server_metrics[server_name] = ToolMetrics()
701
+
702
+ server_metrics = self.server_metrics[server_name]
703
+ server_metrics.total_executions += 1
704
+ server_metrics.last_execution_time = current_time
705
+
706
+ if success:
707
+ server_metrics.successful_executions += 1
708
+ server_metrics.last_success_time = current_time
709
+ server_metrics.consecutive_failures = 0
710
+ else:
711
+ server_metrics.failed_executions += 1
712
+ server_metrics.last_failure_time = current_time
713
+ server_metrics.consecutive_failures += 1
714
+
715
+ def find_tools_by_capability(self, capability: ToolCapability) -> List[str]:
716
+ """Find tools that have a specific capability."""
717
+ return self.tools_by_capability.get(capability, [])
718
+
719
+ def find_best_tool_for_task(
720
+ self,
721
+ task_description: str,
722
+ capability: Optional[ToolCapability] = None,
723
+ ) -> Optional[str]:
724
+ """
725
+ Find the best tool for a given task based on description and capability.
726
+
727
+ Args:
728
+ task_description: Description of the task
729
+ capability: Optional capability requirement
730
+
731
+ Returns:
732
+ Name of the best matching tool or None
733
+ """
734
+ candidates = []
735
+
736
+ if capability:
737
+ # Filter by capability first
738
+ candidates = self.find_tools_by_capability(capability)
739
+ else:
740
+ # Use all available tools
741
+ candidates = list(self.discovered_tools.keys())
742
+
743
+ if not candidates:
744
+ return None
745
+
746
+ # Score tools based on task description
747
+ task_lower = task_description.lower()
748
+ scored_tools = []
749
+
750
+ for tool_name in candidates:
751
+ tool = self.discovered_tools[tool_name]
752
+ score = 0
753
+
754
+ # Score based on name similarity
755
+ if any(word in tool_name.lower() for word in task_lower.split()):
756
+ score += 2
757
+
758
+ # Score based on description similarity
759
+ description = getattr(tool, "description", "")
760
+ if description:
761
+ desc_lower = description.lower()
762
+ common_words = set(task_lower.split()) & set(desc_lower.split())
763
+ score += len(common_words)
764
+
765
+ # Boost score based on reliability
766
+ metrics = self.tool_metrics.get(tool_name)
767
+ if metrics:
768
+ score += metrics.reliability_score * 5
769
+
770
+ # Penalize if tool has been failing recently
771
+ if metrics.consecutive_failures > 2:
772
+ score -= metrics.consecutive_failures
773
+
774
+ scored_tools.append((tool_name, score))
775
+
776
+ # Return the highest scoring tool
777
+ if scored_tools:
778
+ scored_tools.sort(key=lambda x: x[1], reverse=True)
779
+ return scored_tools[0][0]
780
+
781
+ return None
782
+
783
+ def get_tool_metrics(self, tool_name: Optional[str] = None) -> Dict[str, Any]:
784
+ """Get performance metrics for tools."""
785
+ if tool_name:
786
+ metrics = self.tool_metrics.get(tool_name)
787
+ if metrics:
788
+ return {
789
+ "tool_name": tool_name,
790
+ "total_executions": metrics.total_executions,
791
+ "success_rate": metrics.success_rate,
792
+ "reliability_score": metrics.reliability_score,
793
+ "average_execution_time": metrics.average_execution_time,
794
+ "consecutive_failures": metrics.consecutive_failures,
795
+ "error_patterns": dict(metrics.error_patterns),
796
+ }
797
+ return {}
798
+ else:
799
+ # Return summary for all tools
800
+ summary = {}
801
+ for name, metrics in self.tool_metrics.items():
802
+ summary[name] = {
803
+ "success_rate": metrics.success_rate,
804
+ "reliability_score": metrics.reliability_score,
805
+ "total_executions": metrics.total_executions,
806
+ }
807
+ return summary
808
+
809
+ def get_server_metrics(self) -> Dict[str, Any]:
810
+ """Get performance metrics for servers."""
811
+ summary = {}
812
+ for server_name, metrics in self.server_metrics.items():
813
+ summary[server_name] = {
814
+ "success_rate": metrics.success_rate,
815
+ "reliability_score": metrics.reliability_score,
816
+ "total_executions": metrics.total_executions,
817
+ "tool_count": len(self.tools_by_server.get(server_name, [])),
818
+ }
819
+ return summary
820
+
821
+ def get_capability_summary(self) -> Dict[str, int]:
822
+ """Get summary of tools by capability."""
823
+ return {
824
+ capability.value: len(tools)
825
+ for capability, tools in self.tools_by_capability.items()
826
+ if tools
827
+ }
828
+
829
+ def clear_cache(self):
830
+ """Clear the result cache."""
831
+ self.result_cache.clear()
832
+ logger.info("Tool result cache cleared")
833
+
834
+ def reset_metrics(self, tool_name: Optional[str] = None):
835
+ """Reset performance metrics."""
836
+ if tool_name:
837
+ if tool_name in self.tool_metrics:
838
+ self.tool_metrics[tool_name] = ToolMetrics()
839
+ logger.info(f"Reset metrics for tool: {tool_name}")
840
+ else:
841
+ self.tool_metrics.clear()
842
+ self.server_metrics.clear()
843
+ logger.info("Reset all tool metrics")
844
+
845
+ async def health_check(self) -> Dict[str, Any]:
846
+ """Perform health check on the MCP integration layer."""
847
+ health_status = {
848
+ "integrator_healthy": True,
849
+ "mcp_manager_available": bool(self.mcp_manager),
850
+ "mcp_manager_initialized": bool(
851
+ self.mcp_manager and self.mcp_manager.initialized
852
+ ),
853
+ "discovered_tools_count": len(self.discovered_tools),
854
+ "servers_count": len(self.tools_by_server),
855
+ "cache_size": len(self.result_cache),
856
+ "metrics_tracked_tools": len(self.tool_metrics),
857
+ "last_discovery_time": self.last_discovery_time,
858
+ }
859
+
860
+ # Check if discovery is stale
861
+ if time.time() - self.last_discovery_time > self.discovery_interval * 2:
862
+ health_status["discovery_stale"] = True
863
+ health_status["integrator_healthy"] = False
864
+
865
+ # Check for tools with high failure rates
866
+ problematic_tools = []
867
+ for tool_name, metrics in self.tool_metrics.items():
868
+ if metrics.reliability_score < 0.5 and metrics.total_executions >= 5:
869
+ problematic_tools.append(tool_name)
870
+
871
+ if problematic_tools:
872
+ health_status["problematic_tools"] = problematic_tools
873
+ if (
874
+ len(problematic_tools) > len(self.discovered_tools) * 0.3
875
+ ): # More than 30% problematic
876
+ health_status["integrator_healthy"] = False
877
+
878
+ return health_status