Files
PlantAI-rag-new/paper_writer_service/app/services/paper/outline.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

113 lines
4.0 KiB
Python

"""outline.py —— propose_paper_outline:建 papers 行 → search_refs 注入线索 → LLM 出大纲 → 预填 sections。"""
from __future__ import annotations
import uuid
from typing import Any
from ...db import db, dumps_json, now_iso
from . import llm, search
_OUTLINE_SYSTEM = (
"你是植物学论文撰写助手。根据用户给出的论文主题与写作要求,生成论文大纲。"
"只输出一个合法 JSON 对象,结构为:"
'{"title": 论文标题, "abstract_expect": 摘要预期内容(一句话), '
'"sections": [{"section_id": "s1", "title": "节标题", "key_points": ["要点1", "要点2"], "target_words": 1200}, ...]}。'
"章节 5-6 节,覆盖 引言/材料与方法/结果/讨论/结论 等综述常见结构;target_words 每节 1200-2500。"
)
_DEFAULT_SECTIONS = [
("s1", "引言"),
("s2", "材料与方法"),
("s3", "结果"),
("s4", "讨论"),
("s5", "结论与展望"),
]
async def propose_paper_outline(
topic: str,
requirements: str = "",
paper_type: str = "",
language: str = "",
length: str = "",
format_req: str = "",
style: str = "",
kb_hints: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
# 1. 知识库线索:优先用调用方传入的 kb_hints,否则现场 search_refs(失败静默降级)
hints = list(kb_hints or [])
if not hints:
try:
r = await search.search_refs(query=topic, top_k=8)
hints = list(r.get("items") or [])
except Exception:
hints = []
# 2. LLM 出大纲
user = f"论文主题:{topic}\n"
if paper_type:
user += f"论文类型:{paper_type}\n"
if requirements:
user += f"写作要求:{requirements}\n"
if language:
user += f"语言:{language}\n"
if length:
user += f"篇幅:{length}\n"
if format_req:
user += f"格式:{format_req}\n"
if style:
user += f"风格:{style}\n"
if hints:
user += "可参考知识库文献线索(仅选题依据,非硬性引用):\n" + "\n".join(
f"- {h.get('title', '')}: {str(h.get('snippet', ''))[:120]}" for h in hints[:5]
)
try:
outline = await llm.complete_json(_OUTLINE_SYSTEM, user)
except llm.LLMError as exc:
return {"ok": False, "code": "LLM_ERROR", "message": f"大纲生成失败:{exc}"}
title = str(outline.get("title") or topic).strip() or topic
sections = outline.get("sections") or []
if not sections:
sections = [
{"section_id": sid, "title": t, "key_points": [], "target_words": 1500}
for sid, t in _DEFAULT_SECTIONS
]
# 3. 落库 papers + paper_sections
paper_id = uuid.uuid4().hex[:16]
await db.execute(
"INSERT INTO papers (id, title, paper_type, language, format_req, style, status, brief_json, outline_json, created_at, updated_at) "
"VALUES (?, ?, ?, ?, ?, ?, 'outline', ?, ?, ?, ?)",
(
paper_id, title, paper_type, language, format_req, style,
dumps_json({
"topic": topic, "requirements": requirements, "length": length,
"kb_hints": hints,
}),
dumps_json({
"title": title,
"abstract_expect": outline.get("abstract_expect", ""),
"sections": sections,
}),
now_iso(), now_iso(),
),
)
for s in sections:
await db.execute(
"INSERT INTO paper_sections (id, paper_id, section_id, title, key_points, status, updated_at) "
"VALUES (?, ?, ?, ?, ?, 'pending', ?)",
(
uuid.uuid4().hex[:16], paper_id,
s.get("section_id") or "s1", s.get("title", ""),
dumps_json(s.get("key_points") or []), now_iso(),
),
)
return {
"ok": True,
"paper_id": paper_id,
"title": title,
"abstract_expect": outline.get("abstract_expect", ""),
"sections": sections,
}