forked from admin/PlantAI-rag-new
fix(paper): 取消路径(cancel_paper) + 引用元数据清洗(丢弃杜撰 DOI/PMID)
This commit is contained in:
@@ -10,10 +10,11 @@ from .cite import find_cite_numbers, render_gbt7714
|
||||
_WRITE_SYSTEM = (
|
||||
"你是植物学论文撰写助手,正在撰写论文的某一章节。"
|
||||
"引用文献一律用 [n] 指代,n 必须取自下方提供的全局编号列表;如需引用新文献,编号按当前最大编号依次 +1。"
|
||||
"禁止杜撰 DOI/PMID/作者/期刊——只能使用编号列表或知识库线索中真实给出的元数据。"
|
||||
"你只能引用「知识库线索」中列出的文献;新引用的元数据只能照抄线索里的 title 和 doc_id。"
|
||||
"知识库线索里没有 DOI/PMID/作者/期刊/年份,这些字段一律输出空字符串,禁止编造任何 DOI/PMID/作者/期刊。"
|
||||
"只输出一个合法 JSON 对象:{\"markdown\": 章节正文(不含章标题), \"citations_used\": "
|
||||
"[{\"temp_n\": 编号, \"title\": \"...\", \"authors\": \"...\", \"journal\": \"...\", "
|
||||
"\"year\": \"...\", \"doi\": \"...\", \"pmid\": \"...\", \"source_doc_id\": \"...\", \"snippet\": \"...\"}]}。"
|
||||
"[{\"temp_n\": 编号, \"title\": \"...\", \"authors\": \"\", \"journal\": \"\", "
|
||||
"\"year\": \"\", \"doi\": \"\", \"pmid\": \"\", \"source_doc_id\": \"...\", \"snippet\": \"...\"}]}。"
|
||||
"citations_used 必须列出本节正文中出现的每一个 [n] 引用(既有编号也列出其已有元数据)。"
|
||||
)
|
||||
|
||||
@@ -76,6 +77,13 @@ def _build_write_user(paper: dict, outline: dict, sec: dict, cit_text: str, styl
|
||||
parts.append("")
|
||||
parts.append("全局已编号文献列表([n] 引用只能从这里取值,或从最大编号 +1 起新增):")
|
||||
parts.append(cit_text)
|
||||
brief = loads_json(paper.get("brief_json"), {})
|
||||
kb_hints = brief.get("kb_hints") or []
|
||||
if kb_hints:
|
||||
parts.append("")
|
||||
parts.append("知识库线索(新引用只能从这里选,元数据只照抄 title/doc_id):")
|
||||
for h in kb_hints:
|
||||
parts.append(f"- title={h.get('title', '')} | doc_id={h.get('doc_id', '')} | snippet={str(h.get('snippet', ''))[:120]}")
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
@@ -91,6 +99,16 @@ def _kind_of(meta: dict[str, Any]) -> str:
|
||||
return "title"
|
||||
|
||||
|
||||
def _sanitize_citation(meta: dict[str, Any]) -> dict[str, Any]:
|
||||
"""丢弃 LLM 可能杜撰的 doi/pmid/authors/journal/year(知识库线索无这些字段),
|
||||
只保留 title/source_doc_id/snippet,强制走 title kind 降级格式。"""
|
||||
return {
|
||||
"title": (meta.get("title") or "").strip(),
|
||||
"source_doc_id": meta.get("source_doc_id") or "",
|
||||
"snippet": meta.get("snippet") or "",
|
||||
}
|
||||
|
||||
|
||||
async def _persist_new_citations(paper_id: str, section_id: str, used: list[Any], existing_nums: set[int]) -> list[dict[str, Any]]:
|
||||
"""把 citations_used 中的新引用(temp_n 不在既有集合)落库,返回新增摘要。"""
|
||||
new_citations: list[dict[str, Any]] = []
|
||||
@@ -102,13 +120,14 @@ async def _persist_new_citations(paper_id: str, section_id: str, used: list[Any]
|
||||
if not tn or tn in existing_nums or tn in seen_new:
|
||||
continue
|
||||
seen_new.add(tn)
|
||||
ref_text = render_gbt7714(c)
|
||||
meta = _sanitize_citation(c)
|
||||
ref_text = render_gbt7714(meta)
|
||||
await db.execute(
|
||||
"INSERT INTO citations (paper_id, section_id, temp_n, kind, ref_text, source_doc_id, snippet, created_at) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(
|
||||
paper_id, section_id, tn, _kind_of(c), ref_text,
|
||||
c.get("source_doc_id") or "", c.get("snippet") or "", now_iso(),
|
||||
paper_id, section_id, tn, _kind_of(meta), ref_text,
|
||||
meta.get("source_doc_id") or "", meta.get("snippet") or "", now_iso(),
|
||||
),
|
||||
)
|
||||
new_citations.append({"temp_n": tn, "ref_text": ref_text})
|
||||
|
||||
@@ -139,6 +139,14 @@ async def _outline_message(db, session_id: str, outline: dict[str, Any]) -> list
|
||||
return blocks
|
||||
|
||||
|
||||
_CANCEL_WORDS = ("暂停", "取消", "停止", "不写了", "终止", "中止", "算了")
|
||||
|
||||
|
||||
def _is_cancel_request(text: str) -> bool:
|
||||
s = (text or "").strip()
|
||||
return bool(s) and any(w in s for w in _CANCEL_WORDS)
|
||||
|
||||
|
||||
async def _followup(db, session_id: str, text: str, paper_id: str, client: PaperMcpClient) -> list[dict[str, Any]]:
|
||||
prog = await client.call_tool("get_paper_progress", {"paper_id": paper_id})
|
||||
if not prog.get("ok"):
|
||||
@@ -147,6 +155,14 @@ async def _followup(db, session_id: str, text: str, paper_id: str, client: Paper
|
||||
return blocks
|
||||
|
||||
status = prog.get("status")
|
||||
if _is_cancel_request(text):
|
||||
res = await client.call_tool("cancel_paper", {"paper_id": paper_id})
|
||||
if res.get("ok"):
|
||||
blocks = [_text_block("论文已取消。如需重新撰写,请告诉我新主题。")]
|
||||
else:
|
||||
blocks = [_text_block(f"取消失败:{res.get('message', '')}")]
|
||||
await _write_message(db, session_id, blocks=blocks, text=blocks[0]["html"])
|
||||
return blocks
|
||||
if status == "complete":
|
||||
return await _revise(db, session_id, paper_id, text, client)
|
||||
if status == "cancelled":
|
||||
@@ -216,6 +232,9 @@ async def run_writing_loop(db, paper_id: str, session_id: str) -> None:
|
||||
if not prog.get("ok"):
|
||||
await _write_message(db, session_id, blocks=[_text_block("撰写进度查询失败。")], text="撰写失败")
|
||||
return
|
||||
if prog.get("status") == "cancelled":
|
||||
await _write_message(db, session_id, blocks=[_text_block("论文已取消。")], text="已取消")
|
||||
return
|
||||
sections = prog.get("sections") or []
|
||||
pending = [s for s in sections if s.get("status") != "done"]
|
||||
if not pending:
|
||||
|
||||
@@ -82,6 +82,22 @@ def test_write_section_creates_citations(paper_db, fake_llm, fake_search):
|
||||
assert "[1]" in sec["markdown"]
|
||||
|
||||
|
||||
def test_write_section_drops_fabricated_doi(paper_db, fake_llm, fake_search):
|
||||
"""杜撰的 doi/pmid/authors/journal 应被丢弃,强制 title kind 降级格式。"""
|
||||
paper_id = _propose(fake_llm, fake_search)["paper_id"]
|
||||
fake_llm.responses.append({
|
||||
"markdown": "正文[1]。",
|
||||
"citations_used": [{"temp_n": 1, "title": "水稻SPL研究", "authors": "X", "journal": "J",
|
||||
"year": "2020", "doi": "10.1234/fake", "pmid": "123", "source_doc_id": "d1"}],
|
||||
})
|
||||
r = _run(write.write_paper_section(paper_id, "s1"))
|
||||
assert r["ok"] is True
|
||||
cit = _run(dbmod.db.fetchone("SELECT * FROM citations WHERE temp_n = 1"))
|
||||
assert cit["kind"] == "title"
|
||||
assert "知识库资料" in cit["ref_text"]
|
||||
assert "10.1234" not in cit["ref_text"]
|
||||
|
||||
|
||||
def test_write_section_invalid_citation_retries_once(paper_db, fake_llm, fake_search):
|
||||
paper_id = _propose(fake_llm, fake_search)["paper_id"]
|
||||
fake_llm.calls.clear() # 重置计数,只统计写节调用(大纲已消费 1 次)
|
||||
|
||||
Reference in New Issue
Block a user