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,589 @@
1
+ """
2
+ Error handling utilities for Omnimancer.
3
+
4
+ This module provides comprehensive error handling, graceful degradation,
5
+ and user-friendly error messages with suggested solutions.
6
+ """
7
+
8
+ import logging
9
+ from datetime import datetime, timedelta
10
+ from typing import Any, Dict, List, Tuple
11
+
12
+ from .errors import (
13
+ AuthenticationError,
14
+ MCPConnectionError,
15
+ MCPServerError,
16
+ MCPTimeoutError,
17
+ ModelNotFoundError,
18
+ NetworkError,
19
+ ProviderConfigurationError,
20
+ ProviderUnavailableError,
21
+ QuotaExceededError,
22
+ RateLimitError,
23
+ )
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class ErrorHandler:
29
+ """
30
+ Centralized error handling and recovery system.
31
+
32
+ This class provides methods for handling different types of errors,
33
+ suggesting solutions, and implementing graceful degradation.
34
+ """
35
+
36
+ def __init__(self):
37
+ """Initialize the error handler."""
38
+ self.error_history: List[Dict[str, Any]] = []
39
+ self.provider_status: Dict[str, Dict[str, Any]] = {}
40
+ self.mcp_server_status: Dict[str, Dict[str, Any]] = {}
41
+
42
+ def handle_provider_error(
43
+ self,
44
+ error: Exception,
45
+ provider_name: str,
46
+ available_providers: List[str] = None,
47
+ ) -> Tuple[str, List[str], bool]:
48
+ """
49
+ Handle provider-specific errors and suggest solutions.
50
+
51
+ Args:
52
+ error: The exception that occurred
53
+ provider_name: Name of the provider that failed
54
+ available_providers: List of available alternative providers
55
+
56
+ Returns:
57
+ Tuple of (error_message, suggestions, should_retry)
58
+ """
59
+ available_providers = available_providers or []
60
+ suggestions = []
61
+ should_retry = False
62
+
63
+ # Record error in history
64
+ self._record_error(error, provider_name, "provider")
65
+
66
+ if isinstance(error, AuthenticationError):
67
+ error_msg = f"Authentication failed for {provider_name}"
68
+ suggestions.extend(
69
+ [
70
+ f"Check your {provider_name} API key configuration",
71
+ f"Verify API key permissions for {provider_name}",
72
+ "Run '/config' to update your API key",
73
+ ]
74
+ )
75
+
76
+ # Suggest alternative providers
77
+ if available_providers:
78
+ working_providers = self._get_working_providers(available_providers)
79
+ if working_providers:
80
+ suggestions.append(
81
+ f"Try switching to: {', '.join(working_providers)}"
82
+ )
83
+
84
+ elif isinstance(error, RateLimitError):
85
+ error_msg = f"Rate limit exceeded for {provider_name}"
86
+ should_retry = True
87
+
88
+ if hasattr(error, "retry_after") and error.retry_after:
89
+ suggestions.append(f"Wait {error.retry_after} seconds before retrying")
90
+ else:
91
+ suggestions.append("Wait a few minutes before retrying")
92
+
93
+ # Suggest alternative providers for immediate use
94
+ if available_providers:
95
+ working_providers = self._get_working_providers(available_providers)
96
+ if working_providers:
97
+ suggestions.append(
98
+ f"Use alternative provider: {', '.join(working_providers)}"
99
+ )
100
+
101
+ elif isinstance(error, QuotaExceededError):
102
+ error_msg = f"Usage quota exceeded for {provider_name}"
103
+
104
+ if hasattr(error, "reset_date") and error.reset_date:
105
+ suggestions.append(f"Quota resets on: {error.reset_date}")
106
+ else:
107
+ suggestions.append("Check your account usage limits")
108
+
109
+ suggestions.append(f"Review {provider_name} billing and usage")
110
+
111
+ # Suggest alternative providers
112
+ if available_providers:
113
+ working_providers = self._get_working_providers(available_providers)
114
+ if working_providers:
115
+ suggestions.append(f"Switch to: {', '.join(working_providers)}")
116
+
117
+ elif isinstance(error, ModelNotFoundError):
118
+ error_msg = f"Model not found or unavailable on {provider_name}"
119
+
120
+ if hasattr(error, "get_suggestions"):
121
+ model_suggestions = error.get_suggestions()
122
+ if model_suggestions:
123
+ suggestions.append(
124
+ f"Try these models: {', '.join(model_suggestions)}"
125
+ )
126
+
127
+ suggestions.extend(
128
+ [
129
+ f"Check available models with '/models {provider_name}'",
130
+ "Verify model name spelling and availability",
131
+ ]
132
+ )
133
+
134
+ elif isinstance(error, NetworkError):
135
+ error_msg = f"Network error connecting to {provider_name}"
136
+ should_retry = True
137
+
138
+ suggestions.extend(
139
+ [
140
+ "Check your internet connection",
141
+ "Verify firewall settings",
142
+ "Try again in a few moments",
143
+ ]
144
+ )
145
+
146
+ # For Ollama specifically
147
+ if provider_name.lower() == "ollama":
148
+ suggestions.extend(
149
+ [
150
+ "Make sure Ollama is running: 'ollama serve'",
151
+ "Check Ollama server URL in configuration",
152
+ ]
153
+ )
154
+
155
+ elif isinstance(error, ProviderUnavailableError):
156
+ error_msg = f"{provider_name} is temporarily unavailable"
157
+ should_retry = True
158
+
159
+ if hasattr(error, "estimated_recovery") and error.estimated_recovery:
160
+ suggestions.append(f"Estimated recovery: {error.estimated_recovery}")
161
+
162
+ if hasattr(error, "fallback_providers") and error.fallback_providers:
163
+ suggestions.append(
164
+ f"Use fallback: {', '.join(error.fallback_providers)}"
165
+ )
166
+
167
+ elif isinstance(error, ProviderConfigurationError):
168
+ error_msg = f"Configuration error for {provider_name}"
169
+
170
+ if hasattr(error, "suggested_fix") and error.suggested_fix:
171
+ suggestions.append(error.suggested_fix)
172
+
173
+ suggestions.extend(
174
+ [
175
+ f"Review {provider_name} configuration",
176
+ "Run '/config' to update settings",
177
+ ]
178
+ )
179
+
180
+ else:
181
+ error_msg = f"Unexpected error with {provider_name}: {str(error)}"
182
+ suggestions.extend(
183
+ [
184
+ "Try again in a few moments",
185
+ "Check provider status and configuration",
186
+ ]
187
+ )
188
+
189
+ if available_providers:
190
+ working_providers = self._get_working_providers(available_providers)
191
+ if working_providers:
192
+ suggestions.append(
193
+ f"Try alternative: {', '.join(working_providers)}"
194
+ )
195
+
196
+ # Update provider status
197
+ self._update_provider_status(provider_name, error)
198
+
199
+ return error_msg, suggestions, should_retry
200
+
201
+ def handle_mcp_error(
202
+ self,
203
+ error: Exception,
204
+ server_name: str = None,
205
+ available_servers: List[str] = None,
206
+ ) -> Tuple[str, List[str], bool]:
207
+ """
208
+ Handle MCP-specific errors and suggest solutions.
209
+
210
+ Args:
211
+ error: The exception that occurred
212
+ server_name: Name of the MCP server that failed
213
+ available_servers: List of available alternative servers
214
+
215
+ Returns:
216
+ Tuple of (error_message, suggestions, should_retry)
217
+ """
218
+ available_servers = available_servers or []
219
+ suggestions = []
220
+ should_retry = False
221
+
222
+ # Record error in history
223
+ self._record_error(error, server_name or "unknown", "mcp")
224
+
225
+ if isinstance(error, MCPConnectionError):
226
+ error_msg = f"Failed to connect to MCP server"
227
+ if server_name:
228
+ error_msg += f": {server_name}"
229
+
230
+ should_retry = True
231
+ suggestions.extend(
232
+ [
233
+ "Check if the MCP server command is correct",
234
+ "Verify server dependencies are installed",
235
+ "Check server logs for startup errors",
236
+ ]
237
+ )
238
+
239
+ if hasattr(error, "retry_suggestion") and error.retry_suggestion:
240
+ suggestions.append(error.retry_suggestion)
241
+
242
+ elif isinstance(error, MCPTimeoutError):
243
+ error_msg = f"MCP server operation timed out"
244
+ if server_name:
245
+ error_msg += f" on {server_name}"
246
+
247
+ should_retry = True
248
+ suggestions.extend(
249
+ [
250
+ "The operation may be taking longer than expected",
251
+ "Try increasing timeout settings",
252
+ "Check if the server is overloaded",
253
+ ]
254
+ )
255
+
256
+ if hasattr(error, "operation") and error.operation:
257
+ suggestions.append(f"Operation '{error.operation}' timed out")
258
+
259
+ elif isinstance(error, MCPServerError):
260
+ error_msg = f"MCP server error"
261
+ if server_name:
262
+ error_msg += f" on {server_name}"
263
+
264
+ suggestions.extend(
265
+ [
266
+ "Check MCP server configuration",
267
+ "Verify server is properly installed",
268
+ "Review server logs for errors",
269
+ ]
270
+ )
271
+
272
+ # Suggest disabling problematic server
273
+ if server_name:
274
+ suggestions.append(
275
+ f"Consider temporarily disabling '{server_name}' server"
276
+ )
277
+
278
+ else:
279
+ error_msg = f"MCP error: {str(error)}"
280
+ suggestions.extend(
281
+ [
282
+ "Check MCP server configuration",
283
+ "Try restarting MCP servers with '/mcp reload'",
284
+ ]
285
+ )
286
+
287
+ # Update MCP server status
288
+ if server_name:
289
+ self._update_mcp_status(server_name, error)
290
+
291
+ return error_msg, suggestions, should_retry
292
+
293
+ def get_graceful_degradation_options(
294
+ self,
295
+ failed_provider: str,
296
+ available_providers: List[str],
297
+ required_capabilities: List[str] = None,
298
+ ) -> Dict[str, Any]:
299
+ """
300
+ Get options for graceful degradation when a provider fails.
301
+
302
+ Args:
303
+ failed_provider: Name of the provider that failed
304
+ available_providers: List of available providers
305
+ required_capabilities: List of required capabilities (e.g., 'tools', 'multimodal')
306
+
307
+ Returns:
308
+ Dictionary with degradation options
309
+ """
310
+ required_capabilities = required_capabilities or []
311
+
312
+ # Filter working providers
313
+ working_providers = self._get_working_providers(available_providers)
314
+
315
+ # Filter by capabilities if specified
316
+ if required_capabilities:
317
+ # This would need to be implemented with actual capability checking
318
+ # For now, we'll assume all providers are capable
319
+ capable_providers = working_providers
320
+ else:
321
+ capable_providers = working_providers
322
+
323
+ degradation_options = {
324
+ "fallback_providers": capable_providers,
325
+ "degraded_functionality": [],
326
+ "recommendations": [],
327
+ }
328
+
329
+ if not capable_providers:
330
+ degradation_options["degraded_functionality"].extend(
331
+ [
332
+ "No alternative providers available",
333
+ "Some features may be unavailable",
334
+ ]
335
+ )
336
+ degradation_options["recommendations"].extend(
337
+ [
338
+ "Check provider configurations",
339
+ "Verify API keys and connectivity",
340
+ ]
341
+ )
342
+ else:
343
+ degradation_options["recommendations"].extend(
344
+ [
345
+ f"Switch to: {', '.join(capable_providers[:2])}",
346
+ "Configure backup providers for reliability",
347
+ ]
348
+ )
349
+
350
+ return degradation_options
351
+
352
+ def should_retry_operation(
353
+ self, error: Exception, attempt_count: int, max_attempts: int = 3
354
+ ) -> Tuple[bool, float]:
355
+ """
356
+ Determine if an operation should be retried and with what delay.
357
+
358
+ Args:
359
+ error: The exception that occurred
360
+ attempt_count: Current attempt number
361
+ max_attempts: Maximum number of attempts
362
+
363
+ Returns:
364
+ Tuple of (should_retry, delay_seconds)
365
+ """
366
+ if attempt_count >= max_attempts:
367
+ return False, 0.0
368
+
369
+ # Exponential backoff base delay
370
+ base_delay = 2 ** (attempt_count - 1)
371
+
372
+ if isinstance(error, RateLimitError):
373
+ # Use provider-specified retry delay if available
374
+ if hasattr(error, "retry_after") and error.retry_after:
375
+ return True, float(error.retry_after)
376
+ else:
377
+ # Default rate limit backoff
378
+ return True, min(base_delay * 2, 60.0)
379
+
380
+ elif isinstance(error, NetworkError):
381
+ # Network errors are often transient
382
+ return True, min(base_delay, 10.0)
383
+
384
+ elif isinstance(error, ProviderUnavailableError):
385
+ # Provider outages may resolve quickly
386
+ return True, min(base_delay * 3, 30.0)
387
+
388
+ elif isinstance(error, MCPTimeoutError):
389
+ # MCP timeouts might resolve with retry
390
+ return True, min(base_delay, 5.0)
391
+
392
+ elif isinstance(error, MCPConnectionError):
393
+ # Connection errors might be transient
394
+ return True, min(base_delay, 15.0)
395
+
396
+ elif isinstance(
397
+ error,
398
+ (AuthenticationError, QuotaExceededError, ModelNotFoundError),
399
+ ):
400
+ # These errors typically don't resolve with retry
401
+ return False, 0.0
402
+
403
+ else:
404
+ # Generic retry for unknown errors
405
+ return True, min(base_delay, 5.0)
406
+
407
+ def get_error_summary(self, time_window_hours: int = 24) -> Dict[str, Any]:
408
+ """
409
+ Get a summary of recent errors for monitoring and debugging.
410
+
411
+ Args:
412
+ time_window_hours: Hours to look back for errors
413
+
414
+ Returns:
415
+ Dictionary with error summary
416
+ """
417
+ cutoff_time = datetime.now() - timedelta(hours=time_window_hours)
418
+ recent_errors = [
419
+ error for error in self.error_history if error["timestamp"] > cutoff_time
420
+ ]
421
+
422
+ # Group errors by type and component
423
+ error_counts = {}
424
+ provider_errors = {}
425
+ mcp_errors = {}
426
+
427
+ for error in recent_errors:
428
+ error_type = error["error_type"]
429
+ component = error["component"]
430
+ component_name = error["component_name"]
431
+
432
+ # Overall error counts
433
+ error_counts[error_type] = error_counts.get(error_type, 0) + 1
434
+
435
+ # Provider-specific errors
436
+ if component == "provider":
437
+ if component_name not in provider_errors:
438
+ provider_errors[component_name] = {}
439
+ provider_errors[component_name][error_type] = (
440
+ provider_errors[component_name].get(error_type, 0) + 1
441
+ )
442
+
443
+ # MCP-specific errors
444
+ elif component == "mcp":
445
+ if component_name not in mcp_errors:
446
+ mcp_errors[component_name] = {}
447
+ mcp_errors[component_name][error_type] = (
448
+ mcp_errors[component_name].get(error_type, 0) + 1
449
+ )
450
+
451
+ return {
452
+ "time_window_hours": time_window_hours,
453
+ "total_errors": len(recent_errors),
454
+ "error_counts": error_counts,
455
+ "provider_errors": provider_errors,
456
+ "mcp_errors": mcp_errors,
457
+ "most_recent_error": recent_errors[-1] if recent_errors else None,
458
+ }
459
+
460
+ def _record_error(self, error: Exception, component_name: str, component_type: str):
461
+ """Record an error in the error history."""
462
+ error_record = {
463
+ "timestamp": datetime.now(),
464
+ "error_type": type(error).__name__,
465
+ "error_message": str(error),
466
+ "component": component_type,
467
+ "component_name": component_name,
468
+ }
469
+
470
+ self.error_history.append(error_record)
471
+
472
+ # Keep only recent errors (last 1000)
473
+ if len(self.error_history) > 1000:
474
+ self.error_history = self.error_history[-1000:]
475
+
476
+ logger.warning(f"Error recorded: {error_record}")
477
+
478
+ def _update_provider_status(self, provider_name: str, error: Exception):
479
+ """Update provider status based on error."""
480
+ if provider_name not in self.provider_status:
481
+ self.provider_status[provider_name] = {
482
+ "status": "unknown",
483
+ "last_error": None,
484
+ "error_count": 0,
485
+ "last_success": None,
486
+ }
487
+
488
+ status = self.provider_status[provider_name]
489
+ status["last_error"] = datetime.now()
490
+ status["error_count"] += 1
491
+
492
+ # Determine status based on error type
493
+ if isinstance(error, (AuthenticationError, ProviderConfigurationError)):
494
+ status["status"] = "configuration_error"
495
+ elif isinstance(error, (RateLimitError, QuotaExceededError)):
496
+ status["status"] = "quota_limited"
497
+ elif isinstance(error, NetworkError):
498
+ status["status"] = "network_error"
499
+ elif isinstance(error, ProviderUnavailableError):
500
+ status["status"] = "unavailable"
501
+ else:
502
+ status["status"] = "error"
503
+
504
+ def _update_mcp_status(self, server_name: str, error: Exception):
505
+ """Update MCP server status based on error."""
506
+ if server_name not in self.mcp_server_status:
507
+ self.mcp_server_status[server_name] = {
508
+ "status": "unknown",
509
+ "last_error": None,
510
+ "error_count": 0,
511
+ "last_success": None,
512
+ }
513
+
514
+ status = self.mcp_server_status[server_name]
515
+ status["last_error"] = datetime.now()
516
+ status["error_count"] += 1
517
+
518
+ # Determine status based on error type
519
+ if isinstance(error, MCPConnectionError):
520
+ status["status"] = "connection_failed"
521
+ elif isinstance(error, MCPTimeoutError):
522
+ status["status"] = "timeout"
523
+ else:
524
+ status["status"] = "error"
525
+
526
+ def _get_working_providers(self, available_providers: List[str]) -> List[str]:
527
+ """Get list of providers that are likely working based on recent status."""
528
+ working_providers = []
529
+
530
+ for provider in available_providers:
531
+ status = self.provider_status.get(provider, {})
532
+
533
+ # Consider provider working if:
534
+ # 1. No recent errors, or
535
+ # 2. Recent success after errors, or
536
+ # 3. Only transient errors (network, rate limit)
537
+ if (
538
+ not status.get("last_error")
539
+ or (
540
+ status.get("last_success")
541
+ and status.get("last_success") > status.get("last_error")
542
+ )
543
+ or status.get("status") in ["network_error", "quota_limited"]
544
+ ):
545
+ working_providers.append(provider)
546
+
547
+ # If no providers have good status, return all (benefit of doubt)
548
+ if not working_providers:
549
+ working_providers = available_providers
550
+
551
+ return working_providers
552
+
553
+
554
+ # Global error handler instance
555
+ error_handler = ErrorHandler()
556
+
557
+
558
+ def handle_provider_error(
559
+ error: Exception, provider_name: str, available_providers: List[str] = None
560
+ ):
561
+ """Convenience function for handling provider errors."""
562
+ return error_handler.handle_provider_error(
563
+ error, provider_name, available_providers
564
+ )
565
+
566
+
567
+ def handle_mcp_error(
568
+ error: Exception,
569
+ server_name: str = None,
570
+ available_servers: List[str] = None,
571
+ ):
572
+ """Convenience function for handling MCP errors."""
573
+ return error_handler.handle_mcp_error(error, server_name, available_servers)
574
+
575
+
576
+ def should_retry_operation(error: Exception, attempt_count: int, max_attempts: int = 3):
577
+ """Convenience function for retry logic."""
578
+ return error_handler.should_retry_operation(error, attempt_count, max_attempts)
579
+
580
+
581
+ def get_graceful_degradation_options(
582
+ failed_provider: str,
583
+ available_providers: List[str],
584
+ required_capabilities: List[str] = None,
585
+ ):
586
+ """Convenience function for graceful degradation."""
587
+ return error_handler.get_graceful_degradation_options(
588
+ failed_provider, available_providers, required_capabilities
589
+ )