681 行单文件拆为(保留 admin_settings 为 include_router 聚合入口): - admin_datasources 数据源列表/连通性测试/KeyRing/ingest status (5 路由) - admin_config settings CRUD + 运行日志 (4 路由) - admin_llm LLM agents/models/ping (3 路由) - admin_quality stats/data-completeness/data-quality (4 路由) 所有路由仍挂 /api/v1/admin 且带 dependencies=[Depends(require_admin)]。 app.py 注册方式不变(仍 import admin_settings.router)。
127 lines
4.4 KiB
Python
127 lines
4.4 KiB
Python
"""后台管理:LLM 专家/终裁配置、可用模型探测、连通性测试。
|
|
|
|
所有接口需管理员鉴权(require_admin)。路由前缀 /api/v1/admin。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import time
|
|
|
|
import httpx
|
|
from fastapi import APIRouter, Depends
|
|
|
|
from src.api.deps import require_admin
|
|
from src.core.config import settings
|
|
from src.core.http_client import get_client
|
|
from src.core.runtime_config import (
|
|
AGENT_META,
|
|
SETTING_DEFS,
|
|
get_runtime_value,
|
|
get_setting_origin,
|
|
mask_value,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/api/v1/admin", tags=["admin"], dependencies=[Depends(require_admin)])
|
|
|
|
|
|
@router.get("/llm/agents")
|
|
async def list_llm_agents():
|
|
"""各专家/终裁的独立 LLM 配置状态(含当前生效模型的解析结果)。"""
|
|
out = []
|
|
for agent in AGENT_META:
|
|
aid = agent["id"].upper()
|
|
pfx = f"AGENT_{aid}_"
|
|
fields = {}
|
|
for suffix in ("MODEL", "BASE_URL", "API_KEY"):
|
|
origin, value = await get_setting_origin(f"{pfx}{suffix}")
|
|
defn = SETTING_DEFS[f"{pfx}{suffix}"]
|
|
fields[suffix.lower()] = {
|
|
"configured": origin != "none",
|
|
"masked": mask_value(value, defn.sensitive),
|
|
"origin": origin,
|
|
}
|
|
# 生效模型 = 覆盖 → 层级默认(专家/终裁 env) → 全局 LLM_MODEL
|
|
tier_default = (
|
|
settings.LLM_AGGREGATOR_MODEL if agent["id"] == "aggregator" else settings.LLM_SPECIALIST_MODEL
|
|
)
|
|
effective_model = (
|
|
fields["model"]["masked"]
|
|
if fields["model"]["configured"]
|
|
else (tier_default or await get_runtime_value("LLM_MODEL"))
|
|
)
|
|
out.append(
|
|
{
|
|
"id": agent["id"],
|
|
"label": agent["label"],
|
|
"fields": fields,
|
|
"effective_model": effective_model,
|
|
}
|
|
)
|
|
return out
|
|
|
|
|
|
@router.get("/llm/models")
|
|
async def list_llm_models():
|
|
"""探测当前 LLM 服务可用的模型列表(OpenAI 兼容 GET /models)。
|
|
|
|
只读探测,不产生费用;配置缺失或服务不可达时返回 ok=false 与原因。
|
|
"""
|
|
base_url = (await get_runtime_value("LLM_BASE_URL")).rstrip("/")
|
|
api_key = await get_runtime_value("LLM_API_KEY")
|
|
if not base_url or not api_key:
|
|
return {"ok": False, "models": [], "detail": "LLM_BASE_URL 或 LLM_API_KEY 未配置"}
|
|
|
|
client = get_client()
|
|
start = time.monotonic()
|
|
try:
|
|
resp = await client.get(
|
|
f"{base_url}/models",
|
|
headers={"Authorization": f"Bearer {api_key}"},
|
|
timeout=httpx.Timeout(connect=10.0, read=20.0, write=10.0, pool=10.0),
|
|
)
|
|
except Exception as e:
|
|
return {
|
|
"ok": False,
|
|
"models": [],
|
|
"latency_ms": int((time.monotonic() - start) * 1000),
|
|
"detail": f"无法连接 LLM 服务: {e}",
|
|
}
|
|
|
|
latency = int((time.monotonic() - start) * 1000)
|
|
if resp.status_code in (401, 403):
|
|
return {"ok": False, "models": [], "latency_ms": latency, "detail": "密钥无效或无权限(HTTP 401/403)"}
|
|
if resp.status_code != 200:
|
|
return {"ok": False, "models": [], "latency_ms": latency, "detail": f"服务返回 HTTP {resp.status_code}"}
|
|
|
|
try:
|
|
data = resp.json()
|
|
except Exception:
|
|
return {"ok": False, "models": [], "latency_ms": latency, "detail": "响应不是合法 JSON"}
|
|
|
|
models: list[str] = []
|
|
items = data.get("data") if isinstance(data, dict) else None
|
|
if isinstance(items, list):
|
|
models = sorted(
|
|
str(m.get("id")) for m in items if isinstance(m, dict) and m.get("id")
|
|
)
|
|
if not models:
|
|
return {"ok": False, "models": [], "latency_ms": latency, "detail": "服务未返回模型列表"}
|
|
return {"ok": True, "models": models, "latency_ms": latency, "detail": f"共 {len(models)} 个可用模型"}
|
|
|
|
|
|
@router.post("/llm/ping")
|
|
async def llm_ping():
|
|
"""LLM 连通性测试(不依赖比赛)。只发一次 chat 请求验证配置。"""
|
|
from src.llm.provider import get_default_provider
|
|
p = await get_default_provider()
|
|
resp = await p.chat(
|
|
system="你是测试助手。",
|
|
user="ping",
|
|
max_tokens=10,
|
|
)
|
|
if resp.error:
|
|
return {"ok": False, "message": resp.error}
|
|
return {"ok": True, "message": "LLM 连接正常", "model": p.model}
|