""" 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, timedelta from typing import Any from .base import BaseScorer from ..types import BaseContext, ContextType 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