Spaces:
Running
Running
| """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() | |