This commit is contained in:
2025-09-26 17:15:54 +08:00
commit db0e5965ec
211 changed files with 40437 additions and 0 deletions

View File

@@ -0,0 +1 @@
# Empty __init__.py to make this a package

View File

@@ -0,0 +1,165 @@
"""
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()
}

View File

@@ -0,0 +1,94 @@
import logging
import json
import time
from typing import Dict, Any, Optional
from datetime import datetime
def setup_logging(level: str = "INFO", format_type: str = "json") -> None:
"""Setup structured logging"""
if format_type == "json":
formatter = JsonFormatter()
else:
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
handler = logging.StreamHandler()
handler.setFormatter(formatter)
root_logger = logging.getLogger()
root_logger.setLevel(getattr(logging, level.upper()))
root_logger.addHandler(handler)
class JsonFormatter(logging.Formatter):
"""JSON log formatter"""
def format(self, record: logging.LogRecord) -> str:
log_data = {
"timestamp": datetime.utcnow().isoformat(),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
}
# Add extra fields
if hasattr(record, "request_id"):
log_data["request_id"] = getattr(record, "request_id")
if hasattr(record, "session_id"):
log_data["session_id"] = getattr(record, "session_id")
if hasattr(record, "duration_ms"):
log_data["duration_ms"] = getattr(record, "duration_ms")
return json.dumps(log_data)
class Timer:
"""Simple timer context manager"""
def __init__(self):
self.start_time = None
self.end_time = None
def __enter__(self):
self.start_time = time.time()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.end_time = time.time()
@property
def elapsed_ms(self) -> int:
if self.start_time and self.end_time:
return int((self.end_time - self.start_time) * 1000)
return 0
def redact_secrets(data: Dict[str, Any], secret_keys: list[str] | None = None) -> Dict[str, Any]:
"""Redact sensitive information from logs"""
if secret_keys is None:
secret_keys = ["api_key", "password", "token", "secret", "key"]
redacted = {}
for key, value in data.items():
if any(secret in key.lower() for secret in secret_keys):
redacted[key] = "***REDACTED***"
elif isinstance(value, dict):
redacted[key] = redact_secrets(value, secret_keys)
else:
redacted[key] = value
return redacted
def generate_request_id() -> str:
"""Generate unique request ID"""
return f"req_{int(time.time() * 1000)}_{hash(time.time()) % 10000:04d}"
def truncate_text(text: str, max_length: int = 1000, suffix: str = "...") -> str:
"""Truncate text to maximum length"""
if len(text) <= max_length:
return text
return text[:max_length - len(suffix)] + suffix

View File

@@ -0,0 +1,51 @@
"""
Lightweight Error Handling Middleware
"""
from fastapi import Request, HTTPException
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
from .error_handler import StructuredLogger, ErrorCategory, ErrorCode, create_error_response
class ErrorMiddleware(BaseHTTPMiddleware):
"""Concise error handling middleware following DRY principles"""
def __init__(self, app):
super().__init__(app)
self.logger = StructuredLogger(__name__)
async def dispatch(self, request: Request, call_next):
try:
return await call_next(request)
except HTTPException as e:
# HTTP exceptions - map to appropriate categories
category = ErrorCategory.VALIDATION if e.status_code < 500 else ErrorCategory.INTERNAL
error_code = ErrorCode.INVALID_REQUEST if e.status_code < 500 else ErrorCode.INTERNAL_ERROR
self.logger.error(
f"HTTP {e.status_code}: {e.detail}",
category=category,
error_code=error_code,
extra={"path": str(request.url), "method": request.method}
)
return JSONResponse(
status_code=e.status_code,
content=create_error_response(category, error_code, e.detail)
)
except Exception as e:
# Unexpected errors
self.logger.error(
f"Unhandled error: {str(e)}",
error=e,
category=ErrorCategory.INTERNAL,
error_code=ErrorCode.INTERNAL_ERROR,
extra={"path": str(request.url), "method": request.method}
)
return JSONResponse(
status_code=500,
content=create_error_response(ErrorCategory.INTERNAL, ErrorCode.INTERNAL_ERROR)
)

View File

@@ -0,0 +1,103 @@
"""
Template utilities for Jinja2 template rendering with LangChain
"""
import logging
from typing import Dict, Any
from jinja2 import Environment, BaseLoader, TemplateError
logger = logging.getLogger(__name__)
class TemplateRenderer:
"""Jinja2 template renderer for LLM prompts"""
def __init__(self):
self.env = Environment(
loader=BaseLoader(),
# Enable safe variable substitution
autoescape=False,
# Custom delimiters to avoid conflicts with common markdown syntax
variable_start_string='{{',
variable_end_string='}}',
block_start_string='{%',
block_end_string='%}',
comment_start_string='{#',
comment_end_string='#}',
# Keep linebreaks
keep_trailing_newline=True,
# Remove unnecessary whitespace
trim_blocks=True,
lstrip_blocks=True
)
def render_template(self, template_string: str, variables: Dict[str, Any]) -> str:
"""
Render a Jinja2 template string with provided variables
Args:
template_string: The template string with Jinja2 syntax
variables: Dictionary of variables to substitute
Returns:
Rendered template string
Raises:
TemplateError: If template rendering fails
"""
try:
template = self.env.from_string(template_string)
rendered = template.render(**variables)
logger.debug(f"Template rendered successfully with variables: {list(variables.keys())}")
return rendered
except TemplateError as e:
logger.error(f"Template rendering failed: {e}")
logger.error(f"Template: {template_string[:200]}...")
logger.error(f"Variables: {variables}")
raise
except Exception as e:
logger.error(f"Unexpected error during template rendering: {e}")
raise TemplateError(f"Template rendering failed: {e}")
def render_system_prompt(self, template_string: str, variables: Dict[str, Any]) -> str:
"""
Render system prompt template
Args:
template_string: System prompt template
variables: Variables for substitution
Returns:
Rendered system prompt
"""
return self.render_template(template_string, variables)
def render_user_prompt(self, template_string: str, variables: Dict[str, Any]) -> str:
"""
Render user prompt template
Args:
template_string: User prompt template
variables: Variables for substitution
Returns:
Rendered user prompt
"""
return self.render_template(template_string, variables)
# Global template renderer instance
template_renderer = TemplateRenderer()
def render_prompt_template(template_string: str, variables: Dict[str, Any]) -> str:
"""
Convenience function to render prompt templates
Args:
template_string: Template string with Jinja2 syntax
variables: Dictionary of variables to substitute
Returns:
Rendered template string
"""
return template_renderer.render_template(template_string, variables)