Files
PlantAI-rag-new/tests/paper/test_paper_bff_agent.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

124 lines
5.4 KiB
Python

"""tests/paper/test_paper_bff_agent.py —— BFF 论文大脑状态机测试(假 MCP server + 假 LLM)。
跑通 brief→outline→confirm(写节循环)→complete→artifact 全状态机。
"""
from __future__ import annotations
import pytest
from conftest import _run
from app import db as bff_db
from app.routers import docgen as docgen_mod
from app.services.paper import agent
class FakePaperService:
"""脚本化 paper 服务:按工具名返回预设结果,模拟写节循环推进。"""
def __init__(self):
self.paper_id = "p1"
self.s1_done = False
self.s2_done = False
async def call_tool(self, name, arguments):
if name == "propose_paper_outline":
return {"ok": True, "paper_id": self.paper_id, "title": "水稻SPL基因家族研究进展",
"abstract_expect": "综述", "sections": [
{"section_id": "s1", "title": "引言", "key_points": [], "target_words": 1500},
{"section_id": "s2", "title": "讨论", "key_points": [], "target_words": 1500},
]}
if name == "get_paper_progress":
sections = [
{"section_id": "s1", "title": "引言", "status": "done" if self.s1_done else "pending",
"word_count": 0, "citation_count": 0},
{"section_id": "s2", "title": "讨论", "status": "done" if self.s2_done else "pending",
"word_count": 0, "citation_count": 0},
]
return {"ok": True, "paper_id": self.paper_id, "status": "writing",
"sections": sections, "current_section_id": None}
if name == "write_paper_section":
sid = arguments["section_id"]
if sid == "s1":
self.s1_done = True
else:
self.s2_done = True
return {"ok": True, "section_id": sid, "status": "done",
"markdown": "内容" * 50, "citations_used": [], "new_citations": []}
if name == "complete_paper":
return {"ok": True, "full_markdown": "# 全文\n内容",
"references": [{"n": 1, "text": "X.", "kind": "title", "source_doc_id": "d1"}],
"renumber_map": {}, "disclaimer": "d"}
if name == "get_paper":
return {"ok": True, "paper_id": self.paper_id, "title": "水稻SPL基因家族研究进展",
"status": "complete", "full_markdown": "# 全文",
"references": [{"n": 1, "text": "X.", "kind": "title", "source_doc_id": "d1"}],
"disclaimer": "d"}
if name == "cancel_paper":
return {"ok": True}
if name == "revise_paper":
return {"ok": True, "affected_section_ids": ["s1"], "plan": "修订引言"}
if name == "revise_paper_section":
return {"ok": True, "section_id": arguments["section_id"], "markdown": "改后", "citations_used": []}
raise AssertionError(f"unexpected tool: {name}")
@pytest.fixture
def bff_database(tmp_path):
_run(bff_db.init_db(tmp_path / "bff.db"))
_run(bff_db.db.execute(
"INSERT INTO sessions (id, title, status, meta, created_at, updated_at) "
"VALUES ('s1', 't', 'idle', '{}', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z')"))
yield bff_db.db
_run(bff_db.close_db())
def _fake_brief(complete=True):
async def _complete_json(system, user, **kwargs):
if complete:
return {"topic": "水稻SPL基因家族", "paper_type": "综述", "language": "中文",
"length": "5000字", "format_req": "GB/T 7714", "style": "学术严谨", "requirements": ""}
return {"topic": "水稻SPL", "paper_type": "", "language": "", "length": ""}
return _complete_json
@pytest.fixture
def fake_insert_artifact(monkeypatch):
async def _fake(db, **kwargs):
return "art-fake"
monkeypatch.setattr(docgen_mod, "insert_artifact", _fake)
return _fake
def test_trigger_complete_brief_proposes_outline(bff_database, monkeypatch, fake_insert_artifact):
svc = FakePaperService()
monkeypatch.setattr(agent, "_new_client", lambda: svc)
monkeypatch.setattr(agent, "llm_complete_json", _fake_brief(complete=True))
blocks = _run(agent.handle_turn(bff_database, "s1", "帮我写一篇水稻SPL基因家族综述"))
assert blocks[-1]["type"] == "paper_outline"
assert blocks[-1]["paper_id"] == "p1"
rows = _run(bff_database.fetchall("SELECT * FROM messages WHERE session_id = 's1'"))
assert len(rows) == 1
assert rows[0]["kind"] == "paper"
def test_trigger_incomplete_brief_returns_form(bff_database, monkeypatch):
monkeypatch.setattr(agent, "_new_client", lambda: FakePaperService())
monkeypatch.setattr(agent, "llm_complete_json", _fake_brief(complete=False))
blocks = _run(agent.handle_turn(bff_database, "s1", "帮我写论文"))
assert blocks[-1]["type"] == "paper_brief"
assert blocks[-1]["status"] == "pending"
def test_writing_loop_completes_to_artifact(bff_database, monkeypatch, fake_insert_artifact):
svc = FakePaperService()
monkeypatch.setattr(agent, "_new_client", lambda: svc)
_run(agent.run_writing_loop(bff_database, "p1", "s1"))
assert svc.s1_done and svc.s2_done
rows = _run(bff_database.fetchall("SELECT * FROM messages WHERE session_id = 's1' ORDER BY rowid"))
assert "paper_progress" in rows[0]["blocks"]
assert "artifact" in rows[-1]["blocks"]
assert "art-fake" in rows[-1]["blocks"]