BeatDebate / Design /multi_user_session_handling_design.md
SulmanK's picture
Refactor chat interface for per-user session management - Removed global session and conversation history management, transitioning to a per-user model using `gr.State`. Updated message processing to include user-specific session IDs, enhancing multi-user safety and improving fallback handling. This update aims to streamline user interactions and maintain session context effectively.
9610835
|
Raw
History Blame Contribute Delete
19.4 kB

A newer version of the Gradio SDK is available: 6.20.0

Upgrade

Multi-User Session Handling - Design Document

Date: January 2025
Author: BeatDebate Team
Status: βœ… COMPLETED
Review Status: Pending


1. Problem Statement

Objective: Implement secure multi-user session handling for BeatDebate to prevent context bleeding and ensure proper session isolation when deployed on HuggingFace Spaces or any multi-user environment.

Current State: BeatDebate has a critical concurrency vulnerability where:

  • SessionManagerService uses a shared in-memory dictionary (self.session_store = {}) for all users
  • BeatDebateChatInterface maintains a single global session ID (self.session_id = str(uuid.uuid4()))
  • Multiple users can experience context bleeding, session collision, and data corruption

Critical Issues:

  1. Context Bleeding: Users receive recommendations based on other users' conversation histories
  2. Session Collision: Follow-up queries may fail due to contaminated session context
  3. Data Corruption: Concurrent access to shared session state leads to unpredictable behavior
  4. Security Risk: User preferences and interaction patterns leak between sessions

Value Proposition:

  • Multi-User Safety: Each user gets isolated, private session context
  • Reliable Follow-ups: "More tracks" and "similar to these" work correctly per user
  • Production Ready: Safe deployment to HuggingFace Spaces and other shared environments
  • User Experience: Consistent, predictable behavior for concurrent users

2. Goals & Non-Goals

βœ… In Scope

  • Frontend Session Management: Use gr.State to maintain per-user session IDs
  • Session ID Propagation: Pass user-specific session IDs from frontend to backend
  • Session Context Isolation: Ensure each user's conversation history stays separate
  • Follow-up Query Support: Maintain proper context for "more tracks" type queries
  • Backwards Compatibility: Existing backend session logic remains functional
  • HuggingFace Spaces Compatibility: Works correctly in shared deployment environments

❌ Out of Scope (v1)

  • Persistent Session Storage: Sessions still expire when browser is closed
  • User Authentication: No login system, relying on browser session isolation
  • Session Migration: No cross-device session continuity
  • Advanced Session Analytics: No detailed session usage tracking
  • Session Sharing: Users cannot share session contexts

3. Architecture Overview

3.1 Current Architecture (Problematic)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  HuggingFace Spaces                        β”‚
β”‚                                                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚
β”‚  β”‚   User A        β”‚    β”‚   User B                        β”‚β”‚
β”‚  β”‚   Browser       β”‚    β”‚   Browser                       β”‚β”‚
β”‚  β”‚                 β”‚    β”‚                                 β”‚β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
β”‚            β”‚                       β”‚                       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚           Gradio Interface                            β”‚ β”‚
β”‚  β”‚     ❌ SHARED session_id = "global-123"              β”‚ β”‚
β”‚  β”‚     ❌ SHARED conversation_history = []              β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚                        β”‚                                   β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚           FastAPI Backend                             β”‚ β”‚
β”‚  β”‚                                                       β”‚ β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚ β”‚
β”‚  β”‚  β”‚       SessionManagerService                     β”‚  β”‚ β”‚
β”‚  β”‚  β”‚   ❌ SHARED session_store = {                   β”‚  β”‚ β”‚
β”‚  β”‚  β”‚       "global-123": {                           β”‚  β”‚ β”‚
β”‚  β”‚  β”‚         // Mixed context from Users A & B      β”‚  β”‚ β”‚
β”‚  β”‚  β”‚       }                                         β”‚  β”‚ β”‚
β”‚  β”‚  β”‚   }                                             β”‚  β”‚ β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

3.2 Proposed Architecture (Secure)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  HuggingFace Spaces                        β”‚
β”‚                                                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚
β”‚  β”‚   User A        β”‚    β”‚   User B                        β”‚β”‚
β”‚  β”‚   Browser       β”‚    β”‚   Browser                       β”‚β”‚
β”‚  β”‚   gr.State:     β”‚    β”‚   gr.State:                     β”‚β”‚
β”‚  β”‚   "user-a-456"  β”‚    β”‚   "user-b-789"                  β”‚β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
β”‚            β”‚                       β”‚                       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚           Gradio Interface                            β”‚ β”‚
β”‚  β”‚     βœ… ISOLATED session management per user          β”‚ β”‚
β”‚  β”‚     βœ… Session ID passed with each request           β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚                        β”‚                                   β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚           FastAPI Backend                             β”‚ β”‚
β”‚  β”‚                                                       β”‚ β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚ β”‚
β”‚  β”‚  β”‚       SessionManagerService                     β”‚  β”‚ β”‚
β”‚  β”‚  β”‚   βœ… ISOLATED session_store = {                β”‚  β”‚ β”‚
β”‚  β”‚  β”‚       "user-a-456": { /* User A context */ },  β”‚  β”‚ β”‚
β”‚  β”‚  β”‚       "user-b-789": { /* User B context */ }   β”‚  β”‚ β”‚
β”‚  β”‚  β”‚   }                                             β”‚  β”‚ β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

4. Technical Design

4.1 Frontend Changes (Gradio Interface)

4.1.1 Session State Management

File: src/ui/chat_interface.py

Key Changes:

  1. Remove global self.session_id from class initialization
  2. Add gr.State component to store per-user session ID
  3. Update all message processing functions to accept and return session ID
  4. Initialize each new user session with unique UUID
class BeatDebateChatInterface:
    def __init__(self, backend_url: str = "http://localhost:8000"):
        self.backend_url = backend_url
        self.response_formatter = ResponseFormatter()
        self.planning_display = PlanningDisplay()
        # ❌ REMOVE: self.session_id = str(uuid.uuid4())
        # ❌ REMOVE: self.conversation_history = []  # This is also global state
        
        # Initialize fallback service
        self.fallback_service = None
        self._initialize_fallback_service()

    async def process_message(
        self, 
        message: str, 
        history: List[Tuple[str, str]],
        session_id: str  # βœ… ADD: Accept session_id as parameter
    ) -> Tuple[str, List[Tuple[str, str]], str, str]:  # βœ… ADD: Return updated session_id
        """Process message with isolated session context."""
        
        # Pass session_id to backend calls
        recommendations_response = await self._get_recommendations(message, session_id)
        
        # Handle fallback with session isolation
        if should_fallback:
            recommendations_response = await self._get_fallback_recommendations(
                message, trigger_reason, session_id
            )
        
        # Get potentially updated session_id from backend
        updated_session_id = recommendations_response.get("session_id", session_id)
        
        return "", updated_history, lastfm_player_html, updated_session_id

    def create_interface(self) -> gr.Blocks:
        with gr.Blocks(...) as interface:
            # βœ… ADD: Per-user session state
            session_id_state = gr.State(value=str(uuid.uuid4()))
            
            # UI components...
            chatbot = gr.Chatbot(...)
            msg_input = gr.Textbox(...)
            send_btn = gr.Button(...)
            player_display = gr.HTML(...)
            
            # βœ… MODIFY: Event handlers with session state
            async def handle_message(message, history, session_id):
                return await self.process_message(message, history, session_id)
            
            send_btn.click(
                fn=handle_message,
                inputs=[msg_input, chatbot, session_id_state],  # Include session state
                outputs=[msg_input, chatbot, player_display, session_id_state]  # Update session state
            )
            
            msg_input.submit(
                fn=handle_message,
                inputs=[msg_input, chatbot, session_id_state],
                outputs=[msg_input, chatbot, player_display, session_id_state]
            )

4.1.2 Backend Request Updates

All backend requests must include the user-specific session ID:

async def _get_recommendations(self, query: str, session_id: str) -> Optional[Dict]:
    request_data = {
        "query": query,
        "session_id": session_id,  # βœ… Pass user-specific session ID
        "max_recommendations": 10,
        "include_previews": True,
        "chat_context": self._get_chat_context()  # Note: This still needs refactoring
    }

async def _get_fallback_recommendations(
    self, 
    query: str, 
    trigger_reason: FallbackTrigger,
    session_id: str  # βœ… Accept session ID
) -> Optional[Dict[str, Any]]:
    fallback_request = FallbackRequest(
        query=query,
        session_id=session_id,  # βœ… Include in fallback request
        chat_context=self._get_chat_context(),
        trigger_reason=trigger_reason,
        max_recommendations=10
    )

4.2 Backend Verification (SessionManagerService)

File: src/services/session_manager_service.py

Current Implementation Analysis: βœ… Already session-safe! The backend SessionManagerService is already properly designed for multi-user scenarios:

class SessionManagerService:
    def __init__(self, cache_manager=None):
        self.session_store = {}  # This dictionary is keyed by session_id
        
    async def create_or_update_session(self, session_id: str, ...):
        # βœ… Uses session_id as key - already isolated
        if session_id not in self.session_store:
            self.session_store[session_id] = { ... }
        
    async def get_session_context(self, session_id: str):
        # βœ… Returns only the specific session's context
        return self.session_store.get(session_id)

No Changes Required: The backend is already session-safe. The issue was entirely in the frontend's global session management.

4.3 Context Handler Integration

File: src/services/components/context_handler.py

Verification: βœ… Already session-aware! The context handler properly uses session-specific data:

async def process_conversation_history(self, request) -> List[Dict]:
    # βœ… Already retrieves session-specific history
    if hasattr(request, 'session_id') and request.session_id:
        session_context = await self.session_manager.get_session_context(request.session_id)

No Changes Required: Context handler already works correctly with proper session IDs.


5. Implementation Plan

5.1 Phase 1: Frontend Session Management (Priority: Critical)

Tasks:

  1. Remove Global State from BeatDebateChatInterface

    • Remove self.session_id from constructor
    • Remove global self.conversation_history (separate issue, lower priority)
  2. Implement gr.State Session Management

    • Add session_id_state = gr.State(value=str(uuid.uuid4())) to interface
    • Update process_message signature to accept/return session ID
    • Update all event handlers to pass session state
  3. Update Backend Calls

    • Modify _get_recommendations to accept session_id parameter
    • Modify _get_fallback_recommendations to accept session_id parameter
    • Ensure all API calls include user-specific session ID

Files to Modify:

  • src/ui/chat_interface.py (Primary changes)

Testing Strategy:

  • Open multiple browser tabs/windows
  • Verify each tab gets unique session ID
  • Test follow-up queries work independently per tab
  • Ensure no context bleeding between sessions

5.2 Phase 2: Global Conversation History Refactoring (Priority: Medium)

Issue: self.conversation_history in chat interface is still global state

Tasks:

  1. Remove global conversation history from frontend
  2. Rely entirely on backend session storage for conversation context
  3. Update _get_chat_context() to retrieve from backend session store

5.3 Phase 3: Enhanced Session Monitoring (Priority: Low)

Tasks:

  1. Add session creation/destruction logging
  2. Implement session cleanup for memory management
  3. Add metrics for concurrent session count

6. Testing Strategy

6.1 Multi-User Simulation Tests

Scenario 1: Concurrent New Sessions

1. Open 3 browser tabs simultaneously
2. Each should get unique session ID (verify in logs)
3. Make different queries in each tab:
   - Tab 1: "Songs by Radiohead"
   - Tab 2: "Electronic music"  
   - Tab 3: "Jazz for studying"
4. Verify each gets appropriate recommendations

Scenario 2: Follow-up Query Isolation

1. Tab 1: "Songs by Mk.gee" β†’ get Mk.gee recommendations
2. Tab 2: "Electronic music" β†’ get electronic recommendations
3. Tab 1: "More tracks" β†’ should get more Mk.gee, NOT electronic
4. Tab 2: "More tracks" β†’ should get more electronic, NOT Mk.gee

Scenario 3: Session Persistence

1. Tab 1: "Songs by Beatles" β†’ get Beatles songs
2. Tab 1: "More like these" β†’ should get more Beatles-style
3. Refresh Tab 1 β†’ should get new session ID
4. Tab 1: "More tracks" β†’ should NOT have Beatles context

6.2 Performance Tests

Concurrent Load Test:

  • Simulate 10+ concurrent users
  • Verify no session collision
  • Monitor memory usage of session store
  • Ensure reasonable response times

6.3 Error Handling Tests

Session ID Edge Cases:

  • Missing session ID in request
  • Invalid session ID format
  • Backend should gracefully create new session

7. Deployment Considerations

7.1 HuggingFace Spaces Compatibility

Memory Management:

  • Session store grows with concurrent users
  • Implement session timeout/cleanup after inactivity
  • Monitor memory usage in Spaces environment

Gradio State Behavior:

  • gr.State is tied to browser session
  • Refreshing page creates new session (expected behavior)
  • No persistence across device switches (acceptable for v1)

7.2 Rollback Plan

If Issues Arise:

  1. Keep current implementation as backup branch
  2. Frontend changes are isolated and easily reversible
  3. Backend changes are minimal/none
  4. Can quickly revert to single-user mode if needed

8. Success Metrics

8.1 Functional Metrics

  • βœ… Zero context bleeding between users
  • βœ… Follow-up queries work correctly per user
  • βœ… Each browser tab maintains independent session
  • βœ… New sessions start fresh after page refresh

8.2 Performance Metrics

  • Session creation time < 100ms
  • Memory usage scales linearly with active sessions
  • No degradation in recommendation quality
  • Response times remain under 30 seconds per query

8.3 User Experience Metrics

  • Consistent behavior across all user interactions
  • No confusing recommendations from other users' contexts
  • Reliable "more tracks" functionality
  • Smooth multi-tab usage experience

9. Future Enhancements

9.1 Session Persistence (v2)

  • Add session storage backend (Redis/Database)
  • Enable cross-device session continuity
  • Implement session sharing functionality

9.2 Advanced Session Management (v2)

  • User authentication integration
  • Session migration capabilities
  • Advanced session analytics
  • Custom session expiration policies

9.3 Performance Optimizations (v2)

  • Session data compression
  • Lazy loading of session contexts
  • Distributed session storage
  • Session pooling for efficiency

This design ensures BeatDebate can safely handle multiple concurrent users while maintaining the sophisticated follow-up query capabilities and conversation context that make the system valuable. The solution is minimal, focused, and maintains backward compatibility while solving the critical multi-user safety issue.