initial
This commit is contained in:
144
backend/app/api/admin.py
Normal file
144
backend/app/api/admin.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""Admin panel routes: settings, LLM logs, users."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException
|
||||
|
||||
from app.core.settings_service import EDITABLE_SETTING_KEYS, get_all_settings, update_settings
|
||||
from app.db import get_db_dep
|
||||
from app.deps import require_admin
|
||||
from app.models import LlmCallLog, Setting, User
|
||||
from app.schemas import LlmLogOut, SettingsOut, SettingsUpdate
|
||||
|
||||
router = APIRouter(prefix="/api/admin", tags=["admin"])
|
||||
|
||||
|
||||
def _mask_secrets(values: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Mask sensitive api_key fields in outbound responses."""
|
||||
for k in ("llm.api_key", "embedding.api_key"):
|
||||
v = values.get(k)
|
||||
if isinstance(v, str) and v:
|
||||
values[k] = v[:4] + "***" + v[-4:] if len(v) > 8 else "***"
|
||||
# Never expose admin setup token via this endpoint
|
||||
values.pop("admin.setup_token", None)
|
||||
return values
|
||||
|
||||
|
||||
@router.get("/settings", response_model=SettingsOut)
|
||||
async def get_settings_endpoint(
|
||||
db: AsyncSession = Depends(get_db_dep),
|
||||
_: User = Depends(require_admin),
|
||||
):
|
||||
values = await get_all_settings(db)
|
||||
values = _mask_secrets(values)
|
||||
return SettingsOut(values=values, editable_keys=sorted(EDITABLE_SETTING_KEYS.keys()))
|
||||
|
||||
|
||||
@router.put("/settings", response_model=SettingsOut)
|
||||
async def update_settings_endpoint(
|
||||
payload: SettingsUpdate,
|
||||
db: AsyncSession = Depends(get_db_dep),
|
||||
_: User = Depends(require_admin),
|
||||
):
|
||||
# Strip masked api_key fields unless the user typed a new value
|
||||
cleaned: Dict[str, Any] = {}
|
||||
for k, v in (payload.values or {}).items():
|
||||
if k in ("llm.api_key", "embedding.api_key") and isinstance(v, str) and "***" in v:
|
||||
continue
|
||||
cleaned[k] = v
|
||||
new_values = await update_settings(db, cleaned)
|
||||
# If embedding settings changed, drop the cached RAG client so the next
|
||||
# get_rag() call rebuilds it (and reconfigures Qdrant collections if dim changed).
|
||||
if any(k.startswith("embedding.") for k in cleaned):
|
||||
from app.core.rag import reset_rag
|
||||
await reset_rag()
|
||||
new_values = _mask_secrets(new_values)
|
||||
return SettingsOut(values=new_values, editable_keys=sorted(EDITABLE_SETTING_KEYS.keys()))
|
||||
|
||||
|
||||
@router.post("/embeddings/test")
|
||||
async def test_embeddings_endpoint(
|
||||
payload: Dict[str, Any] = Body(default={}),
|
||||
db: AsyncSession = Depends(get_db_dep),
|
||||
_: User = Depends(require_admin),
|
||||
):
|
||||
"""Probe the currently configured embeddings endpoint.
|
||||
|
||||
Accepts an optional `overrides` dict with embedding.* keys (e.g. to test
|
||||
a new endpoint before saving). Returns: ok, provider, base_url, model,
|
||||
dim, sample_norm (or error).
|
||||
"""
|
||||
from app.core.rag import probe_embeddings
|
||||
settings_map = await get_all_settings(db)
|
||||
# Apply ad-hoc overrides (without saving) so the admin can try before save
|
||||
overrides = (payload or {}).get("overrides") or {}
|
||||
for k, v in overrides.items():
|
||||
if k in EDITABLE_SETTING_KEYS:
|
||||
settings_map[k] = v
|
||||
return await probe_embeddings(settings_map)
|
||||
|
||||
|
||||
@router.get("/llm-logs", response_model=List[LlmLogOut])
|
||||
async def list_llm_logs(
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
db: AsyncSession = Depends(get_db_dep),
|
||||
_: User = Depends(require_admin),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(LlmCallLog).order_by(LlmCallLog.created_at.desc()).limit(min(limit, 200)).offset(offset)
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@router.get("/llm-logs/{log_id}")
|
||||
async def get_llm_log(
|
||||
log_id: str,
|
||||
db: AsyncSession = Depends(get_db_dep),
|
||||
_: User = Depends(require_admin),
|
||||
):
|
||||
from uuid import UUID
|
||||
result = await db.execute(select(LlmCallLog).where(LlmCallLog.id == UUID(log_id)))
|
||||
log = result.scalars().first()
|
||||
if not log:
|
||||
raise HTTPException(status_code=404, detail="log_not_found")
|
||||
return {
|
||||
"id": str(log.id),
|
||||
"purpose": log.purpose,
|
||||
"model": log.model,
|
||||
"base_url": log.base_url,
|
||||
"prompt_messages": log.prompt_messages,
|
||||
"tools": log.tools,
|
||||
"response_text": log.response_text,
|
||||
"tool_calls": log.tool_calls,
|
||||
"prompt_tokens": log.prompt_tokens,
|
||||
"completion_tokens": log.completion_tokens,
|
||||
"total_tokens": log.total_tokens,
|
||||
"latency_ms": log.latency_ms,
|
||||
"error": log.error,
|
||||
"created_at": log.created_at.isoformat() if log.created_at else None,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/users")
|
||||
async def list_users(
|
||||
db: AsyncSession = Depends(get_db_dep),
|
||||
_: User = Depends(require_admin),
|
||||
):
|
||||
result = await db.execute(select(User).order_by(User.created_at.desc()))
|
||||
users = result.scalars().all()
|
||||
return [
|
||||
{
|
||||
"id": str(u.id),
|
||||
"email": u.email,
|
||||
"username": u.username,
|
||||
"is_admin": u.is_admin,
|
||||
"is_active": u.is_active,
|
||||
"created_at": u.created_at.isoformat() if u.created_at else None,
|
||||
}
|
||||
for u in users
|
||||
]
|
||||
Reference in New Issue
Block a user