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,568 @@
1
+ """
2
+ Enhanced Provider Fallback and Retry Logic for Omnimancer.
3
+
4
+ This module provides intelligent fallback system with provider health monitoring,
5
+ context preservation, and seamless failover capabilities.
6
+ """
7
+
8
+ import asyncio
9
+ import logging
10
+ import random
11
+ import time
12
+ from dataclasses import dataclass, field
13
+ from enum import Enum
14
+ from typing import Any, Callable, Dict, List, Optional, Set, Tuple
15
+
16
+ from ..utils.errors import (
17
+ NetworkError,
18
+ ProviderError,
19
+ ProviderUnavailableError,
20
+ )
21
+ from .health_monitor import HealthMonitor
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class FallbackReason(Enum):
27
+ """Reasons for provider fallback."""
28
+
29
+ PROVIDER_ERROR = "provider_error"
30
+ NETWORK_ERROR = "network_error"
31
+ RATE_LIMIT = "rate_limit"
32
+ QUOTA_EXCEEDED = "quota_exceeded"
33
+ MODEL_UNAVAILABLE = "model_unavailable"
34
+ TIMEOUT = "timeout"
35
+ HEALTH_CHECK_FAILED = "health_check_failed"
36
+
37
+
38
+ class ProviderRank(Enum):
39
+ """Provider ranking based on reliability and performance."""
40
+
41
+ PRIMARY = 1 # Best performance, highest reliability
42
+ SECONDARY = 2 # Good performance, good reliability
43
+ TERTIARY = 3 # Acceptable performance, lower reliability
44
+ EMERGENCY = 4 # Last resort, basic functionality
45
+
46
+
47
+ @dataclass
48
+ class FallbackAttempt:
49
+ """Record of a fallback attempt."""
50
+
51
+ provider_name: str
52
+ reason: FallbackReason
53
+ error: Optional[str] = None
54
+ timestamp: float = field(default_factory=time.time)
55
+ retry_count: int = 0
56
+ success: bool = False
57
+ response_time: Optional[float] = None
58
+
59
+
60
+ @dataclass
61
+ class ProviderStats:
62
+ """Statistics for provider performance tracking."""
63
+
64
+ success_count: int = 0
65
+ failure_count: int = 0
66
+ avg_response_time: float = 0.0
67
+ last_success: Optional[float] = None
68
+ last_failure: Optional[float] = None
69
+ rank: ProviderRank = ProviderRank.SECONDARY
70
+ consecutive_failures: int = 0
71
+ total_requests: int = 0
72
+
73
+ @property
74
+ def success_rate(self) -> float:
75
+ """Calculate success rate."""
76
+ if self.total_requests == 0:
77
+ return 1.0
78
+ return self.success_count / self.total_requests
79
+
80
+ @property
81
+ def reliability_score(self) -> float:
82
+ """Calculate reliability score (0.0 to 1.0)."""
83
+ base_score = self.success_rate
84
+
85
+ # Penalize consecutive failures
86
+ failure_penalty = min(self.consecutive_failures * 0.1, 0.8)
87
+
88
+ # Bonus for recent success
89
+ recency_bonus = 0.0
90
+ if self.last_success:
91
+ time_since_success = time.time() - self.last_success
92
+ if time_since_success < 300: # 5 minutes
93
+ recency_bonus = 0.1
94
+
95
+ return max(0.0, base_score - failure_penalty + recency_bonus)
96
+
97
+
98
+ class EnhancedProviderFallback:
99
+ """
100
+ Enhanced provider fallback system with intelligent provider selection,
101
+ health monitoring, and context preservation.
102
+ """
103
+
104
+ def __init__(self, core_engine, health_monitor: Optional[HealthMonitor] = None):
105
+ """
106
+ Initialize the enhanced fallback manager.
107
+
108
+ Args:
109
+ core_engine: Reference to CoreEngine instance
110
+ health_monitor: Optional health monitor instance
111
+ """
112
+ self.core_engine = core_engine
113
+ self.health_monitor = health_monitor or HealthMonitor()
114
+
115
+ # Configuration
116
+ self.max_retry_attempts = 3
117
+ self.base_retry_delay = 1.0
118
+ self.max_retry_delay = 32.0
119
+ self.jitter_factor = 0.1
120
+ self.health_check_interval = 300 # 5 minutes
121
+
122
+ # Provider management
123
+ self.fallback_providers: List[str] = []
124
+ self.provider_rankings: Dict[str, ProviderRank] = {}
125
+ self.provider_stats: Dict[str, ProviderStats] = {}
126
+ self.circuit_breaker_threshold = 5 # failures before circuit break
127
+ self.circuit_breaker_recovery_time = 600 # 10 minutes
128
+
129
+ # Context preservation
130
+ self.context_preservation_enabled = True
131
+ self.preserved_context: Optional[Dict[str, Any]] = None
132
+
133
+ # Fallback history
134
+ self.fallback_history: List[FallbackAttempt] = []
135
+ self.max_history_size = 100
136
+
137
+ # Excluded providers (temporarily disabled)
138
+ self.excluded_providers: Set[str] = set()
139
+ self.exclusion_expiry: Dict[str, float] = {}
140
+
141
+ def set_fallback_providers(
142
+ self,
143
+ providers: List[str],
144
+ rankings: Optional[Dict[str, ProviderRank]] = None,
145
+ ):
146
+ """
147
+ Set the list of fallback providers with optional rankings.
148
+
149
+ Args:
150
+ providers: List of provider names in priority order
151
+ rankings: Optional dict mapping provider names to rankings
152
+ """
153
+ self.fallback_providers = providers.copy()
154
+
155
+ if rankings:
156
+ self.provider_rankings.update(rankings)
157
+ else:
158
+ # Auto-assign rankings based on order
159
+ for i, provider in enumerate(providers):
160
+ if i == 0:
161
+ self.provider_rankings[provider] = ProviderRank.PRIMARY
162
+ elif i < 3:
163
+ self.provider_rankings[provider] = ProviderRank.SECONDARY
164
+ elif i < 6:
165
+ self.provider_rankings[provider] = ProviderRank.TERTIARY
166
+ else:
167
+ self.provider_rankings[provider] = ProviderRank.EMERGENCY
168
+
169
+ # Initialize stats for new providers
170
+ for provider in providers:
171
+ if provider not in self.provider_stats:
172
+ self.provider_stats[provider] = ProviderStats()
173
+
174
+ async def execute_with_fallback(
175
+ self,
176
+ operation_func: Callable,
177
+ *args,
178
+ preserve_context: bool = True,
179
+ **kwargs,
180
+ ) -> Any:
181
+ """
182
+ Execute operation with intelligent fallback and retry logic.
183
+
184
+ Args:
185
+ operation_func: Function to execute
186
+ *args: Arguments for the function
187
+ preserve_context: Whether to preserve conversation context
188
+ **kwargs: Keyword arguments for the function
189
+
190
+ Returns:
191
+ Result from successful operation execution
192
+
193
+ Raises:
194
+ Exception: If all providers and retries fail
195
+ """
196
+ original_provider = self.core_engine.current_provider
197
+ original_provider_name = (
198
+ original_provider.get_provider_name() if original_provider else None
199
+ )
200
+
201
+ # Preserve context if enabled
202
+ if preserve_context and self.context_preservation_enabled:
203
+ await self._preserve_context()
204
+
205
+ # Get ranked list of providers to try
206
+ providers_to_try = await self._get_ranked_providers()
207
+
208
+ last_error = None
209
+ fallback_attempts = []
210
+
211
+ for provider_name in providers_to_try:
212
+ # Skip excluded providers
213
+ if self._is_provider_excluded(provider_name):
214
+ continue
215
+
216
+ try:
217
+ # Switch to provider if needed
218
+ if not original_provider or original_provider_name != provider_name:
219
+ switch_success = await self.core_engine.switch_model(provider_name)
220
+ if not switch_success:
221
+ continue
222
+
223
+ # Execute with retries for this provider
224
+ result = await self._execute_with_retries(
225
+ operation_func, provider_name, *args, **kwargs
226
+ )
227
+
228
+ # Record success
229
+ self._record_success(provider_name, time.time())
230
+
231
+ # Restore context if needed
232
+ if preserve_context and self.preserved_context:
233
+ await self._restore_context()
234
+
235
+ return result
236
+
237
+ except Exception as e:
238
+ last_error = e
239
+ reason = self._classify_error(e)
240
+
241
+ # Record failure
242
+ self._record_failure(provider_name, reason, str(e))
243
+
244
+ # Add to fallback attempts
245
+ attempt = FallbackAttempt(
246
+ provider_name=provider_name,
247
+ reason=reason,
248
+ error=str(e),
249
+ success=False,
250
+ )
251
+ fallback_attempts.append(attempt)
252
+
253
+ # Check if provider should be temporarily excluded
254
+ if self._should_exclude_provider(provider_name):
255
+ self._exclude_provider_temporarily(provider_name)
256
+
257
+ logger.warning(f"Provider {provider_name} failed: {e}")
258
+ continue
259
+
260
+ # All providers failed
261
+ self._record_fallback_history(fallback_attempts)
262
+
263
+ # Try to restore original provider
264
+ if original_provider_name:
265
+ try:
266
+ await self.core_engine.switch_model(original_provider_name)
267
+ except Exception:
268
+ pass # Best effort restore
269
+
270
+ # Raise the last error
271
+ if last_error:
272
+ raise ProviderUnavailableError(
273
+ f"All providers failed. Last error: {last_error}",
274
+ fallback_providers=[a.provider_name for a in fallback_attempts],
275
+ )
276
+ else:
277
+ raise ProviderError("No providers available for fallback")
278
+
279
+ async def _execute_with_retries(
280
+ self, operation_func: Callable, provider_name: str, *args, **kwargs
281
+ ) -> Any:
282
+ """Execute operation with exponential backoff retry logic."""
283
+ last_error = None
284
+
285
+ for attempt in range(self.max_retry_attempts):
286
+ try:
287
+ start_time = time.time()
288
+ result = await operation_func(*args, **kwargs)
289
+
290
+ # Record response time
291
+ response_time = time.time() - start_time
292
+ self._update_response_time(provider_name, response_time)
293
+
294
+ return result
295
+
296
+ except Exception as e:
297
+ last_error = e
298
+
299
+ # Don't retry certain types of errors
300
+ if self._is_non_retryable_error(e):
301
+ raise
302
+
303
+ # Calculate delay with exponential backoff and jitter
304
+ if attempt < self.max_retry_attempts - 1:
305
+ delay = min(
306
+ self.base_retry_delay * (2**attempt),
307
+ self.max_retry_delay,
308
+ )
309
+ # Add jitter
310
+ jitter = delay * self.jitter_factor * (random.random() - 0.5)
311
+ delay += jitter
312
+
313
+ logger.debug(
314
+ f"Retrying {provider_name} in {delay:.2f}s (attempt {attempt + 1})"
315
+ )
316
+ await asyncio.sleep(delay)
317
+
318
+ # All retries failed
319
+ if last_error:
320
+ raise last_error
321
+ else:
322
+ raise ProviderError(
323
+ f"All retry attempts failed for provider {provider_name}"
324
+ )
325
+
326
+ async def _get_ranked_providers(self) -> List[str]:
327
+ """Get list of providers ranked by health and performance."""
328
+ available_providers = []
329
+
330
+ # Start with current provider if available
331
+ if self.core_engine.current_provider:
332
+ current_name = self.core_engine.current_provider.get_provider_name()
333
+ if current_name not in self.excluded_providers:
334
+ available_providers.append(current_name)
335
+
336
+ # Add fallback providers
337
+ for provider in self.fallback_providers:
338
+ if (
339
+ provider not in available_providers
340
+ and provider not in self.excluded_providers
341
+ ):
342
+ available_providers.append(provider)
343
+
344
+ # Sort by reliability score and ranking
345
+ def sort_key(provider_name: str) -> Tuple[float, int]:
346
+ stats = self.provider_stats.get(provider_name, ProviderStats())
347
+ ranking = self.provider_rankings.get(provider_name, ProviderRank.SECONDARY)
348
+ return (-stats.reliability_score, ranking.value)
349
+
350
+ available_providers.sort(key=sort_key)
351
+
352
+ logger.debug(f"Ranked providers: {available_providers}")
353
+ return available_providers
354
+
355
+ def _classify_error(self, error: Exception) -> FallbackReason:
356
+ """Classify error to determine appropriate fallback strategy."""
357
+ error_str = str(error).lower()
358
+ type(error).__name__
359
+
360
+ if "rate limit" in error_str or "429" in error_str:
361
+ return FallbackReason.RATE_LIMIT
362
+ elif "quota" in error_str or "usage limit" in error_str:
363
+ return FallbackReason.QUOTA_EXCEEDED
364
+ elif "timeout" in error_str or "timed out" in error_str:
365
+ return FallbackReason.TIMEOUT
366
+ elif "network" in error_str or "connection" in error_str:
367
+ return FallbackReason.NETWORK_ERROR
368
+ elif "model" in error_str and (
369
+ "not found" in error_str or "unavailable" in error_str
370
+ ):
371
+ return FallbackReason.MODEL_UNAVAILABLE
372
+ elif isinstance(error, NetworkError):
373
+ return FallbackReason.NETWORK_ERROR
374
+ else:
375
+ return FallbackReason.PROVIDER_ERROR
376
+
377
+ def _is_non_retryable_error(self, error: Exception) -> bool:
378
+ """Check if error should not be retried."""
379
+ # Don't retry authentication errors, quota exceeded, etc.
380
+ error_str = str(error).lower()
381
+ non_retryable_patterns = [
382
+ "invalid api key",
383
+ "authentication failed",
384
+ "unauthorized",
385
+ "quota exceeded",
386
+ "model not found",
387
+ "invalid model",
388
+ ]
389
+
390
+ return any(pattern in error_str for pattern in non_retryable_patterns)
391
+
392
+ def _record_success(self, provider_name: str, response_time: float):
393
+ """Record successful operation for provider."""
394
+ if provider_name not in self.provider_stats:
395
+ self.provider_stats[provider_name] = ProviderStats()
396
+
397
+ stats = self.provider_stats[provider_name]
398
+ stats.success_count += 1
399
+ stats.total_requests += 1
400
+ stats.last_success = time.time()
401
+ stats.consecutive_failures = 0 # Reset failure counter
402
+
403
+ logger.debug(f"Recorded success for {provider_name}")
404
+
405
+ def _record_failure(
406
+ self, provider_name: str, reason: FallbackReason, error_msg: str
407
+ ):
408
+ """Record failed operation for provider."""
409
+ if provider_name not in self.provider_stats:
410
+ self.provider_stats[provider_name] = ProviderStats()
411
+
412
+ stats = self.provider_stats[provider_name]
413
+ stats.failure_count += 1
414
+ stats.total_requests += 1
415
+ stats.last_failure = time.time()
416
+ stats.consecutive_failures += 1
417
+
418
+ logger.debug(f"Recorded failure for {provider_name}: {reason.value}")
419
+
420
+ def _update_response_time(self, provider_name: str, response_time: float):
421
+ """Update average response time for provider."""
422
+ if provider_name not in self.provider_stats:
423
+ self.provider_stats[provider_name] = ProviderStats()
424
+
425
+ stats = self.provider_stats[provider_name]
426
+ if stats.avg_response_time == 0:
427
+ stats.avg_response_time = response_time
428
+ else:
429
+ # Exponential moving average
430
+ alpha = 0.3
431
+ stats.avg_response_time = (
432
+ alpha * response_time + (1 - alpha) * stats.avg_response_time
433
+ )
434
+
435
+ def _should_exclude_provider(self, provider_name: str) -> bool:
436
+ """Check if provider should be temporarily excluded."""
437
+ stats = self.provider_stats.get(provider_name)
438
+ if not stats:
439
+ return False
440
+
441
+ return stats.consecutive_failures >= self.circuit_breaker_threshold
442
+
443
+ def _exclude_provider_temporarily(self, provider_name: str):
444
+ """Temporarily exclude provider from fallback list."""
445
+ self.excluded_providers.add(provider_name)
446
+ self.exclusion_expiry[provider_name] = (
447
+ time.time() + self.circuit_breaker_recovery_time
448
+ )
449
+
450
+ logger.warning(
451
+ f"Temporarily excluded provider {provider_name} due to consecutive failures"
452
+ )
453
+
454
+ def _is_provider_excluded(self, provider_name: str) -> bool:
455
+ """Check if provider is currently excluded."""
456
+ if provider_name not in self.excluded_providers:
457
+ return False
458
+
459
+ # Check if exclusion has expired
460
+ if provider_name in self.exclusion_expiry:
461
+ if time.time() > self.exclusion_expiry[provider_name]:
462
+ self.excluded_providers.discard(provider_name)
463
+ del self.exclusion_expiry[provider_name]
464
+ logger.info(
465
+ f"Re-enabled provider {provider_name} after recovery period"
466
+ )
467
+ return False
468
+
469
+ return True
470
+
471
+ async def _preserve_context(self):
472
+ """Preserve current conversation context."""
473
+ try:
474
+ if hasattr(self.core_engine, "chat_manager"):
475
+ context = self.core_engine.chat_manager.get_current_context()
476
+ self.preserved_context = {
477
+ "messages": (
478
+ context.messages.copy() if hasattr(context, "messages") else []
479
+ ),
480
+ "session_id": getattr(context, "session_id", None),
481
+ "metadata": getattr(context, "metadata", {}),
482
+ }
483
+ logger.debug("Preserved conversation context")
484
+ except Exception as e:
485
+ logger.warning(f"Failed to preserve context: {e}")
486
+
487
+ async def _restore_context(self):
488
+ """Restore preserved conversation context."""
489
+ try:
490
+ if self.preserved_context and hasattr(self.core_engine, "chat_manager"):
491
+ # This would require implementation in ChatManager
492
+ # For now, just log the attempt
493
+ logger.debug("Context restoration would be implemented here")
494
+ except Exception as e:
495
+ logger.warning(f"Failed to restore context: {e}")
496
+
497
+ def _record_fallback_history(self, attempts: List[FallbackAttempt]):
498
+ """Record fallback attempts in history."""
499
+ self.fallback_history.extend(attempts)
500
+
501
+ # Trim history if too large
502
+ if len(self.fallback_history) > self.max_history_size:
503
+ self.fallback_history = self.fallback_history[-self.max_history_size :]
504
+
505
+ def get_provider_stats(self) -> Dict[str, Dict[str, Any]]:
506
+ """Get statistics for all providers."""
507
+ stats = {}
508
+ for provider_name, provider_stats in self.provider_stats.items():
509
+ stats[provider_name] = {
510
+ "success_rate": provider_stats.success_rate,
511
+ "reliability_score": provider_stats.reliability_score,
512
+ "avg_response_time": provider_stats.avg_response_time,
513
+ "total_requests": provider_stats.total_requests,
514
+ "consecutive_failures": provider_stats.consecutive_failures,
515
+ "rank": provider_stats.rank.name,
516
+ "excluded": provider_name in self.excluded_providers,
517
+ }
518
+ return stats
519
+
520
+ def get_fallback_history(self, limit: int = 10) -> List[Dict[str, Any]]:
521
+ """Get recent fallback history."""
522
+ recent_history = (
523
+ self.fallback_history[-limit:] if limit else self.fallback_history
524
+ )
525
+
526
+ return [
527
+ {
528
+ "provider": attempt.provider_name,
529
+ "reason": attempt.reason.value,
530
+ "error": attempt.error,
531
+ "timestamp": attempt.timestamp,
532
+ "success": attempt.success,
533
+ }
534
+ for attempt in recent_history
535
+ ]
536
+
537
+ async def health_check_all_providers(self) -> Dict[str, Dict[str, Any]]:
538
+ """Perform health check on all configured providers."""
539
+ if not hasattr(self.core_engine, "config_manager"):
540
+ return {}
541
+
542
+ try:
543
+ config = self.core_engine.config_manager.get_config()
544
+ health_result = await self.health_monitor.check_all_providers_health(
545
+ config.providers, force=True
546
+ )
547
+ return health_result
548
+ except Exception as e:
549
+ logger.error(f"Health check failed: {e}")
550
+ return {}
551
+
552
+ def reset_provider_stats(self, provider_name: Optional[str] = None):
553
+ """Reset statistics for specific provider or all providers."""
554
+ if provider_name:
555
+ if provider_name in self.provider_stats:
556
+ self.provider_stats[provider_name] = ProviderStats()
557
+ logger.info(f"Reset stats for provider {provider_name}")
558
+ else:
559
+ self.provider_stats.clear()
560
+ logger.info("Reset stats for all providers")
561
+
562
+ def configure_circuit_breaker(self, threshold: int = 5, recovery_time: int = 600):
563
+ """Configure circuit breaker settings."""
564
+ self.circuit_breaker_threshold = threshold
565
+ self.circuit_breaker_recovery_time = recovery_time
566
+ logger.info(
567
+ f"Circuit breaker configured: threshold={threshold}, recovery_time={recovery_time}s"
568
+ )