| """
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
|
|
| 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"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|
|
|
|
|
| if 'timeout' in exc_msg.lower() or 'Timeout' in exc_name:
|
| return StorageTimeoutError(backend, operation)
|
|
|
|
|
| if any(x in exc_name.lower() for x in ['connection', 'connect', 'network']):
|
| return StorageConnectionError(backend, exc_msg)
|
|
|
|
|
| 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")
|
|
|
|
|
|
|
|
|
|
|
|
|
| __all__ = [
|
|
|
| "MnemoCoreError",
|
| "RecoverableError",
|
| "IrrecoverableError",
|
| "ErrorCategory",
|
|
|
| "StorageError",
|
| "StorageConnectionError",
|
| "StorageTimeoutError",
|
| "DataCorruptionError",
|
|
|
| "VectorError",
|
| "DimensionMismatchError",
|
| "VectorOperationError",
|
|
|
| "ConfigurationError",
|
|
|
| "CircuitOpenError",
|
|
|
| "MemoryOperationError",
|
|
|
| "ValidationError",
|
| "MetadataValidationError",
|
| "AttributeValidationError",
|
|
|
| "NotFoundError",
|
| "AgentNotFoundError",
|
| "MemoryNotFoundError",
|
|
|
| "ProviderError",
|
| "UnsupportedProviderError",
|
| "UnsupportedTransportError",
|
| "DependencyMissingError",
|
|
|
| "wrap_storage_exception",
|
| "is_debug_mode",
|
| ]
|
|
|