Files
PlantAI-rag-new/sdk/mock/mock_server.py
T
jerryandClaude Fable 5 0875bb036c P0/M1: 四部分重构——frontend→ui、backend→sdk 改名 + 引用修复 + 骨架
- git mv 保留历史:ui/(Vue3+Vite)、sdk/(BFF 中间层)
- 路径语义修正:db_path/figures_cache 改相对 sdk/(BFF cwd),
  历史库 sdk/backend/data 迁移至 sdk/data/(会话/图片缓存无损)
- 引用修复 ~15 处:config.py、deploy/*.sh、vitest/playwright 配置、
  单测导入路径、docs、README、gitignore、.env.example
- 骨架:agent/{qa,methodology,legacy-rag,vendor}、knowledge/manifest.yaml、
  tooling/{compile,kb,crypto,assemble,audit,tools,scripts} + README
- 门禁验证:sdk pytest 125 通过、ui vitest 158 通过、
  生产 BFF 脚本重启四上游全绿 + 静态托管 + 会话库无损

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 12:56:47 +08:00

225 lines
6.7 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.
"""PlantAI-rag-new mock server —— 前端开发与 E2E 场景的固定数据源。
监听 127.0.0.1:8099。实现 API.md 的全部接口,返回 fixtures/ 中的固定数据。
场景切换:查询参数 ?scenario= 或请求头 X-Mock-Scenario:
normal(默认)| empty(空结果)| slow(慢响应 5s)| error(上游故障)| degraded(QA 不可用)
SSE 流按真实时序回放,让流式 UI 在没有上游时也能开发。
"""
from __future__ import annotations
import asyncio
import json
import time
from pathlib import Path
from fastapi import FastAPI, Header, Query, Request
from fastapi.responses import JSONResponse, StreamingResponse
FIXTURES = Path(__file__).parent / "fixtures"
app = FastAPI(title="PlantAI-rag-new mock")
def load(name: str):
return json.loads((FIXTURES / name).read_text(encoding="utf-8"))
def scenario_of(request: Request, q: str | None = None) -> str:
s = q or request.headers.get("X-Mock-Scenario") or "normal"
return s if s in {"normal", "empty", "slow", "error", "degraded"} else "normal"
def envelope(code: str, message: str, degraded: bool, status: int):
return JSONResponse(
status_code=status,
content={"ok": False, "error": {"code": code, "message": message, "degraded": degraded}},
)
# ---------- 健康与元信息 ----------
@app.get("/api/health")
async def health():
return load("health.json")
@app.get("/api/qa/info")
async def qa_info():
return load("qa_info.json")
# ---------- QA ----------
@app.post("/api/qa/chat")
async def qa_chat(request: Request):
sc = scenario_of(request)
if sc == "degraded":
return envelope("UPSTREAM_DOWN", "知识问答服务当前不可用,请稍后重试", True, 503)
if sc == "error":
return envelope("UPSTREAM_ERROR", "上游问答服务返回错误", True, 502)
if sc == "slow":
await asyncio.sleep(5)
if sc == "empty":
return load("qa_chat_empty.json")
return load("qa_chat.json")
@app.post("/api/qa/search")
async def qa_search(request: Request):
sc = scenario_of(request)
if sc in ("degraded",):
return envelope("UPSTREAM_DOWN", "知识问答服务当前不可用,请稍后重试", True, 503)
return load("qa_search.json")
@app.post("/api/qa/chat/stream")
async def qa_chat_stream(request: Request):
sc = scenario_of(request)
if sc == "degraded":
return envelope("UPSTREAM_DOWN", "知识问答服务当前不可用,请稍后重试", True, 503)
frames = load("sse_frames.json") # [{stage, phase, label, details, delay_ms}, ..., "__complete__"]
async def gen():
for f in frames:
if f == "__complete__":
yield "event: complete\n"
yield f"data: {json.dumps(load('qa_chat.json'), ensure_ascii=False)}\n\n"
break
await asyncio.sleep(f.get("delay_ms", 600) / 1000)
payload = {
"stage": f["stage"], "phase": f["phase"], "label": f["label"],
"details": f.get("details", {}), "at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
}
yield f"event: progress\n"
yield f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"
if int(f.get("delay_ms", 600)) > 3000: # 慢帧之间补 keepalive
yield ": ping\n\n"
return StreamingResponse(gen(), media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
# ---------- 会话与消息 ----------
_sessions = []
_messages = {}
_seq = 0
def _now():
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
@app.get("/api/sessions")
async def sessions():
return load("sessions.json")
@app.post("/api/sessions")
async def create_session(request: Request):
body = await request.json()
return {**load("session_created.json"), "title": body.get("title") or "新会话"}
@app.get("/api/sessions/{sid}")
async def session_detail(sid: str):
return load("session_detail.json")
@app.patch("/api/sessions/{sid}")
async def session_patch(sid: str, request: Request):
body = await request.json()
return {**load("session_created.json"), **body}
@app.delete("/api/sessions/{sid}")
async def session_delete(sid: str):
return JSONResponse(status_code=204, content=None)
@app.post("/api/sessions/{sid}/messages")
async def create_message(sid: str, request: Request):
body = await request.json()
return {"id": "msg-mock-001", "role": body.get("role", "user"),
"kind": body.get("kind", "qa"), "status": body.get("status", "done"),
"blocks": body.get("blocks", []), "raw": None,
"created_at": _now(), "updated_at": _now()}
@app.patch("/api/sessions/{sid}/messages/{mid}")
async def patch_message(sid: str, mid: str, request: Request):
body = await request.json()
return {"id": mid, "role": "assistant", "kind": "qa",
"status": body.get("status", "done"), "blocks": body.get("blocks", []),
"raw": body.get("raw"), "created_at": _now(), "updated_at": _now()}
@app.get("/api/sessions/{sid}/sources")
async def session_sources(sid: str):
return load("sources_aggregate.json")
# ---------- 知识图谱 ----------
@app.get("/api/kg/status")
async def kg_status():
return load("kg_status.json")
@app.get("/api/kg/overview")
async def kg_overview():
return load("kg_overview.json")
@app.get("/api/kg/entities")
async def kg_entities(q: str = "", limit: int = 20):
return load("kg_entities.json")
@app.get("/api/kg/entities/{name}")
async def kg_entity(name: str):
return load("kg_entity.json")
@app.get("/api/kg/entities/{name}/neighborhood")
async def kg_neighborhood(name: str, depth: int = 2, limit: int = 50):
return load("kg_neighborhood.json")
# ---------- 抽取任务 ----------
@app.get("/api/extraction/jobs")
async def extraction_jobs():
return load("extraction_jobs.json")
@app.get("/api/extraction/jobs/{job_id}")
async def extraction_job(job_id: str):
return load("extraction_job.json")
@app.get("/api/extraction/processing-status")
async def extraction_processing_status():
return load("extraction_processing_status.json")
@app.get("/api/extraction/run-log")
async def extraction_run_log():
return {"text": "2026-08-25 10:00:01 INFO MinerU 解析完成,11 sections\n2026-08-25 10:03:44 INFO 实体识别 712 entities\n2026-08-25 10:12:09 INFO 关系抽取 751 relations\n"}
@app.get("/api/extraction/summary")
async def extraction_summary():
return load("extraction_summary.json")
@app.post("/api/extraction/upload")
async def extraction_upload(request: Request):
return load("extraction_upload.json")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=8099, log_level="info")