pmmdot commited on
Commit
6f26f80
·
1 Parent(s): 1701946

Add HTML assets and update dependencies

Browse files

- Create new index.html asset file
- Update requirements.txt with new dependencies
- Refactor main.py implementation

Files changed (3) hide show
  1. assets/index.html +333 -0
  2. requirements.txt +1 -0
  3. src/main.py +45 -36
assets/index.html ADDED
@@ -0,0 +1,333 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>AskBookie API</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
8
+ <style>
9
+ * {
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ body {
16
+ font-family: 'Inter', sans-serif;
17
+ background: #000;
18
+ color: #fafafa;
19
+ min-height: 100vh;
20
+ padding: 2rem;
21
+ }
22
+
23
+ .container {
24
+ max-width: 1200px;
25
+ margin: 0 auto;
26
+ }
27
+
28
+ .header {
29
+ margin-bottom: 2rem;
30
+ padding-bottom: 1rem;
31
+ border-bottom: 1px solid #27272a;
32
+ }
33
+
34
+ .header h1 {
35
+ font-size: 1.5rem;
36
+ font-weight: 600;
37
+ margin-bottom: 0.25rem;
38
+ }
39
+
40
+ .header p {
41
+ font-size: 0.875rem;
42
+ color: #71717a;
43
+ }
44
+
45
+ .status {
46
+ display: inline-block;
47
+ width: 8px;
48
+ height: 8px;
49
+ background: #22c55e;
50
+ border-radius: 50%;
51
+ margin-right: 0.5rem;
52
+ }
53
+
54
+ .grid {
55
+ display: grid;
56
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
57
+ gap: 1rem;
58
+ margin-bottom: 2rem;
59
+ }
60
+
61
+ .card {
62
+ background: #0a0a0a;
63
+ border: 1px solid #27272a;
64
+ border-radius: 8px;
65
+ padding: 1.25rem;
66
+ }
67
+
68
+ .card:hover {
69
+ border-color: #3f3f46;
70
+ }
71
+
72
+ .card-label {
73
+ font-size: 0.75rem;
74
+ color: #71717a;
75
+ text-transform: uppercase;
76
+ letter-spacing: 0.05em;
77
+ margin-bottom: 0.5rem;
78
+ }
79
+
80
+ .card-value {
81
+ font-size: 2rem;
82
+ font-weight: 600;
83
+ margin-bottom: 0.25rem;
84
+ }
85
+
86
+ .card-desc {
87
+ font-size: 0.875rem;
88
+ color: #a1a1aa;
89
+ }
90
+
91
+ .section {
92
+ background: #0a0a0a;
93
+ border: 1px solid #27272a;
94
+ border-radius: 8px;
95
+ padding: 1.5rem;
96
+ margin-bottom: 1rem;
97
+ }
98
+
99
+ .section-title {
100
+ font-size: 1rem;
101
+ font-weight: 600;
102
+ margin-bottom: 1rem;
103
+ }
104
+
105
+ .user-card {
106
+ background: #000;
107
+ border: 1px solid #27272a;
108
+ border-radius: 6px;
109
+ padding: 1rem;
110
+ margin-bottom: 0.75rem;
111
+ }
112
+
113
+ .user-card:hover {
114
+ border-color: #3f3f46;
115
+ }
116
+
117
+ .user-header {
118
+ display: flex;
119
+ align-items: center;
120
+ gap: 0.5rem;
121
+ margin-bottom: 1rem;
122
+ padding-bottom: 0.75rem;
123
+ border-bottom: 1px solid #27272a;
124
+ }
125
+
126
+ .user-name {
127
+ font-size: 0.875rem;
128
+ font-weight: 500;
129
+ }
130
+
131
+ .badge {
132
+ font-size: 0.625rem;
133
+ padding: 0.125rem 0.5rem;
134
+ background: #27272a;
135
+ border-radius: 4px;
136
+ text-transform: uppercase;
137
+ letter-spacing: 0.05em;
138
+ color: #a1a1aa;
139
+ }
140
+
141
+ .stats-grid {
142
+ display: grid;
143
+ grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
144
+ gap: 0.75rem;
145
+ }
146
+
147
+ .stat {
148
+ text-align: center;
149
+ }
150
+
151
+ .stat-value {
152
+ font-size: 1.25rem;
153
+ font-weight: 600;
154
+ margin-bottom: 0.125rem;
155
+ }
156
+
157
+ .stat-label {
158
+ font-size: 0.75rem;
159
+ color: #71717a;
160
+ }
161
+
162
+ .btn {
163
+ position: fixed;
164
+ bottom: 3rem;
165
+ right: 2rem;
166
+ background: #fafafa;
167
+ color: #000;
168
+ border: none;
169
+ padding: 0.75rem 1.5rem;
170
+ border-radius: 6px;
171
+ font-size: 0.875rem;
172
+ font-weight: 500;
173
+ cursor: pointer;
174
+ font-family: 'Inter', sans-serif;
175
+ }
176
+
177
+ .btn:hover {
178
+ background: #e5e5e5;
179
+ }
180
+
181
+ .btn:active {
182
+ background: #d4d4d4;
183
+ }
184
+
185
+ .footer {
186
+ text-align: center;
187
+ font-size: 0.75rem;
188
+ color: #52525b;
189
+ margin-top: 2rem;
190
+ }
191
+
192
+ @media (max-width: 768px) {
193
+ body {
194
+ padding: 1rem;
195
+ }
196
+ .grid {
197
+ grid-template-columns: 1fr;
198
+ }
199
+ }
200
+ </style>
201
+ </head>
202
+ <body>
203
+ <div class="container">
204
+ <div class="header">
205
+ <h1>AskBookie API</h1>
206
+ <p><span class="status"></span>Live Dashboard</p>
207
+ </div>
208
+
209
+ <div class="grid">
210
+ <div class="card">
211
+ <div class="card-label">Uptime</div>
212
+ <div class="card-value" id="uptime">--</div>
213
+ <div class="card-desc">hours</div>
214
+ </div>
215
+
216
+ <div class="card">
217
+ <div class="card-label">API Calls</div>
218
+ <div class="card-value" id="total-calls">--</div>
219
+ <div class="card-desc">total</div>
220
+ </div>
221
+
222
+ <div class="card">
223
+ <div class="card-label">Questions</div>
224
+ <div class="card-value" id="total-questions">--</div>
225
+ <div class="card-desc">asked</div>
226
+ </div>
227
+
228
+ <div class="card">
229
+ <div class="card-label">Active Jobs</div>
230
+ <div class="card-value" id="active-jobs">--</div>
231
+ <div class="card-desc">processing</div>
232
+ </div>
233
+
234
+ <div class="card">
235
+ <div class="card-label">Memory</div>
236
+ <div class="card-value" id="memory">--</div>
237
+ <div class="card-desc">MB</div>
238
+ </div>
239
+
240
+ <div class="card">
241
+ <div class="card-label">CPU</div>
242
+ <div class="card-value" id="cpu">--</div>
243
+ <div class="card-desc">percent</div>
244
+ </div>
245
+ </div>
246
+
247
+ <div class="section">
248
+ <div class="section-title">Per-User Analytics</div>
249
+ <div id="user-stats"></div>
250
+ </div>
251
+ </div>
252
+
253
+ <button class="btn" onclick="loadMetrics()">
254
+ <span id="refresh-text">Refresh</span>
255
+ </button>
256
+
257
+ <div class="footer" id="last-update"></div>
258
+
259
+ <script>
260
+ async function loadMetrics() {
261
+ const refreshText = document.getElementById('refresh-text');
262
+ const originalText = refreshText.textContent;
263
+ refreshText.textContent = 'Loading...';
264
+
265
+ try {
266
+ const response = await fetch('/health');
267
+ const health = await response.json();
268
+
269
+ document.getElementById('uptime').textContent = (health.uptime_hours || 0).toFixed(2);
270
+ document.getElementById('active-jobs').textContent = health.active_jobs || 0;
271
+ document.getElementById('total-calls').textContent = health.total_api_calls || 0;
272
+ document.getElementById('total-questions').textContent = health.total_questions || 0;
273
+ document.getElementById('memory').textContent = (health.memory_mb || 0).toFixed(0);
274
+ document.getElementById('cpu').textContent = (health.cpu_percent || 0).toFixed(1);
275
+
276
+ renderUserStats(health.per_user || {});
277
+
278
+ document.getElementById('last-update').textContent =
279
+ `Updated ${new Date().toLocaleTimeString()}`;
280
+
281
+ } catch (error) {
282
+ console.error('Failed to load metrics:', error);
283
+ } finally {
284
+ refreshText.textContent = originalText;
285
+ }
286
+ }
287
+
288
+ function renderUserStats(users) {
289
+ const container = document.getElementById('user-stats');
290
+ container.innerHTML = '';
291
+
292
+ for (const [userName, stats] of Object.entries(users)) {
293
+ const userCard = document.createElement('div');
294
+ userCard.className = 'user-card';
295
+
296
+ userCard.innerHTML = `
297
+ <div class="user-header">
298
+ <span class="user-name">${userName}</span>
299
+ <span class="badge">${stats.role || 'user'}</span>
300
+ </div>
301
+ <div class="stats-grid">
302
+ <div class="stat">
303
+ <div class="stat-value">${stats.api_calls || 0}</div>
304
+ <div class="stat-label">calls</div>
305
+ </div>
306
+ <div class="stat">
307
+ <div class="stat-value">${stats.questions_asked || 0}</div>
308
+ <div class="stat-label">questions</div>
309
+ </div>
310
+ <div class="stat">
311
+ <div class="stat-value">${stats.uploads_attempted || 0}</div>
312
+ <div class="stat-label">uploads</div>
313
+ </div>
314
+ <div class="stat">
315
+ <div class="stat-value">${stats.success_rate || 100}%</div>
316
+ <div class="stat-label">success</div>
317
+ </div>
318
+ <div class="stat">
319
+ <div class="stat-value">${stats.average_latency_seconds || 0}s</div>
320
+ <div class="stat-label">latency</div>
321
+ </div>
322
+ </div>
323
+ `;
324
+
325
+ container.appendChild(userCard);
326
+ }
327
+ }
328
+
329
+ setInterval(loadMetrics, 5000);
330
+ loadMetrics();
331
+ </script>
332
+ </body>
333
+ </html>
requirements.txt CHANGED
@@ -8,6 +8,7 @@ langchain-qdrant>=1.1.0
8
  psutil>=7.2.1
9
  pydantic>=2.12.5
10
  pypdf>=6.5.0
 
11
  python-multipart>=0.0.21
12
  qdrant-client>=1.16.2
13
  sentence-transformers>=5.2.0
 
8
  psutil>=7.2.1
9
  pydantic>=2.12.5
10
  pypdf>=6.5.0
11
+ python-dotenv>=1.0.0
12
  python-multipart>=0.0.21
13
  qdrant-client>=1.16.2
14
  sentence-transformers>=5.2.0
src/main.py CHANGED
@@ -1,7 +1,7 @@
1
  from fastapi import FastAPI, HTTPException, UploadFile, File, BackgroundTasks, Form, Depends, Request
2
  from fastapi.middleware.cors import CORSMiddleware
3
  from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
4
- from fastapi.responses import JSONResponse
5
  from pydantic import BaseModel, Field, field_validator
6
  from typing import Optional, List, Dict
7
  import os
@@ -19,6 +19,9 @@ import asyncio
19
  import hashlib
20
  from enum import Enum
21
  import logging
 
 
 
22
 
23
  from rag import RAGService, process_pdf
24
 
@@ -29,9 +32,9 @@ ALLOWED_ORIGINS = [
29
 
30
  API_KEYS = {
31
  os.getenv("ADMIN_API_KEY", "your-admin-secret-key"): {"role": "admin", "name": "admin"},
32
- os.getenv("USER_API_KEY_1", "your-user-secret-key-1"): {"role": "user", "name": "user_1"},
33
- os.getenv("USER_API_KEY_2", "your-user-secret-key-2"): {"role": "user", "name": "user_2"},
34
- os.getenv("USER_API_KEY_3", "your-user-secret-key-3"): {"role": "user", "name": "user_3"},
35
  }
36
 
37
  MAX_FILE_SIZE = 10 * 1024 * 1024
@@ -355,16 +358,51 @@ async def process_with_retry(
355
  except Exception as e:
356
  logger.error(f"Failed to delete temp file {file_path}: {e}")
357
 
358
- @app.get("/", tags=["Health"])
 
 
 
 
 
 
 
359
  async def health_check():
360
- """Health check with system status"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361
  return {
362
  "status": "healthy",
363
  "service": "AskBookie API",
364
  "version": "2.0.0",
365
  "rag_initialized": rag_service is not None,
366
  "active_jobs": job_manager.active_jobs,
367
- "timestamp": datetime.now().isoformat()
 
 
 
 
 
 
368
  }
369
 
370
  @app.post("/embed", tags=["Documents"], status_code=202)
@@ -521,35 +559,6 @@ async def list_jobs(
521
  "jobs": dict(sorted_jobs[:limit])
522
  }
523
 
524
- @app.get("/metrics", tags=["Monitoring"])
525
- async def get_metrics(
526
- user: dict = Depends(verify_api_key)
527
- ):
528
- process = psutil.Process(os.getpid())
529
- memory_mb = process.memory_info().rss / 1024 / 1024
530
- cpu_percent = process.cpu_percent(interval=0.1)
531
-
532
- if user["role"] == "admin":
533
- api_metrics = metrics.get_all_stats()
534
- else:
535
- api_metrics = {
536
- "user": user["name"],
537
- "stats": metrics.get_key_stats(user["key"])
538
- }
539
-
540
- return {
541
- "api": api_metrics,
542
- "system": {
543
- "memory_usage_mb": round(memory_mb, 2),
544
- "cpu_percent": round(cpu_percent, 2)
545
- },
546
- "jobs": {
547
- "active": job_manager.active_jobs,
548
- "total": len(job_manager.jobs),
549
- "max_concurrent": MAX_CONCURRENT_JOBS
550
- }
551
- }
552
-
553
  @app.delete("/jobs/{job_id}", tags=["Jobs"])
554
  async def delete_job(
555
  job_id: str,
 
1
  from fastapi import FastAPI, HTTPException, UploadFile, File, BackgroundTasks, Form, Depends, Request
2
  from fastapi.middleware.cors import CORSMiddleware
3
  from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
4
+ from fastapi.responses import JSONResponse, HTMLResponse
5
  from pydantic import BaseModel, Field, field_validator
6
  from typing import Optional, List, Dict
7
  import os
 
19
  import hashlib
20
  from enum import Enum
21
  import logging
22
+ from dotenv import load_dotenv
23
+
24
+ load_dotenv()
25
 
26
  from rag import RAGService, process_pdf
27
 
 
32
 
33
  API_KEYS = {
34
  os.getenv("ADMIN_API_KEY", "your-admin-secret-key"): {"role": "admin", "name": "admin"},
35
+ os.getenv("USER_API_KEY_1", "your-user-secret-key-1"): {"role": "user", "name": "vercel"},
36
+ os.getenv("USER_API_KEY_2", "your-user-secret-key-2"): {"role": "user", "name": "dotpmm"},
37
+ os.getenv("USER_API_KEY_3", "your-user-secret-key-3"): {"role": "user", "name": "beta"},
38
  }
39
 
40
  MAX_FILE_SIZE = 10 * 1024 * 1024
 
358
  except Exception as e:
359
  logger.error(f"Failed to delete temp file {file_path}: {e}")
360
 
361
+ @app.get("/", tags=["Dashboard"])
362
+ async def dashboard():
363
+ html_path = os.path.join(os.path.dirname(__file__), "..", "assets", "index.html")
364
+ with open(html_path, "r", encoding="utf-8") as f:
365
+ html_content = f.read()
366
+ return HTMLResponse(content=html_content)
367
+
368
+ @app.get("/health", tags=["Health"])
369
  async def health_check():
370
+ process = psutil.Process(os.getpid())
371
+ memory_mb = process.memory_info().rss / 1024 / 1024
372
+ cpu_percent = process.cpu_percent(interval=0.1) / psutil.cpu_count()
373
+ uptime_seconds = (datetime.now() - metrics.start_time).total_seconds()
374
+
375
+ all_stats = metrics.get_all_stats()
376
+
377
+ total_calls = sum(user_stats.get('api_calls', 0) for user_stats in all_stats['per_key'].values())
378
+ total_questions = sum(user_stats.get('questions_asked', 0) for user_stats in all_stats['per_key'].values())
379
+
380
+ per_user = {}
381
+ for user_name, user_stats in all_stats['per_key'].items():
382
+ role = 'user'
383
+ for key, key_info in API_KEYS.items():
384
+ if key_info['name'] == user_name:
385
+ role = key_info['role']
386
+ break
387
+
388
+ per_user[user_name] = {
389
+ **user_stats,
390
+ 'role': role
391
+ }
392
+
393
  return {
394
  "status": "healthy",
395
  "service": "AskBookie API",
396
  "version": "2.0.0",
397
  "rag_initialized": rag_service is not None,
398
  "active_jobs": job_manager.active_jobs,
399
+ "timestamp": datetime.now().isoformat(),
400
+ "uptime_hours": round(uptime_seconds / 3600, 2),
401
+ "total_api_calls": total_calls,
402
+ "total_questions": total_questions,
403
+ "memory_mb": round(memory_mb, 2),
404
+ "cpu_percent": round(cpu_percent, 2),
405
+ "per_user": per_user
406
  }
407
 
408
  @app.post("/embed", tags=["Documents"], status_code=202)
 
559
  "jobs": dict(sorted_jobs[:limit])
560
  }
561
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
562
  @app.delete("/jobs/{job_id}", tags=["Jobs"])
563
  async def delete_job(
564
  job_id: str,