Phase 8 of Context Management Engine - Final Cleanup: - Sort __all__ exports alphabetically - Sort imports per isort conventions - Fix minor linting issues Final test results: - 311 context management tests passing - 2507 total backend tests passing - 85% code coverage Context Management Engine is complete with all 8 phases: 1. Foundation: Types, Config, Exceptions 2. Token Budget Management 3. Context Scoring & Ranking 4. Context Assembly Pipeline 5. Model Adapters (Claude, OpenAI) 6. Caching Layer (Redis + in-memory) 7. Main Engine & Integration 8. Testing & Documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
142 lines
4.4 KiB
Python
142 lines
4.4 KiB
Python
"""
|
|
Recency Scorer for Context Management.
|
|
|
|
Scores context based on how recent it is.
|
|
More recent content gets higher scores.
|
|
"""
|
|
|
|
import math
|
|
from datetime import UTC, datetime
|
|
from typing import Any
|
|
|
|
from ..types import BaseContext, ContextType
|
|
from .base import BaseScorer
|
|
|
|
|
|
class RecencyScorer(BaseScorer):
|
|
"""
|
|
Scores context based on recency.
|
|
|
|
Uses exponential decay to score content based on age.
|
|
More recent content scores higher.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
weight: float = 1.0,
|
|
half_life_hours: float = 24.0,
|
|
type_half_lives: dict[ContextType, float] | None = None,
|
|
) -> None:
|
|
"""
|
|
Initialize recency scorer.
|
|
|
|
Args:
|
|
weight: Scorer weight for composite scoring
|
|
half_life_hours: Default hours until score decays to 0.5
|
|
type_half_lives: Optional context-type-specific half lives
|
|
"""
|
|
super().__init__(weight)
|
|
self._half_life_hours = half_life_hours
|
|
self._type_half_lives = type_half_lives or {}
|
|
|
|
# Set sensible defaults for context types
|
|
if ContextType.CONVERSATION not in self._type_half_lives:
|
|
self._type_half_lives[ContextType.CONVERSATION] = 1.0 # 1 hour
|
|
if ContextType.TOOL not in self._type_half_lives:
|
|
self._type_half_lives[ContextType.TOOL] = 0.5 # 30 minutes
|
|
if ContextType.KNOWLEDGE not in self._type_half_lives:
|
|
self._type_half_lives[ContextType.KNOWLEDGE] = 168.0 # 1 week
|
|
if ContextType.SYSTEM not in self._type_half_lives:
|
|
self._type_half_lives[ContextType.SYSTEM] = 720.0 # 30 days
|
|
if ContextType.TASK not in self._type_half_lives:
|
|
self._type_half_lives[ContextType.TASK] = 24.0 # 1 day
|
|
|
|
async def score(
|
|
self,
|
|
context: BaseContext,
|
|
query: str,
|
|
**kwargs: Any,
|
|
) -> float:
|
|
"""
|
|
Score context based on recency.
|
|
|
|
Args:
|
|
context: Context to score
|
|
query: Query (not used for recency, kept for interface)
|
|
**kwargs: Additional parameters
|
|
- reference_time: Time to measure recency from (default: now)
|
|
|
|
Returns:
|
|
Recency score between 0.0 and 1.0
|
|
"""
|
|
reference_time = kwargs.get("reference_time")
|
|
if reference_time is None:
|
|
reference_time = datetime.now(UTC)
|
|
elif reference_time.tzinfo is None:
|
|
reference_time = reference_time.replace(tzinfo=UTC)
|
|
|
|
# Ensure context timestamp is timezone-aware
|
|
context_time = context.timestamp
|
|
if context_time.tzinfo is None:
|
|
context_time = context_time.replace(tzinfo=UTC)
|
|
|
|
# Calculate age in hours
|
|
age = reference_time - context_time
|
|
age_hours = max(0, age.total_seconds() / 3600)
|
|
|
|
# Get half-life for this context type
|
|
context_type = context.get_type()
|
|
half_life = self._type_half_lives.get(context_type, self._half_life_hours)
|
|
|
|
# Exponential decay
|
|
decay_factor = math.exp(-math.log(2) * age_hours / half_life)
|
|
|
|
return self.normalize_score(decay_factor)
|
|
|
|
def get_half_life(self, context_type: ContextType) -> float:
|
|
"""
|
|
Get half-life for a context type.
|
|
|
|
Args:
|
|
context_type: Context type to get half-life for
|
|
|
|
Returns:
|
|
Half-life in hours
|
|
"""
|
|
return self._type_half_lives.get(context_type, self._half_life_hours)
|
|
|
|
def set_half_life(self, context_type: ContextType, hours: float) -> None:
|
|
"""
|
|
Set half-life for a context type.
|
|
|
|
Args:
|
|
context_type: Context type to set half-life for
|
|
hours: Half-life in hours
|
|
"""
|
|
if hours <= 0:
|
|
raise ValueError("Half-life must be positive")
|
|
self._type_half_lives[context_type] = hours
|
|
|
|
async def score_batch(
|
|
self,
|
|
contexts: list[BaseContext],
|
|
query: str,
|
|
**kwargs: Any,
|
|
) -> list[float]:
|
|
"""
|
|
Score multiple contexts.
|
|
|
|
Args:
|
|
contexts: Contexts to score
|
|
query: Query (not used)
|
|
**kwargs: Additional parameters
|
|
|
|
Returns:
|
|
List of scores (same order as input)
|
|
"""
|
|
scores = []
|
|
for context in contexts:
|
|
score = await self.score(context, query, **kwargs)
|
|
scores.append(score)
|
|
return scores
|