"""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 from fastapi.responses import HTMLResponse _INDEX_PATH = Path(__file__).resolve().parent / "static" / "index.html" 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() @app.get("/", response_class=HTMLResponse, tags=["ui"]) async def index(): """端云协同 Web 界面(单文件前端,无需构建)。""" if _INDEX_PATH.exists(): return HTMLResponse(_INDEX_PATH.read_text(encoding="utf-8")) return HTMLResponse("
缺少 gateway/static/index.html
") # ---------------- 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 # ---------------- v2:workspace / 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)