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.
- omnimancer/__init__.py +42 -0
- omnimancer/__main__.py +9 -0
- omnimancer/cli/__init__.py +8 -0
- omnimancer/cli/agent_manager.py +826 -0
- omnimancer/cli/agent_wizard.py +944 -0
- omnimancer/cli/approval_formatter.py +734 -0
- omnimancer/cli/approval_integration.py +734 -0
- omnimancer/cli/approval_prompt.py +1286 -0
- omnimancer/cli/batch_approval_display.py +617 -0
- omnimancer/cli/batch_approval_filters.py +729 -0
- omnimancer/cli/batch_operation_monitor.py +775 -0
- omnimancer/cli/commands/performance_cmd.py +624 -0
- omnimancer/cli/commands.py +757 -0
- omnimancer/cli/completion_handler.py +281 -0
- omnimancer/cli/config_setup_wizard.py +783 -0
- omnimancer/cli/dynamic_commands.py +371 -0
- omnimancer/cli/handlers/__init__.py +18 -0
- omnimancer/cli/handlers/agent_handler.py +865 -0
- omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
- omnimancer/cli/handlers/custom_agent_handler.py +352 -0
- omnimancer/cli/handlers/permissions_handler.py +1609 -0
- omnimancer/cli/interface.py +6695 -0
- omnimancer/cli/performance_dashboard.py +612 -0
- omnimancer/core/__init__.py +34 -0
- omnimancer/core/agent/__init__.py +256 -0
- omnimancer/core/agent/agent_config.py +715 -0
- omnimancer/core/agent/agent_switcher.py +668 -0
- omnimancer/core/agent/approval_context.py +460 -0
- omnimancer/core/agent/approval_dialog.py +902 -0
- omnimancer/core/agent/approval_interface.py +628 -0
- omnimancer/core/agent/approval_manager.py +760 -0
- omnimancer/core/agent/cli_integration.py +470 -0
- omnimancer/core/agent/config.py +845 -0
- omnimancer/core/agent/custom_agent_integration.py +654 -0
- omnimancer/core/agent/diff_renderer.py +706 -0
- omnimancer/core/agent/file_content_display.py +503 -0
- omnimancer/core/agent/file_modification_workflow.py +658 -0
- omnimancer/core/agent/file_system_manager.py +1790 -0
- omnimancer/core/agent/input_handler.py +614 -0
- omnimancer/core/agent/metrics_collector.py +771 -0
- omnimancer/core/agent/optimization_engine.py +729 -0
- omnimancer/core/agent/performance_monitor.py +717 -0
- omnimancer/core/agent/persona.py +870 -0
- omnimancer/core/agent/persona_validator.py +672 -0
- omnimancer/core/agent/program_executor.py +748 -0
- omnimancer/core/agent/proposed_changes_integration.py +663 -0
- omnimancer/core/agent/read_before_write_errors.py +492 -0
- omnimancer/core/agent/read_before_write_ui.py +647 -0
- omnimancer/core/agent/rich_renderer.py +763 -0
- omnimancer/core/agent/status_config.py +238 -0
- omnimancer/core/agent/status_core.py +446 -0
- omnimancer/core/agent/status_integrated_engine.py +540 -0
- omnimancer/core/agent/status_manager.py +878 -0
- omnimancer/core/agent/status_ui_components.py +535 -0
- omnimancer/core/agent/template_applicator.py +548 -0
- omnimancer/core/agent/terminal_status_listener.py +394 -0
- omnimancer/core/agent/token_tracker.py +679 -0
- omnimancer/core/agent/types.py +60 -0
- omnimancer/core/agent/web_client.py +745 -0
- omnimancer/core/agent/workflow_orchestrator.py +826 -0
- omnimancer/core/agent_engine.py +1625 -0
- omnimancer/core/agent_mode_manager.py +706 -0
- omnimancer/core/agent_progress_ui.py +545 -0
- omnimancer/core/chat_manager.py +141 -0
- omnimancer/core/config_manager.py +2199 -0
- omnimancer/core/config_migration.py +772 -0
- omnimancer/core/config_provider.py +1974 -0
- omnimancer/core/config_security_validator.py +393 -0
- omnimancer/core/config_validator.py +768 -0
- omnimancer/core/conversation_manager.py +352 -0
- omnimancer/core/engine.py +787 -0
- omnimancer/core/enhanced_input.py +523 -0
- omnimancer/core/env_loader.py +83 -0
- omnimancer/core/fallback_manager.py +568 -0
- omnimancer/core/health_monitor.py +462 -0
- omnimancer/core/history_manager.py +462 -0
- omnimancer/core/mcp_integration_layer.py +878 -0
- omnimancer/core/models.py +1889 -0
- omnimancer/core/provider_capabilities.py +403 -0
- omnimancer/core/provider_initializer.py +450 -0
- omnimancer/core/provider_registry.py +432 -0
- omnimancer/core/security/__init__.py +16 -0
- omnimancer/core/security/approval_workflow.py +406 -0
- omnimancer/core/security/audit_logger.py +511 -0
- omnimancer/core/security/permission_controller.py +511 -0
- omnimancer/core/security/sandbox_manager.py +379 -0
- omnimancer/core/security/security_manager.py +530 -0
- omnimancer/core/setup_wizard.py +345 -0
- omnimancer/core/setup_wizard_core.py +449 -0
- omnimancer/core/setup_wizard_guide_generator.py +376 -0
- omnimancer/core/setup_wizard_provider_setup.py +454 -0
- omnimancer/core/setup_wizard_ui.py +317 -0
- omnimancer/core/setup_wizard_validation.py +371 -0
- omnimancer/core/signal_handler.py +174 -0
- omnimancer/mcp/__init__.py +11 -0
- omnimancer/mcp/client.py +725 -0
- omnimancer/mcp/manager.py +649 -0
- omnimancer/providers/__init__.py +24 -0
- omnimancer/providers/azure.py +608 -0
- omnimancer/providers/base.py +262 -0
- omnimancer/providers/bedrock.py +711 -0
- omnimancer/providers/claude.py +426 -0
- omnimancer/providers/claude_code.py +406 -0
- omnimancer/providers/cohere.py +412 -0
- omnimancer/providers/factory.py +988 -0
- omnimancer/providers/gemini.py +708 -0
- omnimancer/providers/mistral.py +554 -0
- omnimancer/providers/ollama.py +510 -0
- omnimancer/providers/openai.py +362 -0
- omnimancer/providers/openrouter.py +709 -0
- omnimancer/providers/perplexity.py +476 -0
- omnimancer/providers/vertex.py +617 -0
- omnimancer/providers/xai.py +484 -0
- omnimancer/ui/__init__.py +6 -0
- omnimancer/ui/cancellation_handler.py +235 -0
- omnimancer/ui/progress_indicator.py +318 -0
- omnimancer/utils/__init__.py +22 -0
- omnimancer/utils/error_handler.py +589 -0
- omnimancer/utils/errors.py +667 -0
- omnimancer/utils/retry.py +238 -0
- omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
- omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
- omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
- omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
- omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
)
|