Files
fast-next-template/backend/app/services/context/scoring/recency.py
Felipe Cardoso 6c7b72f130 chore(context): apply linter fixes and sort imports (#86)
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>
2026-01-04 02:46:56 +01:00

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