langgraph-mcts-demo / src /api /exceptions.py
ianshank
feat: add personality output and bug fixes
40ee6b4
"""
Custom exception hierarchy for LangGraph Multi-Agent MCTS Framework.
Provides:
- Sanitized error messages for production
- Structured error information for logging
- Clear separation between user-facing and internal errors
"""
import re
from datetime import datetime
from typing import Any
class FrameworkError(Exception):
"""
Base exception for all framework errors.
Provides sanitized user-facing messages while preserving
internal details for logging.
"""
def __init__(
self,
user_message: str,
internal_details: str | None = None,
error_code: str | None = None,
context: dict[str, Any] | None = None,
):
"""
Initialize framework error.
Args:
user_message: Safe message to show to users
internal_details: Detailed information for logs (may contain sensitive data)
error_code: Machine-readable error code
context: Additional context for debugging
"""
self.user_message = user_message
self.internal_details = internal_details or user_message
self.error_code = error_code or self.__class__.__name__.upper()
self.context = context or {}
self.timestamp = datetime.utcnow()
super().__init__(user_message)
def sanitize_details(self) -> str:
"""
Remove sensitive information from internal details.
Sanitizes:
- File paths
- API keys
- Passwords
- Connection strings
- IP addresses
"""
sanitized = self.internal_details
# Remove file paths (Unix and Windows)
sanitized = re.sub(r"/[\w/.-]+", "/***", sanitized)
sanitized = re.sub(r"[A-Za-z]:\\[\w\\.-]+", "C:\\***", sanitized)
# Remove API keys and secrets
sanitized = re.sub(
r"(api[_-]?key|secret|password|token|credential)[\s=:]+[\S]+", r"\1=***", sanitized, flags=re.IGNORECASE
)
# Remove connection strings
sanitized = re.sub(r"(mongodb|postgresql|mysql|redis)://[^\s]+", r"\1://***", sanitized, flags=re.IGNORECASE)
# Remove IP addresses
sanitized = re.sub(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", "***.***.***", sanitized)
# Remove email addresses
sanitized = re.sub(r"\b[\w.-]+@[\w.-]+\.\w+\b", "***@***", sanitized)
return sanitized
def to_log_dict(self) -> dict[str, Any]:
"""
Convert exception to dictionary for structured logging.
Returns sanitized version safe for logs.
"""
return {
"error_type": self.__class__.__name__,
"error_code": self.error_code,
"user_message": self.user_message,
"sanitized_details": self.sanitize_details(),
"timestamp": self.timestamp.isoformat(),
"context": {k: str(v) for k, v in self.context.items()},
}
def to_user_response(self) -> dict[str, Any]:
"""
Convert exception to safe user-facing response.
"""
return {
"error": True,
"error_code": self.error_code,
"message": self.user_message,
"timestamp": self.timestamp.isoformat(),
}
class ValidationError(FrameworkError):
"""Raised when input validation fails."""
def __init__(
self,
user_message: str = "Invalid input provided",
internal_details: str | None = None,
field_name: str | None = None,
**kwargs,
):
context = kwargs.pop("context", {})
if field_name:
context["field_name"] = field_name
super().__init__(
user_message=user_message,
internal_details=internal_details,
error_code="VALIDATION_ERROR",
context=context,
**kwargs,
)
self.field_name = field_name
class AuthenticationError(FrameworkError):
"""Raised when authentication fails."""
def __init__(self, user_message: str = "Authentication failed", internal_details: str | None = None, **kwargs):
super().__init__(
user_message=user_message, internal_details=internal_details, error_code="AUTH_ERROR", **kwargs
)
class AuthorizationError(FrameworkError):
"""Raised when authorization fails."""
def __init__(
self,
user_message: str = "Access denied",
internal_details: str | None = None,
required_permission: str | None = None,
**kwargs,
):
context = kwargs.pop("context", {})
if required_permission:
context["required_permission"] = required_permission
super().__init__(
user_message=user_message,
internal_details=internal_details,
error_code="AUTHZ_ERROR",
context=context,
**kwargs,
)
class RateLimitError(FrameworkError):
"""Raised when rate limit is exceeded."""
def __init__(
self,
user_message: str = "Rate limit exceeded. Please try again later.",
internal_details: str | None = None,
retry_after_seconds: int | None = None,
**kwargs,
):
context = kwargs.pop("context", {})
if retry_after_seconds:
context["retry_after_seconds"] = retry_after_seconds
super().__init__(
user_message=user_message,
internal_details=internal_details,
error_code="RATE_LIMIT",
context=context,
**kwargs,
)
self.retry_after_seconds = retry_after_seconds
class LLMError(FrameworkError):
"""Raised when LLM operations fail."""
def __init__(
self,
user_message: str = "Language model service temporarily unavailable",
internal_details: str | None = None,
provider: str | None = None,
**kwargs,
):
context = kwargs.pop("context", {})
if provider:
context["provider"] = provider
super().__init__(
user_message=user_message,
internal_details=internal_details,
error_code="LLM_ERROR",
context=context,
**kwargs,
)
class MCTSError(FrameworkError):
"""Raised when MCTS simulation fails."""
def __init__(
self,
user_message: str = "Tactical simulation failed",
internal_details: str | None = None,
iteration: int | None = None,
**kwargs,
):
context = kwargs.pop("context", {})
if iteration is not None:
context["iteration"] = iteration
super().__init__(
user_message=user_message,
internal_details=internal_details,
error_code="MCTS_ERROR",
context=context,
**kwargs,
)
class RAGError(FrameworkError):
"""Raised when RAG retrieval fails."""
def __init__(self, user_message: str = "Context retrieval failed", internal_details: str | None = None, **kwargs):
super().__init__(user_message=user_message, internal_details=internal_details, error_code="RAG_ERROR", **kwargs)
class TimeoutError(FrameworkError):
"""Raised when operation times out."""
def __init__(
self,
user_message: str = "Operation timed out",
internal_details: str | None = None,
operation: str | None = None,
timeout_seconds: float | None = None,
**kwargs,
):
context = kwargs.pop("context", {})
if operation:
context["operation"] = operation
if timeout_seconds:
context["timeout_seconds"] = timeout_seconds
super().__init__(
user_message=user_message,
internal_details=internal_details,
error_code="TIMEOUT",
context=context,
**kwargs,
)
class ConfigurationError(FrameworkError):
"""Raised when configuration is invalid."""
def __init__(
self,
user_message: str = "System configuration error",
internal_details: str | None = None,
config_key: str | None = None,
**kwargs,
):
context = kwargs.pop("context", {})
if config_key:
context["config_key"] = config_key
super().__init__(
user_message=user_message,
internal_details=internal_details,
error_code="CONFIG_ERROR",
context=context,
**kwargs,
)
# Convenience function for wrapping exceptions
def wrap_exception(
exc: Exception, user_message: str = "An unexpected error occurred", error_class: type = FrameworkError, **kwargs
) -> FrameworkError:
"""
Wrap a standard exception in a FrameworkError with sanitized details.
Args:
exc: Original exception
user_message: Safe user-facing message
error_class: FrameworkError subclass to use
**kwargs: Additional context
Returns:
FrameworkError instance with sanitized details
"""
internal_details = f"{type(exc).__name__}: {str(exc)}"
return error_class(user_message=user_message, internal_details=internal_details, **kwargs)