- 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 单测)
81 lines
2.9 KiB
Python
81 lines
2.9 KiB
Python
"""tests/paper/conftest.py —— 论文服务测试共享夹具(tmp db + 假 LLM + 假检索)。
|
||
|
||
全部 mock LLM/vstore,不依赖生产服务(8090/8094 等绝不探测)。
|
||
镜像 bioflow_service/tests/conftest.py 的 _run(新建事件循环)+ 临时库模式。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
REPO_ROOT = Path(__file__).resolve().parent.parent.parent
|
||
for _p in (str(REPO_ROOT), str(REPO_ROOT / "sdk")):
|
||
if _p not in sys.path:
|
||
sys.path.insert(0, _p)
|
||
|
||
from paper_writer_service.app import db as dbmod # noqa: E402
|
||
from paper_writer_service.app.services.paper import llm as llmmod # noqa: E402
|
||
from paper_writer_service.app.services.paper import search as searchmod # noqa: E402
|
||
|
||
|
||
def _run(coro):
|
||
"""同步跑一个协程(新建 event loop;bioflow/docgen 测试同款可靠模式)。"""
|
||
return asyncio.new_event_loop().run_until_complete(coro)
|
||
|
||
|
||
class FakeLLM:
|
||
"""可编程假 LLM:按顺序返回预设 dict;或抛 LLMError。记录全部调用。"""
|
||
|
||
def __init__(self, responses=None, error=None):
|
||
self.responses = list(responses or [])
|
||
self.error = error
|
||
self.calls: list[dict] = []
|
||
|
||
async def complete_json(self, system, user, **kwargs):
|
||
self.calls.append({"system": system, "user": user})
|
||
if self.error:
|
||
raise llmmod.LLMError(self.error)
|
||
return self.responses.pop(0) if self.responses else {}
|
||
|
||
async def complete_text(self, system, user, **kwargs):
|
||
self.calls.append({"system": system, "user": user})
|
||
if self.error:
|
||
raise llmmod.LLMError(self.error)
|
||
return self.responses.pop(0) if self.responses else ""
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# DB:单例改路径重连(服务模块以 from ...db import db 持有单例引用)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.fixture
|
||
def paper_db(tmp_path):
|
||
dbmod.db._path = Path(tmp_path) / "paper.db"
|
||
_run(dbmod.db.connect())
|
||
yield dbmod.db
|
||
_run(dbmod.db.close())
|
||
|
||
|
||
@pytest.fixture
|
||
def fake_llm(monkeypatch):
|
||
fake = FakeLLM()
|
||
monkeypatch.setattr(llmmod, "complete_json", fake.complete_json)
|
||
monkeypatch.setattr(llmmod, "complete_text", fake.complete_text)
|
||
return fake
|
||
|
||
|
||
@pytest.fixture
|
||
def fake_search(monkeypatch):
|
||
async def _search_refs(paper_id="", query="", top_k=8):
|
||
return {"ok": True, "items": [
|
||
{"doc_id": "d1", "title": "水稻SPL基因家族研究进展", "snippet": "SPL 是植物特有转录因子家族。", "score": 0.9, "meta": {}},
|
||
{"doc_id": "d2", "title": "植物SPL转录因子功能", "snippet": "SPL 调控开花与株型。", "score": 0.8, "meta": {}},
|
||
]}
|
||
|
||
monkeypatch.setattr(searchmod, "search_refs", _search_refs)
|
||
return _search_refs
|