"""Session management for FixMyNeighborhood. Provides: - Session-scoped orchestrator management - Multi-user isolation via gr.State - Session ID generation """ import hashlib import time from typing import Optional from config import ANTHROPIC_API_KEY from agents.controller import AutonomousController def generate_session_id(orchestrator: Optional[AutonomousController] = None) -> str: """ Generate a unique session ID for rate limiting and audit. Args: orchestrator: Optional orchestrator instance for stable ID Returns: 16-character hex session ID """ if orchestrator: seed = str(id(orchestrator)) else: seed = str(time.time()) return hashlib.sha256(seed.encode()).hexdigest()[:16] def get_or_create_orchestrator( session_orchestrator: Optional[AutonomousController] ) -> Optional[AutonomousController]: """ Get existing session orchestrator or create new one. Uses gr.State to maintain orchestrator per browser session: - Same user (same tab): Reuses orchestrator for multi-turn context - Different users (different tabs/browsers): Separate orchestrators Args: session_orchestrator: Existing orchestrator from gr.State (None if first request) Returns: Orchestrator instance for this session, or None if API key not configured """ if session_orchestrator is not None: return session_orchestrator if not ANTHROPIC_API_KEY: return None try: return AutonomousController() except Exception as e: print(f"Orchestrator creation error: {e}") return None class SessionManager: """ Manages session state for multi-user isolation. In Gradio, each browser tab gets its own gr.State values. This class provides additional session management utilities. """ def __init__(self): self._active_sessions: dict = {} def create_session(self) -> str: """Create a new session and return its ID.""" session_id = generate_session_id() self._active_sessions[session_id] = { "created_at": time.time(), "last_activity": time.time(), } return session_id def update_activity(self, session_id: str) -> None: """Update last activity time for a session.""" if session_id in self._active_sessions: self._active_sessions[session_id]["last_activity"] = time.time() def cleanup_stale_sessions(self, max_age_seconds: float = 3600) -> int: """ Remove sessions older than max_age. Args: max_age_seconds: Maximum session age in seconds (default 1 hour) Returns: Number of sessions cleaned up """ now = time.time() stale = [ sid for sid, data in self._active_sessions.items() if now - data["last_activity"] > max_age_seconds ] for sid in stale: del self._active_sessions[sid] return len(stale) def get_active_count(self) -> int: """Get count of active sessions.""" return len(self._active_sessions)