MnemoCore / src /mnemocore /core /exceptions.py
Granis87's picture
Initial upload of MnemoCore
dbb04e4 verified
"""
MnemoCore Domain-Specific Exceptions
=====================================
This module defines a hierarchy of exceptions for consistent error handling
across the MnemoCore system.
Exception Hierarchy:
MnemoCoreError (base)
β”œβ”€β”€ RecoverableError (transient, retry possible)
β”‚ β”œβ”€β”€ StorageConnectionError
β”‚ β”œβ”€β”€ StorageTimeoutError
β”‚ └── CircuitOpenError
β”œβ”€β”€ IrrecoverableError (permanent, requires intervention)
β”‚ β”œβ”€β”€ ConfigurationError
β”‚ β”œβ”€β”€ DataCorruptionError
β”‚ β”œβ”€β”€ ValidationError
β”‚ β”œβ”€β”€ NotFoundError
β”‚ └── UnsupportedOperationError
└── Domain Errors (mixed recoverability)
β”œβ”€β”€ StorageError
β”œβ”€β”€ VectorError
β”‚ β”œβ”€β”€ DimensionMismatchError
β”‚ └── VectorOperationError
└── MemoryOperationError
Usage Guidelines:
- Return None for "not found" scenarios (expected case, not an error)
- Raise exceptions for actual errors (connection failures, validation, corruption)
- Always include context in error messages
- Use error_code for API responses
"""
from typing import Optional, Any
from enum import Enum
import os
class ErrorCategory(Enum):
"""Categories for error classification."""
STORAGE = "STORAGE"
VECTOR = "VECTOR"
CONFIG = "CONFIG"
VALIDATION = "VALIDATION"
MEMORY = "MEMORY"
AGENT = "AGENT"
PROVIDER = "PROVIDER"
SYSTEM = "SYSTEM"
class MnemoCoreError(Exception):
"""
Base exception for all MnemoCore errors.
Attributes:
message: Human-readable error message
error_code: Machine-readable error code for API responses
context: Additional context about the error
recoverable: Whether the error is potentially recoverable
"""
error_code: str = "MNEMO_CORE_ERROR"
recoverable: bool = True
category: ErrorCategory = ErrorCategory.SYSTEM
def __init__(
self,
message: str,
context: Optional[dict] = None,
error_code: Optional[str] = None,
recoverable: Optional[bool] = None
):
super().__init__(message)
self.message = message
self.context = context or {}
if error_code is not None:
self.error_code = error_code
if recoverable is not None:
self.recoverable = recoverable
def __str__(self) -> str:
if self.context:
return f"{self.message} | context={self.context}"
return self.message
def to_dict(self, include_traceback: bool = False) -> dict:
"""
Convert exception to dictionary for JSON response.
Args:
include_traceback: Whether to include stack trace (only in DEBUG mode)
"""
result = {
"error": self.message,
"code": self.error_code,
"recoverable": self.recoverable,
}
if include_traceback:
import traceback
result["traceback"] = traceback.format_exc()
if self.context:
result["context"] = self.context
return result
# =============================================================================
# Base Categories: Recoverable vs Irrecoverable
# =============================================================================
class RecoverableError(MnemoCoreError):
"""
Base class for recoverable errors.
These are transient errors that may succeed on retry:
- Connection failures
- Timeouts
- Circuit breaker open
- Rate limiting
"""
recoverable = True
class IrrecoverableError(MnemoCoreError):
"""
Base class for irrecoverable errors.
These are permanent errors that require intervention:
- Invalid configuration
- Data corruption
- Validation failures
- Resource not found
"""
recoverable = False
# =============================================================================
# Storage Errors
# =============================================================================
class StorageError(MnemoCoreError):
"""Base exception for storage-related errors."""
error_code = "STORAGE_ERROR"
category = ErrorCategory.STORAGE
class StorageConnectionError(RecoverableError, StorageError):
"""Raised when connection to storage backend fails."""
error_code = "STORAGE_CONNECTION_ERROR"
def __init__(self, backend: str, message: str = "Connection failed", context: Optional[dict] = None):
ctx = {"backend": backend}
if context:
ctx.update(context)
super().__init__(f"[{backend}] {message}", ctx)
self.backend = backend
class StorageTimeoutError(RecoverableError, StorageError):
"""Raised when a storage operation times out."""
error_code = "STORAGE_TIMEOUT_ERROR"
def __init__(self, backend: str, operation: str, timeout_ms: Optional[int] = None, context: Optional[dict] = None):
msg = f"[{backend}] Operation '{operation}' timed out"
ctx = {"backend": backend, "operation": operation}
if timeout_ms is not None:
ctx["timeout_ms"] = timeout_ms
if context:
ctx.update(context)
super().__init__(msg, ctx)
self.backend = backend
self.operation = operation
class DataCorruptionError(IrrecoverableError, StorageError):
"""Raised when stored data is corrupt or cannot be deserialized."""
error_code = "DATA_CORRUPTION_ERROR"
def __init__(self, resource_id: str, reason: str = "Data corruption detected", context: Optional[dict] = None):
ctx = {"resource_id": resource_id}
if context:
ctx.update(context)
super().__init__(f"{reason} for resource '{resource_id}'", ctx)
self.resource_id = resource_id
# =============================================================================
# Vector Errors
# =============================================================================
class VectorError(MnemoCoreError):
"""Base exception for vector/hyperdimensional operations."""
error_code = "VECTOR_ERROR"
category = ErrorCategory.VECTOR
class DimensionMismatchError(IrrecoverableError, VectorError):
"""Raised when vector dimensions do not match."""
error_code = "DIMENSION_MISMATCH_ERROR"
def __init__(self, expected: int, actual: int, operation: str = "operation", context: Optional[dict] = None):
ctx = {"expected": expected, "actual": actual, "operation": operation}
if context:
ctx.update(context)
super().__init__(
f"Dimension mismatch in {operation}: expected {expected}, got {actual}",
ctx
)
self.expected = expected
self.actual = actual
self.operation = operation
class VectorOperationError(IrrecoverableError, VectorError):
"""Raised when a vector operation fails."""
error_code = "VECTOR_OPERATION_ERROR"
def __init__(self, operation: str, reason: str, context: Optional[dict] = None):
ctx = {"operation": operation}
if context:
ctx.update(context)
super().__init__(f"Vector operation '{operation}' failed: {reason}", ctx)
self.operation = operation
# =============================================================================
# Configuration Errors
# =============================================================================
class ConfigurationError(IrrecoverableError):
"""Raised when configuration is invalid or missing."""
error_code = "CONFIGURATION_ERROR"
category = ErrorCategory.CONFIG
def __init__(self, config_key: str, reason: str, context: Optional[dict] = None):
ctx = {"config_key": config_key}
if context:
ctx.update(context)
super().__init__(f"Configuration error for '{config_key}': {reason}", ctx)
self.config_key = config_key
# =============================================================================
# Circuit Breaker Errors
# =============================================================================
class CircuitOpenError(RecoverableError):
"""Raised when a circuit breaker is open and blocking requests."""
error_code = "CIRCUIT_OPEN_ERROR"
category = ErrorCategory.SYSTEM
def __init__(self, breaker_name: str, failures: int, context: Optional[dict] = None):
ctx = {"breaker_name": breaker_name, "failures": failures}
if context:
ctx.update(context)
super().__init__(
f"Circuit breaker '{breaker_name}' is OPEN after {failures} failures",
ctx
)
self.breaker_name = breaker_name
self.failures = failures
# =============================================================================
# Memory Operation Errors
# =============================================================================
class MemoryOperationError(MnemoCoreError):
"""Raised when a memory operation (store, retrieve, delete) fails."""
error_code = "MEMORY_OPERATION_ERROR"
category = ErrorCategory.MEMORY
def __init__(self, operation: str, node_id: Optional[str], reason: str, context: Optional[dict] = None):
ctx = {"operation": operation}
if node_id:
ctx["node_id"] = node_id
if context:
ctx.update(context)
super().__init__(f"Memory {operation} failed for '{node_id}': {reason}", ctx)
self.operation = operation
self.node_id = node_id
# =============================================================================
# Validation Errors
# =============================================================================
class ValidationError(IrrecoverableError):
"""Raised when input validation fails."""
error_code = "VALIDATION_ERROR"
category = ErrorCategory.VALIDATION
def __init__(self, field: str, reason: str, value: Any = None, context: Optional[dict] = None):
ctx = {"field": field}
if value is not None:
# Truncate large values
value_str = str(value)
if len(value_str) > 100:
value_str = value_str[:100] + "..."
ctx["value"] = value_str
if context:
ctx.update(context)
super().__init__(f"Validation error for '{field}': {reason}", ctx)
self.field = field
self.reason = reason
class MetadataValidationError(ValidationError):
"""Raised when metadata validation fails."""
error_code = "METADATA_VALIDATION_ERROR"
class AttributeValidationError(ValidationError):
"""Raised when attribute validation fails."""
error_code = "ATTRIBUTE_VALIDATION_ERROR"
# =============================================================================
# Not Found Errors
# =============================================================================
class NotFoundError(IrrecoverableError):
"""Raised when a requested resource is not found."""
error_code = "NOT_FOUND_ERROR"
category = ErrorCategory.SYSTEM
def __init__(self, resource_type: str, resource_id: str, context: Optional[dict] = None):
ctx = {"resource_type": resource_type, "resource_id": resource_id}
if context:
ctx.update(context)
super().__init__(f"{resource_type} '{resource_id}' not found", ctx)
self.resource_type = resource_type
self.resource_id = resource_id
class AgentNotFoundError(NotFoundError):
"""Raised when an agent is not found."""
error_code = "AGENT_NOT_FOUND_ERROR"
category = ErrorCategory.AGENT
def __init__(self, agent_id: str, context: Optional[dict] = None):
super().__init__("Agent", agent_id, context)
self.agent_id = agent_id
class MemoryNotFoundError(NotFoundError):
"""Raised when a memory is not found."""
error_code = "MEMORY_NOT_FOUND_ERROR"
category = ErrorCategory.MEMORY
def __init__(self, memory_id: str, context: Optional[dict] = None):
super().__init__("Memory", memory_id, context)
self.memory_id = memory_id
# =============================================================================
# Provider Errors
# =============================================================================
class ProviderError(MnemoCoreError):
"""Base exception for provider-related errors."""
error_code = "PROVIDER_ERROR"
category = ErrorCategory.PROVIDER
class UnsupportedProviderError(IrrecoverableError, ProviderError):
"""Raised when an unsupported provider is requested."""
error_code = "UNSUPPORTED_PROVIDER_ERROR"
def __init__(self, provider: str, supported_providers: Optional[list] = None, context: Optional[dict] = None):
ctx = {"provider": provider}
if supported_providers:
ctx["supported_providers"] = supported_providers
if context:
ctx.update(context)
msg = f"Unsupported provider: {provider}"
if supported_providers:
msg += f". Supported: {', '.join(supported_providers)}"
super().__init__(msg, ctx)
self.provider = provider
class UnsupportedTransportError(IrrecoverableError, ValueError):
"""Raised when an unsupported transport is requested."""
error_code = "UNSUPPORTED_TRANSPORT_ERROR"
category = ErrorCategory.CONFIG
def __init__(self, transport: str, supported_transports: Optional[list] = None, context: Optional[dict] = None):
ctx = {"transport": transport}
if supported_transports:
ctx["supported_transports"] = supported_transports
if context:
ctx.update(context)
msg = f"Unsupported transport: {transport}"
if supported_transports:
msg += f". Supported: {', '.join(supported_transports)}"
super().__init__(msg, ctx)
self.transport = transport
class DependencyMissingError(IrrecoverableError):
"""Raised when a required dependency is missing."""
error_code = "DEPENDENCY_MISSING_ERROR"
category = ErrorCategory.SYSTEM
def __init__(self, dependency: str, message: str = "", context: Optional[dict] = None):
ctx = {"dependency": dependency}
if context:
ctx.update(context)
msg = f"Missing dependency: {dependency}"
if message:
msg += f". {message}"
super().__init__(msg, ctx)
self.dependency = dependency
# =============================================================================
# Utility Functions
# =============================================================================
def wrap_storage_exception(backend: str, operation: str, exc: Exception) -> StorageError:
"""
Wrap a generic exception into an appropriate StorageError.
Args:
backend: Name of the storage backend (e.g., 'redis', 'qdrant')
operation: Name of the operation that failed
exc: The original exception
Returns:
An appropriate StorageError subclass
"""
exc_name = type(exc).__name__
exc_msg = str(exc)
# Timeout detection
if 'timeout' in exc_msg.lower() or 'Timeout' in exc_name:
return StorageTimeoutError(backend, operation)
# Connection error detection
if any(x in exc_name.lower() for x in ['connection', 'connect', 'network']):
return StorageConnectionError(backend, exc_msg)
# Default to generic storage error
return StorageError(
f"[{backend}] {operation} failed: {exc_msg}",
{"backend": backend, "operation": operation, "original_exception": exc_name}
)
def is_debug_mode() -> bool:
"""Check if debug mode is enabled via environment variable."""
return os.environ.get("MNEMO_DEBUG", "").lower() in ("true", "1", "yes")
# =============================================================================
# Convenience Exports
# =============================================================================
__all__ = [
# Base
"MnemoCoreError",
"RecoverableError",
"IrrecoverableError",
"ErrorCategory",
# Storage
"StorageError",
"StorageConnectionError",
"StorageTimeoutError",
"DataCorruptionError",
# Vector
"VectorError",
"DimensionMismatchError",
"VectorOperationError",
# Config
"ConfigurationError",
# Circuit Breaker
"CircuitOpenError",
# Memory
"MemoryOperationError",
# Validation
"ValidationError",
"MetadataValidationError",
"AttributeValidationError",
# Not Found
"NotFoundError",
"AgentNotFoundError",
"MemoryNotFoundError",
# Provider
"ProviderError",
"UnsupportedProviderError",
"UnsupportedTransportError",
"DependencyMissingError",
# Utilities
"wrap_storage_exception",
"is_debug_mode",
]