225 lines
6.7 KiB
Python
225 lines
6.7 KiB
Python
"""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")
|