Files
projectAIpopular/gateway/api.py
T

262 lines
9.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""FastAPI 网关:对外提供 v2(端云协同)与 v1(legacy 路由)两套接口。
v2 端点(《实现方案_v2》5.3):
POST /chat 端云协同协作管线
POST /chat/legacy 原 v1 L0 行为(离线降级,测试封闭)
GET /runs/{request_id}/workspace 交流文本
GET /runs/{request_id}/artifacts/{name} 工件下载
GET /review/queue、POST /review/{id} 人工检验
GET /metrics 含 v2 token/快路径统计
启动:uvicorn gateway.api:app --host 0.0.0.0 --port 8000
"""
from __future__ import annotations
from pathlib import Path
from typing import List, Optional
from pydantic import BaseModel, Field
from router_system.config import load_config
from router_system.router import Router, build_router
# ---- v2 依赖(惰性导入,缺依赖时降级提示) ----
try:
from router_system.architect import build_architect
from router_system.knowledge import KnowledgeBase
from router_system.pipeline import CollaborativePipeline, build_pipeline
from router_system.review import ReviewQueue
from router_system.v2stats import V2Stats
from router_system.worker import build_worker
_V2_OK = True
except Exception: # pragma: no cover
_V2_OK = False
# ---- v1 全局单例 ----
_router: Optional[Router] = None
# ---- v2 全局单例 ----
_pipeline: Optional["CollaborativePipeline"] = None
_v2stats = V2Stats() if _V2_OK else None
_review = None
def get_router() -> Router:
global _router
if _router is None:
_router = build_router()
return _router
def get_review() -> "ReviewQueue":
global _review
if _review is None and _V2_OK:
cfg = load_config().get("review", {})
_review = ReviewQueue(db_path=cfg.get("queue_db", "data/review.sqlite3"))
return _review
def build_v2_pipeline(worker_cfg_override: Optional[dict] = None):
"""从配置构建 v2 协作管线(architect + worker + pipeline)。
worker_cfg_override 可注入(测试/演示用 mock)。无 API key 时 /chat 会走
本地降级路径(不崩溃)。
"""
global _pipeline
if _pipeline is None:
cfg = load_config()
kb = KnowledgeBase()
architect = build_architect(cfg.get("architect", {}))
wcfg = dict(cfg.get("worker", {}))
if worker_cfg_override:
wcfg.update(worker_cfg_override)
worker = build_worker(wcfg, kb=kb)
_pipeline = build_pipeline(cfg, architect, worker)
return _pipeline
def set_pipeline(pipe) -> None:
"""测试注入替身管线。"""
global _pipeline
_pipeline = pipe
def get_pipeline():
global _pipeline
if _pipeline is None:
return build_v2_pipeline()
return _pipeline
# ---- 请求/响应模型 ----
class QueryRequest(BaseModel):
query: str = Field(..., min_length=1, max_length=8000, description="用户查询")
domain_group: Optional[str] = Field(
None, description="大领域组(两级路由第一级):tech | professional | lifestyle | general;不指定则自动检测"
)
class HealthResponse(BaseModel):
status: str
domains: List[str]
classifier: str
judge: str
fallback: str
# ---- FastAPI 应用 ----
try:
from fastapi import FastAPI, HTTPException
app = FastAPI(
title="端云协同 LLM 协作系统",
description="大模型(Architect) + 本地小模型(Worker) 通过交流文本协作;v1 保留为 legacy 路由",
version="2.0.0",
)
@app.get("/health", response_model=HealthResponse, tags=["system"])
async def health():
return get_router().health()
# ---------------- v2/chat ----------------
@app.post("/chat", tags=["chat"])
async def chat(req: QueryRequest):
try:
result = await get_pipeline().run(req.query)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
# 记账 + 入队
if _v2stats is not None:
_v2stats.record(result)
_maybe_enqueue(result)
return {
"response": result.response,
"request_id": result.request_id,
"status": result.status,
"fast_path": result.fast_path,
"rounds_used": result.rounds_used,
"api_input_tokens": result.api_input_tokens,
"api_output_tokens": result.api_output_tokens,
"cost_est": result.cost_est,
"model_used": result.model_used,
"latency_ms": round(result.latency_ms, 2),
"route": result.route,
"workspace_path": result.workspace_path,
"error": result.error,
}
# ---------------- v1/chat/legacy ----------------
@app.post("/chat/legacy", tags=["chat"])
async def chat_legacy(req: QueryRequest):
"""原 v1 L0 专家系统行为(离线可跑,测试封闭)。"""
try:
result = await get_router().route(req.query, domain_group=req.domain_group)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
return {
"response": result.response,
"domain": result.domain,
"difficulty": result.difficulty,
"confidence": round(result.confidence, 4),
"upgraded": result.upgraded,
"quality_score": round(result.quality_score, 4),
"model_used": result.model_used,
"route": result.route,
"latency_ms": round(result.latency_ms, 2),
"cache_hit": result.cache_hit,
"cache_level": result.cache_level,
"cost_est": round(result.cost_est, 6),
"error": result.error,
"subdomain": result.subdomain,
"subdomain2": result.subdomain2,
"domain_group": result.domain_group,
"request_id": result.request_id,
}
@app.get("/traces/{request_id}", tags=["system"])
async def get_trace(request_id: str):
trace = get_router().trace_store.get(request_id)
if trace is None:
raise HTTPException(status_code=404, detail=f"未找到请求 {request_id} 的推理链")
return trace
# ---------------- v2workspace / artifacts ----------------
@app.get("/runs/{request_id}/workspace", tags=["v2"])
async def get_workspace(request_id: str):
p = Path("runs") / request_id / "workspace.json"
if not p.exists():
raise HTTPException(status_code=404, detail=f"未找到运行 {request_id}")
import json
return json.loads(p.read_text(encoding="utf-8"))
@app.get("/runs/{request_id}/artifacts/{name}", tags=["v2"])
async def get_artifact(request_id: str, name: str):
p = Path("runs") / request_id / "artifacts" / name
if not p.exists():
raise HTTPException(status_code=404, detail=f"未找到工件 {name}")
return {"name": name, "content": p.read_text(encoding="utf-8")}
# ---------------- v2:人工检验 ----------------
@app.get("/review/queue", tags=["v2"])
async def review_queue(status: Optional[str] = None, limit: int = 50):
return get_review().list(status=status, limit=limit)
@app.post("/review/{review_id}", tags=["v2"])
async def review_submit(review_id: int, verdict: str, correction: Optional[str] = None):
try:
ok = get_review().submit(review_id, verdict, correction=correction)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
if not ok:
raise HTTPException(status_code=404, detail=f"审核记录不存在或已审核: {review_id}")
return {"ok": True, "review_id": review_id, "verdict": verdict}
# ---------------- metrics ----------------
@app.get("/metrics", tags=["system"])
async def metrics():
out = {
"router": get_router().stats.summary(),
"cache": get_router().cache.stats(),
}
if _V2_OK and _v2stats is not None:
out["v2"] = _v2stats.summary()
out["review"] = {
"pending": get_review().count(status="pending"),
"total": get_review().count(),
}
return out
except ImportError:
app = None
print("[gateway] 未安装 fastapi,请执行: pip install -r requirements.txt")
def _maybe_enqueue(result) -> None:
"""按 review 配置抽样/强制入队(异步场景用同步快速调用)。"""
if not _V2_OK or get_review() is None:
return
if result.status not in ("done", "escalated", "fast_path"):
return
cfg = load_config().get("review", {})
sample_rate = float(cfg.get("sample_rate", 0.10))
force_tags = cfg.get("force_tags", ["safety"])
# 从交流文本读取 tags(若可)
tags: List[str] = []
if result.workspace_path:
try:
import json
ws = json.loads(Path(result.workspace_path).read_text(encoding="utf-8"))
tags = (ws.get("brief") or {}).get("tags") or []
except Exception:
tags = []
if ReviewQueue.should_enqueue(tags, sample_rate=sample_rate, force_tags=force_tags):
get_review().enqueue(
request_id=result.request_id, query=result.query, response=result.response,
tags=tags, reason="auto", workspace_path=result.workspace_path,
)
if __name__ == "__main__":
import uvicorn
uvicorn.run("gateway.api:app", host="0.0.0.0", port=8000, reload=False)