166 lines
5.8 KiB
Python
166 lines
5.8 KiB
Python
"""
|
|
DRY Error Handling and Logging Utilities
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import traceback
|
|
from datetime import datetime, timezone
|
|
from enum import Enum
|
|
from typing import Any, Dict, Optional, Callable
|
|
from functools import wraps
|
|
|
|
from ..sse import create_error_event, create_tool_error_event
|
|
|
|
|
|
class ErrorCode(Enum):
|
|
"""Error codes for different types of failures"""
|
|
# Client errors (4xxx)
|
|
INVALID_REQUEST = 4001
|
|
MISSING_PARAMETERS = 4002
|
|
INVALID_SESSION = 4003
|
|
|
|
# Server errors (5xxx)
|
|
LLM_ERROR = 5001
|
|
TOOL_ERROR = 5002
|
|
DATABASE_ERROR = 5003
|
|
MEMORY_ERROR = 5004
|
|
EXTERNAL_API_ERROR = 5005
|
|
INTERNAL_ERROR = 5000
|
|
|
|
|
|
class ErrorCategory(Enum):
|
|
"""Error categories for better organization"""
|
|
VALIDATION = "validation"
|
|
LLM = "llm"
|
|
TOOL = "tool"
|
|
DATABASE = "database"
|
|
MEMORY = "memory"
|
|
EXTERNAL_API = "external_api"
|
|
INTERNAL = "internal"
|
|
|
|
|
|
class StructuredLogger:
|
|
"""DRY structured logging with automatic error handling"""
|
|
|
|
def __init__(self, name: str):
|
|
self.logger = logging.getLogger(name)
|
|
|
|
def error(self, msg: str, error: Optional[Exception] = None, category: ErrorCategory = ErrorCategory.INTERNAL,
|
|
error_code: ErrorCode = ErrorCode.INTERNAL_ERROR, extra: Optional[Dict[str, Any]] = None):
|
|
"""Log structured error with stack trace"""
|
|
data: Dict[str, Any] = {
|
|
"message": msg,
|
|
"category": category.value,
|
|
"error_code": error_code.value,
|
|
"timestamp": datetime.now(timezone.utc).isoformat()
|
|
}
|
|
|
|
if error:
|
|
data.update({
|
|
"error_type": type(error).__name__,
|
|
"error_message": str(error),
|
|
"stack_trace": traceback.format_exc()
|
|
})
|
|
|
|
if extra:
|
|
data["extra"] = extra
|
|
|
|
self.logger.error(json.dumps(data))
|
|
|
|
def info(self, msg: str, extra: Optional[Dict[str, Any]] = None):
|
|
"""Log structured info"""
|
|
data: Dict[str, Any] = {"message": msg, "timestamp": datetime.now(timezone.utc).isoformat()}
|
|
if extra:
|
|
data["extra"] = extra
|
|
self.logger.info(json.dumps(data))
|
|
|
|
def warning(self, msg: str, extra: Optional[Dict[str, Any]] = None):
|
|
"""Log structured warning"""
|
|
data: Dict[str, Any] = {"message": msg, "timestamp": datetime.now(timezone.utc).isoformat()}
|
|
if extra:
|
|
data["extra"] = extra
|
|
self.logger.warning(json.dumps(data))
|
|
|
|
|
|
def get_user_message(category: ErrorCategory) -> str:
|
|
"""Get user-friendly error messages in English"""
|
|
messages = {
|
|
ErrorCategory.VALIDATION: "Invalid request parameters. Please check your input.",
|
|
ErrorCategory.LLM: "AI service is temporarily unavailable. Please try again later.",
|
|
ErrorCategory.TOOL: "Tool execution failed. Please retry your request.",
|
|
ErrorCategory.DATABASE: "Database service is temporarily unavailable.",
|
|
ErrorCategory.MEMORY: "Session storage issue occurred. Please refresh the page.",
|
|
ErrorCategory.EXTERNAL_API: "External service connection failed.",
|
|
ErrorCategory.INTERNAL: "Internal server error. We are working to resolve this."
|
|
}
|
|
return messages.get(category, "Unknown error occurred. Please contact technical support.")
|
|
|
|
|
|
def handle_async_errors(category: ErrorCategory, error_code: ErrorCode,
|
|
stream_callback: Optional[Callable] = None, tool_id: Optional[str] = None):
|
|
"""DRY decorator for async error handling with streaming support"""
|
|
def decorator(func):
|
|
@wraps(func)
|
|
async def wrapper(*args, **kwargs):
|
|
logger = StructuredLogger(func.__module__)
|
|
|
|
try:
|
|
return await func(*args, **kwargs)
|
|
except Exception as e:
|
|
user_msg = get_user_message(category)
|
|
|
|
logger.error(
|
|
f"Error in {func.__name__}: {str(e)}",
|
|
error=e,
|
|
category=category,
|
|
error_code=error_code,
|
|
extra={"function": func.__name__, "args_count": len(args)}
|
|
)
|
|
|
|
# Send error event if streaming
|
|
if stream_callback:
|
|
if tool_id:
|
|
await stream_callback(create_tool_error_event(tool_id, func.__name__, user_msg))
|
|
else:
|
|
await stream_callback(create_error_event(user_msg))
|
|
|
|
# Re-raise with user-friendly message for API responses
|
|
raise Exception(user_msg) from e
|
|
return wrapper
|
|
return decorator
|
|
|
|
|
|
def handle_sync_errors(category: ErrorCategory, error_code: ErrorCode):
|
|
"""DRY decorator for sync error handling"""
|
|
def decorator(func):
|
|
@wraps(func)
|
|
def wrapper(*args, **kwargs):
|
|
logger = StructuredLogger(func.__module__)
|
|
|
|
try:
|
|
return func(*args, **kwargs)
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Error in {func.__name__}: {str(e)}",
|
|
error=e,
|
|
category=category,
|
|
error_code=error_code,
|
|
extra={"function": func.__name__}
|
|
)
|
|
raise Exception(get_user_message(category)) from e
|
|
return wrapper
|
|
return decorator
|
|
|
|
|
|
def create_error_response(category: ErrorCategory, error_code: ErrorCode,
|
|
technical_msg: Optional[str] = None) -> Dict[str, Any]:
|
|
"""Create consistent error response format"""
|
|
return {
|
|
"user_message": get_user_message(category),
|
|
"error_code": error_code.value,
|
|
"category": category.value,
|
|
"technical_message": technical_msg,
|
|
"timestamp": datetime.now(timezone.utc).isoformat()
|
|
}
|