Files
PlantAI-rag-new/backend/mock/mock_server.py
T

225 lines
6.7 KiB
Python
Raw Normal View History

"""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")