Arag / app /services /pipeline /cache.py
AuthorBot
Wire upsell strategy into LLM prompts for humanistic persuasion.
a0ab367
Raw
History Blame Contribute Delete
2.03 kB
"""pipeline/cache.py β€” LRU answer cache.
Keyed on MD5(author_id + book_id + normalized query).
Stores raw LLM answers only β€” upsell formatting is re-applied per session on hit.
Max 256 slots. Evicts LRU on overflow.
NOT cached: purchase_intent, complaint, greeting (personal / time-sensitive).
"""
from __future__ import annotations
import hashlib
from collections import OrderedDict
from dataclasses import dataclass, field
@dataclass
class CachedAnswer:
"""Raw generation output β€” upsell/link state is NOT cached."""
raw_response: str
faithfulness_score: float = 1.0
hallucination_detected: bool = False
prompt_tokens: int = 0
completion_tokens: int = 0
top_book_ids: list[str] = field(default_factory=list)
intent: str = "question"
intent_confidence: float = 0.7
_CACHE_MAX = 256
_answer_cache: OrderedDict[str, CachedAnswer] = OrderedDict()
def cache_key(author_id: str, book_id: str | None, query: str) -> str:
"""Generate a stable MD5 cache key."""
raw = f"{author_id}:{book_id or ''}:{query.lower().strip()}"
return hashlib.md5(raw.encode()).hexdigest()
def cache_get(key: str) -> CachedAnswer | None:
"""Return cached raw answer or None. Promotes the key to most-recently-used."""
if key in _answer_cache:
_answer_cache.move_to_end(key)
return _answer_cache[key]
return None
def cache_set(key: str, answer: CachedAnswer) -> None:
"""Store a raw answer. Evicts least-recently-used entry when over capacity."""
_answer_cache[key] = answer
_answer_cache.move_to_end(key)
if len(_answer_cache) > _CACHE_MAX:
_answer_cache.popitem(last=False)
def invalidate_book_cache(author_id: str, book_id: str) -> int: # noqa: ARG001
"""Remove ALL cached answers for this author when a book changes.
Called by the ingest pipeline on re-upload to prevent stale answers.
Returns:
Number of cache entries removed.
"""
count = len(_answer_cache)
_answer_cache.clear()
return count