Files
fast-next-template/backend/app/services/context/exceptions.py
Felipe Cardoso 22ecb5e989 feat(context): Phase 1 - Foundation types, config and exceptions (#79)
Implements the foundation for Context Management Engine:

Types (backend/app/services/context/types/):
- BaseContext: Abstract base with ID, content, priority, scoring
- SystemContext: System prompts, personas, instructions
- KnowledgeContext: RAG results from Knowledge Base MCP
- ConversationContext: Chat history with role support
- TaskContext: Task/issue context with acceptance criteria
- ToolContext: Tool definitions and execution results
- AssembledContext: Final assembled context result

Configuration (config.py):
- Token budget allocation (system 5%, task 10%, knowledge 40%, etc.)
- Scoring weights (relevance 50%, recency 30%, priority 20%)
- Cache settings (TTL, prefix)
- Performance settings (max assembly time, parallel scoring)
- Environment variable overrides with CTX_ prefix

Exceptions (exceptions.py):
- ContextError: Base exception
- BudgetExceededError: Token budget violations
- TokenCountError: Token counting failures
- CompressionError: Compression failures
- AssemblyTimeoutError: Assembly timeout
- ScoringError, FormattingError, CacheError
- ContextNotFoundError, InvalidContextError

All 86 tests pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 02:07:39 +01:00

355 lines
9.5 KiB
Python

"""
Context Management Engine Exceptions.
Provides a hierarchy of exceptions for context assembly,
token budget management, and related operations.
"""
from typing import Any
class ContextError(Exception):
"""
Base exception for all context management errors.
All context-related exceptions should inherit from this class
to allow for catch-all handling when needed.
"""
def __init__(self, message: str, details: dict[str, Any] | None = None) -> None:
"""
Initialize context error.
Args:
message: Human-readable error message
details: Optional dict with additional error context
"""
self.message = message
self.details = details or {}
super().__init__(message)
def to_dict(self) -> dict[str, Any]:
"""Convert exception to dictionary for logging/serialization."""
return {
"error_type": self.__class__.__name__,
"message": self.message,
"details": self.details,
}
class BudgetExceededError(ContextError):
"""
Raised when token budget is exceeded.
This occurs when the assembled context would exceed the
allocated token budget for a specific context type or total.
"""
def __init__(
self,
message: str = "Token budget exceeded",
allocated: int = 0,
requested: int = 0,
context_type: str | None = None,
) -> None:
"""
Initialize budget exceeded error.
Args:
message: Error message
allocated: Tokens allocated for this context type
requested: Tokens requested
context_type: Type of context that exceeded budget
"""
details = {
"allocated": allocated,
"requested": requested,
"overage": requested - allocated,
}
if context_type:
details["context_type"] = context_type
super().__init__(message, details)
self.allocated = allocated
self.requested = requested
self.context_type = context_type
class TokenCountError(ContextError):
"""
Raised when token counting fails.
This typically occurs when the LLM Gateway token counting
service is unavailable or returns an error.
"""
def __init__(
self,
message: str = "Failed to count tokens",
model: str | None = None,
text_length: int | None = None,
) -> None:
"""
Initialize token count error.
Args:
message: Error message
model: Model for which counting was attempted
text_length: Length of text that failed to count
"""
details: dict[str, Any] = {}
if model:
details["model"] = model
if text_length is not None:
details["text_length"] = text_length
super().__init__(message, details)
self.model = model
self.text_length = text_length
class CompressionError(ContextError):
"""
Raised when context compression fails.
This can occur when summarization or truncation cannot
reduce content to fit within the budget.
"""
def __init__(
self,
message: str = "Failed to compress context",
original_tokens: int | None = None,
target_tokens: int | None = None,
achieved_tokens: int | None = None,
) -> None:
"""
Initialize compression error.
Args:
message: Error message
original_tokens: Tokens before compression
target_tokens: Target token count
achieved_tokens: Tokens achieved after compression attempt
"""
details: dict[str, Any] = {}
if original_tokens is not None:
details["original_tokens"] = original_tokens
if target_tokens is not None:
details["target_tokens"] = target_tokens
if achieved_tokens is not None:
details["achieved_tokens"] = achieved_tokens
super().__init__(message, details)
self.original_tokens = original_tokens
self.target_tokens = target_tokens
self.achieved_tokens = achieved_tokens
class AssemblyTimeoutError(ContextError):
"""
Raised when context assembly exceeds time limit.
Context assembly must complete within a configurable
time limit to maintain responsiveness.
"""
def __init__(
self,
message: str = "Context assembly timed out",
timeout_ms: int = 0,
elapsed_ms: float = 0.0,
stage: str | None = None,
) -> None:
"""
Initialize assembly timeout error.
Args:
message: Error message
timeout_ms: Configured timeout in milliseconds
elapsed_ms: Actual elapsed time in milliseconds
stage: Pipeline stage where timeout occurred
"""
details = {
"timeout_ms": timeout_ms,
"elapsed_ms": round(elapsed_ms, 2),
}
if stage:
details["stage"] = stage
super().__init__(message, details)
self.timeout_ms = timeout_ms
self.elapsed_ms = elapsed_ms
self.stage = stage
class ScoringError(ContextError):
"""
Raised when context scoring fails.
This occurs when relevance, recency, or priority scoring
encounters an error.
"""
def __init__(
self,
message: str = "Failed to score context",
scorer_type: str | None = None,
context_id: str | None = None,
) -> None:
"""
Initialize scoring error.
Args:
message: Error message
scorer_type: Type of scorer that failed
context_id: ID of context being scored
"""
details: dict[str, Any] = {}
if scorer_type:
details["scorer_type"] = scorer_type
if context_id:
details["context_id"] = context_id
super().__init__(message, details)
self.scorer_type = scorer_type
self.context_id = context_id
class FormattingError(ContextError):
"""
Raised when context formatting fails.
This occurs when converting assembled context to
model-specific format fails.
"""
def __init__(
self,
message: str = "Failed to format context",
model: str | None = None,
adapter: str | None = None,
) -> None:
"""
Initialize formatting error.
Args:
message: Error message
model: Target model
adapter: Adapter that failed
"""
details: dict[str, Any] = {}
if model:
details["model"] = model
if adapter:
details["adapter"] = adapter
super().__init__(message, details)
self.model = model
self.adapter = adapter
class CacheError(ContextError):
"""
Raised when cache operations fail.
This is typically non-fatal and should be handled
gracefully by falling back to recomputation.
"""
def __init__(
self,
message: str = "Cache operation failed",
operation: str | None = None,
cache_key: str | None = None,
) -> None:
"""
Initialize cache error.
Args:
message: Error message
operation: Cache operation that failed (get, set, delete)
cache_key: Key involved in the failed operation
"""
details: dict[str, Any] = {}
if operation:
details["operation"] = operation
if cache_key:
details["cache_key"] = cache_key
super().__init__(message, details)
self.operation = operation
self.cache_key = cache_key
class ContextNotFoundError(ContextError):
"""
Raised when expected context is not found.
This occurs when required context sources return
no results or are unavailable.
"""
def __init__(
self,
message: str = "Required context not found",
source: str | None = None,
query: str | None = None,
) -> None:
"""
Initialize context not found error.
Args:
message: Error message
source: Source that returned no results
query: Query used to search
"""
details: dict[str, Any] = {}
if source:
details["source"] = source
if query:
details["query"] = query
super().__init__(message, details)
self.source = source
self.query = query
class InvalidContextError(ContextError):
"""
Raised when context data is invalid.
This occurs when context content or metadata
fails validation.
"""
def __init__(
self,
message: str = "Invalid context data",
field: str | None = None,
value: Any | None = None,
reason: str | None = None,
) -> None:
"""
Initialize invalid context error.
Args:
message: Error message
field: Field that is invalid
value: Invalid value (may be redacted for security)
reason: Reason for invalidity
"""
details: dict[str, Any] = {}
if field:
details["field"] = field
if value is not None:
# Avoid logging potentially sensitive values
details["value_type"] = type(value).__name__
if reason:
details["reason"] = reason
super().__init__(message, details)
self.field = field
self.value = value
self.reason = reason