forked from admin/PlantAI-rag-new
- 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 单测)
113 lines
4.0 KiB
Python
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,
|
|
}
|