Spaces:
Sleeping
Sleeping
| """ | |
| Error handling module for the Deep Research AI system. | |
| This module provides comprehensive error handling, recovery strategies, | |
| graceful degradation, and user-friendly error messaging. | |
| """ | |
| import logging | |
| import traceback | |
| from dataclasses import dataclass, field | |
| from datetime import datetime | |
| from enum import Enum | |
| from typing import Any, Callable, TypeVar | |
| from ..config import Config | |
| from ..llm_client import LLMClient | |
| from ..prompts.error_prompts import ( | |
| ERROR_ANALYSIS_PROMPT, | |
| GRACEFUL_DEGRADATION_PROMPT, | |
| USER_ERROR_MESSAGE_PROMPT, | |
| RETRY_STRATEGY_PROMPT, | |
| ERROR_RECOVERY_PROMPT, | |
| FALLBACK_CONTENT_PROMPT, | |
| SYSTEM_HEALTH_PROMPT, | |
| ) | |
| # Set up logging | |
| logger = logging.getLogger(__name__) | |
| class ErrorSeverity(Enum): | |
| """Error severity levels.""" | |
| INFO = "info" | |
| WARNING = "warning" | |
| ERROR = "error" | |
| CRITICAL = "critical" | |
| class ComponentType(Enum): | |
| """System component types.""" | |
| QUERY_UNDERSTANDING = "query_understanding" | |
| WEB_SEARCH = "web_search" | |
| REASONING_ENGINE = "reasoning_engine" | |
| VERIFICATION = "verification" | |
| CITATION = "citation" | |
| OUTPUT_GENERATION = "output_generation" | |
| LLM_CLIENT = "llm_client" | |
| ORCHESTRATOR = "orchestrator" | |
| class ErrorContext: | |
| """Context information for an error.""" | |
| component: ComponentType | |
| operation: str | |
| query: str | None = None | |
| partial_results: dict | None = None | |
| timestamp: datetime = field(default_factory=datetime.now) | |
| attempt_number: int = 1 | |
| max_attempts: int = 3 | |
| class ErrorRecord: | |
| """Record of an error occurrence.""" | |
| error_type: str | |
| error_message: str | |
| context: ErrorContext | |
| severity: ErrorSeverity | |
| traceback_str: str | None = None | |
| recovery_attempted: bool = False | |
| recovery_successful: bool = False | |
| timestamp: datetime = field(default_factory=datetime.now) | |
| class ResearchError(Exception): | |
| """Base exception for research system errors.""" | |
| def __init__( | |
| self, | |
| message: str, | |
| severity: ErrorSeverity = ErrorSeverity.ERROR, | |
| recoverable: bool = True, | |
| context: ErrorContext | None = None | |
| ): | |
| super().__init__(message) | |
| self.message = message | |
| self.severity = severity | |
| self.recoverable = recoverable | |
| self.context = context | |
| class QueryError(ResearchError): | |
| """Error in query understanding.""" | |
| pass | |
| class SearchError(ResearchError): | |
| """Error in web search.""" | |
| pass | |
| class ReasoningError(ResearchError): | |
| """Error in reasoning engine.""" | |
| pass | |
| class VerificationError(ResearchError): | |
| """Error in verification.""" | |
| pass | |
| class CitationError(ResearchError): | |
| """Error in citation generation.""" | |
| pass | |
| class LLMError(ResearchError): | |
| """Error in LLM communication.""" | |
| pass | |
| class RateLimitError(ResearchError): | |
| """Rate limit exceeded error.""" | |
| pass | |
| # Type variable for generic retry function | |
| T = TypeVar('T') | |
| class ErrorHandler: | |
| """ | |
| Comprehensive error handling for the research system. | |
| Provides: | |
| - Error analysis and diagnosis | |
| - Graceful degradation | |
| - Retry strategies | |
| - Recovery orchestration | |
| - User-friendly error messages | |
| """ | |
| def __init__(self, config: Config | None = None) -> None: | |
| """ | |
| Initialize the ErrorHandler. | |
| Args: | |
| config: Configuration object. Uses default if not provided. | |
| """ | |
| self.config = config or Config() | |
| self.llm_client = LLMClient(self.config.llm_config) | |
| self.error_history: list[ErrorRecord] = [] | |
| self.max_history = 100 | |
| def record_error( | |
| self, | |
| error: Exception, | |
| context: ErrorContext, | |
| severity: ErrorSeverity = ErrorSeverity.ERROR | |
| ) -> ErrorRecord: | |
| """ | |
| Record an error occurrence. | |
| Args: | |
| error: The exception that occurred | |
| context: Error context | |
| severity: Error severity level | |
| Returns: | |
| ErrorRecord object | |
| """ | |
| record = ErrorRecord( | |
| error_type=type(error).__name__, | |
| error_message=str(error), | |
| context=context, | |
| severity=severity, | |
| traceback_str=traceback.format_exc() | |
| ) | |
| self.error_history.append(record) | |
| # Trim history if needed | |
| if len(self.error_history) > self.max_history: | |
| self.error_history = self.error_history[-self.max_history:] | |
| # Log the error | |
| log_level = { | |
| ErrorSeverity.INFO: logging.INFO, | |
| ErrorSeverity.WARNING: logging.WARNING, | |
| ErrorSeverity.ERROR: logging.ERROR, | |
| ErrorSeverity.CRITICAL: logging.CRITICAL | |
| }.get(severity, logging.ERROR) | |
| logger.log( | |
| log_level, | |
| f"Error in {context.component.value}: {record.error_message}" | |
| ) | |
| return record | |
| async def analyze_error( | |
| self, | |
| error: Exception, | |
| context: ErrorContext | |
| ) -> dict[str, Any]: | |
| """ | |
| Analyze an error and suggest recovery strategies. | |
| Args: | |
| error: The exception to analyze | |
| context: Error context | |
| Returns: | |
| Dictionary with analysis and recovery suggestions | |
| """ | |
| prompt = ERROR_ANALYSIS_PROMPT.format( | |
| error_type=type(error).__name__, | |
| error_message=str(error), | |
| context=str(context), | |
| component=context.component.value | |
| ) | |
| try: | |
| result = await self.llm_client.call_json(prompt) | |
| return result | |
| except Exception as e: | |
| # Fallback if LLM analysis fails | |
| logger.warning(f"Error analysis failed: {e}") | |
| return { | |
| "analysis": { | |
| "root_cause": "Unknown", | |
| "impact_level": "medium", | |
| "is_recoverable": True | |
| }, | |
| "user_message": "An error occurred. Please try again.", | |
| "recovery_strategies": [] | |
| } | |
| async def get_degraded_response( | |
| self, | |
| operation: str, | |
| partial_results: dict | None, | |
| missing_components: list[str] | |
| ) -> dict[str, Any]: | |
| """ | |
| Get a gracefully degraded response when full operation fails. | |
| Args: | |
| operation: The failed operation | |
| partial_results: Any partial results available | |
| missing_components: Components that failed | |
| Returns: | |
| Dictionary with degraded response strategy | |
| """ | |
| prompt = GRACEFUL_DEGRADATION_PROMPT.format( | |
| operation=operation, | |
| partial_results=str(partial_results) if partial_results else "None", | |
| missing_components=str(missing_components) | |
| ) | |
| try: | |
| result = await self.llm_client.call_json(prompt) | |
| return result | |
| except Exception: | |
| return { | |
| "degraded_response": { | |
| "can_provide_partial": partial_results is not None, | |
| "available_results": partial_results, | |
| "quality_reduction": 0.5 | |
| }, | |
| "user_communication": { | |
| "message": "We encountered some issues but have partial results.", | |
| "limitations_explained": missing_components | |
| } | |
| } | |
| async def generate_user_message( | |
| self, | |
| error: Exception, | |
| user_action: str, | |
| severity: ErrorSeverity | |
| ) -> dict[str, Any]: | |
| """ | |
| Generate a user-friendly error message. | |
| Args: | |
| error: The exception | |
| user_action: What the user was trying to do | |
| severity: Error severity | |
| Returns: | |
| Dictionary with user-friendly message | |
| """ | |
| prompt = USER_ERROR_MESSAGE_PROMPT.format( | |
| error_type=type(error).__name__, | |
| technical_message=str(error), | |
| user_action=user_action, | |
| severity=severity.value | |
| ) | |
| try: | |
| result = await self.llm_client.call_json(prompt) | |
| return result | |
| except Exception: | |
| return { | |
| "user_message": { | |
| "headline": "Something went wrong", | |
| "explanation": "We encountered an issue processing your request.", | |
| "what_to_do": "Please try again. If the problem persists, try rephrasing your query.", | |
| "tone": "apologetic" | |
| }, | |
| "severity_indicator": severity.value | |
| } | |
| async def get_retry_strategy( | |
| self, | |
| operation: str, | |
| failure_reason: str, | |
| attempt_number: int, | |
| context: dict | |
| ) -> dict[str, Any]: | |
| """ | |
| Determine optimal retry strategy. | |
| Args: | |
| operation: Failed operation | |
| failure_reason: Why it failed | |
| attempt_number: Current attempt number | |
| context: Operation context | |
| Returns: | |
| Dictionary with retry strategy | |
| """ | |
| prompt = RETRY_STRATEGY_PROMPT.format( | |
| operation=operation, | |
| failure_reason=failure_reason, | |
| attempt_number=attempt_number, | |
| context=str(context) | |
| ) | |
| try: | |
| result = await self.llm_client.call_json(prompt) | |
| return result | |
| except Exception: | |
| # Default retry strategy | |
| return { | |
| "retry_decision": { | |
| "should_retry": attempt_number < 3, | |
| "max_attempts": 3, | |
| "current_attempt": attempt_number | |
| }, | |
| "timing": { | |
| "delay_seconds": attempt_number * 2, | |
| "backoff_strategy": "exponential" | |
| }, | |
| "modifications": { | |
| "modify_request": False | |
| } | |
| } | |
| async def orchestrate_recovery( | |
| self, | |
| current_state: dict, | |
| error_chain: list[ErrorRecord] | |
| ) -> dict[str, Any]: | |
| """ | |
| Orchestrate recovery from error state. | |
| Args: | |
| current_state: Current system state | |
| error_chain: Chain of errors that occurred | |
| Returns: | |
| Dictionary with recovery plan | |
| """ | |
| error_chain_text = "\n".join([ | |
| f"- {e.error_type}: {e.error_message}" | |
| for e in error_chain | |
| ]) | |
| prompt = ERROR_RECOVERY_PROMPT.format( | |
| current_state=str(current_state), | |
| error_chain=error_chain_text, | |
| available_resources=str(list(ComponentType)) | |
| ) | |
| try: | |
| result = await self.llm_client.call_json(prompt) | |
| return result | |
| except Exception: | |
| return { | |
| "state_assessment": { | |
| "corruption_level": "partial" | |
| }, | |
| "recovery_plan": [ | |
| { | |
| "step": 1, | |
| "action": "Reset to clean state", | |
| "fallback": "Manual intervention required" | |
| } | |
| ] | |
| } | |
| async def generate_fallback_content( | |
| self, | |
| query: str, | |
| available_info: dict | None, | |
| failed_sources: list[str], | |
| cached_data: dict | None = None | |
| ) -> dict[str, Any]: | |
| """ | |
| Generate fallback content when primary sources fail. | |
| Args: | |
| query: Original query | |
| available_info: Any available information | |
| failed_sources: Sources that failed | |
| cached_data: Any cached data available | |
| Returns: | |
| Dictionary with fallback content | |
| """ | |
| prompt = FALLBACK_CONTENT_PROMPT.format( | |
| query=query, | |
| available_info=str(available_info) if available_info else "None", | |
| failed_sources=str(failed_sources), | |
| cached_data=str(cached_data) if cached_data else "None" | |
| ) | |
| try: | |
| result = await self.llm_client.call_json(prompt) | |
| return result | |
| except Exception: | |
| return { | |
| "fallback_content": { | |
| "response": "Unable to complete the research at this time.", | |
| "confidence": 0.0, | |
| "completeness": 0.0 | |
| }, | |
| "limitations_disclosure": { | |
| "what_is_missing": failed_sources, | |
| "quality_impact": "significant" | |
| } | |
| } | |
| async def check_system_health( | |
| self, | |
| health_metrics: dict, | |
| performance_data: dict | |
| ) -> dict[str, Any]: | |
| """ | |
| Check overall system health. | |
| Args: | |
| health_metrics: Health metrics from components | |
| performance_data: Performance statistics | |
| Returns: | |
| Dictionary with health assessment | |
| """ | |
| recent_errors = [ | |
| {"type": e.error_type, "message": e.error_message} | |
| for e in self.error_history[-10:] | |
| ] | |
| prompt = SYSTEM_HEALTH_PROMPT.format( | |
| health_metrics=str(health_metrics), | |
| recent_errors=str(recent_errors), | |
| performance_data=str(performance_data) | |
| ) | |
| try: | |
| result = await self.llm_client.call_json(prompt) | |
| return result | |
| except Exception: | |
| # Calculate simple health based on error rate | |
| error_count = len(self.error_history) | |
| health_score = max(0.0, 1.0 - (error_count / 100)) | |
| return { | |
| "health_status": { | |
| "overall": "healthy" if health_score > 0.7 else "degraded", | |
| "score": health_score | |
| }, | |
| "active_issues": [], | |
| "recommendations": [] | |
| } | |
| async def retry_with_backoff( | |
| self, | |
| func: Callable[..., T], | |
| *args, | |
| max_attempts: int = 3, | |
| initial_delay: float = 1.0, | |
| backoff_factor: float = 2.0, | |
| **kwargs | |
| ) -> T: | |
| """ | |
| Retry a function with exponential backoff. | |
| Args: | |
| func: Async function to retry | |
| *args: Positional arguments for func | |
| max_attempts: Maximum retry attempts | |
| initial_delay: Initial delay in seconds | |
| backoff_factor: Backoff multiplier | |
| **kwargs: Keyword arguments for func | |
| Returns: | |
| Result from successful function call | |
| Raises: | |
| Last exception if all retries fail | |
| """ | |
| import asyncio | |
| last_exception = None | |
| delay = initial_delay | |
| for attempt in range(1, max_attempts + 1): | |
| try: | |
| return await func(*args, **kwargs) | |
| except Exception as e: | |
| last_exception = e | |
| if attempt < max_attempts: | |
| logger.warning( | |
| f"Attempt {attempt} failed: {e}. Retrying in {delay}s..." | |
| ) | |
| await asyncio.sleep(delay) | |
| delay *= backoff_factor | |
| raise last_exception | |
| def get_error_summary(self) -> dict[str, Any]: | |
| """ | |
| Get a summary of recent errors. | |
| Returns: | |
| Dictionary with error statistics and recent errors | |
| """ | |
| if not self.error_history: | |
| return { | |
| "total_errors": 0, | |
| "by_severity": {}, | |
| "by_component": {}, | |
| "recent_errors": [] | |
| } | |
| by_severity = {} | |
| by_component = {} | |
| for record in self.error_history: | |
| sev = record.severity.value | |
| by_severity[sev] = by_severity.get(sev, 0) + 1 | |
| comp = record.context.component.value | |
| by_component[comp] = by_component.get(comp, 0) + 1 | |
| return { | |
| "total_errors": len(self.error_history), | |
| "by_severity": by_severity, | |
| "by_component": by_component, | |
| "recent_errors": [ | |
| { | |
| "type": e.error_type, | |
| "message": e.error_message, | |
| "component": e.context.component.value, | |
| "timestamp": e.timestamp.isoformat() | |
| } | |
| for e in self.error_history[-5:] | |
| ] | |
| } | |
| def clear_error_history(self) -> None: | |
| """Clear the error history.""" | |
| self.error_history = [] | |
| logger.info("Error history cleared") | |
| # Module singleton instance | |
| error_handler = ErrorHandler() | |