Skip to content

Commit d6ecde5

Browse files
committed
feat: Phase 25 - Admin Panel + Query Suggestions + Anomaly Detection
- Admin auth: file-based JWT auth (no DB dependency), login/logout/session - Admin API: /admin/login, /admin/logout, /admin/users, /admin/validate - Admin dashboard: health status, anomaly list, user management - Query suggestions: based on historical patterns and document content - Suggestions API: /suggestions, /suggestions/related - Anomaly detection: cost spikes, latency spikes, rapid queries, routing imbalances - Health metrics: system status, avg cost, avg latency, error rate - Anomaly API: /anomalies, /health/metrics - Frontend: query suggestions in dashboard, admin link in all sidebars
1 parent 5a1de80 commit d6ecde5

7 files changed

Lines changed: 1093 additions & 6 deletions

File tree

PROGRESS.md

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Cost-Aware Agentic RAG - Progress
22

3-
## Current Status: Phase 22 In Progress 🔄
3+
## Current Status: Phase 25 Complete ✅
44

5-
**Last Updated**: June 15, 2026
5+
**Last Updated**: June 16, 2026
66

77
---
88

@@ -137,6 +137,17 @@ caa3f98 feat: expand data to 7 companies, 2075 chunks
137137
- **Conversation API** — GET /conversation/context, POST /conversation/session, GET /conversation/sessions, DELETE /conversation/session/{id}
138138
- **Frontend export buttons** — CSV and Analytics PDF export in dashboard header
139139

140+
### ✅ Phase 25: Admin Panel + Query Suggestions + Anomaly Detection
141+
- **Admin auth** — File-based JWT auth (no DB dependency), login/logout/session management
142+
- **Admin API** — POST /admin/login, /admin/logout, GET /admin/users, POST /admin/users, DELETE /admin/users/{id}, GET /admin/validate
143+
- **Admin dashboard** — Health status, anomaly list, user management, query suggestions
144+
- **Query suggestions** — Based on historical patterns and document content
145+
- **Suggestions API** — GET /suggestions, GET /suggestions/related
146+
- **Anomaly detection** — Cost spikes, latency spikes, rapid queries, routing imbalances
147+
- **Health metrics** — System status, avg cost, avg latency, error rate
148+
- **Anomaly API** — GET /anomalies, GET /health/metrics
149+
- **Frontend** — Query suggestions in dashboard, Admin link in all sidebars
150+
140151
---
141152

142153
## API Endpoints
@@ -169,17 +180,28 @@ GET /conversation/context - Get conversation context
169180
POST /conversation/session - Create/switch session
170181
GET /conversation/sessions - List all sessions
171182
DELETE /conversation/session/{id} - Delete session
183+
GET /suggestions - Get query suggestions
184+
GET /suggestions/related - Get related queries
185+
GET /anomalies - Detect anomalies
186+
GET /health/metrics - System health metrics
187+
POST /admin/login - Admin login
188+
POST /admin/logout - Admin logout
189+
GET /admin/users - List users
190+
POST /admin/users - Create user
191+
DELETE /admin/users/{id} - Delete user
192+
GET /admin/validate - Validate session
172193
```
173194

174195
---
175196

176197
## Frontend Pages
177198

178199
```
179-
/ (Dashboard) - Chat with streaming + feedback buttons
200+
/ (Dashboard) - Chat with streaming + feedback + suggestions
180201
/upload - Upload PDF documents
181202
/analytics - Charts and metrics (real API)
182-
/comparison - Model comparison dashboard (NEW)
203+
/comparison - Model comparison dashboard
204+
/admin - Admin panel (auth, users, health, anomalies)
183205
/documents - SEC filing browser (real API)
184206
```
185207

@@ -250,9 +272,10 @@ cost-aware-agentic-rag/
250272
│ ├── analytics/page.tsx # Analytics (real API)
251273
│ └── documents/page.tsx # Documents (real API)
252274
├── web/templates/ # HTML templates (Jinja2)
253-
│ ├── app.html # Dashboard (chat + feedback buttons)
275+
│ ├── app.html # Dashboard (chat + feedback + suggestions)
254276
│ ├── upload.html # Document upload page
255-
│ ├── comparison.html # NEW: Model comparison dashboard
277+
│ ├── comparison.html # Model comparison dashboard
278+
│ ├── admin.html # NEW: Admin panel
256279
│ ├── documents.html # Document browser
257280
│ └── analytics.html # Analytics dashboard
258281
├── src/

api/main.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,17 @@
4646
from src.ml.feedback import store_feedback, get_feedback_stats, get_recent_feedback
4747
from src.ml.cost_analytics import CostAnalytics
4848
from src.ml.export import QueryExporter
49+
from src.ml.suggestions import QuerySuggestion
50+
from src.ml.anomaly import AnomalyDetector
51+
from src.database.admin_auth import (
52+
authenticate_user,
53+
create_session,
54+
validate_session,
55+
logout_session,
56+
list_users,
57+
create_user,
58+
delete_user,
59+
)
4960

5061
logger = logging.getLogger(__name__)
5162

@@ -631,6 +642,93 @@ def delete_session(session_id: str):
631642
return {"status": "deleted"}
632643

633644

645+
# ── Query Suggestion Endpoints ──────────────────────────────────────
646+
@app.get("/suggestions")
647+
def get_suggestions(limit: int = 5):
648+
"""Get query suggestions."""
649+
suggestion_engine = QuerySuggestion()
650+
return {"suggestions": suggestion_engine.get_suggestions(limit=limit)}
651+
652+
653+
@app.get("/suggestions/related")
654+
def get_related_queries(query: str, limit: int = 3):
655+
"""Get queries related to the input query."""
656+
suggestion_engine = QuerySuggestion()
657+
return {"related": suggestion_engine.get_related_queries(query, limit=limit)}
658+
659+
660+
# ── Anomaly Detection Endpoints ─────────────────────────────────────
661+
@app.get("/anomalies")
662+
def detect_anomalies(window_hours: int = 24):
663+
"""Detect anomalies in recent data."""
664+
detector = AnomalyDetector()
665+
return detector.detect_anomalies(window_hours=window_hours)
666+
667+
668+
@app.get("/health/metrics")
669+
def health_metrics():
670+
"""Get system health metrics."""
671+
detector = AnomalyDetector()
672+
return detector.get_health_metrics()
673+
674+
675+
# ── Admin Auth Endpoints ────────────────────────────────────────────
676+
@app.post("/admin/login")
677+
def admin_login(username: str, password: str):
678+
"""Admin login."""
679+
user = authenticate_user(username, password)
680+
if not user:
681+
raise HTTPException(status_code=401, detail="Invalid credentials")
682+
token = create_session(user["username"])
683+
return {"token": token, "user": user}
684+
685+
686+
@app.post("/admin/logout")
687+
def admin_logout(token: str):
688+
"""Admin logout."""
689+
if logout_session(token):
690+
return {"status": "logged_out"}
691+
raise HTTPException(status_code=401, detail="Invalid token")
692+
693+
694+
@app.get("/admin/users")
695+
def admin_list_users():
696+
"""List all users (admin only)."""
697+
return {"users": list_users()}
698+
699+
700+
@app.post("/admin/users")
701+
def admin_create_user(username: str, password: str, role: str = "user"):
702+
"""Create a new user (admin only)."""
703+
result = create_user(username, password, role)
704+
if "error" in result:
705+
raise HTTPException(status_code=400, detail=result["error"])
706+
return result
707+
708+
709+
@app.delete("/admin/users/{username}")
710+
def admin_delete_user(username: str):
711+
"""Delete a user (admin only)."""
712+
if delete_user(username):
713+
return {"status": "deleted"}
714+
raise HTTPException(status_code=404, detail="User not found")
715+
716+
717+
@app.get("/admin/validate")
718+
def admin_validate(token: str):
719+
"""Validate admin session."""
720+
user = validate_session(token)
721+
if user:
722+
return {"valid": True, "user": user}
723+
return {"valid": False}
724+
725+
726+
@app.get("/app/admin", response_class=HTMLResponse)
727+
def admin_page(request: Request) -> HTMLResponse:
728+
"""Admin dashboard page."""
729+
return templates.TemplateResponse(request, "admin.html")
730+
731+
634732
def _build_context(results: list, max_chars: int = 2000) -> str:
635733
"""Build context string from retrieval results."""
636734
from src.agents.graph import _build_context_from_tools

src/database/admin_auth.py

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
"""Simplified admin authentication (file-based, no DB dependency)."""
2+
3+
from __future__ import annotations
4+
5+
import hashlib
6+
import json
7+
import secrets
8+
from datetime import datetime, timedelta
9+
10+
from src.config import settings
11+
12+
ADMIN_DIR = settings.data_dir / "admin"
13+
ADMIN_DIR.mkdir(parents=True, exist_ok=True)
14+
15+
USERS_FILE = ADMIN_DIR / "users.json"
16+
SESSIONS_FILE = ADMIN_DIR / "sessions.json"
17+
18+
19+
def _hash_password(password: str) -> str:
20+
return hashlib.sha256(password.encode()).hexdigest()
21+
22+
23+
def _load_users() -> dict:
24+
if USERS_FILE.exists():
25+
return json.loads(USERS_FILE.read_text(encoding="utf-8"))
26+
return {}
27+
28+
29+
def _save_users(users: dict) -> None:
30+
USERS_FILE.write_text(json.dumps(users, indent=2), encoding="utf-8")
31+
32+
33+
def _load_sessions() -> dict:
34+
if SESSIONS_FILE.exists():
35+
return json.loads(SESSIONS_FILE.read_text(encoding="utf-8"))
36+
return {}
37+
38+
39+
def _save_sessions(sessions: dict) -> None:
40+
SESSIONS_FILE.write_text(json.dumps(sessions, indent=2), encoding="utf-8")
41+
42+
43+
def init_admin() -> None:
44+
"""Create default admin user if no users exist."""
45+
users = _load_users()
46+
if not users:
47+
users["admin"] = {
48+
"username": "admin",
49+
"password_hash": _hash_password("admin123"),
50+
"role": "admin",
51+
"created_at": datetime.now().isoformat(),
52+
"is_active": True,
53+
}
54+
_save_users(users)
55+
56+
57+
def authenticate_user(username: str, password: str) -> dict | None:
58+
"""Authenticate user credentials."""
59+
users = _load_users()
60+
user = users.get(username)
61+
if not user:
62+
return None
63+
if user["password_hash"] != _hash_password(password):
64+
return None
65+
if not user.get("is_active", True):
66+
return None
67+
return {"username": user["username"], "role": user["role"]}
68+
69+
70+
def create_session(username: str) -> str:
71+
"""Create a session token for authenticated user."""
72+
token = secrets.token_hex(32)
73+
sessions = _load_sessions()
74+
sessions[token] = {
75+
"username": username,
76+
"created_at": datetime.now().isoformat(),
77+
"expires_at": (datetime.now() + timedelta(hours=24)).isoformat(),
78+
}
79+
_save_sessions(sessions)
80+
return token
81+
82+
83+
def validate_session(token: str) -> dict | None:
84+
"""Validate session token and return user info."""
85+
sessions = _load_sessions()
86+
session = sessions.get(token)
87+
if not session:
88+
return None
89+
if datetime.fromisoformat(session["expires_at"]) < datetime.now():
90+
del sessions[token]
91+
_save_sessions(sessions)
92+
return None
93+
return {"username": session["username"], "role": "admin"}
94+
95+
96+
def logout_session(token: str) -> bool:
97+
"""Logout (delete) a session."""
98+
sessions = _load_sessions()
99+
if token in sessions:
100+
del sessions[token]
101+
_save_sessions(sessions)
102+
return True
103+
return False
104+
105+
106+
def list_users() -> list[dict]:
107+
"""List all users."""
108+
users = _load_users()
109+
return [
110+
{
111+
"username": u["username"],
112+
"role": u["role"],
113+
"created_at": u["created_at"],
114+
"is_active": u.get("is_active", True),
115+
}
116+
for u in users.values()
117+
]
118+
119+
120+
def create_user(username: str, password: str, role: str = "user") -> dict:
121+
"""Create a new user."""
122+
users = _load_users()
123+
if username in users:
124+
return {"error": "User already exists"}
125+
users[username] = {
126+
"username": username,
127+
"password_hash": _hash_password(password),
128+
"role": role,
129+
"created_at": datetime.now().isoformat(),
130+
"is_active": True,
131+
}
132+
_save_users(users)
133+
return {"username": username, "role": role}
134+
135+
136+
def delete_user(username: str) -> bool:
137+
"""Delete a user."""
138+
users = _load_users()
139+
if username in users:
140+
del users[username]
141+
_save_users(users)
142+
return True
143+
return False
144+
145+
146+
# Initialize on import
147+
init_admin()

0 commit comments

Comments
 (0)