| | """
|
| | 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",
|
| | ]
|
| |
|