Files
PlantAI-rag-new/sdk/app/routers/paper.py
T
admin 91f6a1c242 feat: 新增论文撰写功能 paper-writer(独立 MCP 服务 + BFF 大脑 + 前端三卡)
- paper_writer_service(8097):FastAPI + MCP streamable-http 单端点,9 工具
  (propose_paper_outline/write_paper_section/get_paper_progress/search_refs/
  revise_paper_section/revise_paper/get_paper/complete_paper/cancel_paper),
  SQLite 三表(papers/paper_sections/citations,WAL),GB/T 7714 引用协议
- BFF:services/paper/{mcp_client,agent}.py(裸 JSON-RPC 客户端 + 规则状态机)、
  routers/paper.py、schemas.py 4 个 Paper*Block + ArtifactBlock 加 paper_id/citation_count、
  config.py 开关、main.py 条件注册、docgen.py 抽 insert_artifact 公共 helper
- 前端:intent.js paperIntent 纯函数、Composer docgen 分支 sub-route、sendPaper/轮询、
  PaperBriefCard/PaperOutlineCard/PaperProgressCard、DocPane cite-chip
- deploy/paper_restart.sh + toggle_paper.sh;compile_sdk.sh 白名单追加 paper 文件
- tests/paper 26 条(工具逻辑 + MCP 协议 + BFF 状态机 + 块 schema + 前端 intent 单测)
2026-09-30 22:29:22 +08:00

91 lines
3.1 KiB
Python

"""routers/paper.py —— 论文撰写路由(对话大脑)。
条件注册(PAPER_WRITER_ENABLED 为假不注册,同 bioflow_proxy 模式)。
端点:health(探活反代)/ turns(一轮状态机)/ confirm / resume / progress / citations。
"""
from __future__ import annotations
import asyncio
from typing import Any
import httpx
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from ..config import get_settings
from ..db import Database, get_db
from ..services.paper import agent
from ..services.paper.mcp_client import PaperMcpClient
router = APIRouter(prefix="/api/paper", tags=["paper"])
class PaperTurnRequest(BaseModel):
session_id: str
text: str
paper_id: str | None = None
class PaperConfirmRequest(BaseModel):
session_id: str
def _client() -> PaperMcpClient:
s = get_settings()
return PaperMcpClient(s.paper_writer_url, s.paper_writer_read_timeout)
@router.get("/health")
async def paper_health() -> dict[str, Any]:
"""探活反代 8097 /health;离线返回 offline 标记(前端据此隐藏 paper 入口)。"""
s = get_settings()
try:
async with httpx.AsyncClient(timeout=10.0) as client:
resp = await client.get(f"{s.paper_writer_url.rstrip('/')}/health")
resp.raise_for_status()
return resp.json()
except Exception: # noqa: BLE001
return {"ok": False, "status": "offline"}
@router.post("/turns")
async def paper_turn(payload: PaperTurnRequest, db: Database = Depends(get_db)) -> dict[str, Any]:
blocks = await agent.handle_turn(db, payload.session_id, payload.text, payload.paper_id)
return {"ok": True, "blocks": blocks}
@router.post("/papers/{paper_id}/confirm")
async def paper_confirm(paper_id: str, payload: PaperConfirmRequest, db: Database = Depends(get_db)) -> dict[str, Any]:
asyncio.create_task(agent.run_writing_loop(db, paper_id, payload.session_id))
return {"ok": True, "paper_id": paper_id}
@router.post("/papers/{paper_id}/resume")
async def paper_resume(paper_id: str, payload: PaperConfirmRequest, db: Database = Depends(get_db)) -> dict[str, Any]:
asyncio.create_task(agent.run_writing_loop(db, paper_id, payload.session_id))
return {"ok": True, "paper_id": paper_id}
@router.get("/papers/{paper_id}")
async def paper_progress(paper_id: str) -> dict[str, Any]:
client = _client()
try:
return await client.call_tool("get_paper_progress", {"paper_id": paper_id})
except Exception as exc: # noqa: BLE001
return {"ok": False, "code": "UPSTREAM_DOWN", "message": str(exc)}
@router.get("/papers/{paper_id}/citations/{n}")
async def paper_citation(paper_id: str, n: int) -> dict[str, Any]:
client = _client()
try:
paper = await client.call_tool("get_paper", {"paper_id": paper_id})
except Exception as exc: # noqa: BLE001
return {"ok": False, "code": "UPSTREAM_DOWN", "message": str(exc)}
if not paper.get("ok"):
return paper
for ref in paper.get("references") or []:
if ref.get("n") == n:
return {"ok": True, "citation": ref}
return {"ok": False, "code": "NOT_FOUND", "message": f"引用 [{n}] 不存在"}