Spaces:
Sleeping
Sleeping
| """ | |
| app/session_store.py β Redis abstraction with in-memory fallback. | |
| V2 Fix: V1 used a plain dict β sessions lost on restart. | |
| V2 uses Upstash Redis (free tier). If Redis is unavailable, falls back to | |
| an in-memory dict so the episode never crashes. Worst case: sessions are | |
| process-local again, same as V1. | |
| The rest of the codebase never touches Redis directly β only load/save/delete. | |
| """ | |
| import os | |
| import pickle | |
| from typing import Optional | |
| # ββ Lazy Redis client ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _redis_client = None | |
| _local_cache: dict = {} # In-memory fallback β activated when Redis is down | |
| REDIS_URL = os.getenv("REDIS_URL", "") | |
| SESSION_TTL = 3600 # 1 hour β episodes expire after inactivity | |
| def _get_redis(): | |
| """Lazy singleton. Returns Redis client or None if unavailable.""" | |
| global _redis_client | |
| if _redis_client is not None: | |
| return _redis_client | |
| if not REDIS_URL: | |
| return None | |
| try: | |
| import redis as redis_lib | |
| _redis_client = redis_lib.from_url(REDIS_URL, decode_responses=False, socket_timeout=2) | |
| _redis_client.ping() # Fail fast if connection is broken | |
| return _redis_client | |
| except Exception: | |
| return None | |
| def load(session_id: str): | |
| """Fetch EpisodeState from Redis, fall back to local cache.""" | |
| key = f"session:{session_id}" | |
| r = _get_redis() | |
| if r: | |
| try: | |
| data = r.get(key) | |
| return pickle.loads(data) if data else None | |
| except Exception: | |
| pass | |
| # Fallback: local memory | |
| return _local_cache.get(session_id) | |
| def save(session_id: str, state) -> None: | |
| """Persist EpisodeState to Redis + local cache (dual write for resilience).""" | |
| key = f"session:{session_id}" | |
| _local_cache[session_id] = state # Always write locally | |
| r = _get_redis() | |
| if r: | |
| try: | |
| r.setex(key, SESSION_TTL, pickle.dumps(state)) | |
| except Exception: | |
| pass # Redis outage β local cache is the fallback | |
| def delete(session_id: str) -> None: | |
| """Remove session after episode completes.""" | |
| _local_cache.pop(session_id, None) | |
| r = _get_redis() | |
| if r: | |
| try: | |
| r.delete(f"session:{session_id}") | |
| except Exception: | |
| pass | |