"""Author RAG — Session Store Service. Reader session CRUD using Redis for hot state and JSON files for persistence. Per implementation plan. """ import json import uuid from pathlib import Path import structlog from app.config import get_settings logger = structlog.get_logger(__name__) cfg = get_settings() SESSIONS_DIR = Path("/data/sessions") if Path("/data").exists() else Path("data/sessions") SESSIONS_DIR.mkdir(parents=True, exist_ok=True) SESSION_TTL = cfg.RAG_SESSION_TTL_MINUTES * 60 class SessionStore: """Manages reader sessions in Redis (hot) + JSON files (cold).""" def __init__(self, redis=None): self.redis = redis def _redis_key(self, session_id: str, author_id: str) -> str: return f"session:{author_id}:{session_id}" def _file_path(self, session_id: str) -> Path: return SESSIONS_DIR / f"{session_id}.json" async def create(self, author_id: str) -> str: """Create a new empty session. Returns session_id.""" session_id = str(uuid.uuid4()) data = {"session_id": session_id, "author_id": author_id, "history": [], "turn_count": 0} await self.save(session_id, author_id, data) return session_id async def load(self, session_id: str, author_id: str) -> dict: """Load session from Redis or file fallback.""" key = self._redis_key(session_id, author_id) if self.redis: try: raw = await self.redis.get(key) if raw: return json.loads(raw) except Exception as e: logger.warning("Redis session load failed", error=str(e)) # File fallback path = self._file_path(session_id) if path.exists(): return json.loads(path.read_text()) return {"session_id": session_id, "author_id": author_id, "history": [], "turn_count": 0} async def save(self, session_id: str, author_id: str, data: dict) -> None: """Save session to Redis + file.""" key = self._redis_key(session_id, author_id) raw = json.dumps(data) if self.redis: try: await self.redis.setex(key, SESSION_TTL, raw) except Exception as e: logger.warning("Redis session save failed", error=str(e)) # Always persist to file self._file_path(session_id).write_text(raw) async def delete(self, session_id: str, author_id: str) -> None: """Delete session from Redis and file.""" key = self._redis_key(session_id, author_id) if self.redis: try: await self.redis.delete(key) except Exception: pass path = self._file_path(session_id) if path.exists(): path.unlink()