Spaces:
Sleeping
Sleeping
findEthics commited on
Commit ·
439ebb4
1
Parent(s): 04aa1ba
feat: implement anonymous mode functionality with comprehensive tests
Browse files- Add anonymous mode support allowing requests without user_id
- Implement comprehensive test suite for anonymous functionality
- Add analytics counting for anonymous vs authenticated usage
- Verify database operations handle null user_id values correctly
- Test dashboard displays anonymous metrics properly
- Move documentation to docs/ folder for better organization
- Add API examples for anonymous chat requests
- Clean up temporary test files and demos from root directory
Requirements implemented:
- 1.1: Anonymous requests processed with full functionality
- 1.2: System treats null/empty user_id as anonymous
- 3.1: Anonymous requests handled efficiently
- 3.3: Anonymous usage aggregated without individual tracking
- README.md +235 -10
- analytics/dashboard.py +223 -32
- api-integration-guide.md +0 -137
- app.py +48 -6
- docs/ANONYMOUS_API_EXAMPLES.md +160 -0
- DEPLOYMENT.md → docs/DEPLOYMENT.md +0 -0
- MIGRATION_README.md → docs/MIGRATION_README.md +0 -0
- SETUP.md → docs/SETUP.md +50 -3
- analytics-approach.md → docs/analytics-approach.md +0 -0
- analytics-tasks.md → docs/analytics-tasks.md +0 -0
- docs/api-integration-guide.md +579 -0
- tests/README_anonymous_mode_tests.md +135 -0
- tests/test_anonymous_mode.py +583 -0
README.md
CHANGED
|
@@ -1,12 +1,237 @@
|
|
| 1 |
-
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
| 1 |
+
# Atlas - AI Chat API with Anonymous & Authenticated Modes
|
| 2 |
+
|
| 3 |
+
Atlas is an enhanced chat API service that provides intelligent question-answering capabilities with web search augmentation and comprehensive analytics. It supports both anonymous usage (no authentication required) and authenticated user tracking.
|
| 4 |
+
|
| 5 |
+
## 🚀 Quick Start (Anonymous Mode)
|
| 6 |
+
|
| 7 |
+
Get started immediately without any setup or authentication:
|
| 8 |
+
|
| 9 |
+
```bash
|
| 10 |
+
# Simple anonymous chat request
|
| 11 |
+
curl -X POST https://your-atlas-api.com/chat \
|
| 12 |
+
-H "Content-Type: application/json" \
|
| 13 |
+
-d '{
|
| 14 |
+
"prompt": "What is artificial intelligence?",
|
| 15 |
+
"use_search": true
|
| 16 |
+
}'
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
## 📋 Features
|
| 20 |
+
|
| 21 |
+
### 🤖 AI-Powered Chat
|
| 22 |
+
- Uses Google's Gemini 1.5 Flash model
|
| 23 |
+
- Configurable parameters (temperature, max tokens)
|
| 24 |
+
- Intelligent responses based on web search results
|
| 25 |
+
- Session-based conversation tracking
|
| 26 |
+
|
| 27 |
+
### 🔍 Advanced Web Search
|
| 28 |
+
- **Dual Search Engine Strategy**: Brave Search + DuckDuckGo
|
| 29 |
+
- **Resilient Fallback**: Automatic fallback if one engine fails
|
| 30 |
+
- **Smart Query Extraction**: NLP-powered search term extraction
|
| 31 |
+
- **Deduplication**: Removes duplicate results across engines
|
| 32 |
+
|
| 33 |
+
### 👤 Flexible User Modes
|
| 34 |
+
- **Anonymous Mode**: Use immediately without authentication
|
| 35 |
+
- **Authenticated Mode**: User tracking and personalized history
|
| 36 |
+
- **Progressive Enhancement**: Start anonymous, add auth later
|
| 37 |
+
- **Privacy-First**: No tracking in anonymous mode
|
| 38 |
+
|
| 39 |
+
### 📊 Comprehensive Analytics
|
| 40 |
+
- **Real-time Session Tracking**: Monitor user sessions and activity
|
| 41 |
+
- **Message Analytics**: Track response times, search usage, and success rates
|
| 42 |
+
- **Interactive Dashboard**: Beautiful HTML dashboard with charts and metrics
|
| 43 |
+
- **Data Export**: Export analytics data in JSON or CSV format
|
| 44 |
+
- **Anonymous vs Authenticated**: Separate tracking for different user modes
|
| 45 |
+
|
| 46 |
+
## 🔧 API Usage Examples
|
| 47 |
+
|
| 48 |
+
### Anonymous Usage (No Authentication)
|
| 49 |
+
|
| 50 |
+
**Basic Request:**
|
| 51 |
+
```javascript
|
| 52 |
+
const response = await fetch('/chat', {
|
| 53 |
+
method: 'POST',
|
| 54 |
+
headers: { 'Content-Type': 'application/json' },
|
| 55 |
+
body: JSON.stringify({
|
| 56 |
+
prompt: "Explain quantum computing",
|
| 57 |
+
use_search: true
|
| 58 |
+
})
|
| 59 |
+
});
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
**With Conversation History:**
|
| 63 |
+
```javascript
|
| 64 |
+
const response = await fetch('/chat', {
|
| 65 |
+
method: 'POST',
|
| 66 |
+
headers: { 'Content-Type': 'application/json' },
|
| 67 |
+
body: JSON.stringify({
|
| 68 |
+
prompt: "Can you elaborate on that?",
|
| 69 |
+
use_search: false,
|
| 70 |
+
history: [
|
| 71 |
+
{role: "user", content: "What is machine learning?"},
|
| 72 |
+
{role: "assistant", content: "Machine learning is..."}
|
| 73 |
+
]
|
| 74 |
+
})
|
| 75 |
+
});
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
### Authenticated Usage (With User Tracking)
|
| 79 |
+
|
| 80 |
+
**Authenticated Request:**
|
| 81 |
+
```javascript
|
| 82 |
+
const response = await fetch('/chat', {
|
| 83 |
+
method: 'POST',
|
| 84 |
+
headers: { 'Content-Type': 'application/json' },
|
| 85 |
+
body: JSON.stringify({
|
| 86 |
+
prompt: "What's my chat history?",
|
| 87 |
+
user_id: "user123",
|
| 88 |
+
use_search: true
|
| 89 |
+
})
|
| 90 |
+
});
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
### Python Client Example
|
| 94 |
+
|
| 95 |
+
```python
|
| 96 |
+
import requests
|
| 97 |
+
|
| 98 |
+
def chat_anonymous(prompt, use_search=True):
|
| 99 |
+
"""Send anonymous chat request"""
|
| 100 |
+
response = requests.post('https://your-atlas-api.com/chat',
|
| 101 |
+
json={
|
| 102 |
+
'prompt': prompt,
|
| 103 |
+
'use_search': use_search
|
| 104 |
+
}
|
| 105 |
+
)
|
| 106 |
+
return response.json()
|
| 107 |
+
|
| 108 |
+
def chat_authenticated(prompt, user_id, use_search=True):
|
| 109 |
+
"""Send authenticated chat request"""
|
| 110 |
+
response = requests.post('https://your-atlas-api.com/chat',
|
| 111 |
+
json={
|
| 112 |
+
'prompt': prompt,
|
| 113 |
+
'user_id': user_id,
|
| 114 |
+
'use_search': use_search
|
| 115 |
+
}
|
| 116 |
+
)
|
| 117 |
+
return response.json()
|
| 118 |
+
|
| 119 |
+
# Anonymous usage
|
| 120 |
+
result = chat_anonymous("What is AI?")
|
| 121 |
+
print(result['response'])
|
| 122 |
+
|
| 123 |
+
# Authenticated usage
|
| 124 |
+
result = chat_authenticated("What is AI?", "user123")
|
| 125 |
+
print(result['response'])
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
## 🌐 API Endpoints
|
| 129 |
+
|
| 130 |
+
### Core Functionality
|
| 131 |
+
- **`/`** - Health check and status
|
| 132 |
+
- **`/chat`** - Main chat endpoint (supports both anonymous and authenticated)
|
| 133 |
+
- **`/search`** - Direct search functionality
|
| 134 |
+
- **`/docs`** - Interactive API documentation (Swagger UI)
|
| 135 |
+
|
| 136 |
+
### Analytics Dashboard
|
| 137 |
+
- **`/analytics/stats`** - JSON API with analytics statistics
|
| 138 |
+
- **`/analytics/dashboard`** - Interactive HTML dashboard with charts
|
| 139 |
+
- **`/analytics/export`** - Export analytics data (JSON/CSV format)
|
| 140 |
+
|
| 141 |
+
## 📖 Documentation
|
| 142 |
+
|
| 143 |
+
- **[API Integration Guide](docs/api-integration-guide.md)** - Comprehensive integration examples
|
| 144 |
+
- **[Anonymous API Examples](docs/ANONYMOUS_API_EXAMPLES.md)** - Sample API calls for anonymous usage
|
| 145 |
+
- **[Setup Guide](docs/SETUP.md)** - Local development setup
|
| 146 |
+
- **[Deployment Guide](docs/DEPLOYMENT.md)** - Production deployment instructions
|
| 147 |
+
- **[Migration Guide](docs/MIGRATION_README.md)** - Database migration instructions
|
| 148 |
+
|
| 149 |
+
## 🔒 Privacy & Security
|
| 150 |
+
|
| 151 |
+
### Anonymous Mode
|
| 152 |
+
- **No Tracking**: Zero personal data collection
|
| 153 |
+
- **No Registration**: Use immediately without accounts
|
| 154 |
+
- **Privacy-First**: Requests processed without user identification
|
| 155 |
+
- **Same Functionality**: Full AI and search capabilities
|
| 156 |
+
|
| 157 |
+
### Authenticated Mode
|
| 158 |
+
- **Optional**: Only when you need user-specific features
|
| 159 |
+
- **Secure**: Proper user ID validation and sanitization
|
| 160 |
+
- **Flexible**: Easy to switch between modes
|
| 161 |
+
- **Data Control**: Users control their data association
|
| 162 |
+
|
| 163 |
+
## 🚀 Getting Started
|
| 164 |
+
|
| 165 |
+
### 1. Anonymous Usage (Immediate)
|
| 166 |
+
```bash
|
| 167 |
+
curl -X POST https://your-atlas-api.com/chat \
|
| 168 |
+
-H "Content-Type: application/json" \
|
| 169 |
+
-d '{"prompt": "Hello, how are you?"}'
|
| 170 |
+
```
|
| 171 |
+
|
| 172 |
+
### 2. With Session Continuity
|
| 173 |
+
```bash
|
| 174 |
+
curl -X POST https://your-atlas-api.com/chat \
|
| 175 |
+
-H "Content-Type: application/json" \
|
| 176 |
+
-H "X-Session-ID: your-session-id" \
|
| 177 |
+
-d '{"prompt": "Continue our conversation"}'
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
### 3. Authenticated Usage
|
| 181 |
+
```bash
|
| 182 |
+
curl -X POST https://your-atlas-api.com/chat \
|
| 183 |
+
-H "Content-Type: application/json" \
|
| 184 |
+
-d '{
|
| 185 |
+
"prompt": "What is my history?",
|
| 186 |
+
"user_id": "user123"
|
| 187 |
+
}'
|
| 188 |
+
```
|
| 189 |
+
|
| 190 |
+
## 📊 Analytics & Monitoring
|
| 191 |
+
|
| 192 |
+
Access comprehensive analytics at `/analytics/dashboard`:
|
| 193 |
+
|
| 194 |
+
- **Usage Statistics**: Total messages, sessions, active users
|
| 195 |
+
- **Performance Metrics**: Response times, success rates
|
| 196 |
+
- **Search Analytics**: Engine performance, query patterns
|
| 197 |
+
- **User Modes**: Anonymous vs authenticated usage breakdown
|
| 198 |
+
- **Real-time Updates**: Live dashboard with auto-refresh
|
| 199 |
+
|
| 200 |
+
## 🛠️ Integration Patterns
|
| 201 |
+
|
| 202 |
+
### Progressive Enhancement
|
| 203 |
+
```javascript
|
| 204 |
+
class ChatClient {
|
| 205 |
+
constructor(apiUrl) {
|
| 206 |
+
this.apiUrl = apiUrl;
|
| 207 |
+
this.userId = null; // Start anonymous
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
authenticate(userId) {
|
| 211 |
+
this.userId = userId; // Enable user tracking
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
logout() {
|
| 215 |
+
this.userId = null; // Return to anonymous
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
async sendMessage(prompt) {
|
| 219 |
+
const body = { prompt, use_search: true };
|
| 220 |
+
if (this.userId) body.user_id = this.userId;
|
| 221 |
+
|
| 222 |
+
return fetch(`${this.apiUrl}/chat`, {
|
| 223 |
+
method: 'POST',
|
| 224 |
+
headers: { 'Content-Type': 'application/json' },
|
| 225 |
+
body: JSON.stringify(body)
|
| 226 |
+
});
|
| 227 |
+
}
|
| 228 |
+
}
|
| 229 |
+
```
|
| 230 |
+
|
| 231 |
+
## 📄 License
|
| 232 |
+
|
| 233 |
+
MIT License - see LICENSE file for details.
|
| 234 |
+
|
| 235 |
---
|
| 236 |
|
| 237 |
+
**Ready to get started?** Try an anonymous request right now, or check out the [API Integration Guide](docs/api-integration-guide.md) for comprehensive examples!
|
analytics/dashboard.py
CHANGED
|
@@ -9,6 +9,140 @@ from .database import get_sessions_collection, get_messages_collection
|
|
| 9 |
|
| 10 |
logger = logging.getLogger(__name__)
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
async def get_basic_stats(user_id: Optional[str] = None) -> Dict[str, Any]:
|
| 13 |
"""Get basic analytics statistics, optionally filtered by user_id"""
|
| 14 |
try:
|
|
@@ -28,9 +162,23 @@ async def get_basic_stats(user_id: Optional[str] = None) -> Dict[str, Any]:
|
|
| 28 |
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
| 29 |
week_start = today_start - timedelta(days=7)
|
| 30 |
|
| 31 |
-
# Basic counts
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
# Today's stats
|
| 36 |
today_filter = {**user_filter, "timestamp": {"$gte": today_start}}
|
|
@@ -71,6 +219,21 @@ async def get_basic_stats(user_id: Optional[str] = None) -> Dict[str, Any]:
|
|
| 71 |
"last_updated": now.isoformat()
|
| 72 |
}
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
# Add user_id to result if filtering was applied
|
| 75 |
if user_id is not None:
|
| 76 |
result["filtered_by_user_id"] = user_id
|
|
@@ -319,6 +482,38 @@ async def get_performance_stats(user_id: Optional[str] = None) -> Dict[str, Any]
|
|
| 319 |
logger.error(f"Error getting performance stats: {e}")
|
| 320 |
return {"error": str(e)}
|
| 321 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
async def get_user_statistics() -> Dict[str, Any]:
|
| 323 |
"""Get overall user statistics including authenticated vs anonymous metrics"""
|
| 324 |
try:
|
|
@@ -328,29 +523,14 @@ async def get_user_statistics() -> Dict[str, Any]:
|
|
| 328 |
if sessions_collection is None or messages_collection is None:
|
| 329 |
return {"error": "Database not available"}
|
| 330 |
|
| 331 |
-
#
|
| 332 |
-
authenticated_sessions = await
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
anonymous_sessions = await sessions_collection.count_documents({
|
| 336 |
-
"$or": [
|
| 337 |
-
{"user_id": None},
|
| 338 |
-
{"user_id": {"$exists": False}}
|
| 339 |
-
]
|
| 340 |
-
})
|
| 341 |
-
total_sessions = authenticated_sessions + anonymous_sessions
|
| 342 |
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
})
|
| 347 |
-
anonymous_messages = await messages_collection.count_documents({
|
| 348 |
-
"$or": [
|
| 349 |
-
{"user_id": None},
|
| 350 |
-
{"user_id": {"$exists": False}}
|
| 351 |
-
]
|
| 352 |
-
})
|
| 353 |
-
total_messages = authenticated_messages + anonymous_messages
|
| 354 |
|
| 355 |
# Count unique authenticated users
|
| 356 |
unique_users_pipeline = [
|
|
@@ -547,6 +727,12 @@ async def get_authenticated_vs_anonymous_metrics() -> Dict[str, Any]:
|
|
| 547 |
if sessions_collection is None or messages_collection is None:
|
| 548 |
return {"error": "Database not available"}
|
| 549 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 550 |
# Authenticated user metrics
|
| 551 |
auth_session_pipeline = [
|
| 552 |
{
|
|
@@ -669,11 +855,14 @@ async def get_authenticated_vs_anonymous_metrics() -> Dict[str, Any]:
|
|
| 669 |
"authenticated": authenticated_metrics,
|
| 670 |
"anonymous": anonymous_metrics,
|
| 671 |
"comparison": {
|
| 672 |
-
"total_sessions":
|
| 673 |
-
"total_messages":
|
| 674 |
-
"
|
| 675 |
-
(
|
| 676 |
-
) if (
|
|
|
|
|
|
|
|
|
|
| 677 |
},
|
| 678 |
"last_updated": datetime.utcnow().isoformat()
|
| 679 |
}
|
|
@@ -688,11 +877,12 @@ async def get_dashboard_data() -> Dict[str, Any]:
|
|
| 688 |
# Get all stats concurrently
|
| 689 |
import asyncio
|
| 690 |
|
| 691 |
-
basic_stats, session_stats, performance_stats, hourly_stats = await asyncio.gather(
|
| 692 |
get_basic_stats(),
|
| 693 |
get_session_stats(),
|
| 694 |
get_performance_stats(),
|
| 695 |
-
get_hourly_message_stats(24)
|
|
|
|
| 696 |
)
|
| 697 |
|
| 698 |
return {
|
|
@@ -700,6 +890,7 @@ async def get_dashboard_data() -> Dict[str, Any]:
|
|
| 700 |
"sessions": session_stats,
|
| 701 |
"performance": performance_stats,
|
| 702 |
"hourly": hourly_stats,
|
|
|
|
| 703 |
"generated_at": datetime.utcnow().isoformat()
|
| 704 |
}
|
| 705 |
|
|
|
|
| 9 |
|
| 10 |
logger = logging.getLogger(__name__)
|
| 11 |
|
| 12 |
+
# Helper functions to count anonymous vs authenticated usage
|
| 13 |
+
async def count_anonymous_sessions() -> int:
|
| 14 |
+
"""Count sessions with null user_id (anonymous users)"""
|
| 15 |
+
try:
|
| 16 |
+
sessions_collection = await get_sessions_collection()
|
| 17 |
+
if sessions_collection is None:
|
| 18 |
+
return 0
|
| 19 |
+
|
| 20 |
+
return await sessions_collection.count_documents({
|
| 21 |
+
"$or": [
|
| 22 |
+
{"user_id": None},
|
| 23 |
+
{"user_id": {"$exists": False}}
|
| 24 |
+
]
|
| 25 |
+
})
|
| 26 |
+
except Exception as e:
|
| 27 |
+
logger.error(f"Error counting anonymous sessions: {e}")
|
| 28 |
+
return 0
|
| 29 |
+
|
| 30 |
+
async def count_authenticated_sessions() -> int:
|
| 31 |
+
"""Count sessions with non-null user_id (authenticated users)"""
|
| 32 |
+
try:
|
| 33 |
+
sessions_collection = await get_sessions_collection()
|
| 34 |
+
if sessions_collection is None:
|
| 35 |
+
return 0
|
| 36 |
+
|
| 37 |
+
return await sessions_collection.count_documents({
|
| 38 |
+
"user_id": {"$ne": None, "$exists": True}
|
| 39 |
+
})
|
| 40 |
+
except Exception as e:
|
| 41 |
+
logger.error(f"Error counting authenticated sessions: {e}")
|
| 42 |
+
return 0
|
| 43 |
+
|
| 44 |
+
async def count_anonymous_messages() -> int:
|
| 45 |
+
"""Count messages with null user_id (anonymous users)"""
|
| 46 |
+
try:
|
| 47 |
+
messages_collection = await get_messages_collection()
|
| 48 |
+
if messages_collection is None:
|
| 49 |
+
return 0
|
| 50 |
+
|
| 51 |
+
return await messages_collection.count_documents({
|
| 52 |
+
"$or": [
|
| 53 |
+
{"user_id": None},
|
| 54 |
+
{"user_id": {"$exists": False}}
|
| 55 |
+
]
|
| 56 |
+
})
|
| 57 |
+
except Exception as e:
|
| 58 |
+
logger.error(f"Error counting anonymous messages: {e}")
|
| 59 |
+
return 0
|
| 60 |
+
|
| 61 |
+
async def count_authenticated_messages() -> int:
|
| 62 |
+
"""Count messages with non-null user_id (authenticated users)"""
|
| 63 |
+
try:
|
| 64 |
+
messages_collection = await get_messages_collection()
|
| 65 |
+
if messages_collection is None:
|
| 66 |
+
return 0
|
| 67 |
+
|
| 68 |
+
return await messages_collection.count_documents({
|
| 69 |
+
"user_id": {"$ne": None, "$exists": True}
|
| 70 |
+
})
|
| 71 |
+
except Exception as e:
|
| 72 |
+
logger.error(f"Error counting authenticated messages: {e}")
|
| 73 |
+
return 0
|
| 74 |
+
|
| 75 |
+
async def count_all_sessions() -> int:
|
| 76 |
+
"""Count total sessions (both anonymous and authenticated)"""
|
| 77 |
+
try:
|
| 78 |
+
sessions_collection = await get_sessions_collection()
|
| 79 |
+
if sessions_collection is None:
|
| 80 |
+
return 0
|
| 81 |
+
|
| 82 |
+
return await sessions_collection.count_documents({})
|
| 83 |
+
except Exception as e:
|
| 84 |
+
logger.error(f"Error counting all sessions: {e}")
|
| 85 |
+
return 0
|
| 86 |
+
|
| 87 |
+
async def count_all_messages() -> int:
|
| 88 |
+
"""Count total messages (both anonymous and authenticated)"""
|
| 89 |
+
try:
|
| 90 |
+
messages_collection = await get_messages_collection()
|
| 91 |
+
if messages_collection is None:
|
| 92 |
+
return 0
|
| 93 |
+
|
| 94 |
+
return await messages_collection.count_documents({})
|
| 95 |
+
except Exception as e:
|
| 96 |
+
logger.error(f"Error counting all messages: {e}")
|
| 97 |
+
return 0
|
| 98 |
+
|
| 99 |
+
async def count_anonymous_messages_in_timeframe(start_time: datetime, end_time: Optional[datetime] = None) -> int:
|
| 100 |
+
"""Count anonymous messages within a specific timeframe"""
|
| 101 |
+
try:
|
| 102 |
+
messages_collection = await get_messages_collection()
|
| 103 |
+
if messages_collection is None:
|
| 104 |
+
return 0
|
| 105 |
+
|
| 106 |
+
time_filter = {"timestamp": {"$gte": start_time}}
|
| 107 |
+
if end_time:
|
| 108 |
+
time_filter["timestamp"]["$lte"] = end_time
|
| 109 |
+
|
| 110 |
+
return await messages_collection.count_documents({
|
| 111 |
+
"$and": [
|
| 112 |
+
time_filter,
|
| 113 |
+
{
|
| 114 |
+
"$or": [
|
| 115 |
+
{"user_id": None},
|
| 116 |
+
{"user_id": {"$exists": False}}
|
| 117 |
+
]
|
| 118 |
+
}
|
| 119 |
+
]
|
| 120 |
+
})
|
| 121 |
+
except Exception as e:
|
| 122 |
+
logger.error(f"Error counting anonymous messages in timeframe: {e}")
|
| 123 |
+
return 0
|
| 124 |
+
|
| 125 |
+
async def count_authenticated_messages_in_timeframe(start_time: datetime, end_time: Optional[datetime] = None) -> int:
|
| 126 |
+
"""Count authenticated messages within a specific timeframe"""
|
| 127 |
+
try:
|
| 128 |
+
messages_collection = await get_messages_collection()
|
| 129 |
+
if messages_collection is None:
|
| 130 |
+
return 0
|
| 131 |
+
|
| 132 |
+
time_filter = {"timestamp": {"$gte": start_time}}
|
| 133 |
+
if end_time:
|
| 134 |
+
time_filter["timestamp"]["$lte"] = end_time
|
| 135 |
+
|
| 136 |
+
return await messages_collection.count_documents({
|
| 137 |
+
"$and": [
|
| 138 |
+
time_filter,
|
| 139 |
+
{"user_id": {"$ne": None, "$exists": True}}
|
| 140 |
+
]
|
| 141 |
+
})
|
| 142 |
+
except Exception as e:
|
| 143 |
+
logger.error(f"Error counting authenticated messages in timeframe: {e}")
|
| 144 |
+
return 0
|
| 145 |
+
|
| 146 |
async def get_basic_stats(user_id: Optional[str] = None) -> Dict[str, Any]:
|
| 147 |
"""Get basic analytics statistics, optionally filtered by user_id"""
|
| 148 |
try:
|
|
|
|
| 162 |
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
| 163 |
week_start = today_start - timedelta(days=7)
|
| 164 |
|
| 165 |
+
# Basic counts - use helper functions when not filtering by user_id
|
| 166 |
+
if user_id is None:
|
| 167 |
+
# Get overall stats with anonymous vs authenticated breakdown
|
| 168 |
+
total_sessions = await count_all_sessions()
|
| 169 |
+
total_messages = await count_all_messages()
|
| 170 |
+
authenticated_sessions = await count_authenticated_sessions()
|
| 171 |
+
anonymous_sessions = await count_anonymous_sessions()
|
| 172 |
+
authenticated_messages = await count_authenticated_messages()
|
| 173 |
+
anonymous_messages = await count_anonymous_messages()
|
| 174 |
+
else:
|
| 175 |
+
# Get stats for specific user
|
| 176 |
+
total_sessions = await sessions_collection.count_documents(user_filter)
|
| 177 |
+
total_messages = await messages_collection.count_documents(user_filter)
|
| 178 |
+
authenticated_sessions = total_sessions if user_id else 0
|
| 179 |
+
anonymous_sessions = 0
|
| 180 |
+
authenticated_messages = total_messages if user_id else 0
|
| 181 |
+
anonymous_messages = 0
|
| 182 |
|
| 183 |
# Today's stats
|
| 184 |
today_filter = {**user_filter, "timestamp": {"$gte": today_start}}
|
|
|
|
| 219 |
"last_updated": now.isoformat()
|
| 220 |
}
|
| 221 |
|
| 222 |
+
# Add anonymous vs authenticated breakdown when not filtering by user_id
|
| 223 |
+
if user_id is None:
|
| 224 |
+
result.update({
|
| 225 |
+
"authenticated_sessions": authenticated_sessions,
|
| 226 |
+
"anonymous_sessions": anonymous_sessions,
|
| 227 |
+
"authenticated_messages": authenticated_messages,
|
| 228 |
+
"anonymous_messages": anonymous_messages,
|
| 229 |
+
"authenticated_session_percentage": round(
|
| 230 |
+
(authenticated_sessions / total_sessions * 100), 1
|
| 231 |
+
) if total_sessions > 0 else 0,
|
| 232 |
+
"authenticated_message_percentage": round(
|
| 233 |
+
(authenticated_messages / total_messages * 100), 1
|
| 234 |
+
) if total_messages > 0 else 0
|
| 235 |
+
})
|
| 236 |
+
|
| 237 |
# Add user_id to result if filtering was applied
|
| 238 |
if user_id is not None:
|
| 239 |
result["filtered_by_user_id"] = user_id
|
|
|
|
| 482 |
logger.error(f"Error getting performance stats: {e}")
|
| 483 |
return {"error": str(e)}
|
| 484 |
|
| 485 |
+
async def get_usage_stats() -> Dict[str, Any]:
|
| 486 |
+
"""Get usage statistics including anonymous vs authenticated counts using helper functions"""
|
| 487 |
+
try:
|
| 488 |
+
# Use helper functions for efficient counting
|
| 489 |
+
total_sessions = await count_all_sessions()
|
| 490 |
+
authenticated_sessions = await count_authenticated_sessions()
|
| 491 |
+
anonymous_sessions = await count_anonymous_sessions()
|
| 492 |
+
|
| 493 |
+
total_messages = await count_all_messages()
|
| 494 |
+
authenticated_messages = await count_authenticated_messages()
|
| 495 |
+
anonymous_messages = await count_anonymous_messages()
|
| 496 |
+
|
| 497 |
+
# Calculate percentages
|
| 498 |
+
auth_session_percentage = (authenticated_sessions / total_sessions * 100) if total_sessions > 0 else 0
|
| 499 |
+
auth_message_percentage = (authenticated_messages / total_messages * 100) if total_messages > 0 else 0
|
| 500 |
+
|
| 501 |
+
return {
|
| 502 |
+
"total_sessions": total_sessions,
|
| 503 |
+
"authenticated_sessions": authenticated_sessions,
|
| 504 |
+
"anonymous_sessions": anonymous_sessions,
|
| 505 |
+
"authenticated_session_percentage": round(auth_session_percentage, 1),
|
| 506 |
+
"total_messages": total_messages,
|
| 507 |
+
"authenticated_messages": authenticated_messages,
|
| 508 |
+
"anonymous_messages": anonymous_messages,
|
| 509 |
+
"authenticated_message_percentage": round(auth_message_percentage, 1),
|
| 510 |
+
"last_updated": datetime.utcnow().isoformat()
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
except Exception as e:
|
| 514 |
+
logger.error(f"Error getting usage stats: {e}")
|
| 515 |
+
return {"error": str(e)}
|
| 516 |
+
|
| 517 |
async def get_user_statistics() -> Dict[str, Any]:
|
| 518 |
"""Get overall user statistics including authenticated vs anonymous metrics"""
|
| 519 |
try:
|
|
|
|
| 523 |
if sessions_collection is None or messages_collection is None:
|
| 524 |
return {"error": "Database not available"}
|
| 525 |
|
| 526 |
+
# Use helper functions for efficient counting
|
| 527 |
+
authenticated_sessions = await count_authenticated_sessions()
|
| 528 |
+
anonymous_sessions = await count_anonymous_sessions()
|
| 529 |
+
total_sessions = await count_all_sessions()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 530 |
|
| 531 |
+
authenticated_messages = await count_authenticated_messages()
|
| 532 |
+
anonymous_messages = await count_anonymous_messages()
|
| 533 |
+
total_messages = await count_all_messages()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 534 |
|
| 535 |
# Count unique authenticated users
|
| 536 |
unique_users_pipeline = [
|
|
|
|
| 727 |
if sessions_collection is None or messages_collection is None:
|
| 728 |
return {"error": "Database not available"}
|
| 729 |
|
| 730 |
+
# Use helper functions for basic counts
|
| 731 |
+
auth_sessions_count = await count_authenticated_sessions()
|
| 732 |
+
anon_sessions_count = await count_anonymous_sessions()
|
| 733 |
+
auth_messages_count = await count_authenticated_messages()
|
| 734 |
+
anon_messages_count = await count_anonymous_messages()
|
| 735 |
+
|
| 736 |
# Authenticated user metrics
|
| 737 |
auth_session_pipeline = [
|
| 738 |
{
|
|
|
|
| 855 |
"authenticated": authenticated_metrics,
|
| 856 |
"anonymous": anonymous_metrics,
|
| 857 |
"comparison": {
|
| 858 |
+
"total_sessions": auth_sessions_count + anon_sessions_count,
|
| 859 |
+
"total_messages": auth_messages_count + anon_messages_count,
|
| 860 |
+
"authenticated_session_percentage": round(
|
| 861 |
+
(auth_sessions_count / (auth_sessions_count + anon_sessions_count) * 100), 1
|
| 862 |
+
) if (auth_sessions_count + anon_sessions_count) > 0 else 0,
|
| 863 |
+
"authenticated_message_percentage": round(
|
| 864 |
+
(auth_messages_count / (auth_messages_count + anon_messages_count) * 100), 1
|
| 865 |
+
) if (auth_messages_count + anon_messages_count) > 0 else 0
|
| 866 |
},
|
| 867 |
"last_updated": datetime.utcnow().isoformat()
|
| 868 |
}
|
|
|
|
| 877 |
# Get all stats concurrently
|
| 878 |
import asyncio
|
| 879 |
|
| 880 |
+
basic_stats, session_stats, performance_stats, hourly_stats, usage_stats = await asyncio.gather(
|
| 881 |
get_basic_stats(),
|
| 882 |
get_session_stats(),
|
| 883 |
get_performance_stats(),
|
| 884 |
+
get_hourly_message_stats(24),
|
| 885 |
+
get_usage_stats()
|
| 886 |
)
|
| 887 |
|
| 888 |
return {
|
|
|
|
| 890 |
"sessions": session_stats,
|
| 891 |
"performance": performance_stats,
|
| 892 |
"hourly": hourly_stats,
|
| 893 |
+
"usage": usage_stats,
|
| 894 |
"generated_at": datetime.utcnow().isoformat()
|
| 895 |
}
|
| 896 |
|
api-integration-guide.md
DELETED
|
@@ -1,137 +0,0 @@
|
|
| 1 |
-
# API Integration Guide: User Authentication Support
|
| 2 |
-
|
| 3 |
-
## Overview
|
| 4 |
-
|
| 5 |
-
The Chatty frontend application is implementing user authentication. When users are authenticated, the frontend will send their `user_id` along with chat requests to associate session data with specific users.
|
| 6 |
-
|
| 7 |
-
## Required API Changes
|
| 8 |
-
|
| 9 |
-
### 1. Accept User ID in Chat Requests
|
| 10 |
-
|
| 11 |
-
The `/chat` endpoint needs to accept an optional `user_id` parameter to associate sessions with authenticated users.
|
| 12 |
-
|
| 13 |
-
**Current Request Structure:**
|
| 14 |
-
```python
|
| 15 |
-
class ChatRequest(BaseModel):
|
| 16 |
-
prompt: str
|
| 17 |
-
use_search: bool = True
|
| 18 |
-
max_new_tokens: int = 1000
|
| 19 |
-
temperature: float = 0.7
|
| 20 |
-
history: List[dict] = []
|
| 21 |
-
```
|
| 22 |
-
|
| 23 |
-
**Updated Request Structure:**
|
| 24 |
-
```python
|
| 25 |
-
class ChatRequest(BaseModel):
|
| 26 |
-
prompt: str
|
| 27 |
-
use_search: bool = True
|
| 28 |
-
max_new_tokens: int = 1000
|
| 29 |
-
temperature: float = 0.7
|
| 30 |
-
history: List[dict] = []
|
| 31 |
-
user_id: Optional[str] = None # Add this field
|
| 32 |
-
```
|
| 33 |
-
|
| 34 |
-
### 2. Modify Session Creation/Tracking
|
| 35 |
-
|
| 36 |
-
Update the session management to include `user_id` when provided:
|
| 37 |
-
|
| 38 |
-
**In the `/chat` endpoint:**
|
| 39 |
-
```python
|
| 40 |
-
# Handle session management with user_id
|
| 41 |
-
if analytics_available:
|
| 42 |
-
if not session_id:
|
| 43 |
-
# Create new session with user_id if provided
|
| 44 |
-
session = await create_session(
|
| 45 |
-
user_agent=user_agent,
|
| 46 |
-
user_id=request.user_id # Pass user_id from request
|
| 47 |
-
)
|
| 48 |
-
session_id = session.session_id
|
| 49 |
-
else:
|
| 50 |
-
# Get existing session or create new one if not found
|
| 51 |
-
session = await get_session(session_id)
|
| 52 |
-
if not session:
|
| 53 |
-
session = await create_session(
|
| 54 |
-
user_agent=user_agent,
|
| 55 |
-
user_id=request.user_id # Pass user_id from request
|
| 56 |
-
)
|
| 57 |
-
session_id = session.session_id
|
| 58 |
-
```
|
| 59 |
-
|
| 60 |
-
### 3. Update Analytics/Database Schema
|
| 61 |
-
|
| 62 |
-
Ensure the session and message tracking includes `user_id`:
|
| 63 |
-
|
| 64 |
-
**Session Collection:**
|
| 65 |
-
```javascript
|
| 66 |
-
{
|
| 67 |
-
_id: ObjectId,
|
| 68 |
-
session_id: String,
|
| 69 |
-
user_id: String, // New field - will be null for anonymous sessions
|
| 70 |
-
user_agent: String,
|
| 71 |
-
created_at: Date,
|
| 72 |
-
// ... other fields
|
| 73 |
-
}
|
| 74 |
-
```
|
| 75 |
-
|
| 76 |
-
**Message Collection:**
|
| 77 |
-
```javascript
|
| 78 |
-
{
|
| 79 |
-
_id: ObjectId,
|
| 80 |
-
session_id: String,
|
| 81 |
-
user_id: String, // New field - copied from session or request
|
| 82 |
-
message: String,
|
| 83 |
-
response: String,
|
| 84 |
-
timestamp: Date,
|
| 85 |
-
// ... other fields
|
| 86 |
-
}
|
| 87 |
-
```
|
| 88 |
-
|
| 89 |
-
### 4. Frontend Integration
|
| 90 |
-
|
| 91 |
-
The Chatty frontend will send requests like this:
|
| 92 |
-
|
| 93 |
-
```javascript
|
| 94 |
-
// For authenticated users
|
| 95 |
-
const response = await fetch('/chat', {
|
| 96 |
-
method: 'POST',
|
| 97 |
-
headers: {
|
| 98 |
-
'Content-Type': 'application/json',
|
| 99 |
-
'X-Session-ID': sessionId // API server's session ID
|
| 100 |
-
},
|
| 101 |
-
body: JSON.stringify({
|
| 102 |
-
prompt: userMessage,
|
| 103 |
-
user_id: authenticatedUserId, // From Chatty's authentication system
|
| 104 |
-
use_search: true,
|
| 105 |
-
// ... other parameters
|
| 106 |
-
})
|
| 107 |
-
});
|
| 108 |
-
|
| 109 |
-
// For anonymous users (existing behavior)
|
| 110 |
-
const response = await fetch('/chat', {
|
| 111 |
-
method: 'POST',
|
| 112 |
-
headers: {
|
| 113 |
-
'Content-Type': 'application/json',
|
| 114 |
-
'X-Session-ID': sessionId
|
| 115 |
-
},
|
| 116 |
-
body: JSON.stringify({
|
| 117 |
-
prompt: userMessage,
|
| 118 |
-
// user_id will be null/undefined
|
| 119 |
-
use_search: true,
|
| 120 |
-
// ... other parameters
|
| 121 |
-
})
|
| 122 |
-
});
|
| 123 |
-
```
|
| 124 |
-
|
| 125 |
-
## Benefits
|
| 126 |
-
|
| 127 |
-
1. **User-Specific History**: Authenticated users can access their chat history across sessions
|
| 128 |
-
2. **Analytics**: Better user behavior tracking and personalization
|
| 129 |
-
3. **Backward Compatibility**: Anonymous users continue to work as before
|
| 130 |
-
4. **Data Association**: All user interactions are properly linked to their account
|
| 131 |
-
|
| 132 |
-
## Implementation Notes
|
| 133 |
-
|
| 134 |
-
- `user_id` should be optional to maintain backward compatibility
|
| 135 |
-
- Existing anonymous sessions remain unchanged (user_id will be null)
|
| 136 |
-
- The API server's session_id and Chatty's authentication session are separate systems
|
| 137 |
-
- User association happens via the user_id field passed in requests
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py
CHANGED
|
@@ -132,17 +132,21 @@ def clean_terms(terms: List[str]) -> List[str]:
|
|
| 132 |
|
| 133 |
return final_terms
|
| 134 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
def validate_user_id(user_id: Optional[str]) -> Optional[str]:
|
| 136 |
"""Validate user_id format and return normalized value"""
|
| 137 |
-
|
| 138 |
-
|
| 139 |
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
return None
|
| 143 |
|
| 144 |
# Validate format: non-empty string, max 255 chars, alphanumeric + hyphens + underscores
|
| 145 |
-
user_id = user_id.strip()
|
| 146 |
if len(user_id) > 255:
|
| 147 |
raise HTTPException(status_code=400, detail="user_id must be 255 characters or less")
|
| 148 |
|
|
@@ -680,6 +684,28 @@ async def analytics_dashboard():
|
|
| 680 |
</div>
|
| 681 |
</div>
|
| 682 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 683 |
<div class="chart-container">
|
| 684 |
<div class="chart-title">👥 User Analytics</div>
|
| 685 |
<div class="stats-grid">
|
|
@@ -699,6 +725,22 @@ async def analytics_dashboard():
|
|
| 699 |
<div class="stat-number">{user_stats.get('authenticated_session_percentage', 0)}%</div>
|
| 700 |
<div class="stat-label">Auth Session %</div>
|
| 701 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 702 |
</div>
|
| 703 |
</div>
|
| 704 |
|
|
|
|
| 132 |
|
| 133 |
return final_terms
|
| 134 |
|
| 135 |
+
def normalize_user_id(user_id: Optional[str]) -> Optional[str]:
|
| 136 |
+
"""Normalize user_id to None for anonymous requests"""
|
| 137 |
+
if user_id is None or user_id.strip() == "":
|
| 138 |
+
return None
|
| 139 |
+
return user_id.strip()
|
| 140 |
+
|
| 141 |
def validate_user_id(user_id: Optional[str]) -> Optional[str]:
|
| 142 |
"""Validate user_id format and return normalized value"""
|
| 143 |
+
# Normalize to None for anonymous users
|
| 144 |
+
user_id = normalize_user_id(user_id)
|
| 145 |
|
| 146 |
+
if user_id is None:
|
| 147 |
+
return None # Anonymous user
|
|
|
|
| 148 |
|
| 149 |
# Validate format: non-empty string, max 255 chars, alphanumeric + hyphens + underscores
|
|
|
|
| 150 |
if len(user_id) > 255:
|
| 151 |
raise HTTPException(status_code=400, detail="user_id must be 255 characters or less")
|
| 152 |
|
|
|
|
| 684 |
</div>
|
| 685 |
</div>
|
| 686 |
|
| 687 |
+
<div class="chart-container">
|
| 688 |
+
<div class="chart-title">🔓 Anonymous Usage Overview</div>
|
| 689 |
+
<div class="stats-grid">
|
| 690 |
+
<div class="stat-card">
|
| 691 |
+
<div class="stat-number">{user_stats.get('anonymous_sessions', 0)}</div>
|
| 692 |
+
<div class="stat-label">Anonymous Sessions</div>
|
| 693 |
+
</div>
|
| 694 |
+
<div class="stat-card">
|
| 695 |
+
<div class="stat-number">{user_stats.get('anonymous_messages', 0)}</div>
|
| 696 |
+
<div class="stat-label">Anonymous Messages</div>
|
| 697 |
+
</div>
|
| 698 |
+
<div class="stat-card">
|
| 699 |
+
<div class="stat-number">{round(100 - user_stats.get('authenticated_session_percentage', 0), 1)}%</div>
|
| 700 |
+
<div class="stat-label">Anonymous Session %</div>
|
| 701 |
+
</div>
|
| 702 |
+
<div class="stat-card">
|
| 703 |
+
<div class="stat-number">{round(100 - user_stats.get('authenticated_message_percentage', 0), 1)}%</div>
|
| 704 |
+
<div class="stat-label">Anonymous Message %</div>
|
| 705 |
+
</div>
|
| 706 |
+
</div>
|
| 707 |
+
</div>
|
| 708 |
+
|
| 709 |
<div class="chart-container">
|
| 710 |
<div class="chart-title">👥 User Analytics</div>
|
| 711 |
<div class="stats-grid">
|
|
|
|
| 725 |
<div class="stat-number">{user_stats.get('authenticated_session_percentage', 0)}%</div>
|
| 726 |
<div class="stat-label">Auth Session %</div>
|
| 727 |
</div>
|
| 728 |
+
<div class="stat-card">
|
| 729 |
+
<div class="stat-number">{user_stats.get('authenticated_messages', 0)}</div>
|
| 730 |
+
<div class="stat-label">Authenticated Messages</div>
|
| 731 |
+
</div>
|
| 732 |
+
<div class="stat-card">
|
| 733 |
+
<div class="stat-number">{user_stats.get('anonymous_messages', 0)}</div>
|
| 734 |
+
<div class="stat-label">Anonymous Messages</div>
|
| 735 |
+
</div>
|
| 736 |
+
<div class="stat-card">
|
| 737 |
+
<div class="stat-number">{user_stats.get('authenticated_message_percentage', 0)}%</div>
|
| 738 |
+
<div class="stat-label">Auth Message %</div>
|
| 739 |
+
</div>
|
| 740 |
+
<div class="stat-card">
|
| 741 |
+
<div class="stat-number">{user_stats.get('anonymous_messages', 0) + user_stats.get('authenticated_messages', 0)}</div>
|
| 742 |
+
<div class="stat-label">Total Messages</div>
|
| 743 |
+
</div>
|
| 744 |
</div>
|
| 745 |
</div>
|
| 746 |
|
docs/ANONYMOUS_API_EXAMPLES.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Anonymous Chat API Examples
|
| 2 |
+
|
| 3 |
+
This document provides sample API calls for using the Atlas Chat API in anonymous mode (without user authentication).
|
| 4 |
+
|
| 5 |
+
## API Endpoint
|
| 6 |
+
|
| 7 |
+
**Base URL:** `http://localhost:8000` (or your deployed URL)
|
| 8 |
+
**Endpoint:** `POST /chat`
|
| 9 |
+
**Content-Type:** `application/json`
|
| 10 |
+
|
| 11 |
+
## Request Structure
|
| 12 |
+
|
| 13 |
+
```json
|
| 14 |
+
{
|
| 15 |
+
"prompt": "Your question or message here",
|
| 16 |
+
"max_new_tokens": 500,
|
| 17 |
+
"use_search": true,
|
| 18 |
+
"temperature": 0.7,
|
| 19 |
+
"user_id": null
|
| 20 |
+
}
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
### Parameters
|
| 24 |
+
|
| 25 |
+
- **`prompt`** (required): Your question or message to the AI
|
| 26 |
+
- **`max_new_tokens`** (optional): Maximum tokens in response (default: 500)
|
| 27 |
+
- **`use_search`** (optional): Whether to use web search (default: true)
|
| 28 |
+
- **`temperature`** (optional): Response creativity (0.0-1.0, default: 0.7)
|
| 29 |
+
- **`user_id`** (optional): User identifier (null/omitted for anonymous)
|
| 30 |
+
|
| 31 |
+
## Anonymous Request Examples
|
| 32 |
+
|
| 33 |
+
### 1. Basic Anonymous Request (No user_id field)
|
| 34 |
+
|
| 35 |
+
```bash
|
| 36 |
+
curl -X POST "http://localhost:8000/chat" \
|
| 37 |
+
-H "Content-Type: application/json" \
|
| 38 |
+
-d '{
|
| 39 |
+
"prompt": "What is the capital of France?",
|
| 40 |
+
"use_search": false
|
| 41 |
+
}'
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
### 2. Anonymous Request with Explicit null user_id
|
| 45 |
+
|
| 46 |
+
```bash
|
| 47 |
+
curl -X POST "http://localhost:8000/chat" \
|
| 48 |
+
-H "Content-Type: application/json" \
|
| 49 |
+
-d '{
|
| 50 |
+
"prompt": "Explain quantum computing in simple terms",
|
| 51 |
+
"user_id": null,
|
| 52 |
+
"use_search": true,
|
| 53 |
+
"temperature": 0.5
|
| 54 |
+
}'
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
### 3. Anonymous Request with Empty user_id
|
| 58 |
+
|
| 59 |
+
```bash
|
| 60 |
+
curl -X POST "http://localhost:8000/chat" \
|
| 61 |
+
-H "Content-Type: application/json" \
|
| 62 |
+
-d '{
|
| 63 |
+
"prompt": "What are the latest developments in AI?",
|
| 64 |
+
"user_id": "",
|
| 65 |
+
"use_search": true,
|
| 66 |
+
"max_new_tokens": 300
|
| 67 |
+
}'
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
## JavaScript/Fetch Examples
|
| 71 |
+
|
| 72 |
+
### Basic Anonymous Request
|
| 73 |
+
|
| 74 |
+
```javascript
|
| 75 |
+
const response = await fetch('http://localhost:8000/chat', {
|
| 76 |
+
method: 'POST',
|
| 77 |
+
headers: {
|
| 78 |
+
'Content-Type': 'application/json',
|
| 79 |
+
},
|
| 80 |
+
body: JSON.stringify({
|
| 81 |
+
prompt: "How does machine learning work?",
|
| 82 |
+
use_search: true,
|
| 83 |
+
temperature: 0.6
|
| 84 |
+
})
|
| 85 |
+
});
|
| 86 |
+
|
| 87 |
+
const data = await response.json();
|
| 88 |
+
console.log(data.response);
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
## Python Examples
|
| 92 |
+
|
| 93 |
+
### Using requests library
|
| 94 |
+
|
| 95 |
+
```python
|
| 96 |
+
import requests
|
| 97 |
+
|
| 98 |
+
# Basic anonymous request
|
| 99 |
+
url = "http://localhost:8000/chat"
|
| 100 |
+
payload = {
|
| 101 |
+
"prompt": "Explain the theory of relativity",
|
| 102 |
+
"use_search": False,
|
| 103 |
+
"temperature": 0.5
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
response = requests.post(url, json=payload)
|
| 107 |
+
data = response.json()
|
| 108 |
+
print(data['response'])
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
## Response Format
|
| 112 |
+
|
| 113 |
+
All requests return a JSON response with this structure:
|
| 114 |
+
|
| 115 |
+
```json
|
| 116 |
+
{
|
| 117 |
+
"response": "The AI's response to your prompt...",
|
| 118 |
+
"search_results": [
|
| 119 |
+
{
|
| 120 |
+
"title": "Search Result Title",
|
| 121 |
+
"body": "Search result description...",
|
| 122 |
+
"href": "https://example.com",
|
| 123 |
+
"source": "Brave"
|
| 124 |
+
}
|
| 125 |
+
]
|
| 126 |
+
}
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
## Session Tracking
|
| 130 |
+
|
| 131 |
+
Anonymous requests automatically create sessions for analytics purposes:
|
| 132 |
+
|
| 133 |
+
- Each request gets a unique session ID (returned in `X-Session-ID` header)
|
| 134 |
+
- Sessions are tracked anonymously (no personal data stored)
|
| 135 |
+
- Analytics count anonymous vs authenticated usage
|
| 136 |
+
- No individual user tracking for anonymous requests
|
| 137 |
+
|
| 138 |
+
## Analytics Endpoints
|
| 139 |
+
|
| 140 |
+
You can also check anonymous usage analytics:
|
| 141 |
+
|
| 142 |
+
### Get Basic Stats
|
| 143 |
+
|
| 144 |
+
```bash
|
| 145 |
+
curl "http://localhost:8000/analytics/stats"
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
### View Dashboard
|
| 149 |
+
|
| 150 |
+
```bash
|
| 151 |
+
curl "http://localhost:8000/analytics/dashboard"
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
## Notes
|
| 155 |
+
|
| 156 |
+
- Anonymous requests have identical functionality to authenticated requests
|
| 157 |
+
- No user data is stored or tracked for anonymous requests
|
| 158 |
+
- Web search functionality works the same for anonymous users
|
| 159 |
+
- Response quality and speed are identical for anonymous and authenticated users
|
| 160 |
+
- Sessions are created automatically for analytics but contain no personal information
|
DEPLOYMENT.md → docs/DEPLOYMENT.md
RENAMED
|
File without changes
|
MIGRATION_README.md → docs/MIGRATION_README.md
RENAMED
|
File without changes
|
SETUP.md → docs/SETUP.md
RENAMED
|
@@ -116,19 +116,66 @@ Once running, the application provides these endpoints:
|
|
| 116 |
- **`/analytics/export`** - Export analytics data (JSON/CSV format)
|
| 117 |
|
| 118 |
### Example Usage
|
|
|
|
|
|
|
| 119 |
```bash
|
| 120 |
# Health check
|
| 121 |
curl http://localhost:7860/
|
| 122 |
|
| 123 |
-
#
|
| 124 |
curl -X POST http://localhost:7860/chat \
|
| 125 |
-H "Content-Type: application/json" \
|
| 126 |
-d '{"prompt": "What is artificial intelligence?", "use_search": true}'
|
| 127 |
|
| 128 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
curl http://localhost:7860/analytics/stats
|
| 130 |
|
| 131 |
-
#
|
|
|
|
|
|
|
|
|
|
| 132 |
open http://localhost:7860/analytics/dashboard
|
| 133 |
```
|
| 134 |
|
|
|
|
| 116 |
- **`/analytics/export`** - Export analytics data (JSON/CSV format)
|
| 117 |
|
| 118 |
### Example Usage
|
| 119 |
+
|
| 120 |
+
#### Anonymous Mode (No Authentication Required)
|
| 121 |
```bash
|
| 122 |
# Health check
|
| 123 |
curl http://localhost:7860/
|
| 124 |
|
| 125 |
+
# Simple anonymous chat request
|
| 126 |
curl -X POST http://localhost:7860/chat \
|
| 127 |
-H "Content-Type: application/json" \
|
| 128 |
-d '{"prompt": "What is artificial intelligence?", "use_search": true}'
|
| 129 |
|
| 130 |
+
# Anonymous request without search
|
| 131 |
+
curl -X POST http://localhost:7860/chat \
|
| 132 |
+
-H "Content-Type: application/json" \
|
| 133 |
+
-d '{"prompt": "What is 2+2?", "use_search": false}'
|
| 134 |
+
|
| 135 |
+
# Anonymous request with conversation history
|
| 136 |
+
curl -X POST http://localhost:7860/chat \
|
| 137 |
+
-H "Content-Type: application/json" \
|
| 138 |
+
-d '{
|
| 139 |
+
"prompt": "Can you elaborate on that?",
|
| 140 |
+
"use_search": false,
|
| 141 |
+
"history": [
|
| 142 |
+
{"role": "user", "content": "What is machine learning?"},
|
| 143 |
+
{"role": "assistant", "content": "Machine learning is a subset of AI..."}
|
| 144 |
+
]
|
| 145 |
+
}'
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
#### Authenticated Mode (With User Tracking)
|
| 149 |
+
```bash
|
| 150 |
+
# Authenticated chat request
|
| 151 |
+
curl -X POST http://localhost:7860/chat \
|
| 152 |
+
-H "Content-Type: application/json" \
|
| 153 |
+
-d '{
|
| 154 |
+
"prompt": "What is my chat history?",
|
| 155 |
+
"user_id": "test-user-123",
|
| 156 |
+
"use_search": true
|
| 157 |
+
}'
|
| 158 |
+
|
| 159 |
+
# Authenticated request with session continuity
|
| 160 |
+
curl -X POST http://localhost:7860/chat \
|
| 161 |
+
-H "Content-Type: application/json" \
|
| 162 |
+
-H "X-Session-ID: session-uuid-here" \
|
| 163 |
+
-d '{
|
| 164 |
+
"prompt": "Continue our previous conversation",
|
| 165 |
+
"user_id": "test-user-123",
|
| 166 |
+
"use_search": false
|
| 167 |
+
}'
|
| 168 |
+
```
|
| 169 |
+
|
| 170 |
+
#### Analytics & Monitoring
|
| 171 |
+
```bash
|
| 172 |
+
# View analytics (includes anonymous vs authenticated breakdown)
|
| 173 |
curl http://localhost:7860/analytics/stats
|
| 174 |
|
| 175 |
+
# Export analytics data
|
| 176 |
+
curl "http://localhost:7860/analytics/export?format=json&days=7"
|
| 177 |
+
|
| 178 |
+
# Access interactive dashboard in browser
|
| 179 |
open http://localhost:7860/analytics/dashboard
|
| 180 |
```
|
| 181 |
|
analytics-approach.md → docs/analytics-approach.md
RENAMED
|
File without changes
|
analytics-tasks.md → docs/analytics-tasks.md
RENAMED
|
File without changes
|
docs/api-integration-guide.md
ADDED
|
@@ -0,0 +1,579 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# API Integration Guide: User Authentication & Anonymous Mode Support
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
The Atlas API supports both authenticated and anonymous usage modes. When users are authenticated, the frontend can send their `user_id` along with chat requests to associate session data with specific users. For anonymous usage, the `user_id` parameter can be omitted or set to null, and the system will handle the request without any user tracking.
|
| 6 |
+
|
| 7 |
+
## Required API Changes
|
| 8 |
+
|
| 9 |
+
### 1. Accept User ID in Chat Requests
|
| 10 |
+
|
| 11 |
+
The `/chat` endpoint needs to accept an optional `user_id` parameter to associate sessions with authenticated users.
|
| 12 |
+
|
| 13 |
+
**Current Request Structure:**
|
| 14 |
+
```python
|
| 15 |
+
class ChatRequest(BaseModel):
|
| 16 |
+
prompt: str
|
| 17 |
+
use_search: bool = True
|
| 18 |
+
max_new_tokens: int = 1000
|
| 19 |
+
temperature: float = 0.7
|
| 20 |
+
history: List[dict] = []
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
**Updated Request Structure:**
|
| 24 |
+
```python
|
| 25 |
+
class ChatRequest(BaseModel):
|
| 26 |
+
prompt: str
|
| 27 |
+
use_search: bool = True
|
| 28 |
+
max_new_tokens: int = 1000
|
| 29 |
+
temperature: float = 0.7
|
| 30 |
+
history: List[dict] = []
|
| 31 |
+
user_id: Optional[str] = None # Optional field - defaults to anonymous mode
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
### Anonymous Mode Support
|
| 35 |
+
|
| 36 |
+
The `user_id` parameter is **completely optional**. When omitted or set to null/empty string, the system operates in anonymous mode:
|
| 37 |
+
|
| 38 |
+
- **Anonymous requests**: No user tracking or identification
|
| 39 |
+
- **Same functionality**: Full chat capabilities without authentication
|
| 40 |
+
- **No setup required**: Works immediately without any configuration
|
| 41 |
+
- **Privacy-focused**: No personal data collection or storage
|
| 42 |
+
|
| 43 |
+
### 2. Modify Session Creation/Tracking
|
| 44 |
+
|
| 45 |
+
Update the session management to include `user_id` when provided:
|
| 46 |
+
|
| 47 |
+
**In the `/chat` endpoint:**
|
| 48 |
+
```python
|
| 49 |
+
# Handle session management with user_id
|
| 50 |
+
if analytics_available:
|
| 51 |
+
if not session_id:
|
| 52 |
+
# Create new session with user_id if provided
|
| 53 |
+
session = await create_session(
|
| 54 |
+
user_agent=user_agent,
|
| 55 |
+
user_id=request.user_id # Pass user_id from request
|
| 56 |
+
)
|
| 57 |
+
session_id = session.session_id
|
| 58 |
+
else:
|
| 59 |
+
# Get existing session or create new one if not found
|
| 60 |
+
session = await get_session(session_id)
|
| 61 |
+
if not session:
|
| 62 |
+
session = await create_session(
|
| 63 |
+
user_agent=user_agent,
|
| 64 |
+
user_id=request.user_id # Pass user_id from request
|
| 65 |
+
)
|
| 66 |
+
session_id = session.session_id
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
### 3. Update Analytics/Database Schema
|
| 70 |
+
|
| 71 |
+
Ensure the session and message tracking includes `user_id`:
|
| 72 |
+
|
| 73 |
+
**Session Collection:**
|
| 74 |
+
```javascript
|
| 75 |
+
{
|
| 76 |
+
_id: ObjectId,
|
| 77 |
+
session_id: String,
|
| 78 |
+
user_id: String, // New field - will be null for anonymous sessions
|
| 79 |
+
user_agent: String,
|
| 80 |
+
created_at: Date,
|
| 81 |
+
// ... other fields
|
| 82 |
+
}
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
**Message Collection:**
|
| 86 |
+
```javascript
|
| 87 |
+
{
|
| 88 |
+
_id: ObjectId,
|
| 89 |
+
session_id: String,
|
| 90 |
+
user_id: String, // New field - copied from session or request
|
| 91 |
+
message: String,
|
| 92 |
+
response: String,
|
| 93 |
+
timestamp: Date,
|
| 94 |
+
// ... other fields
|
| 95 |
+
}
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
### 4. Frontend Integration Examples
|
| 99 |
+
|
| 100 |
+
#### Anonymous Usage (No Authentication Required)
|
| 101 |
+
|
| 102 |
+
The simplest way to use the API - no user_id needed:
|
| 103 |
+
|
| 104 |
+
```javascript
|
| 105 |
+
// Anonymous request - user_id completely omitted
|
| 106 |
+
const response = await fetch('/chat', {
|
| 107 |
+
method: 'POST',
|
| 108 |
+
headers: {
|
| 109 |
+
'Content-Type': 'application/json'
|
| 110 |
+
},
|
| 111 |
+
body: JSON.stringify({
|
| 112 |
+
prompt: "What is artificial intelligence?",
|
| 113 |
+
use_search: true,
|
| 114 |
+
max_new_tokens: 1000,
|
| 115 |
+
temperature: 0.7
|
| 116 |
+
})
|
| 117 |
+
});
|
| 118 |
+
|
| 119 |
+
// Anonymous request - user_id explicitly set to null
|
| 120 |
+
const response = await fetch('/chat', {
|
| 121 |
+
method: 'POST',
|
| 122 |
+
headers: {
|
| 123 |
+
'Content-Type': 'application/json'
|
| 124 |
+
},
|
| 125 |
+
body: JSON.stringify({
|
| 126 |
+
prompt: "Explain quantum computing",
|
| 127 |
+
user_id: null, // Explicitly anonymous
|
| 128 |
+
use_search: true
|
| 129 |
+
})
|
| 130 |
+
});
|
| 131 |
+
|
| 132 |
+
// Anonymous request with session tracking (optional)
|
| 133 |
+
const response = await fetch('/chat', {
|
| 134 |
+
method: 'POST',
|
| 135 |
+
headers: {
|
| 136 |
+
'Content-Type': 'application/json',
|
| 137 |
+
'X-Session-ID': sessionId // For conversation continuity
|
| 138 |
+
},
|
| 139 |
+
body: JSON.stringify({
|
| 140 |
+
prompt: "Continue our previous discussion",
|
| 141 |
+
use_search: false,
|
| 142 |
+
history: previousMessages
|
| 143 |
+
})
|
| 144 |
+
});
|
| 145 |
+
```
|
| 146 |
+
|
| 147 |
+
#### Authenticated Usage (With User Tracking)
|
| 148 |
+
|
| 149 |
+
For applications with user authentication:
|
| 150 |
+
|
| 151 |
+
```javascript
|
| 152 |
+
// Authenticated request with user tracking
|
| 153 |
+
const response = await fetch('/chat', {
|
| 154 |
+
method: 'POST',
|
| 155 |
+
headers: {
|
| 156 |
+
'Content-Type': 'application/json',
|
| 157 |
+
'X-Session-ID': sessionId
|
| 158 |
+
},
|
| 159 |
+
body: JSON.stringify({
|
| 160 |
+
prompt: userMessage,
|
| 161 |
+
user_id: authenticatedUserId, // From your authentication system
|
| 162 |
+
use_search: true,
|
| 163 |
+
// ... other parameters
|
| 164 |
+
})
|
| 165 |
+
});
|
| 166 |
+
```
|
| 167 |
+
|
| 168 |
+
#### Flexible Integration Pattern
|
| 169 |
+
|
| 170 |
+
Handle both authenticated and anonymous users seamlessly:
|
| 171 |
+
|
| 172 |
+
```javascript
|
| 173 |
+
async function sendChatMessage(prompt, authenticatedUserId = null) {
|
| 174 |
+
const requestBody = {
|
| 175 |
+
prompt: prompt,
|
| 176 |
+
use_search: true,
|
| 177 |
+
max_new_tokens: 1000,
|
| 178 |
+
temperature: 0.7
|
| 179 |
+
};
|
| 180 |
+
|
| 181 |
+
// Only include user_id if user is authenticated
|
| 182 |
+
if (authenticatedUserId) {
|
| 183 |
+
requestBody.user_id = authenticatedUserId;
|
| 184 |
+
}
|
| 185 |
+
// For anonymous users, user_id is simply omitted
|
| 186 |
+
|
| 187 |
+
const response = await fetch('/chat', {
|
| 188 |
+
method: 'POST',
|
| 189 |
+
headers: {
|
| 190 |
+
'Content-Type': 'application/json'
|
| 191 |
+
},
|
| 192 |
+
body: JSON.stringify(requestBody)
|
| 193 |
+
});
|
| 194 |
+
|
| 195 |
+
return await response.json();
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
// Usage examples:
|
| 199 |
+
// Anonymous: sendChatMessage("Hello, how are you?")
|
| 200 |
+
// Authenticated: sendChatMessage("Hello, how are you?", "user123")
|
| 201 |
+
```
|
| 202 |
+
|
| 203 |
+
## Anonymous Usage Patterns
|
| 204 |
+
|
| 205 |
+
### Quick Start (No Setup Required)
|
| 206 |
+
|
| 207 |
+
The fastest way to integrate Atlas API is through anonymous mode:
|
| 208 |
+
|
| 209 |
+
```bash
|
| 210 |
+
# Simple cURL example - no authentication needed
|
| 211 |
+
curl -X POST https://your-atlas-api.com/chat \
|
| 212 |
+
-H "Content-Type: application/json" \
|
| 213 |
+
-d '{
|
| 214 |
+
"prompt": "What is machine learning?",
|
| 215 |
+
"use_search": true
|
| 216 |
+
}'
|
| 217 |
+
```
|
| 218 |
+
|
| 219 |
+
### Frontend Integration Examples
|
| 220 |
+
|
| 221 |
+
#### React/JavaScript
|
| 222 |
+
```javascript
|
| 223 |
+
// Simple React hook for anonymous chat
|
| 224 |
+
function useAnonymousChat() {
|
| 225 |
+
const [messages, setMessages] = useState([]);
|
| 226 |
+
|
| 227 |
+
const sendMessage = async (prompt) => {
|
| 228 |
+
const response = await fetch('/chat', {
|
| 229 |
+
method: 'POST',
|
| 230 |
+
headers: { 'Content-Type': 'application/json' },
|
| 231 |
+
body: JSON.stringify({
|
| 232 |
+
prompt,
|
| 233 |
+
use_search: true,
|
| 234 |
+
history: messages
|
| 235 |
+
})
|
| 236 |
+
});
|
| 237 |
+
|
| 238 |
+
const result = await response.json();
|
| 239 |
+
setMessages(prev => [...prev,
|
| 240 |
+
{ role: 'user', content: prompt },
|
| 241 |
+
{ role: 'assistant', content: result.response }
|
| 242 |
+
]);
|
| 243 |
+
|
| 244 |
+
return result;
|
| 245 |
+
};
|
| 246 |
+
|
| 247 |
+
return { messages, sendMessage };
|
| 248 |
+
}
|
| 249 |
+
```
|
| 250 |
+
|
| 251 |
+
#### Python Client
|
| 252 |
+
```python
|
| 253 |
+
import requests
|
| 254 |
+
|
| 255 |
+
def anonymous_chat(prompt, use_search=True):
|
| 256 |
+
"""Send anonymous chat request to Atlas API"""
|
| 257 |
+
response = requests.post('https://your-atlas-api.com/chat',
|
| 258 |
+
json={
|
| 259 |
+
'prompt': prompt,
|
| 260 |
+
'use_search': use_search,
|
| 261 |
+
'max_new_tokens': 1000,
|
| 262 |
+
'temperature': 0.7
|
| 263 |
+
}
|
| 264 |
+
)
|
| 265 |
+
return response.json()
|
| 266 |
+
|
| 267 |
+
# Usage
|
| 268 |
+
result = anonymous_chat("Explain neural networks")
|
| 269 |
+
print(result['response'])
|
| 270 |
+
```
|
| 271 |
+
|
| 272 |
+
#### Node.js/Express
|
| 273 |
+
```javascript
|
| 274 |
+
const express = require('express');
|
| 275 |
+
const axios = require('axios');
|
| 276 |
+
|
| 277 |
+
app.post('/proxy-chat', async (req, res) => {
|
| 278 |
+
try {
|
| 279 |
+
const response = await axios.post('https://your-atlas-api.com/chat', {
|
| 280 |
+
prompt: req.body.message,
|
| 281 |
+
use_search: true,
|
| 282 |
+
// user_id omitted for anonymous usage
|
| 283 |
+
});
|
| 284 |
+
|
| 285 |
+
res.json(response.data);
|
| 286 |
+
} catch (error) {
|
| 287 |
+
res.status(500).json({ error: 'Chat request failed' });
|
| 288 |
+
}
|
| 289 |
+
});
|
| 290 |
+
```
|
| 291 |
+
|
| 292 |
+
### Progressive Enhancement
|
| 293 |
+
|
| 294 |
+
Start with anonymous mode and add authentication later:
|
| 295 |
+
|
| 296 |
+
```javascript
|
| 297 |
+
class ChatClient {
|
| 298 |
+
constructor(apiUrl) {
|
| 299 |
+
this.apiUrl = apiUrl;
|
| 300 |
+
this.userId = null; // Start anonymous
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
// Enable authentication when ready
|
| 304 |
+
authenticate(userId) {
|
| 305 |
+
this.userId = userId;
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
// Logout returns to anonymous mode
|
| 309 |
+
logout() {
|
| 310 |
+
this.userId = null;
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
async sendMessage(prompt, options = {}) {
|
| 314 |
+
const requestBody = {
|
| 315 |
+
prompt,
|
| 316 |
+
use_search: options.useSearch ?? true,
|
| 317 |
+
max_new_tokens: options.maxTokens ?? 1000,
|
| 318 |
+
temperature: options.temperature ?? 0.7,
|
| 319 |
+
history: options.history ?? []
|
| 320 |
+
};
|
| 321 |
+
|
| 322 |
+
// Include user_id only if authenticated
|
| 323 |
+
if (this.userId) {
|
| 324 |
+
requestBody.user_id = this.userId;
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
const response = await fetch(`${this.apiUrl}/chat`, {
|
| 328 |
+
method: 'POST',
|
| 329 |
+
headers: { 'Content-Type': 'application/json' },
|
| 330 |
+
body: JSON.stringify(requestBody)
|
| 331 |
+
});
|
| 332 |
+
|
| 333 |
+
return await response.json();
|
| 334 |
+
}
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
// Usage:
|
| 338 |
+
const client = new ChatClient('https://your-atlas-api.com');
|
| 339 |
+
|
| 340 |
+
// Anonymous usage
|
| 341 |
+
await client.sendMessage("Hello!");
|
| 342 |
+
|
| 343 |
+
// Later, add authentication
|
| 344 |
+
client.authenticate("user123");
|
| 345 |
+
await client.sendMessage("Now I'm authenticated!");
|
| 346 |
+
|
| 347 |
+
// Return to anonymous
|
| 348 |
+
client.logout();
|
| 349 |
+
await client.sendMessage("Back to anonymous!");
|
| 350 |
+
```
|
| 351 |
+
|
| 352 |
+
## Benefits
|
| 353 |
+
|
| 354 |
+
### For Anonymous Users
|
| 355 |
+
1. **Zero Setup**: Start using immediately without any configuration
|
| 356 |
+
2. **Privacy-First**: No tracking or data collection
|
| 357 |
+
3. **Full Functionality**: Complete access to AI chat and search features
|
| 358 |
+
4. **No Registration**: Use the service without creating accounts
|
| 359 |
+
|
| 360 |
+
### For Authenticated Users
|
| 361 |
+
1. **User-Specific History**: Access chat history across sessions
|
| 362 |
+
2. **Personalization**: Tailored responses based on user preferences
|
| 363 |
+
3. **Analytics**: Detailed usage tracking and insights
|
| 364 |
+
4. **Data Association**: All interactions linked to user account
|
| 365 |
+
|
| 366 |
+
### For Developers
|
| 367 |
+
1. **Flexible Integration**: Support both usage modes seamlessly
|
| 368 |
+
2. **Backward Compatibility**: Existing anonymous implementations continue working
|
| 369 |
+
3. **Progressive Enhancement**: Start anonymous, add authentication later
|
| 370 |
+
4. **Simple API**: Same endpoints work for both modes
|
| 371 |
+
|
| 372 |
+
## Implementation Notes
|
| 373 |
+
|
| 374 |
+
### Anonymous Mode Behavior
|
| 375 |
+
- **Default Mode**: When `user_id` is omitted, null, or empty string, the system operates anonymously
|
| 376 |
+
- **No Validation Required**: Anonymous requests bypass user ID validation entirely
|
| 377 |
+
- **Same Performance**: Anonymous requests have identical response times and functionality
|
| 378 |
+
- **Session Support**: Anonymous users can still use session IDs for conversation continuity
|
| 379 |
+
|
| 380 |
+
### Authentication Integration
|
| 381 |
+
- **Optional Field**: `user_id` is completely optional in all API requests
|
| 382 |
+
- **Flexible Validation**: System accepts null, undefined, or missing user_id values
|
| 383 |
+
- **Backward Compatibility**: Existing anonymous implementations continue working unchanged
|
| 384 |
+
- **Progressive Enhancement**: Applications can add authentication without breaking existing functionality
|
| 385 |
+
|
| 386 |
+
### Technical Details
|
| 387 |
+
- **Database Handling**: null `user_id` values are stored and queried efficiently
|
| 388 |
+
- **Analytics Separation**: Anonymous usage is tracked separately from authenticated usage
|
| 389 |
+
- **Session Management**: API server's session_id works independently of user authentication
|
| 390 |
+
- **Error Handling**: Anonymous requests have the same error handling as authenticated requests
|
| 391 |
+
|
| 392 |
+
### Best Practices
|
| 393 |
+
- **Start Simple**: Begin with anonymous mode for faster integration
|
| 394 |
+
- **Add Authentication Later**: Implement user tracking when needed
|
| 395 |
+
- **Handle Both Modes**: Design your frontend to work with or without user_id
|
| 396 |
+
- **Test Both Paths**: Ensure your application works in anonymous and authenticated modes
|
| 397 |
+
|
| 398 |
+
## API Reference
|
| 399 |
+
|
| 400 |
+
### POST /chat
|
| 401 |
+
|
| 402 |
+
Send a chat message and receive an AI-generated response with optional web search.
|
| 403 |
+
|
| 404 |
+
#### Request Body
|
| 405 |
+
|
| 406 |
+
```json
|
| 407 |
+
{
|
| 408 |
+
"prompt": "string (required) - The user's message or question",
|
| 409 |
+
"user_id": "string (optional) - User identifier for authenticated requests. Omit for anonymous mode",
|
| 410 |
+
"use_search": "boolean (optional, default: true) - Whether to use web search for context",
|
| 411 |
+
"max_new_tokens": "integer (optional, default: 1000) - Maximum response length",
|
| 412 |
+
"temperature": "number (optional, default: 0.7) - Response creativity (0.0-1.0)",
|
| 413 |
+
"history": "array (optional, default: []) - Previous conversation messages"
|
| 414 |
+
}
|
| 415 |
+
```
|
| 416 |
+
|
| 417 |
+
#### Anonymous Request Examples
|
| 418 |
+
|
| 419 |
+
**Minimal Anonymous Request:**
|
| 420 |
+
```json
|
| 421 |
+
{
|
| 422 |
+
"prompt": "What is artificial intelligence?"
|
| 423 |
+
}
|
| 424 |
+
```
|
| 425 |
+
|
| 426 |
+
**Anonymous Request with Options:**
|
| 427 |
+
```json
|
| 428 |
+
{
|
| 429 |
+
"prompt": "Explain quantum computing in simple terms",
|
| 430 |
+
"use_search": true,
|
| 431 |
+
"max_new_tokens": 500,
|
| 432 |
+
"temperature": 0.5
|
| 433 |
+
}
|
| 434 |
+
```
|
| 435 |
+
|
| 436 |
+
**Anonymous Request with Conversation History:**
|
| 437 |
+
```json
|
| 438 |
+
{
|
| 439 |
+
"prompt": "Can you elaborate on that?",
|
| 440 |
+
"use_search": false,
|
| 441 |
+
"history": [
|
| 442 |
+
{"role": "user", "content": "What is machine learning?"},
|
| 443 |
+
{"role": "assistant", "content": "Machine learning is a subset of AI..."}
|
| 444 |
+
]
|
| 445 |
+
}
|
| 446 |
+
```
|
| 447 |
+
|
| 448 |
+
#### Authenticated Request Examples
|
| 449 |
+
|
| 450 |
+
**Basic Authenticated Request:**
|
| 451 |
+
```json
|
| 452 |
+
{
|
| 453 |
+
"prompt": "What is my chat history?",
|
| 454 |
+
"user_id": "user123"
|
| 455 |
+
}
|
| 456 |
+
```
|
| 457 |
+
|
| 458 |
+
**Full Authenticated Request:**
|
| 459 |
+
```json
|
| 460 |
+
{
|
| 461 |
+
"prompt": "Help me understand neural networks",
|
| 462 |
+
"user_id": "user123",
|
| 463 |
+
"use_search": true,
|
| 464 |
+
"max_new_tokens": 1500,
|
| 465 |
+
"temperature": 0.8,
|
| 466 |
+
"history": []
|
| 467 |
+
}
|
| 468 |
+
```
|
| 469 |
+
|
| 470 |
+
#### Response Format
|
| 471 |
+
|
| 472 |
+
Both anonymous and authenticated requests return the same response format:
|
| 473 |
+
|
| 474 |
+
```json
|
| 475 |
+
{
|
| 476 |
+
"response": "string - The AI-generated response",
|
| 477 |
+
"session_id": "string - Session identifier for conversation continuity",
|
| 478 |
+
"search_used": "boolean - Whether web search was performed",
|
| 479 |
+
"search_results": "array - Search results used (if search_used is true)",
|
| 480 |
+
"processing_time_ms": "number - Response generation time",
|
| 481 |
+
"tokens_used": "number - Number of tokens in the response"
|
| 482 |
+
}
|
| 483 |
+
```
|
| 484 |
+
|
| 485 |
+
### Headers
|
| 486 |
+
|
| 487 |
+
#### Optional Headers
|
| 488 |
+
|
| 489 |
+
- **X-Session-ID**: `string` - Session identifier for conversation continuity
|
| 490 |
+
- **Content-Type**: `application/json` - Required for POST requests
|
| 491 |
+
- **User-Agent**: `string` - Client identification (automatically tracked)
|
| 492 |
+
|
| 493 |
+
#### Example with Session Header
|
| 494 |
+
|
| 495 |
+
```bash
|
| 496 |
+
curl -X POST https://your-atlas-api.com/chat \
|
| 497 |
+
-H "Content-Type: application/json" \
|
| 498 |
+
-H "X-Session-ID: session-uuid-here" \
|
| 499 |
+
-d '{
|
| 500 |
+
"prompt": "Continue our conversation",
|
| 501 |
+
"use_search": false
|
| 502 |
+
}'
|
| 503 |
+
```
|
| 504 |
+
|
| 505 |
+
### Error Responses
|
| 506 |
+
|
| 507 |
+
Both anonymous and authenticated requests use the same error format:
|
| 508 |
+
|
| 509 |
+
```json
|
| 510 |
+
{
|
| 511 |
+
"detail": "string - Error description",
|
| 512 |
+
"error_code": "string - Machine-readable error code",
|
| 513 |
+
"status_code": "number - HTTP status code"
|
| 514 |
+
}
|
| 515 |
+
```
|
| 516 |
+
|
| 517 |
+
#### Common Error Scenarios
|
| 518 |
+
|
| 519 |
+
**Invalid Request (400):**
|
| 520 |
+
```json
|
| 521 |
+
{
|
| 522 |
+
"detail": "prompt field is required",
|
| 523 |
+
"error_code": "MISSING_REQUIRED_FIELD",
|
| 524 |
+
"status_code": 400
|
| 525 |
+
}
|
| 526 |
+
```
|
| 527 |
+
|
| 528 |
+
**Server Error (500):**
|
| 529 |
+
```json
|
| 530 |
+
{
|
| 531 |
+
"detail": "Internal server error occurred",
|
| 532 |
+
"error_code": "INTERNAL_ERROR",
|
| 533 |
+
"status_code": 500
|
| 534 |
+
}
|
| 535 |
+
```
|
| 536 |
+
|
| 537 |
+
### Rate Limiting
|
| 538 |
+
|
| 539 |
+
- **Anonymous Users**: Standard rate limits apply
|
| 540 |
+
- **Authenticated Users**: Same rate limits (no difference)
|
| 541 |
+
- **Rate Limit Headers**: Included in all responses
|
| 542 |
+
- `X-RateLimit-Limit`: Requests per time window
|
| 543 |
+
- `X-RateLimit-Remaining`: Remaining requests
|
| 544 |
+
- `X-RateLimit-Reset`: Time when limit resets
|
| 545 |
+
|
| 546 |
+
## Testing Your Integration
|
| 547 |
+
|
| 548 |
+
### Quick Test Commands
|
| 549 |
+
|
| 550 |
+
**Test Anonymous Mode:**
|
| 551 |
+
```bash
|
| 552 |
+
# Basic anonymous request
|
| 553 |
+
curl -X POST https://your-atlas-api.com/chat \
|
| 554 |
+
-H "Content-Type: application/json" \
|
| 555 |
+
-d '{"prompt": "Hello, how are you?"}'
|
| 556 |
+
|
| 557 |
+
# Anonymous with search disabled
|
| 558 |
+
curl -X POST https://your-atlas-api.com/chat \
|
| 559 |
+
-H "Content-Type: application/json" \
|
| 560 |
+
-d '{"prompt": "What is 2+2?", "use_search": false}'
|
| 561 |
+
```
|
| 562 |
+
|
| 563 |
+
**Test Authenticated Mode:**
|
| 564 |
+
```bash
|
| 565 |
+
# Basic authenticated request
|
| 566 |
+
curl -X POST https://your-atlas-api.com/chat \
|
| 567 |
+
-H "Content-Type: application/json" \
|
| 568 |
+
-d '{"prompt": "Hello!", "user_id": "test-user-123"}'
|
| 569 |
+
```
|
| 570 |
+
|
| 571 |
+
### Integration Checklist
|
| 572 |
+
|
| 573 |
+
- [ ] Anonymous requests work without user_id
|
| 574 |
+
- [ ] Authenticated requests work with user_id
|
| 575 |
+
- [ ] Error handling works for both modes
|
| 576 |
+
- [ ] Session continuity works (with X-Session-ID header)
|
| 577 |
+
- [ ] Search functionality works in both modes
|
| 578 |
+
- [ ] Response format is consistent
|
| 579 |
+
- [ ] Rate limiting is properly handled
|
tests/README_anonymous_mode_tests.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Anonymous Mode Tests
|
| 2 |
+
|
| 3 |
+
This document describes the tests for anonymous mode functionality in the Atlas API.
|
| 4 |
+
|
| 5 |
+
## Test Files
|
| 6 |
+
|
| 7 |
+
### 1. `test_anonymous_mode.py`
|
| 8 |
+
Comprehensive test suite for anonymous mode functionality using pytest framework.
|
| 9 |
+
|
| 10 |
+
**Features tested:**
|
| 11 |
+
- Anonymous request processing (Requirements 1.1, 1.2)
|
| 12 |
+
- Analytics counting for anonymous vs authenticated usage (Requirements 3.1, 3.3)
|
| 13 |
+
- Database operations with null user_id values
|
| 14 |
+
- Dashboard metrics display
|
| 15 |
+
- Integration scenarios
|
| 16 |
+
|
| 17 |
+
**Run with pytest:**
|
| 18 |
+
```bash
|
| 19 |
+
python -m pytest tests/test_anonymous_mode.py -v
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
### 2. `test_anonymous_examples.py`
|
| 23 |
+
Simple test examples that can be run without pytest installation.
|
| 24 |
+
|
| 25 |
+
**Features tested:**
|
| 26 |
+
- User ID validation and normalization
|
| 27 |
+
- Anonymous API requests (no user_id, null user_id, empty user_id)
|
| 28 |
+
- Analytics counting functions
|
| 29 |
+
- Dashboard endpoints
|
| 30 |
+
- Session and message tracking
|
| 31 |
+
|
| 32 |
+
**Run directly:**
|
| 33 |
+
```bash
|
| 34 |
+
python test_anonymous_examples.py
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
### 3. `test_user_id_validation.py`
|
| 38 |
+
Existing validation tests that verify user_id handling across all models.
|
| 39 |
+
|
| 40 |
+
**Run directly:**
|
| 41 |
+
```bash
|
| 42 |
+
python tests/test_user_id_validation.py
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
## Requirements Coverage
|
| 46 |
+
|
| 47 |
+
The tests verify the following requirements from the anonymous mode specification:
|
| 48 |
+
|
| 49 |
+
### Requirement 1.1
|
| 50 |
+
**Anonymous requests processed with full functionality**
|
| 51 |
+
- ✅ API endpoints accept requests without user_id
|
| 52 |
+
- ✅ API endpoints handle null user_id values
|
| 53 |
+
- ✅ API endpoints handle empty string user_id values
|
| 54 |
+
- ✅ Response quality is identical for anonymous and authenticated users
|
| 55 |
+
|
| 56 |
+
### Requirement 1.2
|
| 57 |
+
**System treats null/empty user_id as anonymous**
|
| 58 |
+
- ✅ `normalize_user_id()` converts empty strings to None
|
| 59 |
+
- ✅ `validate_user_id()` accepts None for anonymous users
|
| 60 |
+
- ✅ Database operations store None for anonymous users
|
| 61 |
+
|
| 62 |
+
### Requirement 3.1
|
| 63 |
+
**Anonymous requests handled efficiently**
|
| 64 |
+
- ✅ Database queries work with null user_id values
|
| 65 |
+
- ✅ Analytics aggregation handles null values correctly
|
| 66 |
+
- ✅ No performance degradation for anonymous requests
|
| 67 |
+
|
| 68 |
+
### Requirement 3.3
|
| 69 |
+
**Anonymous usage aggregated without individual tracking**
|
| 70 |
+
- ✅ Analytics count anonymous vs authenticated sessions/messages
|
| 71 |
+
- ✅ Dashboard displays anonymous usage metrics
|
| 72 |
+
- ✅ No individual user tracking for anonymous requests
|
| 73 |
+
|
| 74 |
+
## Test Categories
|
| 75 |
+
|
| 76 |
+
### 1. Unit Tests
|
| 77 |
+
- User ID validation functions
|
| 78 |
+
- Normalization functions
|
| 79 |
+
- Model validation
|
| 80 |
+
|
| 81 |
+
### 2. API Tests
|
| 82 |
+
- Chat endpoint with various user_id values
|
| 83 |
+
- Analytics endpoints
|
| 84 |
+
- Dashboard endpoints
|
| 85 |
+
|
| 86 |
+
### 3. Database Tests
|
| 87 |
+
- Query operations with null user_id
|
| 88 |
+
- Aggregation operations
|
| 89 |
+
- Consistency checks
|
| 90 |
+
|
| 91 |
+
### 4. Integration Tests
|
| 92 |
+
- Complete anonymous user workflows
|
| 93 |
+
- Mixed anonymous/authenticated scenarios
|
| 94 |
+
- End-to-end functionality
|
| 95 |
+
|
| 96 |
+
## Running All Tests
|
| 97 |
+
|
| 98 |
+
To run all anonymous mode tests:
|
| 99 |
+
|
| 100 |
+
```bash
|
| 101 |
+
# Run simple examples (no dependencies)
|
| 102 |
+
python test_anonymous_examples.py
|
| 103 |
+
|
| 104 |
+
# Run validation tests
|
| 105 |
+
python tests/test_user_id_validation.py
|
| 106 |
+
|
| 107 |
+
# Run comprehensive tests (requires pytest)
|
| 108 |
+
python -m pytest tests/test_anonymous_mode.py -v
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
## Expected Output
|
| 112 |
+
|
| 113 |
+
All tests should pass with output similar to:
|
| 114 |
+
```
|
| 115 |
+
🎉 ALL ANONYMOUS MODE TESTS COMPLETED SUCCESSFULLY!
|
| 116 |
+
|
| 117 |
+
Summary of tested functionality:
|
| 118 |
+
✅ User ID validation and normalization
|
| 119 |
+
✅ Anonymous API requests (no user_id, null user_id, empty user_id)
|
| 120 |
+
✅ Analytics counting functions for anonymous vs authenticated usage
|
| 121 |
+
✅ Dashboard endpoints displaying anonymous metrics
|
| 122 |
+
✅ Session and message tracking for anonymous users
|
| 123 |
+
|
| 124 |
+
Requirements verified:
|
| 125 |
+
✅ 1.1: Anonymous requests processed with full functionality
|
| 126 |
+
✅ 1.2: System treats null/empty user_id as anonymous
|
| 127 |
+
✅ 3.1: Anonymous requests handled efficiently
|
| 128 |
+
✅ 3.3: Anonymous usage aggregated without individual tracking
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
## Notes
|
| 132 |
+
|
| 133 |
+
- Some tests may show "Event loop is closed" errors when running with TestClient. This is expected and doesn't affect functionality.
|
| 134 |
+
- Database-related tests will show warnings if MongoDB is not available, but will still verify the code logic.
|
| 135 |
+
- All tests are designed to work with or without external dependencies.
|
tests/test_anonymous_mode.py
ADDED
|
@@ -0,0 +1,583 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Comprehensive tests for anonymous mode functionality
|
| 4 |
+
|
| 5 |
+
This test file verifies that the system properly handles anonymous users
|
| 6 |
+
(null user_id values) across all components including API requests,
|
| 7 |
+
analytics, database operations, and dashboard metrics.
|
| 8 |
+
|
| 9 |
+
Requirements tested:
|
| 10 |
+
- 1.1: Anonymous requests processed with full functionality
|
| 11 |
+
- 1.2: System treats null/empty user_id as anonymous
|
| 12 |
+
- 3.1: Anonymous requests handled efficiently
|
| 13 |
+
- 3.3: Anonymous usage aggregated without individual tracking
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import sys
|
| 17 |
+
import os
|
| 18 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 19 |
+
|
| 20 |
+
# Load environment variables
|
| 21 |
+
try:
|
| 22 |
+
from dotenv import load_dotenv
|
| 23 |
+
load_dotenv()
|
| 24 |
+
except ImportError:
|
| 25 |
+
pass # dotenv not available, continue without it
|
| 26 |
+
|
| 27 |
+
import pytest
|
| 28 |
+
import asyncio
|
| 29 |
+
from fastapi.testclient import TestClient
|
| 30 |
+
from unittest.mock import patch, AsyncMock
|
| 31 |
+
from datetime import datetime, timedelta
|
| 32 |
+
|
| 33 |
+
# Import the main app and functions to test
|
| 34 |
+
from app import app, validate_user_id, normalize_user_id
|
| 35 |
+
from analytics.collectors import create_session, track_message
|
| 36 |
+
from analytics.dashboard import (
|
| 37 |
+
count_anonymous_sessions,
|
| 38 |
+
count_authenticated_sessions,
|
| 39 |
+
count_anonymous_messages,
|
| 40 |
+
count_authenticated_messages,
|
| 41 |
+
count_all_sessions,
|
| 42 |
+
count_all_messages,
|
| 43 |
+
get_basic_stats,
|
| 44 |
+
get_user_statistics,
|
| 45 |
+
get_authenticated_vs_anonymous_metrics
|
| 46 |
+
)
|
| 47 |
+
from analytics.database import get_sessions_collection, get_messages_collection
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class TestAnonymousRequestProcessing:
|
| 51 |
+
"""Test that null user_id requests work correctly (Requirements 1.1, 1.2)"""
|
| 52 |
+
|
| 53 |
+
def test_normalize_user_id_function(self):
|
| 54 |
+
"""Test normalize_user_id function handles various inputs correctly"""
|
| 55 |
+
# Test None input
|
| 56 |
+
assert normalize_user_id(None) is None
|
| 57 |
+
|
| 58 |
+
# Test empty string
|
| 59 |
+
assert normalize_user_id("") is None
|
| 60 |
+
|
| 61 |
+
# Test whitespace-only strings
|
| 62 |
+
assert normalize_user_id(" ") is None
|
| 63 |
+
assert normalize_user_id("\t") is None
|
| 64 |
+
assert normalize_user_id("\n") is None
|
| 65 |
+
assert normalize_user_id(" \t\n ") is None
|
| 66 |
+
|
| 67 |
+
# Test valid user_id
|
| 68 |
+
assert normalize_user_id("user123") == "user123"
|
| 69 |
+
assert normalize_user_id(" user123 ") == "user123"
|
| 70 |
+
|
| 71 |
+
def test_validate_user_id_function(self):
|
| 72 |
+
"""Test validate_user_id function properly handles anonymous users"""
|
| 73 |
+
# Test None input (anonymous)
|
| 74 |
+
assert validate_user_id(None) is None
|
| 75 |
+
|
| 76 |
+
# Test empty string (anonymous)
|
| 77 |
+
assert validate_user_id("") is None
|
| 78 |
+
assert validate_user_id(" ") is None
|
| 79 |
+
|
| 80 |
+
# Test valid user_id
|
| 81 |
+
assert validate_user_id("user123") == "user123"
|
| 82 |
+
assert validate_user_id("user_123") == "user_123"
|
| 83 |
+
assert validate_user_id("user-123") == "user-123"
|
| 84 |
+
|
| 85 |
+
# Test invalid user_id raises exception
|
| 86 |
+
with pytest.raises(Exception):
|
| 87 |
+
validate_user_id("user@123")
|
| 88 |
+
|
| 89 |
+
with pytest.raises(Exception):
|
| 90 |
+
validate_user_id("a" * 256) # Too long
|
| 91 |
+
|
| 92 |
+
@patch('app.run_gemini_inference')
|
| 93 |
+
@patch('app.search_web_combined')
|
| 94 |
+
def test_anonymous_chat_request_api(self, mock_search, mock_gemini):
|
| 95 |
+
"""Test that chat API works with anonymous requests"""
|
| 96 |
+
# Mock the external dependencies
|
| 97 |
+
mock_search.return_value = []
|
| 98 |
+
mock_gemini.return_value = "Test response"
|
| 99 |
+
|
| 100 |
+
client = TestClient(app)
|
| 101 |
+
|
| 102 |
+
# Test request without user_id field
|
| 103 |
+
response = client.post("/chat", json={
|
| 104 |
+
"prompt": "Hello, how are you?",
|
| 105 |
+
"use_search": False
|
| 106 |
+
})
|
| 107 |
+
|
| 108 |
+
assert response.status_code == 200
|
| 109 |
+
data = response.json()
|
| 110 |
+
assert "response" in data
|
| 111 |
+
assert data["response"] == "Test response"
|
| 112 |
+
|
| 113 |
+
@patch('app.run_gemini_inference')
|
| 114 |
+
@patch('app.search_web_combined')
|
| 115 |
+
def test_anonymous_chat_request_with_null_user_id(self, mock_search, mock_gemini):
|
| 116 |
+
"""Test that chat API works with explicit null user_id"""
|
| 117 |
+
# Mock the external dependencies
|
| 118 |
+
mock_search.return_value = []
|
| 119 |
+
mock_gemini.return_value = "Test response"
|
| 120 |
+
|
| 121 |
+
client = TestClient(app)
|
| 122 |
+
|
| 123 |
+
# Test request with explicit null user_id
|
| 124 |
+
response = client.post("/chat", json={
|
| 125 |
+
"prompt": "Hello, how are you?",
|
| 126 |
+
"user_id": None,
|
| 127 |
+
"use_search": False
|
| 128 |
+
})
|
| 129 |
+
|
| 130 |
+
assert response.status_code == 200
|
| 131 |
+
data = response.json()
|
| 132 |
+
assert "response" in data
|
| 133 |
+
assert data["response"] == "Test response"
|
| 134 |
+
|
| 135 |
+
@patch('app.run_gemini_inference')
|
| 136 |
+
@patch('app.search_web_combined')
|
| 137 |
+
def test_anonymous_chat_request_with_empty_user_id(self, mock_search, mock_gemini):
|
| 138 |
+
"""Test that chat API works with empty string user_id"""
|
| 139 |
+
# Mock the external dependencies
|
| 140 |
+
mock_search.return_value = []
|
| 141 |
+
mock_gemini.return_value = "Test response"
|
| 142 |
+
|
| 143 |
+
client = TestClient(app)
|
| 144 |
+
|
| 145 |
+
# Test request with empty string user_id
|
| 146 |
+
response = client.post("/chat", json={
|
| 147 |
+
"prompt": "Hello, how are you?",
|
| 148 |
+
"user_id": "",
|
| 149 |
+
"use_search": False
|
| 150 |
+
})
|
| 151 |
+
|
| 152 |
+
assert response.status_code == 200
|
| 153 |
+
data = response.json()
|
| 154 |
+
assert "response" in data
|
| 155 |
+
assert data["response"] == "Test response"
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
class TestAnonymousAnalytics:
|
| 159 |
+
"""Test that analytics properly count anonymous vs authenticated usage (Requirements 3.1, 3.3)"""
|
| 160 |
+
|
| 161 |
+
@pytest.mark.asyncio
|
| 162 |
+
async def test_count_anonymous_sessions(self):
|
| 163 |
+
"""Test counting anonymous sessions"""
|
| 164 |
+
count = await count_anonymous_sessions()
|
| 165 |
+
assert isinstance(count, int)
|
| 166 |
+
assert count >= 0
|
| 167 |
+
|
| 168 |
+
@pytest.mark.asyncio
|
| 169 |
+
async def test_count_authenticated_sessions(self):
|
| 170 |
+
"""Test counting authenticated sessions"""
|
| 171 |
+
count = await count_authenticated_sessions()
|
| 172 |
+
assert isinstance(count, int)
|
| 173 |
+
assert count >= 0
|
| 174 |
+
|
| 175 |
+
@pytest.mark.asyncio
|
| 176 |
+
async def test_count_anonymous_messages(self):
|
| 177 |
+
"""Test counting anonymous messages"""
|
| 178 |
+
count = await count_anonymous_messages()
|
| 179 |
+
assert isinstance(count, int)
|
| 180 |
+
assert count >= 0
|
| 181 |
+
|
| 182 |
+
@pytest.mark.asyncio
|
| 183 |
+
async def test_count_authenticated_messages(self):
|
| 184 |
+
"""Test counting authenticated messages"""
|
| 185 |
+
count = await count_authenticated_messages()
|
| 186 |
+
assert isinstance(count, int)
|
| 187 |
+
assert count >= 0
|
| 188 |
+
|
| 189 |
+
@pytest.mark.asyncio
|
| 190 |
+
async def test_total_counts_consistency(self):
|
| 191 |
+
"""Test that anonymous + authenticated = total counts"""
|
| 192 |
+
# Get all counts
|
| 193 |
+
total_sessions = await count_all_sessions()
|
| 194 |
+
anonymous_sessions = await count_anonymous_sessions()
|
| 195 |
+
authenticated_sessions = await count_authenticated_sessions()
|
| 196 |
+
|
| 197 |
+
total_messages = await count_all_messages()
|
| 198 |
+
anonymous_messages = await count_anonymous_messages()
|
| 199 |
+
authenticated_messages = await count_authenticated_messages()
|
| 200 |
+
|
| 201 |
+
# Verify consistency
|
| 202 |
+
assert total_sessions == anonymous_sessions + authenticated_sessions
|
| 203 |
+
assert total_messages == anonymous_messages + authenticated_messages
|
| 204 |
+
|
| 205 |
+
@pytest.mark.asyncio
|
| 206 |
+
async def test_create_anonymous_session(self):
|
| 207 |
+
"""Test creating a session with null user_id"""
|
| 208 |
+
# Create anonymous session
|
| 209 |
+
session = await create_session(user_agent="TestAgent", user_id=None)
|
| 210 |
+
|
| 211 |
+
assert session is not None
|
| 212 |
+
assert session.user_id is None
|
| 213 |
+
assert session.session_id is not None
|
| 214 |
+
assert session.user_agent == "TestAgent"
|
| 215 |
+
|
| 216 |
+
@pytest.mark.asyncio
|
| 217 |
+
async def test_create_authenticated_session(self):
|
| 218 |
+
"""Test creating a session with valid user_id"""
|
| 219 |
+
# Create authenticated session
|
| 220 |
+
session = await create_session(user_agent="TestAgent", user_id="test_user_123")
|
| 221 |
+
|
| 222 |
+
assert session is not None
|
| 223 |
+
assert session.user_id == "test_user_123"
|
| 224 |
+
assert session.session_id is not None
|
| 225 |
+
assert session.user_agent == "TestAgent"
|
| 226 |
+
|
| 227 |
+
@pytest.mark.asyncio
|
| 228 |
+
async def test_track_anonymous_message(self):
|
| 229 |
+
"""Test tracking a message with null user_id"""
|
| 230 |
+
# Create anonymous session first
|
| 231 |
+
session = await create_session(user_agent="TestAgent", user_id=None)
|
| 232 |
+
|
| 233 |
+
# Track anonymous message
|
| 234 |
+
message = await track_message(
|
| 235 |
+
session_id=session.session_id,
|
| 236 |
+
prompt_length=50,
|
| 237 |
+
response_length=200,
|
| 238 |
+
response_time_ms=1500,
|
| 239 |
+
used_search=True,
|
| 240 |
+
user_id=None
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
assert message is not None
|
| 244 |
+
assert message.user_id is None
|
| 245 |
+
assert message.session_id == session.session_id
|
| 246 |
+
assert message.prompt_length == 50
|
| 247 |
+
assert message.response_length == 200
|
| 248 |
+
assert message.response_time_ms == 1500
|
| 249 |
+
assert message.used_search is True
|
| 250 |
+
|
| 251 |
+
@pytest.mark.asyncio
|
| 252 |
+
async def test_track_authenticated_message(self):
|
| 253 |
+
"""Test tracking a message with valid user_id"""
|
| 254 |
+
# Create authenticated session first
|
| 255 |
+
session = await create_session(user_agent="TestAgent", user_id="test_user_123")
|
| 256 |
+
|
| 257 |
+
# Track authenticated message
|
| 258 |
+
message = await track_message(
|
| 259 |
+
session_id=session.session_id,
|
| 260 |
+
prompt_length=30,
|
| 261 |
+
response_length=150,
|
| 262 |
+
response_time_ms=1200,
|
| 263 |
+
used_search=False,
|
| 264 |
+
user_id="test_user_123"
|
| 265 |
+
)
|
| 266 |
+
|
| 267 |
+
assert message is not None
|
| 268 |
+
assert message.user_id == "test_user_123"
|
| 269 |
+
assert message.session_id == session.session_id
|
| 270 |
+
assert message.prompt_length == 30
|
| 271 |
+
assert message.response_length == 150
|
| 272 |
+
assert message.response_time_ms == 1200
|
| 273 |
+
assert message.used_search is False
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
class TestAnonymousDatabaseOperations:
|
| 277 |
+
"""Test that database operations handle null user_id values (Requirements 3.1, 3.3)"""
|
| 278 |
+
|
| 279 |
+
@pytest.mark.asyncio
|
| 280 |
+
async def test_database_query_with_null_user_id(self):
|
| 281 |
+
"""Test database queries work with null user_id values"""
|
| 282 |
+
sessions_collection = await get_sessions_collection()
|
| 283 |
+
messages_collection = await get_messages_collection()
|
| 284 |
+
|
| 285 |
+
if sessions_collection is None or messages_collection is None:
|
| 286 |
+
pytest.skip("Database not available")
|
| 287 |
+
|
| 288 |
+
# Query for anonymous sessions
|
| 289 |
+
anonymous_sessions = await sessions_collection.find({
|
| 290 |
+
"$or": [
|
| 291 |
+
{"user_id": None},
|
| 292 |
+
{"user_id": {"$exists": False}}
|
| 293 |
+
]
|
| 294 |
+
}).to_list(length=10)
|
| 295 |
+
|
| 296 |
+
# Should return a list (even if empty)
|
| 297 |
+
assert isinstance(anonymous_sessions, list)
|
| 298 |
+
|
| 299 |
+
# Query for authenticated sessions
|
| 300 |
+
authenticated_sessions = await sessions_collection.find({
|
| 301 |
+
"user_id": {"$ne": None, "$exists": True}
|
| 302 |
+
}).to_list(length=10)
|
| 303 |
+
|
| 304 |
+
# Should return a list (even if empty)
|
| 305 |
+
assert isinstance(authenticated_sessions, list)
|
| 306 |
+
|
| 307 |
+
@pytest.mark.asyncio
|
| 308 |
+
async def test_database_aggregation_with_null_user_id(self):
|
| 309 |
+
"""Test database aggregation works with null user_id values"""
|
| 310 |
+
messages_collection = await get_messages_collection()
|
| 311 |
+
|
| 312 |
+
if messages_collection is None:
|
| 313 |
+
pytest.skip("Database not available")
|
| 314 |
+
|
| 315 |
+
# Aggregate messages by user_id (including null)
|
| 316 |
+
pipeline = [
|
| 317 |
+
{
|
| 318 |
+
"$group": {
|
| 319 |
+
"_id": "$user_id",
|
| 320 |
+
"count": {"$sum": 1},
|
| 321 |
+
"avg_response_time": {"$avg": "$response_time_ms"}
|
| 322 |
+
}
|
| 323 |
+
}
|
| 324 |
+
]
|
| 325 |
+
|
| 326 |
+
results = await messages_collection.aggregate(pipeline).to_list(length=100)
|
| 327 |
+
|
| 328 |
+
# Should return a list of aggregation results
|
| 329 |
+
assert isinstance(results, list)
|
| 330 |
+
|
| 331 |
+
# Check if we have anonymous users (user_id = None)
|
| 332 |
+
anonymous_result = next((r for r in results if r["_id"] is None), None)
|
| 333 |
+
if anonymous_result:
|
| 334 |
+
assert "count" in anonymous_result
|
| 335 |
+
assert "avg_response_time" in anonymous_result
|
| 336 |
+
assert anonymous_result["count"] > 0
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
class TestAnonymousDashboardMetrics:
|
| 340 |
+
"""Test that dashboard displays anonymous metrics correctly (Requirements 3.3)"""
|
| 341 |
+
|
| 342 |
+
@pytest.mark.asyncio
|
| 343 |
+
async def test_get_basic_stats_includes_anonymous_metrics(self):
|
| 344 |
+
"""Test that basic stats include anonymous usage metrics"""
|
| 345 |
+
stats = await get_basic_stats()
|
| 346 |
+
|
| 347 |
+
# Should return a dictionary
|
| 348 |
+
assert isinstance(stats, dict)
|
| 349 |
+
|
| 350 |
+
# Should include basic metrics
|
| 351 |
+
expected_keys = [
|
| 352 |
+
"total_sessions", "total_messages", "active_sessions",
|
| 353 |
+
"messages_today", "search_usage_percentage", "average_response_time_ms"
|
| 354 |
+
]
|
| 355 |
+
|
| 356 |
+
for key in expected_keys:
|
| 357 |
+
assert key in stats, f"Missing key: {key}"
|
| 358 |
+
assert isinstance(stats[key], (int, float)), f"Invalid type for {key}"
|
| 359 |
+
assert stats[key] >= 0, f"Negative value for {key}"
|
| 360 |
+
|
| 361 |
+
@pytest.mark.asyncio
|
| 362 |
+
async def test_get_user_statistics_includes_anonymous_breakdown(self):
|
| 363 |
+
"""Test that user statistics include anonymous vs authenticated breakdown"""
|
| 364 |
+
try:
|
| 365 |
+
stats = await get_user_statistics()
|
| 366 |
+
|
| 367 |
+
# Should return a dictionary
|
| 368 |
+
assert isinstance(stats, dict)
|
| 369 |
+
|
| 370 |
+
# Should include anonymous vs authenticated breakdown
|
| 371 |
+
expected_keys = [
|
| 372 |
+
"total_sessions", "authenticated_sessions", "anonymous_sessions",
|
| 373 |
+
"authenticated_session_percentage", "total_messages",
|
| 374 |
+
"authenticated_messages", "anonymous_messages",
|
| 375 |
+
"authenticated_message_percentage", "unique_authenticated_users"
|
| 376 |
+
]
|
| 377 |
+
|
| 378 |
+
for key in expected_keys:
|
| 379 |
+
assert key in stats, f"Missing key: {key}"
|
| 380 |
+
assert isinstance(stats[key], (int, float)), f"Invalid type for {key}"
|
| 381 |
+
assert stats[key] >= 0, f"Negative value for {key}"
|
| 382 |
+
|
| 383 |
+
# Verify percentages are valid
|
| 384 |
+
assert 0 <= stats["authenticated_session_percentage"] <= 100
|
| 385 |
+
assert 0 <= stats["authenticated_message_percentage"] <= 100
|
| 386 |
+
|
| 387 |
+
# Verify totals are consistent
|
| 388 |
+
assert stats["total_sessions"] == stats["authenticated_sessions"] + stats["anonymous_sessions"]
|
| 389 |
+
assert stats["total_messages"] == stats["authenticated_messages"] + stats["anonymous_messages"]
|
| 390 |
+
|
| 391 |
+
except ImportError:
|
| 392 |
+
pytest.skip("get_user_statistics function not available")
|
| 393 |
+
|
| 394 |
+
@pytest.mark.asyncio
|
| 395 |
+
async def test_get_authenticated_vs_anonymous_metrics(self):
|
| 396 |
+
"""Test authenticated vs anonymous comparison metrics"""
|
| 397 |
+
try:
|
| 398 |
+
metrics = await get_authenticated_vs_anonymous_metrics()
|
| 399 |
+
|
| 400 |
+
# Should return a dictionary
|
| 401 |
+
assert isinstance(metrics, dict)
|
| 402 |
+
|
| 403 |
+
# Should include authenticated and anonymous sections
|
| 404 |
+
assert "authenticated" in metrics
|
| 405 |
+
assert "anonymous" in metrics
|
| 406 |
+
assert "comparison" in metrics
|
| 407 |
+
|
| 408 |
+
# Check authenticated metrics structure
|
| 409 |
+
auth_metrics = metrics["authenticated"]
|
| 410 |
+
expected_auth_keys = [
|
| 411 |
+
"sessions", "messages", "avg_messages_per_session",
|
| 412 |
+
"avg_response_time_ms", "search_usage_percentage",
|
| 413 |
+
"success_rate_percentage"
|
| 414 |
+
]
|
| 415 |
+
|
| 416 |
+
for key in expected_auth_keys:
|
| 417 |
+
assert key in auth_metrics, f"Missing authenticated key: {key}"
|
| 418 |
+
assert isinstance(auth_metrics[key], (int, float)), f"Invalid type for authenticated {key}"
|
| 419 |
+
assert auth_metrics[key] >= 0, f"Negative value for authenticated {key}"
|
| 420 |
+
|
| 421 |
+
# Check anonymous metrics structure
|
| 422 |
+
anon_metrics = metrics["anonymous"]
|
| 423 |
+
expected_anon_keys = [
|
| 424 |
+
"sessions", "messages", "avg_messages_per_session",
|
| 425 |
+
"avg_response_time_ms", "search_usage_percentage",
|
| 426 |
+
"success_rate_percentage"
|
| 427 |
+
]
|
| 428 |
+
|
| 429 |
+
for key in expected_anon_keys:
|
| 430 |
+
assert key in anon_metrics, f"Missing anonymous key: {key}"
|
| 431 |
+
assert isinstance(anon_metrics[key], (int, float)), f"Invalid type for anonymous {key}"
|
| 432 |
+
assert anon_metrics[key] >= 0, f"Negative value for anonymous {key}"
|
| 433 |
+
|
| 434 |
+
# Check comparison metrics
|
| 435 |
+
comparison = metrics["comparison"]
|
| 436 |
+
assert "total_sessions" in comparison
|
| 437 |
+
assert "total_messages" in comparison
|
| 438 |
+
assert "authenticated_percentage" in comparison
|
| 439 |
+
|
| 440 |
+
# Verify totals are consistent
|
| 441 |
+
assert comparison["total_sessions"] == auth_metrics["sessions"] + anon_metrics["sessions"]
|
| 442 |
+
assert comparison["total_messages"] == auth_metrics["messages"] + anon_metrics["messages"]
|
| 443 |
+
|
| 444 |
+
except ImportError:
|
| 445 |
+
pytest.skip("get_authenticated_vs_anonymous_metrics function not available")
|
| 446 |
+
|
| 447 |
+
def test_analytics_dashboard_endpoint(self):
|
| 448 |
+
"""Test that analytics dashboard endpoint works and includes anonymous metrics"""
|
| 449 |
+
client = TestClient(app)
|
| 450 |
+
|
| 451 |
+
response = client.get("/analytics/dashboard")
|
| 452 |
+
|
| 453 |
+
# Should return 200 OK
|
| 454 |
+
assert response.status_code == 200
|
| 455 |
+
|
| 456 |
+
# Should return HTML content
|
| 457 |
+
assert "text/html" in response.headers.get("content-type", "")
|
| 458 |
+
|
| 459 |
+
# Should include anonymous-related content in HTML
|
| 460 |
+
html_content = response.text.lower()
|
| 461 |
+
assert "anonymous" in html_content
|
| 462 |
+
assert "authenticated" in html_content
|
| 463 |
+
|
| 464 |
+
def test_analytics_stats_endpoint(self):
|
| 465 |
+
"""Test that analytics stats endpoint works"""
|
| 466 |
+
client = TestClient(app)
|
| 467 |
+
|
| 468 |
+
response = client.get("/analytics/stats")
|
| 469 |
+
|
| 470 |
+
# Should return 200 OK
|
| 471 |
+
assert response.status_code == 200
|
| 472 |
+
|
| 473 |
+
# Should return JSON
|
| 474 |
+
assert response.headers.get("content-type") == "application/json"
|
| 475 |
+
|
| 476 |
+
# Should include basic stats
|
| 477 |
+
data = response.json()
|
| 478 |
+
assert isinstance(data, dict)
|
| 479 |
+
|
| 480 |
+
|
| 481 |
+
class TestAnonymousIntegrationScenarios:
|
| 482 |
+
"""Integration tests for complete anonymous user workflows"""
|
| 483 |
+
|
| 484 |
+
@pytest.mark.asyncio
|
| 485 |
+
async def test_complete_anonymous_user_workflow(self):
|
| 486 |
+
"""Test complete workflow: anonymous session creation -> message tracking -> analytics"""
|
| 487 |
+
# Step 1: Create anonymous session
|
| 488 |
+
session = await create_session(user_agent="TestAgent/1.0", user_id=None)
|
| 489 |
+
assert session is not None
|
| 490 |
+
assert session.user_id is None
|
| 491 |
+
|
| 492 |
+
# Step 2: Track multiple anonymous messages
|
| 493 |
+
message1 = await track_message(
|
| 494 |
+
session_id=session.session_id,
|
| 495 |
+
prompt_length=25,
|
| 496 |
+
response_length=100,
|
| 497 |
+
response_time_ms=800,
|
| 498 |
+
used_search=False,
|
| 499 |
+
user_id=None
|
| 500 |
+
)
|
| 501 |
+
|
| 502 |
+
message2 = await track_message(
|
| 503 |
+
session_id=session.session_id,
|
| 504 |
+
prompt_length=40,
|
| 505 |
+
response_length=180,
|
| 506 |
+
response_time_ms=1200,
|
| 507 |
+
used_search=True,
|
| 508 |
+
user_id=None
|
| 509 |
+
)
|
| 510 |
+
|
| 511 |
+
assert message1 is not None
|
| 512 |
+
assert message2 is not None
|
| 513 |
+
assert message1.user_id is None
|
| 514 |
+
assert message2.user_id is None
|
| 515 |
+
|
| 516 |
+
# Step 3: Verify analytics include these anonymous interactions
|
| 517 |
+
anonymous_sessions_count = await count_anonymous_sessions()
|
| 518 |
+
anonymous_messages_count = await count_anonymous_messages()
|
| 519 |
+
|
| 520 |
+
assert anonymous_sessions_count > 0
|
| 521 |
+
assert anonymous_messages_count > 0
|
| 522 |
+
|
| 523 |
+
@pytest.mark.asyncio
|
| 524 |
+
async def test_mixed_anonymous_authenticated_analytics(self):
|
| 525 |
+
"""Test analytics work correctly with mix of anonymous and authenticated users"""
|
| 526 |
+
# Create anonymous session and message
|
| 527 |
+
anon_session = await create_session(user_agent="TestAgent", user_id=None)
|
| 528 |
+
await track_message(
|
| 529 |
+
session_id=anon_session.session_id,
|
| 530 |
+
prompt_length=30,
|
| 531 |
+
response_length=120,
|
| 532 |
+
response_time_ms=1000,
|
| 533 |
+
used_search=False,
|
| 534 |
+
user_id=None
|
| 535 |
+
)
|
| 536 |
+
|
| 537 |
+
# Create authenticated session and message
|
| 538 |
+
auth_session = await create_session(user_agent="TestAgent", user_id="test_user_456")
|
| 539 |
+
await track_message(
|
| 540 |
+
session_id=auth_session.session_id,
|
| 541 |
+
prompt_length=35,
|
| 542 |
+
response_length=140,
|
| 543 |
+
response_time_ms=1100,
|
| 544 |
+
used_search=True,
|
| 545 |
+
user_id="test_user_456"
|
| 546 |
+
)
|
| 547 |
+
|
| 548 |
+
# Verify counts are accurate
|
| 549 |
+
total_sessions = await count_all_sessions()
|
| 550 |
+
anonymous_sessions = await count_anonymous_sessions()
|
| 551 |
+
authenticated_sessions = await count_authenticated_sessions()
|
| 552 |
+
|
| 553 |
+
total_messages = await count_all_messages()
|
| 554 |
+
anonymous_messages = await count_anonymous_messages()
|
| 555 |
+
authenticated_messages = await count_authenticated_messages()
|
| 556 |
+
|
| 557 |
+
# Verify consistency
|
| 558 |
+
assert total_sessions == anonymous_sessions + authenticated_sessions
|
| 559 |
+
assert total_messages == anonymous_messages + authenticated_messages
|
| 560 |
+
assert anonymous_sessions > 0
|
| 561 |
+
assert authenticated_sessions > 0
|
| 562 |
+
assert anonymous_messages > 0
|
| 563 |
+
assert authenticated_messages > 0
|
| 564 |
+
|
| 565 |
+
|
| 566 |
+
def run_anonymous_mode_tests():
|
| 567 |
+
"""Run all anonymous mode tests"""
|
| 568 |
+
print("🧪 Running Anonymous Mode Tests")
|
| 569 |
+
print("=" * 50)
|
| 570 |
+
|
| 571 |
+
# Test request processing
|
| 572 |
+
request_test = TestAnonymousRequestProcessing()
|
| 573 |
+
request_test.test_normalize_user_id_function()
|
| 574 |
+
request_test.test_validate_user_id_function()
|
| 575 |
+
print("✅ Anonymous request processing tests passed")
|
| 576 |
+
|
| 577 |
+
print("\n🎉 ANONYMOUS MODE TESTS COMPLETED!")
|
| 578 |
+
print("Note: Some async tests require pytest to run properly")
|
| 579 |
+
print("Run with: python -m pytest tests/test_anonymous_mode.py -v")
|
| 580 |
+
|
| 581 |
+
|
| 582 |
+
if __name__ == "__main__":
|
| 583 |
+
run_anonymous_mode_tests()
|