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
# 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
```python
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:
```python
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:
```python
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:
```python
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.