refactor(skills): unify publication retrieval
This commit is contained in:
parent
5d9ee823b5
commit
ad522baf94
|
|
@ -90,6 +90,8 @@ yaml 是手填的,probe 用真实调用对账(basic_chat/parallel_tools/thinking
|
|||
### 3.5 Skill 系统(Anthropic 渐进披露)
|
||||
三层加载:Discovery(name+description,几百 token)→ Activation(`load_skill` 完整 SKILL.md)→ Execution(references 按需拉)。写 WHY+WHAT 不写 Step 1/2/3;description 决定触发。
|
||||
|
||||
**出版物检索统一入口**(2026-08-25):`literature` 按用户任务而非数据源划分 skill,统一论文、书籍、章节、会议论文、学位论文、报告、标准、专利、预印本及未来出版物的发现、获取、跨源去重与证据核验;`paper_server`、内部材料知识库及未来来源只是可替换后端。出版物类型与来源正交,同一出版物可有多条来源记录;跨源机械层只自动合并 DOI、ISBN+版次、标准号、公开号等精确键或题名+主要责任者+年份完全一致项,疑似重复、不同版次、章节、标准修订版和专利族成员保守保留。题录、摘要、片段、全文四级证据分开标注,下载成功不等于全文已经核验。取舍是不为每个数据库新增 skill,也不把检索编排固化成统一高级工具:skill 负责路由与判断,现有 helper/host-side tool 保持各自信任域,确定性标准化与精确去重下沉到可重建脚本。
|
||||
|
||||
**用户私有 skill**(2026-06-11):registry 收有序来源列表——内置 `ROOT/skills`(只读)+ 用户 `user_root/.skills`(可写)。取舍:① **user wins 同名覆盖**(核心用例是"copy 内置再改",覆盖只作用于本人会话,blast radius 锁死),覆盖显式标注不静默;② **创作走 host-side `save_skill`/`fork_skill`**——fs 工具的 base_dir 跨 backend 够不到 `user_root/.skills`,host 工具一个落点两模式通吃;③ 用户 skill 加载失败收进 `load_errors` 注入 prompt 提示修,不崩整次扫描。
|
||||
|
||||
**Skill 定向模型**(frontmatter `model:`,2026-07-06):内置 skill 可声明"该工作流用这个模型最好";当前未配置定向模型的 skill。**单一执行点**:对话中 `load_skill` 命中 → run 内热切(loop 换 `self.llm/self.caps`,下一轮生效)+ 持久化 task 模型;切失败降级原模型。取舍:**跳档位门控**(产品决策,任何档位可用);**只信内置 skill**(用户 skill 的 model 忽略,防自写 frontmatter 绕门控);**不自动切回**("skill 结束"不可判定);**不设 DB 开关、不做建 task 预切**——frontmatter 那一行本身就是热配置(删行即停,per-skill 粒度),全局开关是第二事实源、预切是第二执行点,都砍;**已选国际旗舰模型(unifyllm 网关族:Claude/GPT/Gemini)豁免切换**(2026-07-15)——定向 pin 本意是给较弱的国产默认模型托底产物质量,用户已主动选了旗舰模型则尊重其选择、不降级(判据:current profile 的 family==unifyllm)。
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。
|
||||
|
||||
最后更新:2026-08-24(Origin 图型扩展,bump 0.68.0)
|
||||
最后更新:2026-08-25(literature 统一出版物检索入口,未发版)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
| Phase | 标题 | 状态 | 备注 |
|
||||
|---|---|---|---|
|
||||
| 1-3 | 骨架 + Skill + run_python | ✅ | 多 skill(coding/proposal/ppt/research/documents/imagegen/videogen/review/patent);CoreCoder 唯一匹配 edit;敏感 env 过滤 |
|
||||
| 1-3 | 骨架 + Skill + run_python | ✅ | 多 skill(coding/proposal/ppt/literature/imagegen/videogen/review/patent);CoreCoder 唯一匹配 edit;敏感 env 过滤 |
|
||||
| 4 | 演化性能力 | 🟡 | Model Profile + Probing ✅;版本化 prompt 未做 |
|
||||
| 5 | Eval Suite | ⏸ 不做 | dogfooding 替代,probe 覆盖健康检查 |
|
||||
| 6 | 长任务工程化 | 🟡 | task + 恢复 ✅;双层记忆 ✅;context 压缩 ✅(加压力门槛);长会话中段折叠摘要 ✅(§8.8 Phase 2) |
|
||||
|
|
@ -20,6 +20,8 @@
|
|||
---
|
||||
## 已完成关键能力
|
||||
|
||||
- **08-25 / Unreleased / literature 统一出版物检索入口**:删除按数据源拆分的 `research` / `documents` skill,新增 `literature` 统一论文、书籍、章节及未来学术与技术出版物的发现、获取、跨源去重和证据核验;paper_server helper 归入新 skill,内部材料库客户端移到 host-side `tools/` 保持密钥隔离。新增开放出版物模型、来源与证据 references,以及只自动合并精确键、保守标记疑似重复的 `merge_publications.py`;`brief/paper/review/rebuttal/patent/standard/analyze` 和 Skill 清单统一改走新入口,不提升版本号。
|
||||
|
||||
- **08-24 / 0.68.0 / Origin 小提琴分布图**:`origin.plot@v2` / adapter 1.3.0 新增 `violin`,每条 `input/y` 系列作为一组原始观测,Worker 复制到受控全 Y 展示表并通过 Origin 2024 原生 `Violin.otpu` 一次性生成分类分布图;默认隐藏冗余图例,保留分类标签并为 X 轴标题预留底部空间。首版不开放逐系列 style、分裂/半小提琴及带箱线变体,避免把未稳定映射的模板属性写入公共契约。固定 QA 新增三组抗压强度分布、中位数 oracle、OPJU 重开与全格式复导;Origin 2024 / originpro 1.1.15 数据、视觉与进程释放门禁通过,Origin/合同/Job/Node 专项 126 项 unittest、Ruff 致命规则、py_compile、diff 检查与独立 adapter 打包通过,未连接或写入数据库。
|
||||
- **08-24 / 0.68.0 / Origin 材料谱图错位叠加**:`origin.plot@v2` / adapter 1.2.0 新增 `stacked_line`,面向 XRD、XPS 和光谱多曲线比较;请求显式声明间隔百分比,Worker 保留原始工作表并生成基线对齐的受控展示表,按全体最大谱幅确定逐条偏移,允许不同 X 采样点且不依赖节点模板。默认使用曲线右端直标并按画布定位标题,显式图例请求仍兼容。固定 QA harness 同步修正 OriginPro 列式读取、空白工作簿及短/长名称和有效列标签识别;Origin 2024 / originpro 1.1.15 真机生成、OPJU 重开、PNG/SVG/PDF 复导、偏移基线 oracle、视觉检查与进程释放通过。Origin/合同/Job/Node 专项 124 项 unittest、ruff 致命规则、py_compile 与独立 adapter 打包校验通过,未连接或写入数据库。
|
||||
- **08-24 / 0.67.0 / Origin 真机 QA harness MVP**:Origin adapter 新增独立 `acceptance.py`,以 annotations、2×2 Recipe、heatmap、surface_3d、stacked、band 六个固定合成用例通过生产 `worker.py` 子进程执行 OPJU/PNG/SVG/PDF 全格式任务,再由独立 Origin 会话重开工程、核对图层/系列/工作表/矩阵与固定数值 oracle 并复导三种图件;报告记录环境指纹、请求/产物摘要、像素尺寸和 Origin 进程释放结果,失败也原子落盘。独立 adapter 包同步携带验收脚本;Origin/合同/Job/Node 专项 121 项 unittest、ruff 致命规则、py_compile 与独立 adapter 打包校验通过,目标 Origin 2024 真机执行待部署节点完成,未连接或写入数据库。
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# zcbot Skill 清单
|
||||
|
||||
服务对象:中国建筑材料科学研究总院 —— 无机非金属材料 R&D(水泥 / 混凝土 / 玻璃 / 陶瓷 / 耐火 / 新型建材)
|
||||
最后更新:2026-08-25(research / documents 统一支持论文、书籍与章节查阅,并精简检索路由约束)
|
||||
Skill 总数:18
|
||||
最后更新:2026-08-25(literature 统一所有学术与技术出版物的检索、跨源去重和证据核验)
|
||||
Skill 总数:17
|
||||
|
||||
zcbot 的"skill"是一份可加载的工作流脚本(`skills/<name>/SKILL.md` + 配套 templates / scripts / Python helper),模型在识别用户意图后挂载对应 skill,按其内置的阶段化流程产出可交付物。本文档面向**使用方 / 协作方**,按"做什么、什么时候用、什么时候别用、典型产物"组织。
|
||||
|
||||
|
|
@ -22,9 +22,8 @@ zcbot 的"skill"是一份可加载的工作流脚本(`skills/<name>/SKILL.md` +
|
|||
| 科研写作 | [rebuttal](#rebuttal) | 回复审稿意见(修回):逐点回复信 + 稿件修改清单 + 修回 cover letter |
|
||||
| 演示出图 | [ppt](#ppt) | 生成可编辑 PowerPoint(SVG-first:逐页手写 SVG → 原生 DrawingML;总院红品牌模板可选) |
|
||||
| 演示出图 | [plot_pub](#plot_pub) | 出版级 matplotlib 学术图(中文 + viridis + 矢量 + 投稿级复合图设计纪律) |
|
||||
| 文献检索 | [research](#research) | 查 paper_server 中的论文、书籍与章节(题录 / 摘要 / 可用全文) |
|
||||
| 文献检索 | [documents](#documents) | 查内部 7 学科材料知识库(论文 / 书籍 / 全文语义检索) |
|
||||
| 文献检索 | [brief](#brief) | 科研方向简报:三路检索(research + 内部库取文献 / web 取动向)→ 重要论文列表(带摘要概述)+ 内容总结,只描述不给建议 |
|
||||
| 文献检索 | [literature](#literature) | 查论文、书籍、章节及其他出版物;统一 paper_server 与内部材料库,跨源去重并核验证据 |
|
||||
| 文献检索 | [brief](#brief) | 科研方向简报:literature 两路取文献 + web 取动向 → 重要论文列表(带摘要概述)+ 内容总结,只描述不给建议 |
|
||||
| 科研计算 | [pymatgen](#pymatgen) | 晶体结构 / XRD 模拟 / 相图 / Materials Project(host-side tool 持 key) |
|
||||
| 科研计算 | [stats_ml](#stats_ml) | 配方-性能建模与机器学习(三库分工) |
|
||||
| 内容生成 | [imagegen](#imagegen) | 豆包 Seedream 5.0 文生图 + 改图 i2i(¥0.22 / 张) |
|
||||
|
|
@ -51,11 +50,11 @@ zcbot 的"skill"是一份可加载的工作流脚本(`skills/<name>/SKILL.md` +
|
|||
**何时不用**:
|
||||
- ⛔ 只改 / 润色已有稿 → 走 review
|
||||
- ⛔ 写本子 / 申报书 → 走 proposal;写交底书 → patent;写标准 → standard
|
||||
- ⛔ 只查文献 → research / documents;只出图 → plot_pub
|
||||
- ⛔ 只查出版物 → literature;只出图 → plot_pub
|
||||
|
||||
**核心能力**:
|
||||
- 三类论文 × 中英双语的 IMRaD / 主题式骨架 + 篇幅预算(`paper_types.md`)
|
||||
- **引文三角核验**(`citation_verify.md`,移植 ARS 思路、后端换成自有 documents/research 库):存在性 → 三角印证 → 支撑度(抓原文比对 ≤25 词锚点,partial 就改论断迁就证据),编造引文零容忍
|
||||
- **引文三角核验**(`citation_verify.md`,移植 ARS 思路、检索接 literature 的 paper_server 与内部材料库):存在性 → 三角印证 → 支撑度(抓原文比对 ≤25 词锚点,partial 就改论断迁就证据),编造引文零容忍
|
||||
- "先定图表再写正文"纪律(接 plot_pub 出 figure)+ **图表纪律**(`figure_discipline.md`,移植 figure-planner:一图一论断、panel 角色分工、锚 panel、主图 vs 补充材料、图例四规则)+ 文献矩阵立证据底座
|
||||
- 写作顺序 Methods→Results→Intro→Discussion→Abstract→Title;关键章一段一卡 + 预告下一段
|
||||
- `quality_check.py`:结构 / 占位符 / 过度宣称 + **引文交叉核对**(orphan / uncited / 编号连续);docx/pdf 调平台渲染层 `rendering/render.py --profile paper`(中英字体切换 + 图题自增);`word_count.py` 按类型 × 语言核篇幅
|
||||
|
|
@ -155,7 +154,7 @@ zcbot 的"skill"是一份可加载的工作流脚本(`skills/<name>/SKILL.md` +
|
|||
|
||||
**何时不用**:
|
||||
- 从零起草新文本 → 走对应写作 skill(proposal / patent)
|
||||
- 事实核查 / 文献真伪 / 最新政策 → 先用 research / documents / web 核验
|
||||
- 事实核查 / 出版物真伪 / 最新政策 → 先用 literature / web 核验
|
||||
- 代码审查 → 走 coding
|
||||
|
||||
**审稿顺序**(先大后小,分层处理):
|
||||
|
|
@ -185,7 +184,7 @@ zcbot 的"skill"是一份可加载的工作流脚本(`skills/<name>/SKILL.md` +
|
|||
|
||||
**核心能力**:
|
||||
- **拆条分诊**:编辑要求 E.x / 审稿意见 Rn.m 原子化编号 → 8 类意见 × 4 档严重度 × 12 个动作标签(ACCEPT_TEXT / CLARIFY_EXISTING / SOFTEN_CLAIM / OUT_OF_SCOPE / AUTHOR_INPUT_NEEDED / BLOCKING...)逐条建 tracker
|
||||
- **零编造铁律**:不虚构实验 / 数据 / 行号 / 图号 / 引文 / "已修改"声明;用户腹稿"我们已经改了"类含糊陈述一律追问到位置和内容(中文备注九种含糊的转换表);补引文走 documents/research + 引文三角核验
|
||||
- **零编造铁律**:不虚构实验 / 数据 / 行号 / 图号 / 引文 / "已修改"声明;用户腹稿"我们已经改了"类含糊陈述一律追问到位置和内容(中文备注九种含糊的转换表);补引文走 literature + 引文三角核验
|
||||
- **先改稿后写信**:回复信对着改完的稿子写;每条回复三段式(原评论全文引用 → 头 1-2 句直说动作 → 修改后原文节选斜体)
|
||||
- **语气纪律**:合作但不卑;不同意走五步窄口径结构;拒做实验只用研究设计 / 范围边界作理由(时间经费借口禁用);"审稿人误解了"一律转写成稿件清晰度问题
|
||||
- **疑难案例库**:做不了的实验 / 审稿人事实错误 / 两审稿人打架 / 统计大修 / 合规缺件 / 转投稿 / appeal 识别
|
||||
|
|
@ -253,55 +252,30 @@ zcbot 的"skill"是一份可加载的工作流脚本(`skills/<name>/SKILL.md` +
|
|||
|
||||
## 文献检索
|
||||
|
||||
### research
|
||||
**检索独立 `paper_server` 项目中的论文、书籍和书籍章节。**
|
||||
### literature
|
||||
**检索、获取、合并与核验所有学术和技术出版物。**
|
||||
|
||||
`research` 是 zcbot 对 `paper_server` 的客户端适配,可取得题录、DOI、摘要及记录实际提供的 PDF/XML。适合跨学科发现、精确题名或 DOI 查找、出版类型识别和引文核验;材料领域任务可与 `documents` 并查。
|
||||
`literature` 把论文、书籍、章节、会议论文、学位论文、技术报告、标准、专利、预印本及未来接入的其他出版物视为统一对象;出版物类型与数据源分离。当前接入两个来源:
|
||||
|
||||
按任务所需证据深度选择题录、摘要或全文:结构化段落适合 XML,页码、公式、图表与版式核验适合 PDF。输出明确区分“仅题录”“摘要已核对”“全文已核对”。
|
||||
- `paper_server`:基于 OpenAlex 元数据,擅长 DOI、题名、作者、期刊、年份和跨学科发现,可取得摘要及记录实际提供的 PDF/XML。
|
||||
- 内部材料知识库:覆盖胶凝、陶瓷、玻璃、晶体、复合、耐火和检验检测,擅长跨语言全文语义检索、Markdown 正文片段和原件下载。
|
||||
|
||||
**四个 helper**:`search` / `get_paper` / `fetch_pdf` / `fetch_xml`。
|
||||
材料性能、工艺和表征优先内部材料库;精确 DOI 和跨学科发现优先 `paper_server`;系统调研、重要引用和关键论断并查可用来源。跨源结果先按 DOI、ISBN、标准号、公开号等类型标识去重,无稳定标识时只对题名、主要责任者和年份完全一致的记录自动合并;版本、章节、标准修订版和专利族成员保守保留。
|
||||
|
||||
**典型产物**:论文、书籍和章节候选清单,以及可核验的摘要、全文和引用条目。
|
||||
证据统一标记为 `metadata_only` / `abstract_verified` / `snippet_verified` / `fulltext_verified`。下载原件但未实际读取,不算全文已核对;出版物真实存在也不代表它支持当前论断。
|
||||
|
||||
---
|
||||
**主要能力**:`search` / `get_paper` / `fetch_pdf` / `fetch_xml`,以及 host-side `document_list_kb` / `document_search` / `document_download`;`merge_publications.py` 负责确定性的标准化、精确合并和疑似重复标记。
|
||||
|
||||
### documents
|
||||
**检索内部 7 个材料学科知识库中的论文、书籍及其他学术文档。**
|
||||
|
||||
按 `kb_name` 分为胶凝、陶瓷、玻璃、晶体、复合、耐火和检验检测 7 个材料学科库,提供跨语言语义检索、Markdown 正文片段和原件下载。
|
||||
|
||||
**7 大学科库**(`classification_id` 1-7):
|
||||
| 学科 | 内容 |
|
||||
|---|---|
|
||||
| 胶凝材料 | 水泥 / 混凝土 / 砂浆 |
|
||||
| 陶瓷基材料 | 结构 / 功能 / 先进陶瓷 |
|
||||
| 玻璃基材料 | 平板 / 光学 / 玻璃陶瓷 |
|
||||
| 晶体材料 | 单晶 / 多晶 / 晶体生长 |
|
||||
| 复合材料 | 纤维 / 颗粒 / 层状复合 |
|
||||
| 耐火材料 | 耐火砖 / 不定形 / 高温防护 |
|
||||
| 检验检测 | 表征方法 / 标准 / 仪器 |
|
||||
|
||||
**核心特色**:
|
||||
- **跨语言语义检索** —— 中英文查询均可用于发现学术文档
|
||||
- **Markdown 正文** —— 可直接筛选论点、数据和章节,必要时再读取原件
|
||||
|
||||
**何时用**:
|
||||
- 查材料领域论文、书籍、性能数据或工艺信息
|
||||
- 需要全文语义检索,或为申报书、方案、报告和综述寻找可核验证据
|
||||
|
||||
**关系**:与 `research` 互补。`documents` 擅长材料领域全文语义检索,`research` 擅长跨学科题录和 DOI 检索;重要任务可并查,并按 DOI 或规范化题名去重。论文或书籍的来源类型不作为排他路由条件。
|
||||
|
||||
**三个 host-side tool**:`document_list_kb` / `document_search` / `document_download`。只有宿主配置 `DOCUMENT_SEARCH_API_KEY` 时注册;key 不进入 sandbox。
|
||||
**典型产物**:多类型出版物候选清单、规范化 `publications.json`、可核验摘要或全文、引文与论断证据台账。
|
||||
|
||||
---
|
||||
|
||||
### brief
|
||||
**生成科研方向简报(重要文献速览)。**
|
||||
|
||||
给定一个研究方向 + 时间窗,从各大相关期刊(**Elsevier 数据库优先**)挑选近期**重要论文**,产出两段式简报:**先一份重要论文列表(每篇带标题/作者/期刊/年月/DOI + 一段简介或摘要概述),再对这批论文做内容总结**。三路取数:research(逐刊精确取最新 Elsevier 论文 + DOI)+ documents(内部材料库取全文)取文献,web search 取政策·标准·产业动向(单列)。**只描述不给建议**——呈现"发了什么、讲了什么",判断留给读者。简报 ≠ 综述论文,要**快、准、客观**,5–20 分钟掌握一个方向近期发了哪些重要论文。
|
||||
给定一个研究方向 + 时间窗,从各大相关期刊(**Elsevier 数据库优先**)挑选近期**重要论文**,产出两段式简报:**先一份重要论文列表(每篇带标题/作者/期刊/年月/DOI + 一段简介或摘要概述),再对这批论文做内容总结**。三路取数:`literature` 的 paper_server 与内部材料库取文献,web search 取政策·标准·产业动向(单列)。**只描述不给建议**——呈现"发了什么、讲了什么",判断留给读者。简报 ≠ 综述论文,要**快、准、客观**,5–20 分钟掌握一个方向近期发了哪些重要论文。
|
||||
|
||||
**五阶段**:定题对齐 spec(方向+边界 / 时间窗 / 期刊范围 / 深度 / 数据源 / 语言 / 关注点)→ 三路取数(research+documents 取文献、web 取动向;中→英术语转译 + 跨源去重)→ 列清单(带摘要概述)+ 内容总结 → 引文核验 → 渲染验收。
|
||||
**五阶段**:定题对齐 spec(方向+边界 / 时间窗 / 期刊范围 / 深度 / 数据源 / 语言 / 关注点)→ 三路取数(literature 两个来源取文献、web 取动向;中→英术语转译 + 跨源去重)→ 列清单(带摘要概述)+ 内容总结 → 引文核验 → 渲染验收。
|
||||
|
||||
**深度三档(按篇数)**:`flash` 10–20 篇 / `standard` 20–40 篇 / `deep` 40–80 篇。
|
||||
|
||||
|
|
@ -310,14 +284,14 @@ zcbot 的"skill"是一份可加载的工作流脚本(`skills/<name>/SKILL.md` +
|
|||
- ✅ 立项前想快速摸清一个方向近期发了哪些重要论文(产出可喂 proposal / analyze)
|
||||
|
||||
**何时不用**:
|
||||
- ⛔ 只要文献清单 / DOI / PDF → research / documents
|
||||
- ⛔ 只要出版物清单 / DOI / PDF → literature
|
||||
- ⛔ 要写可投稿的综述论文(几十页、定论)→ paper(review 类型)
|
||||
- ⛔ 要把模糊科学问题拆成子问题 + 路线图 → analyze
|
||||
- ⛔ 要"对本院的建议" / 写本子 → proposal
|
||||
|
||||
**核心能力**:
|
||||
- **逐刊取重要论文**(`references/journals.md`):各建材子领域主流期刊清单(Elsevier 优先),精确 `publication_name` + `year_gte` 取最新,0 命中降级到 keyword 搜;按重要性(期刊层级 + 主题相关性 + 发现分量)筛、按 publication_date 留最新
|
||||
- **三路分工 + 去重**:research+documents 取文献(同 DOI 一条、documents 全文优先)、web 单列产业政策动向不混论文总结;中文方向→英文术语转译(SCM/LC3 等缩写展开)
|
||||
- **三路分工 + 去重**:literature 的 paper_server + 内部材料库取文献(同 DOI 一条、正文更完整来源优先作内容证据)、web 单列产业政策动向不混论文总结;中文方向→英文术语转译(SCM/LC3 等缩写展开)
|
||||
- **每篇带摘要概述**:列表不只标题,每篇 2–4 句讲研究对象/方法/主要发现,基于 abstract 或全文、不夸张不评判
|
||||
- **引文核验**:存在性 / DOI 真伪(以库返回字段为准)/ 支撑度(摘要概述与原文一致,partial 改概述迁就证据),编造零容忍
|
||||
- **平台渲染层 `rendering/render.py --profile brief`**(docx/pdf):商务红主题 + 论文列表 `[n]` 作锚点、正文 `[n]`/`[Wn]` 引文上标回链 + DOI/URL 可点击超链接(条目内 DOI 子串也链)+ 化学式下标(CO₂/C₃S...,白名单不误伤 LC3/Ca2+);pdf 走沙盒 chromium;做 deck 转 ppt
|
||||
|
|
@ -460,7 +434,7 @@ zcbot 的"skill"是一份可加载的工作流脚本(`skills/<name>/SKILL.md` +
|
|||
### analyze
|
||||
**科学问题分析 / 拆解 / 引导。**
|
||||
|
||||
用户抛过来一个模糊的高层科研问题,本 skill 引导用户把它**翻译成可操作的子问题 + 实施路线图**,然后把每个子问题接力给合适的下游 skill(proposal / research / pymatgen / stats_ml ...)。
|
||||
用户抛过来一个模糊的高层科研问题,本 skill 引导用户把它**翻译成可操作的子问题 + 实施路线图**,然后把每个子问题接力给合适的下游 skill(proposal / literature / pymatgen / stats_ml ...)。
|
||||
|
||||
**本 skill 不直接产出最终交付物**(不查文献 / 不写本子 / 不跑模型),只产出 `analysis.md` —— 一个共识性的"问题理解 + 路线图"文件。
|
||||
|
||||
|
|
@ -474,7 +448,7 @@ zcbot 的"skill"是一份可加载的工作流脚本(`skills/<name>/SKILL.md` +
|
|||
- ⛔ 用户已经写明要做什么(配方对比 / 标准检测 / 文献综述 / 写本子)→ 直接走对应 skill
|
||||
- ⛔ 用户问通识知识 / 名词定义 → 直接答
|
||||
- ⛔ 用户要审稿 / 改文 → 走 review
|
||||
- ⛔ 用户做异常排查但已经定位到具体环节 → 走 research
|
||||
- ⛔ 用户做异常排查但已经定位到具体环节 → 走 literature
|
||||
|
||||
**四段工作流**:
|
||||
1. **PICO 化**(BLOCKING)—— P 对象 / I 干预 / C 对照 / O 输出(必须量化:指标 + 单位 + 期望值),跑 FINER 自检
|
||||
|
|
@ -539,13 +513,13 @@ zcbot 的"skill"是一份可加载的工作流脚本(`skills/<name>/SKILL.md` +
|
|||
|
||||
实际任务往往跨多个 skill,典型组合:
|
||||
|
||||
- **写论文全流程**:analyze(拆问题) → stats_ml / pymatgen(算数据出结果) → research / documents(建文献矩阵) → plot_pub(先定图表) → paper(逐章起草 + 引文三角核验) → review(投稿前反谄媚终审) → 投出后收到审稿意见 → rebuttal(修回)
|
||||
- **修回全流程**:rebuttal(拆条分诊 + 逐点回复信) → review(需大改的章节深审 / 英文润色子模式) → documents / research(审稿人要求补引文时检索核验) → plot_pub(要求补图 / 改图时)
|
||||
- **写本子全流程**:analyze(拆问题) → research / documents(查文献) → stats_ml(算配方-性能模型出预实验数据) → plot_pub(出图) → proposal(写本子) → review(审稿)
|
||||
- **写专利全流程**:patent(挖点 + 检索 + 起草) → research(查现有技术) → plot_pub(出附图) → review(终审)
|
||||
- **写标准全流程**:analyze(定标准化对象) → stats_ml(配方-性能 / 精密度试验数据定指标) → research / documents(查国内外现有标准与现状) → standard(起草标准 + 编制说明) → plot_pub(出图) → review(送审前终审)
|
||||
- **写论文全流程**:analyze(拆问题) → stats_ml / pymatgen(算数据出结果) → literature(建文献矩阵) → plot_pub(先定图表) → paper(逐章起草 + 引文三角核验) → review(投稿前反谄媚终审) → 投出后收到审稿意见 → rebuttal(修回)
|
||||
- **修回全流程**:rebuttal(拆条分诊 + 逐点回复信) → review(需大改的章节深审 / 英文润色子模式) → literature(审稿人要求补引文时检索核验) → plot_pub(要求补图 / 改图时)
|
||||
- **写本子全流程**:analyze(拆问题) → literature(查出版物) → stats_ml(算配方-性能模型出预实验数据) → plot_pub(出图) → proposal(写本子) → review(审稿)
|
||||
- **写专利全流程**:patent(挖点 + 检索 + 起草) → literature(查现有技术出版物) → plot_pub(出附图) → review(终审)
|
||||
- **写标准全流程**:analyze(定标准化对象) → stats_ml(配方-性能 / 精密度试验数据定指标) → literature(查国内外现有标准与现状) → standard(起草标准 + 编制说明) → plot_pub(出图) → review(送审前终审)
|
||||
- **方向简报 → 立项**:brief(三路取数,出重要论文列表 + 内容总结,只描述) → analyze(把方向拆成子问题 + 路线图) → proposal(写本子、给建议) / paper(写综述);简报要做成汇报 → ppt
|
||||
- **PPT 汇报**:analyze(提炼论点) → research / documents(找数据 + 引文) → plot_pub(出图) → ppt(组装 deck) → imagegen(可选,做封面 / 引子页)
|
||||
- **PPT 汇报**:analyze(提炼论点) → literature(找数据 + 引文) → plot_pub(出图) → ppt(组装 deck) → imagegen(可选,做封面 / 引子页)
|
||||
- **晶体计算**:pymatgen(算 XRD / 相图) → plot_pub(出图) → proposal / patent(写到本子 / 交底书里)
|
||||
- **定制能力**:skill-creator(fork 某内置 skill,如 ppt / proposal) → 改造成本组 / 本人专属版本(术语 / 模板 / 默认值),之后日常任务直接用改造版
|
||||
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ _CONTAINER_ENV = {
|
|||
"HOME": "/tmp",
|
||||
}
|
||||
|
||||
# research skill 在容器里直连 paper_server,这两个 env 按需透传(host .env → docker exec -e)。
|
||||
# literature skill 在容器里直连 paper_server,这两个 env 按需透传(host .env → docker exec -e)。
|
||||
# PAPER_SERVER_API_KEY 是唯一刻意放进 sandbox 的凭证(低价值只读文献库 key,可随时在
|
||||
# paper_server admin 撤销),与「Bocha/ARK 等高价值 key 不入容器」的规矩不冲突;
|
||||
# PAPER_SERVER_URL 顺带透传,修掉 host 覆盖 URL 时 docker 模式仍用硬编码默认值的缺口。
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""Smoke: paper_server → zcbot research skill 三步链路。
|
||||
"""Smoke: paper_server → zcbot literature skill 三步链路。
|
||||
|
||||
跑法: .venv/Scripts/python.exe scripts/smoke_paper_skill.py
|
||||
|
||||
|
|
@ -33,7 +33,7 @@ if env_file.exists():
|
|||
k, _, v = line.partition("=")
|
||||
os.environ.setdefault(k.strip(), v.strip())
|
||||
|
||||
from skills.research.paper import _BASE_URL, fetch_pdf, fetch_xml, get_paper, search
|
||||
from skills.literature.paper import _BASE_URL, fetch_pdf, fetch_xml, get_paper, search
|
||||
|
||||
|
||||
def _hr(title: str) -> None:
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ description: 科学问题分析 / 拆解 / 引导。用户提出模糊的高层
|
|||
- 用户已经写明白要做什么(配方对比 / 标准检测 / 文献综述 / 写本子)→ 直接走对应 skill
|
||||
- 用户问通识知识 / 名词定义 → 直接答
|
||||
- 用户要审稿 / 改文 → 走 review
|
||||
- 用户要做异常排查但已经定位到具体环节(只差查文献验证) → 走 research
|
||||
- 用户要做异常排查但已经定位到具体环节(只差查出版物验证) → 走 literature
|
||||
|
||||
## 资源
|
||||
|
||||
|
|
@ -106,10 +106,10 @@ description: 科学问题分析 / 拆解 / 引导。用户提出模糊的高层
|
|||
|
||||
- 阶段一不让用户确认 PICO 就开拆解(O 没量化的 issue tree 拆出来全是空话)
|
||||
- Issue tree 拆到 4 层以上(LLM 容易堆细节,3 层足够 R&D 决策用)
|
||||
- 叶子节点写"用 research skill"/"用 stats_ml skill"(skill 名硬编码,将来 skill 改名要回来改;用能力描述代替)
|
||||
- 叶子节点写"用 literature skill"/"用 stats_ml skill"(skill 名硬编码,将来 skill 改名要回来改;用能力描述代替)
|
||||
- 用户已经知道要做什么时强行让填 PICO(走对应 skill 即可,不要浪费时间)
|
||||
- 真去执行子问题(查文献 / 算 XRD / 跑回归) —— analyze 只做"想清楚",执行交棒下游
|
||||
- 编造文献 / 数据支撑某个分支的可行性(不知道就标 `<TODO>` + 建议接 research 验证)
|
||||
- 编造出版物 / 数据支撑某个分支的可行性(不知道就标 `<TODO>` + 建议接 literature 验证)
|
||||
- TRIZ / DoE 全套强推 —— 只在叶子节点性质匹配时用对应分支,不为了用框架而用
|
||||
- 报告写成学术综述(分支只是工具,产物是路线图不是 paper)
|
||||
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ P42.5 早强偏低
|
|||
|
||||
- 拆出的叶子节点之间有重叠(违反 MECE 的 ME)—— 例:既有"原料 SiO2 含量"又有"配料 SiO2 比例",合并
|
||||
- 漏掉显然该有的分支(违反 MECE 的 CE)—— 例:讨论强度问题但漏了"养护条件"
|
||||
- 叶子标"用 research skill"/"用 stats_ml skill" —— 写能力描述,不写 skill 名(改名要回来改)
|
||||
- 叶子标"用 literature skill"/"用 stats_ml skill" —— 写能力描述,不写 skill 名(改名要回来改)
|
||||
- 叶子优先级全标"高" —— 假装 MECE,等于没排
|
||||
- 拆到 4 层才发现叶子全是 `<TODO>` —— 说明 PICO 阶段问题没问清,回阶段一
|
||||
- 拆完 tree 自己开始查文献做分析 —— analyze 只拆,执行交棒下游
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ PICO 填完后,5 个维度各给 1-5 分 + 一句话说明:
|
|||
|
||||
- 用户问的是**文献调研类**("综述 X 领域") —— 直接 research,PICO 强行套反而别扭
|
||||
- 用户问的是**工程实施类**("怎么把 X 推到生产") —— 走工艺优化路线,PICO 仍可用但 I/C 改成"工艺参数 vs 现行工艺"
|
||||
- 用户的问题本身就是**工具选型**("用什么仪器测 X") —— 不需要 PICO,直接给经验答 / 走 research
|
||||
- 用户的问题本身就是**工具选型**("用什么仪器测 X") —— 不需要 PICO,直接给经验答 / 走 literature
|
||||
|
||||
## 拒绝信号(说明问题还没准备好)
|
||||
|
||||
|
|
|
|||
|
|
@ -91,5 +91,5 @@ Why 5: ... → ...
|
|||
- Fishbone 列了 30 条 —— 大杂烩,失去优先级意义;每支 2-5 条,总数 < 20
|
||||
- "原因"和"现象"混用(原因是过程:"温度漂移",不是名词标签:"温度") —— 拆错粒度
|
||||
- 标"高优先"但没给优先理由 —— 写一句"为什么这条优先"
|
||||
- 在 analyze skill 内部自己跑 5 Whys 自圆其说(模型自答自证) —— 验证步骤交棒下游(research 查文献 / 实验设计走 DoE)
|
||||
- 在 analyze skill 内部自己跑 5 Whys 自圆其说(模型自答自证) —— 验证步骤交棒下游(literature 查出版物 / 实验设计走 DoE)
|
||||
- 假设单根因(只有一条原因) —— 工程问题常多根因叠加,Fishbone 阶段就要并列多个高优先项
|
||||
|
|
|
|||
|
|
@ -84,4 +84,4 @@
|
|||
|
||||
---
|
||||
|
||||
**下一步建议**:<由 LLM 按当下 skill 清单匹配,例:"先用 research 能力查证假设 X,完成后再回 analyze 更新 §5">
|
||||
**下一步建议**:<由 LLM 按当下 skill 清单匹配,例:"先用 literature 能力查证假设 X,完成后再回 analyze 更新 §5">
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ description: 生成科研方向简报(research direction briefing / 重要文献
|
|||
|
||||
## 边界(免得和别的 skill 撞)
|
||||
|
||||
- vs `research`/`documents`:它们**只取文献**;brief 把取回的论文**组织成可读列表 + 客观总结**。
|
||||
- vs `literature`:它**只取出版物并核验证据**;brief 把取回的论文**组织成可读列表 + 客观总结**。
|
||||
- vs `paper`(review):paper 写**可投稿综述**(几十页、定论);brief 出**轻量速览**(几页、客观、不给判断)。
|
||||
- vs `analyze`:analyze 拆**科学问题**;brief 围绕**已定方向**列近期重要论文。
|
||||
- vs `proposal`:proposal 写**本子、给建议**;brief 只列论文 + 客观总结。要"对本院的建议" → 转 proposal。
|
||||
|
|
@ -35,18 +35,18 @@ description: 生成科研方向简报(research direction briefing / 重要文献
|
|||
2. **时间窗**:默认**近 1 年**(简报是"最新文献",窗口宜短);换算成 `year_gte`(今年见 system prompt)
|
||||
3. **期刊范围**:默认按方向所属子领域取 `journals.md` 主流期刊(Elsevier 优先);用户可增删指定刊
|
||||
4. **深度 / 篇数**:`flash` 10–20 篇 / `standard`(默认)20–40 篇 / `deep` 40–80 篇
|
||||
5. **数据源(默认三路并用)**:research + documents **都是获取文献的主力**(research 按期刊精确取最新 Elsevier 论文 + DOI;documents 取内部材料库全文),web search 取政策·标准·产业动向(**单列、不混进论文总结**)。某一路不可用时降级用其余两路,不整体放弃
|
||||
5. **数据源(默认三路并用)**:`literature` 的 paper_server + 内部材料库**都是获取文献的主力**(前者按期刊精确取最新 Elsevier 论文 + DOI,后者取内部材料库全文),web search 取政策·标准·产业动向(**单列、不混进论文总结**)。某一路不可用时降级用其余来源,不整体放弃
|
||||
6. **语言**:中文(默认)/ 英文
|
||||
7. **特殊关注点**(可选):想重点呈现的材料体系 / 方法(仍只描述,不给建议)
|
||||
|
||||
## 阶段二:三路取数(research + documents 取文献 / web 取动向)
|
||||
## 阶段二:三路取数(literature 两个来源取文献 / web 取动向)
|
||||
|
||||
**先读 `references/journals.md`**。**中文方向先转专业英文术语**(库主语料英文):低碳水泥→low-carbon cement / clinker substitution;SCM→supplementary cementitious materials / fly ash / GGBFS / calcined clay;LC3→limestone calcined clay cement;碳化养护→CO2 curing / carbonation。缩写与全称都试。
|
||||
|
||||
**research(逐刊取最新 Elsevier 论文 + DOI)** —— `run_python`:
|
||||
**literature / paper_server(逐刊取最新 Elsevier 论文 + DOI)** —— `run_python`:
|
||||
|
||||
```python
|
||||
from skills.research.paper import search
|
||||
from skills.literature.paper import search
|
||||
# 逐刊拉最新:publication_name 精确匹配 + 时间窗;list 自带 abstract,看前 200-400 字判切题与分量
|
||||
for jname in ["Cement and Concrete Research", "Cement and Concrete Composites",
|
||||
"Construction and Building Materials", "Journal of Cleaner Production"]:
|
||||
|
|
@ -55,12 +55,12 @@ for jname in ["Cement and Concrete Research", "Cement and Concrete Composites",
|
|||
```
|
||||
某刊精确名 0 命中 → 换 `keyword=<方向英文术语>` 再搜,从返回里挑 `publication_name` 命中目标刊的;仍空记"该刊本窗口库内无收录"。
|
||||
|
||||
**documents(内部材料库取全文,材料类首选)** —— host-side tool `document_search`,中英 query 都行(后端跨语言语义检索);胶凝材料库 `classification_id=1`。取 `md_content` 既做候选也供引文核验抓锚点最顺。
|
||||
**literature / 内部材料库(取全文,材料类首选)** —— host-side tool `document_search`,中英 query 都行(后端跨语言语义检索);胶凝材料库 `classification_id=1`。取 `md_content` 既做候选也供引文核验抓锚点最顺。
|
||||
|
||||
**web search(取动向)** —— 政策(双碳/碳配额)、标准(新国标/团标)、行业会议、企业产线中试。**单列"其他动向",不混进论文列表与总结**。
|
||||
|
||||
- 汇成证据表 `<task_dir>/evidence.md`:期刊 | 标题 | 第一作者(机构)| 年-月 | 摘要概述 | DOI | 来源(research/documents/web)。
|
||||
- 跨源去重:同 DOI 一条(documents 全文优先,DOI 记自 research);web 不与论文去重、单列。
|
||||
- 汇成证据表 `<task_dir>/evidence.md`:期刊 | 标题 | 第一作者(机构)| 年-月 | 摘要概述 | DOI | 来源(paper_server/materials_library/web)。
|
||||
- 跨源去重:按 `literature` 规则合并,同 DOI 一条(内部材料库全文优先作内容证据,paper_server 的 DOI 元数据优先);web 不与论文去重、单列。
|
||||
|
||||
> **context 纪律(省时省钱,务必遵守)**:检索结果(尤其全文 abstract)**落进 `evidence.md` / `selected_papers.json` 文件**,**不要在对话里反复 `run_python`/`print` 把整批 abstract 灌进上下文**。工具输出会永久留在 context 并每轮重发——同一批摘要 dump 三次,context 就滚成雪球(实测一次简报因此累计烧 2.5M 输入 token、跑满超时被掐断)。需要看某几篇时按需 `read` 文件片段,看完即弃,别整批重打。
|
||||
|
||||
|
|
@ -89,9 +89,9 @@ for jname in ["Cement and Concrete Research", "Cement and Concrete Composites",
|
|||
|
||||
## 阶段四:引文核验(渲染前必跑)
|
||||
|
||||
论文直接来自 research/documents,DOI 以**库返回字段为准**(不沿用记忆、不编造)。逐条核验:
|
||||
论文直接来自 `literature`,DOI 以**来源返回字段为准**(不沿用记忆、不编造)。逐条核验:
|
||||
|
||||
1. **存在性**:`search()`/`get_paper(doi)` 或 documents 命中确认真实存在;查不到 → 标 `[未核实]`,告诉用户"找不到来源,请提供 DOI 或删去",**不编造**。
|
||||
1. **存在性**:`search()`/`get_paper(doi)` 或内部材料库命中确认真实存在;查不到 → 标 `[未核实]`,告诉用户"找不到来源,请提供 DOI 或删去",**不编造**。
|
||||
2. **支撑度**:摘要概述 / `[n]` 论断要和 abstract(或全文)一致;不一致 → **改概述迁就证据**,不是改证据。
|
||||
3. **web**:记原始 URL + 访问日期 + 发布机构,标"截至 <日期>";不当学术结论引。
|
||||
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
---
|
||||
name: documents
|
||||
description: 检索内部 7 个材料学科知识库中的论文、书籍及其他学术文档。适合材料领域主题检索、全文语义检索、性能或工艺数据查找;可与 research(paper_server)并用并按 DOI 或题名去重。
|
||||
---
|
||||
|
||||
# Documents
|
||||
|
||||
`documents` 是内部材料知识库的客户端 skill。知识库覆盖胶凝、陶瓷、玻璃、晶体、复合、耐火和检验检测,提供跨语言语义检索、Markdown 正文片段和原件下载。
|
||||
|
||||
## 选择与协作
|
||||
|
||||
- 材料领域主题、性能数据或全文语义检索:优先使用 `documents`。
|
||||
- 跨学科发现、精确 DOI 或题录检索:优先使用 `research`。
|
||||
- 系统调研、综述或重要引用:可同时查询两者;按 DOI 去重,无 DOI 时按规范化题名、作者和年份去重。
|
||||
- 用户已提供文件或个人知识库路径:直接读取该来源。
|
||||
|
||||
论文、书籍和书籍章节都可作为候选,来源类型本身不决定使用哪个 skill;以相关性、元数据完整度和可核验正文为准。
|
||||
|
||||
## 工具
|
||||
|
||||
本 skill 使用宿主工具,API Key 不进入 sandbox:
|
||||
|
||||
- `document_list_kb()`:列出当前有效知识库。用户未指定材料方向且需要缩窄库范围时调用。
|
||||
- `document_search(queries, kb_names=None, classification_ids=None, max_documents=6, content_chars_per_doc=1200)`:批量搜索,返回文件元数据与截断的 `md_content`。中文和英文均可,专业术语可同时准备中英文表达。
|
||||
- `document_download(items)`:把选定原件下载到当前任务的 `documents/` 目录。`items` 使用搜索结果中的 `file_name` 和 `kb_name`。
|
||||
|
||||
工具会自动限制批量大小、去重并控制返回体积。需要更多正文时,对少量高相关候选增加 `content_chars_per_doc`,或下载原件后读取。
|
||||
|
||||
## 工作流
|
||||
|
||||
1. 根据用户问题形成少量有区分度的查询;需要分类时先列知识库。
|
||||
2. 搜索并依据题名、文件名、正文片段和库来源筛选候选。
|
||||
3. 相关性判断可使用片段;论点、数据、公式、表格、章节或页码核验应读取足够正文或原件。
|
||||
4. 与 `research` 并查时合并去重,优先保留正文更完整、元数据更可靠的记录。
|
||||
5. 输出时区分“仅题录”“片段已核对”“全文已核对”,并标注可追溯的文件名、DOI 或其他真实标识。
|
||||
|
||||
## 失败与真实性
|
||||
|
||||
- 工具不可用或认证失败时,说明 `document_search` 当前不可用,并改用 `research` 或用户提供的文件。
|
||||
- 未命中时可调整术语、语言或库范围;没有新增检索思路时如实说明覆盖不足。
|
||||
- 只使用工具返回的 `file_name`、`kb_name` 和来源信息,不推测缺失的作者、DOI、ISBN、页码或正文内容。
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
---
|
||||
name: literature
|
||||
description: 检索、获取、合并与核验各类学术和技术出版物,包括期刊论文、书籍、书籍章节、会议论文、学位论文、技术报告、标准、专利、预印本及未来接入的其他出版物。适用于主题检索、精确题名/作者/DOI/ISBN/标准号查找、全文获取、跨来源去重、引文真实性和论断支撑度核验;按任务选择 paper_server、内部材料知识库及其他可用来源。不用于从零撰写论文、标准、专利或科研简报。
|
||||
---
|
||||
|
||||
# Literature
|
||||
|
||||
把出版物视为统一检索对象,把 `paper_server`、内部材料知识库和未来来源视为可替换的数据后端。按用户问题选择来源、证据深度和输出形式,不按来源拆成多个 skill。
|
||||
|
||||
## 边界
|
||||
|
||||
- 本 skill 负责发现、精确查找、题录识别、原件获取、跨源去重和证据核验。
|
||||
- 论文、书籍、章节、会议论文、学位论文、报告、标准、专利和预印本都可作为候选;新类型保留来源原始类型,不强行归为论文。
|
||||
- 写论文或综述使用 `paper`,生成科研简报使用 `brief`,写标准使用 `standard`,写专利交底书使用 `patent`;这些 skill 可消费本 skill 的证据。
|
||||
- 用户已提供文件或个人知识库路径时,直接读取该来源;只有任务要求补充、核验或扩展出版物时再检索。
|
||||
|
||||
## 按需读取
|
||||
|
||||
- 任何任务先读 `references/publication-model.md`,统一类型、标识符、来源和证据字段。
|
||||
- 使用 `paper_server` 时读 `references/source-paper-server.md`。
|
||||
- 使用内部材料知识库时读 `references/source-materials-library.md`。
|
||||
- 合并两个以上来源或处理版本、章节、标准、专利时读 `references/deduplication.md`。
|
||||
- 核验引文、数据、公式、表格、页码或具体论断时读 `references/evidence-verification.md`。
|
||||
|
||||
## 来源选择
|
||||
|
||||
- 材料主题、性能、配方、工艺、表征或全文语义检索:优先内部材料知识库。
|
||||
- DOI、题名、作者、期刊、年份或跨学科发现:优先 `paper_server`。
|
||||
- 系统调研、综述、重要引用或关键论断:并查可用来源。
|
||||
- 用户指定来源:只查指定来源,除非该来源无法满足用户要求并需要说明降级方案。
|
||||
- 某一路不可用:继续使用其他来源,并明确实际覆盖范围。
|
||||
|
||||
出版物类型与来源是正交字段。例如一本书可来自内部材料库,一篇论文可同时来自两个后端;来源不同不等于出版物不同。
|
||||
|
||||
## 工作流
|
||||
|
||||
1. 识别任务是主题发现、精确查找、原件获取还是论断核验;提取类型、主题、时间、作者、载体和标识符约束。
|
||||
2. 形成少量有区分度的中英文查询。英文题名占多数的来源优先使用标准英文术语,同时保留缩写、全称和必要同义词。
|
||||
3. 按来源规则搜索并筛选候选。题名只用于初筛;摘要、正文片段或全文决定能否支持具体论断。
|
||||
4. 将多来源原始结果分别保存为 JSON,需要合并时运行:
|
||||
|
||||
```bash
|
||||
python <skill_dir>/scripts/merge_publications.py \
|
||||
--input paper_server=raw-paper-server.json \
|
||||
--input materials_library=raw-materials-library.json \
|
||||
--output publications.json
|
||||
```
|
||||
|
||||
5. 只对精确标识或完全一致的题名、主要责任者和年份自动合并;疑似重复保留并标记。不同版次、章节、标准修订版和专利族成员不得直接覆盖。
|
||||
6. 根据问题所需深度读取摘要、片段或全文。下载原件但没有实际读取,不得写成“全文已核对”。
|
||||
7. 输出真实题名、责任者、年份、载体、标识符、来源和证据等级,并列出未核实字段、来源冲突和可能重复项。
|
||||
|
||||
## 证据等级
|
||||
|
||||
- `metadata_only`:仅题录或标识符已核对。
|
||||
- `abstract_verified`:摘要已读取并核对。
|
||||
- `snippet_verified`:相关正文片段已读取并核对。
|
||||
- `fulltext_verified`:全文中的相关部分已读取并核对。
|
||||
|
||||
出版物真实存在不代表它支持当前论断。支撑不足时缩窄或删除论断,不修改证据迁就论断。
|
||||
|
||||
## 失败与真实性
|
||||
|
||||
- 认证、网络或来源不可用时,指出具体来源和失败类型,不把单源失败写成全部文献不可用。
|
||||
- 未命中时可调整术语、语言、类型或过滤条件;没有新的检索思路时如实说明覆盖不足。
|
||||
- 只使用来源实际返回的题名、责任者、标识符、版本、页码和正文,不推测缺失字段。
|
||||
- 不把搜索摘要当作者摘要,不把目录命中当章节正文,不把下载成功当内容已经核验。
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
interface:
|
||||
display_name: "文献与出版物"
|
||||
short_description: "检索、获取、合并、去重并系统核验各类学术与技术出版物"
|
||||
default_prompt: "Use $literature to find and verify publications for this research question."
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
"""paper_server 客户端 helper。base_url 默认硬编码,env 可覆盖。"""
|
||||
"""literature skill 的 paper_server 客户端 helper。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
# 出版物去重
|
||||
|
||||
自动合并必须保守。先规范化标识符,再按出版物类型选择键;不确定时保留两条并标记可能重复。
|
||||
|
||||
## 精确键
|
||||
|
||||
- 论文、预印本、章节:规范化 DOI。
|
||||
- 书籍:规范化 ISBN 与版次;不同版次不合并。
|
||||
- 会议论文:DOI,或完全一致的题名、主要责任者、会议和年份。
|
||||
- 学位论文:完全一致的题名、作者、授予机构和年份。
|
||||
- 标准:发布机构、标准号和年份或版本。
|
||||
- 专利:公开号;申请号和专利族只用于关联。
|
||||
- 技术报告:发布机构和报告号。
|
||||
|
||||
DOI 去除 `doi:`、`https://doi.org/`、空白和末尾标点,并转为小写。ISBN 去除分隔符并统一 ISBN-10/13 表示。
|
||||
|
||||
## 无稳定标识符
|
||||
|
||||
只有规范化题名、主要责任者和年份完全一致时自动合并。题名高度相似但字段不全或存在冲突时,只加入 `possible_duplicates`。
|
||||
|
||||
章节不得与整书合并;不同版本、译本、标准修订版和专利族成员不得因题名相似自动覆盖。
|
||||
|
||||
## 合并优先级
|
||||
|
||||
- 保留所有来源的 `backend` 和 `source_id`。
|
||||
- 正式标识元数据优先于文件名推断。
|
||||
- 正文更完整的来源用于内容证据,但不因此覆盖另一来源更可靠的题录。
|
||||
- 非空字段冲突时保留首条值并记录 `conflicts`,不静默改写。
|
||||
|
||||
使用 `scripts/merge_publications.py` 做机械规范化和精确合并。学术相关性、版本关系和论断支撑度仍由模型基于原始记录判断。
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
# 出版物证据核验
|
||||
|
||||
把“出版物真实存在”和“出版物支持当前论断”分开核验。
|
||||
|
||||
## 四级证据
|
||||
|
||||
- `metadata_only`:只核对题录或标识符。
|
||||
- `abstract_verified`:已读取摘要并核对。
|
||||
- `snippet_verified`:已读取相关正文片段并核对。
|
||||
- `fulltext_verified`:已读取全文中的相关部分并核对。
|
||||
|
||||
文件已经下载但未读取时仍不能标记 `fulltext_verified`。搜索引擎生成的摘要不是作者摘要,目录或题名命中不是正文证据。
|
||||
|
||||
## 三步核验
|
||||
|
||||
1. **存在性**:从可用来源取得真实题名、责任者、年份、载体和标识符;查不到则标记未核实。
|
||||
2. **交叉印证**:关键论断尽量由两个独立来源确认题录;字段冲突以可核验的正式标识元数据为准,并保留冲突记录。
|
||||
3. **支撑度**:定位与论断相关的摘要或正文锚点,判定为 `support`、`partial` 或 `not_support`。
|
||||
|
||||
`partial` 时缩窄论断使其与证据一致;`not_support` 时删除引用或更换出版物。无法取得正文时可以用摘要做弱核验,但必须明确等级。
|
||||
|
||||
## 特殊出版物
|
||||
|
||||
- 书籍:核对具体版次和章节,不能用整书题录替代章节证据。
|
||||
- 标准:核对发布机构、标准号、年份和有效版本,具体要求需定位条款。
|
||||
- 专利:核对公开号、申请人和公开日期;权利要求、实施例和法律状态不能互相替代。
|
||||
- 报告与学位论文:核对发布或授予机构,避免把二次转载当原始来源。
|
||||
|
||||
输出时为每条关键证据保留来源标识、核验层级和必要的位置说明。不得为补齐引用而推测出版信息。
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
# 统一出版物模型
|
||||
|
||||
把“出版物是什么”和“从哪里取得”分开记录。保留来源原始字段;统一字段只用于跨源整理,不替代原始记录。
|
||||
|
||||
## 类型
|
||||
|
||||
使用以下开放枚举:
|
||||
|
||||
- `article`:期刊论文
|
||||
- `book`:书籍、专著
|
||||
- `book_chapter`:书籍章节
|
||||
- `conference_paper`:会议论文
|
||||
- `proceedings`:会议论文集
|
||||
- `thesis`:学位论文
|
||||
- `report`:技术或研究报告
|
||||
- `standard`:标准
|
||||
- `patent`:专利文献
|
||||
- `preprint`:预印本
|
||||
- `dataset_publication`:数据集出版记录
|
||||
- `other`:尚未归类的出版物
|
||||
|
||||
同时保留 `raw_type`。无法可靠映射时使用 `other`,不凭题名猜测。
|
||||
|
||||
## 统一字段
|
||||
|
||||
```yaml
|
||||
type: article
|
||||
raw_type: journal-article
|
||||
title: ""
|
||||
creators: []
|
||||
issued: {year: null, month: null, day: null}
|
||||
container: {title: "", type: "", volume: "", issue: "", pages: ""}
|
||||
publisher: {name: "", place: ""}
|
||||
edition: {label: "", number: null}
|
||||
identifiers:
|
||||
doi: ""
|
||||
isbn: []
|
||||
issn: []
|
||||
standard_number: ""
|
||||
patent_application_number: ""
|
||||
patent_publication_number: ""
|
||||
report_number: ""
|
||||
relations:
|
||||
is_part_of: ""
|
||||
has_parts: []
|
||||
edition_of: ""
|
||||
translation_of: ""
|
||||
patent_family: ""
|
||||
language: ""
|
||||
abstract: ""
|
||||
access: {has_abstract: false, has_snippet: false, has_fulltext: false, formats: [], local_paths: []}
|
||||
evidence_level: metadata_only
|
||||
sources: []
|
||||
```
|
||||
|
||||
`creators` 可包含 `name`、`role` 和来源提供的 ORCID 等标识。`container` 表示论文所属期刊、章节所属书籍或会议论文所属论文集。
|
||||
|
||||
## 层级与版本
|
||||
|
||||
- 章节与整书是不同记录,通过 `container` 或来源关系关联。
|
||||
- 不同版次是不同记录;电子版、精装和平装是否属于同一版次,以版次和来源元数据判断。
|
||||
- 预印本与期刊版、会议版与扩展期刊版可建立关系,但不自动互相覆盖。
|
||||
- 标准修订版、修改单和旧版分别保留。
|
||||
- 专利族表示关联,不表示各公开号是重复记录。
|
||||
|
||||
## 溯源
|
||||
|
||||
每个 `sources` 条目至少保留 `backend`、`source_id` 和检索时间。原始批量结果单独落盘;统一记录不重复嵌入大段摘要或全文,避免文件和上下文膨胀。
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# 内部材料知识库来源
|
||||
|
||||
内部知识库覆盖胶凝、陶瓷、玻璃、晶体、复合、耐火和检验检测,提供跨语言语义检索、Markdown 正文片段和原件下载。API Key 留在宿主工具中,不进入 sandbox。
|
||||
|
||||
## 工具
|
||||
|
||||
- `document_list_kb()`:列出当前有效知识库。用户未指定材料方向且需要缩窄范围时调用。
|
||||
- `document_search(queries, kb_names=None, classification_ids=None, max_documents=6, content_chars_per_doc=1200)`:批量检索并返回元数据和截断的 `md_content`。
|
||||
- `document_download(items)`:把选定原件下载到当前任务的 `documents/` 目录。
|
||||
|
||||
查询可使用中文或英文;复杂专业术语通常使用英文更精确。先规划互不重复的查询并批量调用,不逐个近义词反复搜索。
|
||||
|
||||
## 使用深度
|
||||
|
||||
- 题名、文件名、正文片段和知识库来源用于候选筛选。
|
||||
- 片段足以核验局部论断时无需下载原件。
|
||||
- 数据、公式、表格、章节、页码或版式需要更多上下文时,提高少量候选的片段长度或下载原件。
|
||||
- 下载只使用搜索返回的 `file_name` 和 `kb_name`,不编造来源标识。
|
||||
|
||||
## 失败
|
||||
|
||||
- 工具不存在表示宿主未配置 `DOCUMENT_SEARCH_API_KEY`;继续使用其他来源。
|
||||
- 认证、网络或知识库错误时说明内部材料库当前不可用。
|
||||
- 未命中时调整术语、语言或库范围;没有新增思路时如实说明覆盖不足。
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
# paper_server 来源
|
||||
|
||||
`paper_server` 以 OpenAlex 元数据为基础,并按记录实际可用性提供摘要、PDF 或 XML。当前语料以英文为主,也包含中文记录。
|
||||
|
||||
## 调用
|
||||
|
||||
通过 `run_python` 使用 helper:
|
||||
|
||||
```python
|
||||
from skills.literature.paper import search, get_paper, fetch_pdf, fetch_xml
|
||||
```
|
||||
|
||||
- `search(keyword="", year=None, year_gte=None, year_lte=None, doi="", first_author="", publication_name="", has_pdf=None, is_oa=None, limit=10)`:搜索题录并返回摘要和全文可用状态。
|
||||
- `get_paper(id_or_doi)`:按内部 ID 或 DOI 获取完整记录。
|
||||
- `fetch_xml(id_or_doi, working_dir)` / `fetch_pdf(id_or_doi, working_dir)`:把可用全文保存到任务的 `papers/` 目录。
|
||||
|
||||
helper 自动使用 `PAPER_SERVER_API_KEY` 和可选的 `PAPER_SERVER_URL`。只通过 helper 访问,由它处理认证、URL 和下载路径。
|
||||
|
||||
## 查询与全文
|
||||
|
||||
- 将中文概念转换为常用专业英文术语,并按需补中文、缩写、全称或同义词。
|
||||
- 用 DOI、题名、作者、出版物、年份和来源返回的 `type` 筛选候选。
|
||||
- 相关性初筛使用题名和摘要;精确数据、章节、图表或页码必须读取正文。
|
||||
- 结构化段落和参考文献优先 XML;版式、页码、公式和图表优先 PDF。只获取记录实际提供的格式。
|
||||
|
||||
## 失败
|
||||
|
||||
- 认证失败需要管理员检查 `PAPER_SERVER_API_KEY`。
|
||||
- DOI 未命中、格式缺失或服务器文件不存在时,改查其他候选或使用现有摘要,并降低证据等级。
|
||||
- 只引用返回记录中的真实字段,不补写缺失的 ISBN、出版社、版本或页码。
|
||||
|
|
@ -0,0 +1,390 @@
|
|||
"""Normalize and conservatively merge publication records from multiple backends."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import unicodedata
|
||||
from difflib import SequenceMatcher
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
|
||||
|
||||
EVIDENCE_LEVELS = (
|
||||
"metadata_only",
|
||||
"abstract_verified",
|
||||
"snippet_verified",
|
||||
"fulltext_verified",
|
||||
)
|
||||
|
||||
TYPE_ALIASES = {
|
||||
"article": "article",
|
||||
"journal-article": "article",
|
||||
"journal_article": "article",
|
||||
"book": "book",
|
||||
"book-chapter": "book_chapter",
|
||||
"book_chapter": "book_chapter",
|
||||
"chapter": "book_chapter",
|
||||
"conference-paper": "conference_paper",
|
||||
"conference_paper": "conference_paper",
|
||||
"proceedings-article": "conference_paper",
|
||||
"proceedings": "proceedings",
|
||||
"thesis": "thesis",
|
||||
"dissertation": "thesis",
|
||||
"report": "report",
|
||||
"standard": "standard",
|
||||
"patent": "patent",
|
||||
"preprint": "preprint",
|
||||
"dataset": "dataset_publication",
|
||||
"dataset_publication": "dataset_publication",
|
||||
}
|
||||
|
||||
|
||||
def normalize_doi(value: Any) -> str:
|
||||
doi = str(value or "").strip().lower()
|
||||
doi = re.sub(r"^(?:doi\s*:\s*|https?://(?:dx\.)?doi\.org/)", "", doi)
|
||||
return doi.rstrip(".,;:)]}")
|
||||
|
||||
|
||||
def _isbn13_from_10(value: str) -> str:
|
||||
core = "978" + value[:9]
|
||||
total = sum((1 if i % 2 == 0 else 3) * int(ch) for i, ch in enumerate(core))
|
||||
return core + str((10 - total % 10) % 10)
|
||||
|
||||
|
||||
def normalize_isbn(value: Any) -> str:
|
||||
isbn = re.sub(r"[^0-9Xx]", "", str(value or "")).upper()
|
||||
if len(isbn) == 10 and isbn[:9].isdigit() and (isbn[-1].isdigit() or isbn[-1] == "X"):
|
||||
return _isbn13_from_10(isbn)
|
||||
return isbn if len(isbn) == 13 and isbn.isdigit() else ""
|
||||
|
||||
|
||||
def normalize_title(value: Any) -> str:
|
||||
title = unicodedata.normalize("NFKC", str(value or "")).casefold()
|
||||
title = re.sub(r"[^\w]+", " ", title, flags=re.UNICODE)
|
||||
return " ".join(title.split())
|
||||
|
||||
|
||||
def normalize_creator(value: Any) -> str:
|
||||
if isinstance(value, dict):
|
||||
value = value.get("name") or value.get("literal") or ""
|
||||
return normalize_title(value)
|
||||
|
||||
|
||||
def normalize_type(value: Any) -> tuple[str, str]:
|
||||
raw = str(value or "").strip()
|
||||
key = raw.casefold().replace(" ", "-")
|
||||
return TYPE_ALIASES.get(key, "other"), raw
|
||||
|
||||
|
||||
def _first(record: dict, *keys: str) -> Any:
|
||||
for key in keys:
|
||||
value = record.get(key)
|
||||
if value not in (None, "", [], {}):
|
||||
return value
|
||||
return ""
|
||||
|
||||
|
||||
def _creator_list(record: dict) -> list[dict[str, str]]:
|
||||
raw = _first(record, "creators", "authors")
|
||||
if not raw:
|
||||
raw = [_first(record, "first_author", "author", "creator")]
|
||||
elif not isinstance(raw, list):
|
||||
raw = [raw]
|
||||
creators: list[dict[str, str]] = []
|
||||
for item in raw:
|
||||
if isinstance(item, dict):
|
||||
name = str(item.get("name") or item.get("literal") or "").strip()
|
||||
role = str(item.get("role") or "author").strip()
|
||||
orcid = str(item.get("orcid") or "").strip()
|
||||
else:
|
||||
name, role, orcid = str(item or "").strip(), "author", ""
|
||||
if name:
|
||||
creators.append({"name": name, "role": role, "orcid": orcid})
|
||||
return creators
|
||||
|
||||
|
||||
def _issued(record: dict) -> dict[str, int | None]:
|
||||
issued = record.get("issued")
|
||||
current = dict(issued) if isinstance(issued, dict) else {}
|
||||
date_parts = str(record.get("publication_date") or "").split("-")
|
||||
value = current.get("year") or _first(record, "publication_year", "year")
|
||||
try:
|
||||
year = int(value) if value not in (None, "") else int(date_parts[0]) if date_parts and date_parts[0] else None
|
||||
except (TypeError, ValueError):
|
||||
year = None
|
||||
parts: dict[str, int | None] = {"year": year, "month": None, "day": None}
|
||||
for index, key in ((1, "month"), (2, "day")):
|
||||
candidate = current.get(key)
|
||||
if candidate in (None, "") and len(date_parts) > index:
|
||||
candidate = date_parts[index]
|
||||
try:
|
||||
parts[key] = int(candidate) if candidate not in (None, "") else None
|
||||
except (TypeError, ValueError):
|
||||
parts[key] = None
|
||||
return parts
|
||||
|
||||
|
||||
def _string_list(value: Any, normalizer=None) -> list[str]:
|
||||
values = value if isinstance(value, list) else [value]
|
||||
out: list[str] = []
|
||||
for item in values:
|
||||
normalized = normalizer(item) if normalizer else str(item or "").strip()
|
||||
if normalized and normalized not in out:
|
||||
out.append(normalized)
|
||||
return out
|
||||
|
||||
|
||||
def normalize_record(record: dict[str, Any], backend: str) -> dict[str, Any]:
|
||||
identifiers = dict(record.get("identifiers") or {})
|
||||
doi = normalize_doi(_first(identifiers, "doi") or record.get("doi"))
|
||||
isbn = _string_list(_first(identifiers, "isbn") or record.get("isbn"), normalize_isbn)
|
||||
issn = _string_list(_first(identifiers, "issn") or record.get("issn"))
|
||||
publication_type, raw_type = normalize_type(_first(record, "type", "publication_type"))
|
||||
title = str(_first(record, "title", "file_name") or "").strip()
|
||||
creators = _creator_list(record)
|
||||
abstract = str(record.get("abstract") or "").strip()
|
||||
snippet = str(_first(record, "snippet", "md_content") or "").strip()
|
||||
formats = _string_list((record.get("access") or {}).get("formats") if isinstance(record.get("access"), dict) else [])
|
||||
if record.get("has_fulltext_pdf") and "pdf" not in formats:
|
||||
formats.append("pdf")
|
||||
if record.get("has_fulltext_xml") and "xml" not in formats:
|
||||
formats.append("xml")
|
||||
evidence = str(record.get("evidence_level") or "metadata_only")
|
||||
if evidence not in EVIDENCE_LEVELS:
|
||||
evidence = "metadata_only"
|
||||
source_id = str(_first(record, "id", "source_id", "file_name") or "")
|
||||
container = dict(record.get("container") or {})
|
||||
container.setdefault("title", str(_first(record, "publication_name", "container_title") or ""))
|
||||
container.setdefault("type", "")
|
||||
container.setdefault("volume", str(record.get("volume") or ""))
|
||||
container.setdefault("issue", str(record.get("issue") or ""))
|
||||
container.setdefault("pages", str(_first(record, "pages", "page") or ""))
|
||||
raw_publisher = record.get("publisher")
|
||||
if isinstance(raw_publisher, str):
|
||||
publisher = {"name": raw_publisher, "place": ""}
|
||||
else:
|
||||
publisher = dict(raw_publisher or {})
|
||||
publisher.setdefault("name", str(record.get("publisher_name") or ""))
|
||||
publisher.setdefault("place", str(record.get("publisher_place") or ""))
|
||||
raw_edition = record.get("edition")
|
||||
if isinstance(raw_edition, str):
|
||||
edition = {"label": raw_edition, "number": None}
|
||||
else:
|
||||
edition = dict(raw_edition or {})
|
||||
edition.setdefault("label", str(record.get("edition_label") or ""))
|
||||
edition.setdefault("number", record.get("edition_number"))
|
||||
raw_relations = record.get("relations")
|
||||
relations = dict(raw_relations) if isinstance(raw_relations, dict) else {}
|
||||
relations.setdefault("is_part_of", str(record.get("is_part_of") or ""))
|
||||
relations["has_parts"] = _string_list(relations.get("has_parts") or record.get("has_parts") or [])
|
||||
relations.setdefault("edition_of", str(record.get("edition_of") or ""))
|
||||
relations.setdefault("translation_of", str(record.get("translation_of") or ""))
|
||||
relations.setdefault("patent_family", str(record.get("patent_family") or ""))
|
||||
return {
|
||||
"type": publication_type,
|
||||
"raw_type": raw_type,
|
||||
"title": title,
|
||||
"creators": creators,
|
||||
"issued": _issued(record),
|
||||
"container": container,
|
||||
"publisher": publisher,
|
||||
"edition": edition,
|
||||
"identifiers": {
|
||||
"doi": doi,
|
||||
"isbn": isbn,
|
||||
"issn": issn,
|
||||
"standard_number": str(_first(identifiers, "standard_number") or record.get("standard_number") or "").strip(),
|
||||
"patent_application_number": str(_first(identifiers, "patent_application_number") or record.get("patent_application_number") or "").strip(),
|
||||
"patent_publication_number": str(_first(identifiers, "patent_publication_number") or record.get("patent_publication_number") or "").strip(),
|
||||
"report_number": str(_first(identifiers, "report_number") or record.get("report_number") or "").strip(),
|
||||
},
|
||||
"relations": relations,
|
||||
"language": str(record.get("language") or "").strip(),
|
||||
"abstract": abstract,
|
||||
"access": {
|
||||
"has_abstract": bool(abstract or record.get("has_abstract")),
|
||||
"has_snippet": bool(snippet),
|
||||
"has_fulltext": bool(formats or record.get("has_fulltext")),
|
||||
"formats": formats,
|
||||
"local_paths": _string_list((record.get("access") or {}).get("local_paths") if isinstance(record.get("access"), dict) else record.get("local_paths") or []),
|
||||
},
|
||||
"evidence_level": evidence,
|
||||
"sources": [{
|
||||
"backend": backend,
|
||||
"source_id": source_id,
|
||||
"retrieved_at": str(record.get("retrieved_at") or ""),
|
||||
}],
|
||||
"conflicts": {},
|
||||
}
|
||||
|
||||
|
||||
def _edition_key(record: dict[str, Any]) -> str:
|
||||
edition = record.get("edition") or {}
|
||||
return normalize_title(edition.get("number") or edition.get("label"))
|
||||
|
||||
|
||||
def exact_key(record: dict[str, Any]) -> str:
|
||||
ids = record["identifiers"]
|
||||
if ids.get("doi"):
|
||||
return "doi:" + ids["doi"]
|
||||
year = record["issued"].get("year") or ""
|
||||
publisher = normalize_title(record["publisher"].get("name"))
|
||||
if record["type"] == "standard" and ids.get("standard_number"):
|
||||
return f"standard:{publisher}:{normalize_title(ids['standard_number'])}:{year}"
|
||||
if record["type"] == "patent" and ids.get("patent_publication_number"):
|
||||
return "patent:" + normalize_title(ids["patent_publication_number"])
|
||||
if record["type"] == "report" and ids.get("report_number"):
|
||||
return f"report:{publisher}:{normalize_title(ids['report_number'])}"
|
||||
if record["type"] == "book" and ids.get("isbn"):
|
||||
return f"book:{ids['isbn'][0]}:{_edition_key(record)}"
|
||||
title = normalize_title(record.get("title"))
|
||||
creators = record.get("creators") or []
|
||||
creator = normalize_creator(creators[0]) if creators else ""
|
||||
if title and creator and year:
|
||||
extra = _edition_key(record) if record["type"] == "book" else ""
|
||||
if record["type"] == "book_chapter":
|
||||
extra = normalize_title(record["container"].get("title"))
|
||||
return f"fallback:{record['type']}:{title}:{creator}:{year}:{extra}"
|
||||
return ""
|
||||
|
||||
|
||||
def _add_conflict(target: dict[str, Any], field: str, value: Any) -> None:
|
||||
if value in (None, "", [], {}):
|
||||
return
|
||||
values = target["conflicts"].setdefault(field, [])
|
||||
if value not in values:
|
||||
values.append(value)
|
||||
|
||||
|
||||
def merge_record(target: dict[str, Any], incoming: dict[str, Any]) -> None:
|
||||
for source in incoming["sources"]:
|
||||
if source not in target["sources"]:
|
||||
target["sources"].append(source)
|
||||
if EVIDENCE_LEVELS.index(incoming["evidence_level"]) > EVIDENCE_LEVELS.index(target["evidence_level"]):
|
||||
target["evidence_level"] = incoming["evidence_level"]
|
||||
if not target["abstract"] and incoming["abstract"]:
|
||||
target["abstract"] = incoming["abstract"]
|
||||
elif target["abstract"] and incoming["abstract"] and target["abstract"] != incoming["abstract"]:
|
||||
_add_conflict(target, "abstract", incoming["abstract"])
|
||||
for flag in ("has_abstract", "has_snippet", "has_fulltext"):
|
||||
target["access"][flag] = target["access"][flag] or incoming["access"][flag]
|
||||
for field in ("formats", "local_paths"):
|
||||
for value in incoming["access"][field]:
|
||||
if value not in target["access"][field]:
|
||||
target["access"][field].append(value)
|
||||
for field in ("doi", "standard_number", "patent_application_number", "patent_publication_number", "report_number"):
|
||||
current, value = target["identifiers"].get(field), incoming["identifiers"].get(field)
|
||||
if not current and value:
|
||||
target["identifiers"][field] = value
|
||||
elif current and value and current != value:
|
||||
_add_conflict(target, f"identifiers.{field}", value)
|
||||
for field in ("isbn", "issn"):
|
||||
for value in incoming["identifiers"].get(field, []):
|
||||
if value not in target["identifiers"][field]:
|
||||
target["identifiers"][field].append(value)
|
||||
for field in ("title", "raw_type", "language"):
|
||||
current, value = target.get(field), incoming.get(field)
|
||||
if not current and value:
|
||||
target[field] = value
|
||||
elif current and value and normalize_title(current) != normalize_title(value):
|
||||
_add_conflict(target, field, value)
|
||||
for field in ("container", "publisher", "edition"):
|
||||
for key, value in incoming[field].items():
|
||||
current = target[field].get(key)
|
||||
if not current and value not in (None, ""):
|
||||
target[field][key] = value
|
||||
elif current and value not in (None, "") and current != value:
|
||||
_add_conflict(target, f"{field}.{key}", value)
|
||||
for key, value in incoming["relations"].items():
|
||||
current = target["relations"].get(key)
|
||||
if isinstance(value, list):
|
||||
if not isinstance(current, list):
|
||||
current = []
|
||||
target["relations"][key] = current
|
||||
for item in value:
|
||||
if item not in current:
|
||||
current.append(item)
|
||||
elif not current and value:
|
||||
target["relations"][key] = value
|
||||
elif current and value and current != value:
|
||||
_add_conflict(target, f"relations.{key}", value)
|
||||
|
||||
|
||||
def merge_publications(records: Iterable[tuple[str, dict[str, Any]]]) -> dict[str, Any]:
|
||||
publications: list[dict[str, Any]] = []
|
||||
key_to_index: dict[str, int] = {}
|
||||
for backend, raw in records:
|
||||
normalized = normalize_record(raw, backend)
|
||||
key = exact_key(normalized)
|
||||
if key and key in key_to_index:
|
||||
merge_record(publications[key_to_index[key]], normalized)
|
||||
else:
|
||||
if key:
|
||||
key_to_index[key] = len(publications)
|
||||
publications.append(normalized)
|
||||
|
||||
possible: list[dict[str, Any]] = []
|
||||
for left_index, left in enumerate(publications):
|
||||
left_title = normalize_title(left["title"])
|
||||
if not left_title:
|
||||
continue
|
||||
for right_index in range(left_index + 1, len(publications)):
|
||||
right = publications[right_index]
|
||||
if left["type"] != right["type"]:
|
||||
continue
|
||||
left_year, right_year = left["issued"]["year"], right["issued"]["year"]
|
||||
if left_year and right_year and abs(left_year - right_year) > 1:
|
||||
continue
|
||||
ratio = SequenceMatcher(None, left_title, normalize_title(right["title"])).ratio()
|
||||
if ratio >= 0.93:
|
||||
possible.append({
|
||||
"left_index": left_index,
|
||||
"right_index": right_index,
|
||||
"left_title": left["title"],
|
||||
"right_title": right["title"],
|
||||
"title_similarity": round(ratio, 4),
|
||||
})
|
||||
return {"publications": publications, "possible_duplicates": possible}
|
||||
|
||||
|
||||
def _load_records(path: Path) -> list[dict[str, Any]]:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
if isinstance(data, list):
|
||||
return data
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError(f"{path}: JSON 顶层必须是列表或对象")
|
||||
for key in ("records", "publications", "results"):
|
||||
if isinstance(data.get(key), list):
|
||||
return data[key]
|
||||
nested = data.get("data")
|
||||
if isinstance(nested, list):
|
||||
return nested
|
||||
raise ValueError(f"{path}: 未找到 records/publications/results 列表")
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--input", action="append", required=True, metavar="BACKEND=PATH")
|
||||
parser.add_argument("--output", required=True, type=Path)
|
||||
args = parser.parse_args(argv)
|
||||
records: list[tuple[str, dict[str, Any]]] = []
|
||||
for spec in args.input:
|
||||
if "=" not in spec:
|
||||
parser.error("--input 必须使用 BACKEND=PATH")
|
||||
backend, raw_path = spec.split("=", 1)
|
||||
backend = backend.strip()
|
||||
if not backend:
|
||||
parser.error("--input 的 BACKEND 不可为空")
|
||||
records.extend((backend, item) for item in _load_records(Path(raw_path)))
|
||||
result = merge_publications(records)
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"[OK] merged {len(records)} source records into {len(result['publications'])} publications")
|
||||
if result["possible_duplicates"]:
|
||||
print(f"[WARN] possible duplicates: {len(result['possible_duplicates'])}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -15,10 +15,10 @@ description: 撰写学术期刊投稿论文(中文核心 / 英文 SCI;原创研
|
|||
|---|---|
|
||||
| vs `proposal` | proposal 写**本子/任务书**(立项依据骨架);paper 写**期刊投稿稿**(IMRaD 骨架)。两者各自独立 |
|
||||
| vs `review` | review 改**已有稿**;paper **从零起草**。paper 阶段六终审**调用** review 的协议,不重复造 |
|
||||
| vs `research`/`documents` | 它们查文献;paper 是消费方,引文核验(阶段五)接到它们头上 |
|
||||
| vs `literature` | 它查出版物并核验证据;paper 是消费方,引文核验(阶段五)接到它头上 |
|
||||
| vs `patent`/`standard` | 写交底书→patent;写标准→standard |
|
||||
|
||||
**何时不用**:只改不写→review;写本子→proposal;只查文献→research/documents;只出图→plot_pub。
|
||||
**何时不用**:只改不写→review;写本子→proposal;只查出版物→literature;只出图→plot_pub。
|
||||
|
||||
## 资源
|
||||
|
||||
|
|
@ -35,7 +35,7 @@ description: 撰写学术期刊投稿论文(中文核心 / 英文 SCI;原创研
|
|||
- `references/figure_discipline.md` —— 一图一论断 / panel 角色 / 主图 vs 补充材料 / 图例规则(plot_pub 管画好一张图,这份管图和论文的关系)
|
||||
|
||||
**阶段五必读**:
|
||||
- `references/citation_verify.md` —— 引文三角核验协议(存在性 / 三角印证 / 支撑度,接 documents/research)
|
||||
- `references/citation_verify.md` —— 引文三角核验协议(存在性 / 三角印证 / 支撑度,接 literature)
|
||||
|
||||
**阶段六必读**:
|
||||
- `references/submission_audit.md` —— 投稿前审计六步(前半对齐 / 图例同步 / 术语漂移 / 机理语言)+ 数据可用性声明模式
|
||||
|
|
@ -93,8 +93,8 @@ spec 定下「类型 + 语言」后,**按 §资源 条件加载**对应的 cite_
|
|||
|
||||
> 移植自 ARS,后端用 zcbot 自己的库。Introduction 与 Discussion 靠这份矩阵,不靠记忆。
|
||||
|
||||
1. 据 spec §3/§4 的贡献与 gap,列要查的主题(英文 keyword 优先,见 research/documents 规则)
|
||||
2. 用 `documents`(材料类优先,中英 query 都行)/ `research`(要 DOI 走这个)检索,建矩阵到 `<task_dir>/lit_matrix.md`:
|
||||
1. 据 spec §3/§4 的贡献与 gap,列要查的主题(英文 keyword 优先,见 literature 规则)
|
||||
2. 用 `literature` 检索(材料类全文语义优先内部材料库,精确 DOI 优先 paper_server),建矩阵到 `<task_dir>/lit_matrix.md`:
|
||||
|
||||
| 文献(真实条目) | DOI | 一句话贡献 | 在本文用在哪(Intro/Methods/Disc) |
|
||||
|---|---|---|---|
|
||||
|
|
@ -143,7 +143,7 @@ spec 定下「类型 + 语言」后,**按 §资源 条件加载**对应的 cite_
|
|||
|
||||
> 论文最致命的失分是编造引文 / 引而不实。**逐条**走 `references/citation_verify.md` 三层:
|
||||
|
||||
1. **存在性**:每条引文在 documents/research 查到真实条目,字段以库返回为准;查不到标 `[未核实]`,**不编造**
|
||||
1. **存在性**:每条引文在 literature 查到真实条目,字段以来源返回为准;查不到标 `[未核实]`,**不编造**
|
||||
2. **三角印证**:关键论断的支撑引文至少两个独立来源一致
|
||||
3. **支撑度**:抓回 md_content/PDF,定位 ≤25 词锚点原文,判 support/partial/not-support;partial→**改论断迁就证据**,not-support→删或换
|
||||
4. 台账写 `<task_dir>/CITATIONS.md`;只有 verified 的进编号
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
论文最致命的失分是**编造引文**(hallucinated citation)与**引而不实**(cite 的文献不支撑该论断)。
|
||||
本协议把每条引文从"看起来对"逼到"经得起查"。移植自 ARS 的 triangulation + claim-faithfulness 思路,
|
||||
**后端换成 zcbot 自己的 `documents` / `research` 库**(它们本就带 DOI + md_content,做 anchor 比对反而更顺)。
|
||||
**检索入口换成 zcbot 的 `literature` skill**(paper_server 与内部材料库可交叉提供 DOI、摘要和正文锚点)。
|
||||
|
||||
> 这是**协议**不是脚本 —— 你(模型)拿 host-side tool 逐条执行。quality_check.py 只做机械的
|
||||
> orphan/uncited/编号核对,真伪与支撑度靠本协议。
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
每条引文先确认"这篇文献真实存在":
|
||||
|
||||
1. `documents` 库语义检索(材料类优先,中英 query 都行)/ `research` 库 `search()` / `get_paper(doi)`
|
||||
1. 用 `literature` 检索:材料类优先内部材料库语义检索,精确 DOI 使用 paper_server 的 `search()` / `get_paper(doi)`
|
||||
2. 命中 → 记下真实 DOI / 作者 / 年份 / 期刊 / 卷期页;**以库里返回为准**,不沿用记忆里的字段
|
||||
3. 两个库都查不到 → 标 `[未核实]`,**不得编造条目**;告诉用户"这条找不到来源,请提供 PDF/DOI 或删去该论断"
|
||||
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
关键论断(创新点对比、机理依据、定量结论)的支撑引文,**至少两个独立信息源一致**才算稳:
|
||||
|
||||
- documents 命中 + research/DOI 一致 → 通过
|
||||
- 内部材料库命中 + paper_server/DOI 一致 → 通过
|
||||
- 仅单一来源 → 标"单源,谨慎",提示用户复核
|
||||
- 不同来源字段冲突(年份/卷期不一致)→ 以可验证的 DOI 元数据为准,修正条目
|
||||
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
|
||||
最容易翻车的一层:文献存在,但**并不支撑你写的那句话**。逐条做:
|
||||
|
||||
1. 抓回该文献的 `md_content`(documents 直接给整篇 Markdown)/ `fetch_xml` / `fetch_pdf`(research)
|
||||
1. 抓回该文献的 `md_content`(内部材料库正文片段)/ `fetch_xml` / `fetch_pdf`(paper_server)
|
||||
2. 在原文里定位与论断相关的**锚点证据**:一句 ≤25 词的原文引语 + 出现的段落/小节位置
|
||||
3. 判定支撑度三档:
|
||||
- **support**:原文明确支撑该论断 → 通过
|
||||
|
|
@ -50,7 +50,7 @@
|
|||
# 引文核验台账
|
||||
> 每条引文的存在性/三角/支撑度核验结果。渲染前所有条目应为 verified 或经用户确认。
|
||||
|
||||
- [1] Provis & Bernal 2014, Annu. Rev. Mater. Res. 44:299 | exists:✓(documents+DOI) | triangulate:✓ | claim:support "geopolymers form via dissolution-polymerisation"(§2.1) | status: verified
|
||||
- [1] Provis & Bernal 2014, Annu. Rev. Mater. Res. 44:299 | exists:✓(materials_library+DOI) | triangulate:✓ | claim:support "geopolymers form via dissolution-polymerisation"(§2.1) | status: verified
|
||||
- [2] <author> <year> ... | exists:✓ | claim:partial → 已把"显著提高"改为"在 28d 提高约 12%" | status: verified-revised
|
||||
- [3] <author> ... | exists:✗ 两库未命中 | status: 待用户提供来源
|
||||
```
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
## 真实性铁律
|
||||
|
||||
- ❌ 不可编造 authors / year / journal / volume / pages / DOI
|
||||
- ✅ 引文必须经 `citation_verify.md` 核验(优先 documents / research 库)
|
||||
- ✅ 引文必须经 `citation_verify.md` 核验(走 literature 的可用来源)
|
||||
- ✅ 用户给 BibTeX / RIS 你只排版
|
||||
|
||||
## 文中标注(numbered)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
- ❌ **不可编造**作者 / 年份 / 期刊 / 卷期 / 页码 / DOI
|
||||
- ❌ **不可凭印象**写"某某 2020 提到过…" —— 大概率错
|
||||
- ✅ 引文必须经 `citation_verify.md` 的核验流程拿到真实条目(优先查 documents / research 库)
|
||||
- ✅ 引文必须经 `citation_verify.md` 的核验流程拿到真实条目(使用 literature)
|
||||
- ✅ 用户给 BibTeX / EndNote / 纯文本均可,你只**排版**,不补全凭空内容
|
||||
|
||||
## 文中标注
|
||||
|
|
@ -42,7 +42,7 @@ Provis 等 [2] 提出了地聚物的纳米结构演化模型。
|
|||
## 写作流程(与 citation_verify 配合)
|
||||
|
||||
1. 起草正文时,引用处先放占位符 `[CITE-<关键词>]`(如 `[CITE-geopolymer-strength]`)
|
||||
2. 草稿成形后,走 `citation_verify.md`:对每个占位逐条查 documents / research 拿真实文献
|
||||
2. 草稿成形后,走 `citation_verify.md`:对每个占位逐条用 literature 拿真实文献
|
||||
3. 核验通过的文献按**文中首次出现顺序**编号,占位符替换成 `[1][2][3]...`
|
||||
4. 按 GB/T 7714 重排 `06_references.md`
|
||||
|
||||
|
|
|
|||
|
|
@ -59,8 +59,8 @@ markitdown https://example.com/ -o <task_dir>/source/外部.md
|
|||
3. **执行检索**(优先级从高到低):
|
||||
- **`web_search`** —— 优先搜中国专利文库 / Google Patents / 期刊综述。query 里带 `site:patents.google.com` / `专利` / `CN10` 等限定符可显著提升信号
|
||||
- **`web_fetch`** —— 命中的关键专利/论文页拉全文摘要
|
||||
- **`documents` skill** —— 本地材料学科库(7 个学科共 100W+ 论文)如果命中
|
||||
- **`research` skill** —— OpenAlex 学术文献库,找综述与对比方案
|
||||
- **`literature` 的内部材料库来源** —— 本地 7 个材料学科库,如果命中则读取正文证据
|
||||
- **`literature` 的 paper_server 来源** —— OpenAlex 元数据基础的出版物库,找综述与对比方案
|
||||
4. **每条命中归档** (写到 spec §4):
|
||||
- 公开号 / 标题 / 申请人 / 公开日 (专利) 或 DOI / 作者 / 期刊 / 年 (论文)
|
||||
- 一句话概括其技术方案
|
||||
|
|
|
|||
|
|
@ -51,9 +51,9 @@ A ∩ (B | C) # 核心 + (对象 | 效果) — 较宽
|
|||
|---|---|---|---|
|
||||
| 1 | **中国专利公开** | `web_search` + `site:patents.google.com country:CN` 或 `site:cnipa.gov.cn` | **必查** — 同地区先发的优先权可能挡你的路 |
|
||||
| 2 | **国际专利** | `web_search` + `site:patents.google.com` / `site:wipo.int` | **必查** — 国外同款方案也算现有技术 |
|
||||
| 3 | **学术论文** | `research` skill (OpenAlex) / `documents` skill (材料库) / `web_search` + `site:arxiv.org` | **强烈推荐** — 论文公开早于专利,常是创造性杀手 |
|
||||
| 3 | **学术论文** | `literature` skill (paper_server + 内部材料库) / `web_search` + `site:arxiv.org` | **强烈推荐** — 论文公开早于专利,常是创造性杀手 |
|
||||
| 4 | **行业产品/公开演示** | `web_search` 一般 query | 视情况 — 大厂博客 / 产品文档 / 会议演示 |
|
||||
| 5 | **本地文献库** | `documents` skill (材料学科 7 个库) / `research` skill (paper_server) | 涉及材料 / 化学 / 工程领域时优先 |
|
||||
| 5 | **本地文献库** | `literature` skill 的内部材料库与 paper_server 来源 | 涉及材料 / 化学 / 工程领域时优先 |
|
||||
|
||||
> 注:CNIPA 官网爬虫本 skill **不实现**(反爬重 + 维护成本高)。如果用户要正式可作为 IDS 提交的检索证据,建议人工跑专利数据库(智慧芽 / Patentics / incoPat / 谷歌 Patents 自己手动检索)。本 skill 出的检索结论定位为"代理师写文件前的尽职检索 + 风险预警",不替代正式律所/代理所检索。
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ description: 回复期刊审稿意见(修回)。把编辑决定信 / 审稿人
|
|||
|---|---|
|
||||
| vs `paper` | paper **从零写投稿稿**;rebuttal 处理**投出去之后收到的审稿意见**。paper 阶段六的投稿件(首投 cover letter)也不归这里 —— 这里只管修回 |
|
||||
| vs `review` | review 是**替作者审自己的稿**(审稿人视角挑毛病);rebuttal 是**答别人审出来的毛病**。修回中要大改某章时可调 review 深审该章 |
|
||||
| vs `research`/`documents` | 审稿人要求补引文时,引文的真实性核验接到它们头上(复用 paper 的 citation_verify 三层协议) |
|
||||
| vs `literature` | 审稿人要求补引文时,引文的真实性核验接到它头上(复用 paper 的 citation_verify 三层协议) |
|
||||
|
||||
**何时不用**:还没投稿 / 没有审稿意见→paper 或 review;申诉拒稿决定(appeal)→ 不是修回,流程不同,明确告知用户本 skill 只处理修回,申诉信可给建议但不套本流程。
|
||||
|
||||
|
|
@ -81,7 +81,7 @@ markitdown <path>/manuscript.docx -o <task_dir>/source/manuscript.md
|
|||
|
||||
**A. 稿件修改**(有原稿时):
|
||||
1. 按 tracker 逐条落实改动,产出 `<task_dir>/manuscript_changes.md`:每条 ID 对应「修改位置 + 修改前 → 修改后」对照(位置用章节名;**行号只有用户给了才写,不编**)
|
||||
2. 需要大改某章(如重写 Discussion)→ 调 `review` skill 深审该章;需要补引文 → 走 `documents`/`research` 检索并按 paper 的 `citation_verify.md` 三层核验,**未核验的引文不进回复信**
|
||||
2. 需要大改某章(如重写 Discussion)→ 调 `review` skill 深审该章;需要补引文 → 走 `literature` 检索并按 paper 的 `citation_verify.md` 三层核验,**未核验的引文不进回复信**
|
||||
3. 补的实验 / 分析数据**只能来自用户**,数据没到就保持占位
|
||||
|
||||
**B. 逐审稿人起草回复信**(写到 `<task_dir>/response/`,一个审稿人一个文件 `NN_reviewer<N>.md`):
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
| 方法 | 方法细节缺失 / 不可复现 / 仪器参数与标准未写 | 向用户要精确细节(型号 / 参数 / 标准号) |
|
||||
| 统计 | 检验不当 / 缺效应量 / 多重检验 / 样本量不足 | 向用户要检验名 / 样本量 / 置信区间;**不许编数** |
|
||||
| 数据与材料 | 缺数据可用性 / 原始数据不可得 | 向用户要存储库 / DOI / 获取途径 |
|
||||
| 引文与定位 | 漏引前人工作 / 新颖性表述不准 | 只加**经核验**的引文(走 documents/research + citation_verify) |
|
||||
| 引文与定位 | 漏引前人工作 / 新颖性表述不准 | 只加**经核验**的引文(走 literature + citation_verify) |
|
||||
| 范围与可行性 | 要求超出本文范围的实验 / 期刊契合度 | 认可价值 → 给替代证据 → 以研究设计为界婉拒 |
|
||||
| 伦理与合规 | 缺审批号 / 知情同意 / 图像完整性 | 通常 `blocking`,向用户要准确审批信息 |
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ R1 要求删的,R2 要求扩;R1 要简化模型,R2 要加参数。
|
|||
|
||||
## 6. 要求引用特定文献(疑似 coercive citation)
|
||||
|
||||
- 逐篇判断**真实相关性**:相关 → 走 documents/research 核验后加(`ADD_CITATION`),措辞中性;不相关 → 婉拒,说明本文定位,不暗示审稿人自引
|
||||
- 逐篇判断**真实相关性**:相关 → 走 literature 核验后加(`ADD_CITATION`),措辞中性;不相关 → 婉拒,说明本文定位,不暗示审稿人自引
|
||||
- 不核验就照单全收 = 把编造引文风险带进修回
|
||||
|
||||
## 7. 转投稿(transfer after review)
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
---
|
||||
name: research
|
||||
description: 检索 paper_server 中的论文、书籍和书籍章节,获取题录、DOI、摘要及可用的 PDF/XML 全文。适合跨学科文献发现、精确文献查找和引文核验;材料领域可与 documents 并用。
|
||||
---
|
||||
|
||||
# Research
|
||||
|
||||
`research` 是 zcbot 对独立 `paper_server` 项目的客户端适配。`paper_server` 以 OpenAlex 元数据为基础,并按记录实际可用性提供摘要、PDF 或 XML;当前语料以英文为主,也包含中文文献。
|
||||
|
||||
## 选择与协作
|
||||
|
||||
- 跨学科发现、题名或 DOI 检索、出版类型识别:优先使用 `research`。
|
||||
- 材料领域全文语义检索、性能或工艺数据:优先使用 `documents`。
|
||||
- 系统调研、综述或重要引用:可同时查询两者;按 DOI 去重,无 DOI 时按规范化题名、作者和年份去重。
|
||||
- 用户已提供具体文件:直接读取;用户只提供题录而任务要求核验论断时,继续获取摘要或全文。
|
||||
|
||||
论文、书籍和书籍章节均可检索。是否读取摘要、XML 或 PDF,由用户问题所需的证据深度决定。
|
||||
|
||||
## 调用方式
|
||||
|
||||
通过 `run_python` 使用 helper:
|
||||
|
||||
```python
|
||||
from skills.research.paper import search, get_paper, fetch_pdf, fetch_xml
|
||||
```
|
||||
|
||||
- `search(keyword="", year=None, year_gte=None, year_lte=None, doi="", first_author="", publication_name="", has_pdf=None, is_oa=None, limit=10)`:搜索题录,返回类型、题名、作者、年份、期刊或出版物、DOI、摘要及全文可用状态。
|
||||
- `get_paper(id_or_doi)`:按 `paper_server` ID 或 DOI 获取单条完整记录。
|
||||
- `fetch_xml(id_or_doi, working_dir)` / `fetch_pdf(id_or_doi, working_dir)`:把可用全文下载到当前任务的 `papers/` 目录并返回相对路径。
|
||||
|
||||
helper 自动使用 `PAPER_SERVER_API_KEY` 和可选的 `PAPER_SERVER_URL`。通过 helper 访问 `paper_server`,由它处理认证、URL 和下载路径。
|
||||
|
||||
## 工作流
|
||||
|
||||
1. 将用户概念转换为常用专业术语;英文题名占多数时优先使用英文,并可用中文或同义词补充。
|
||||
2. 用主题、年份、作者、出版物或 DOI 搜索并筛选候选;`type` 可帮助识别论文、书籍和章节。
|
||||
3. 相关性初筛可使用题名和摘要;精确论断、数据、章节、图表或页码应获取足够正文。
|
||||
4. 结构化段落和参考文献通常适合读取 XML;版式、页码、公式和图表通常适合读取 PDF。只获取记录实际提供的格式。
|
||||
5. 与 `documents` 并查时合并去重,输出中区分“仅题录”“摘要已核对”“全文已核对”。
|
||||
|
||||
## 失败与真实性
|
||||
|
||||
- 认证或网络失败时,明确说明 `paper_server` 当前不可用;认证失败需要管理员检查 zcbot 的 `PAPER_SERVER_API_KEY` 配置。
|
||||
- DOI 未命中、全文格式缺失或服务器文件不存在时,可改查其他候选或使用现有摘要,不把“已收录”写成“已读全文”。
|
||||
- 未命中时可调整术语和过滤条件;没有新增检索思路时如实说明覆盖不足。
|
||||
- 只引用返回记录中的真实题名、作者、DOI 和正文,不推测缺失的 ISBN、出版社、版本、页码或内容。
|
||||
|
|
@ -17,7 +17,7 @@ description: 审稿、润色、校对文本时使用。用户要求检查语法
|
|||
## 何时不用
|
||||
|
||||
- 用户只是要从零起草新文本: 按对应写作 skill 或通用写作处理
|
||||
- 用户要求事实核查、文献真伪、最新政策: 需要先用 research / documents / web 或用户给的材料核验
|
||||
- 用户要求事实核查、出版物真伪、最新政策: 需要先用 literature / web 或用户给的材料核验
|
||||
- 用户给的是代码审查: 用 coding
|
||||
|
||||
## 审稿顺序
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ description 是**唯一进每轮 skill 列表、决定路由**的字段。写糊
|
|||
- 别写成功能罗列,要写成"路由信号"
|
||||
|
||||
照抄内置的结构,比如:
|
||||
> `description: 生成 PowerPoint(.pptx)。✅ 触发:PPT / 幻灯片 / slide / deck / .pptx。⛔ 不触发:报告 / 文档 / 纪要(走 documents/proposal)。...`
|
||||
> `description: 生成 PowerPoint(.pptx)。✅ 触发:PPT / 幻灯片 / slide / deck / .pptx。⛔ 不触发:报告 / 文档 / 纪要(走对应写作 skill)。...`
|
||||
|
||||
### 4. SKILL.md 正文骨架(从零时给用户的模板)
|
||||
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@ python /sandbox/rendering/render.py --profile proposal --format docx <task_dir>/
|
|||
- **可考核**:删"高/好/适当/尽量/合理"等模糊词;每条要求 = 量+数值+单位+判定方向(≥/≤)+对应试验方法
|
||||
- **指标闭环**:每条技术要求在标准内有方可验(要求章 ↔ 试验方法章呼应)
|
||||
- **术语规则**:一术一义、替换式定义、引用标来源、不重复已有国标术语
|
||||
- **引用真实**:标准号/名称/条款号一字不编,存疑用 research/web 核;固定引导语见 gbt_1_1_structure.md §5
|
||||
- **引用真实**:标准号/名称/条款号一字不编,存疑用 literature/web 核;固定引导语见 gbt_1_1_structure.md §5
|
||||
- **量与单位**:法定计量单位(SI),量符号斜体单位正体,修约按 GB/T 8170
|
||||
- **编制说明必交**:报批稿必附,§4 逐条解释关键指标确定依据
|
||||
- **命名**:按标准名命名 docx,不要 output.docx / 标准.docx
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@
|
|||
|
||||
## 5. 引用真实性(不可编造)
|
||||
|
||||
- **标准号、标准名、来源条款号一字不编**。不确定真实性 → 用 research / web_search 核实,核不到就标 `<TODO 待核实标准号>`,不靠训练数据脑补。
|
||||
- **标准号、标准名、来源条款号一字不编**。不确定真实性 → 用 literature / web_search 核实,核不到就标 `<TODO 待核实标准号>`,不靠训练数据脑补。
|
||||
- 建材常用真实基础标准(可直接引,仍建议核版本年号):
|
||||
- GB/T 8170 数值修约规则与极限数值的表示和判定
|
||||
- GB/T 17671 水泥胶砂强度检验方法(ISO 法)
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ GB/T 17671 水泥胶砂强度检验方法(ISO 法)
|
|||
- 文件号在前、文件名在后,**按标准号字母+数字升序排**(GB 在前,然后 GB/T、JC/T、ISO…)。
|
||||
- **只列正文中以规范性方式引用到的**(即引用后构成必须遵守的条款);仅作背景参考的放"参考文献"。
|
||||
- 若无规范性引用文件,本章可省略(但试验/产品标准几乎都有)。
|
||||
- 不编标准号 —— 拿不准真实性时走 research/web 核实,见 drafting_rules.md §引用真实性。
|
||||
- 不编标准号 —— 拿不准真实性时走 literature/web 核实,见 drafting_rules.md §引用真实性。
|
||||
|
||||
## 6. 封面要素排布(自上而下)
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@
|
|||
[采用国际标准时] 本标准 <TODO 等同/修改/非等效> 采用 <ISO/EN ...>。
|
||||
```
|
||||
|
||||
> 此节常需真实文献/标准支撑 → 可联动 research / documents / web_search 查证,**不编标准号**。
|
||||
> 此节常需真实出版物/标准支撑 → 可联动 literature / web_search 查证,**不编标准号**。
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from core.skills import SkillRegistry
|
||||
from skills.literature.scripts.merge_publications import (
|
||||
exact_key,
|
||||
main,
|
||||
merge_publications,
|
||||
normalize_doi,
|
||||
normalize_isbn,
|
||||
normalize_record,
|
||||
)
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class LiteratureSkillTests(unittest.TestCase):
|
||||
def test_literature_is_the_only_publication_search_skill(self) -> None:
|
||||
registry = SkillRegistry(ROOT / "skills")
|
||||
self.assertIn("literature", registry.skills)
|
||||
self.assertNotIn("research", registry.skills)
|
||||
self.assertNotIn("documents", registry.skills)
|
||||
|
||||
|
||||
class LiteratureNormalizationTests(unittest.TestCase):
|
||||
def test_normalizes_doi_and_isbn(self) -> None:
|
||||
self.assertEqual(normalize_doi("https://doi.org/10.1000/ABC.1."), "10.1000/abc.1")
|
||||
self.assertEqual(normalize_isbn("0-306-40615-2"), "9780306406157")
|
||||
|
||||
def test_preserves_unknown_raw_type(self) -> None:
|
||||
record = normalize_record({"title": "A", "type": "future-object"}, "future")
|
||||
self.assertEqual(record["type"], "other")
|
||||
self.assertEqual(record["raw_type"], "future-object")
|
||||
|
||||
def test_different_book_editions_have_different_keys(self) -> None:
|
||||
first = normalize_record(
|
||||
{"title": "Materials", "type": "book", "isbn": "9780306406157", "edition": "1st"},
|
||||
"books",
|
||||
)
|
||||
second = normalize_record(
|
||||
{"title": "Materials", "type": "book", "isbn": "9780306406157", "edition": "2nd"},
|
||||
"books",
|
||||
)
|
||||
self.assertNotEqual(exact_key(first), exact_key(second))
|
||||
|
||||
def test_preserves_date_and_relations(self) -> None:
|
||||
record = normalize_record(
|
||||
{
|
||||
"title": "Chapter",
|
||||
"type": "book-chapter",
|
||||
"publication_date": "2025-03-09",
|
||||
"is_part_of": "Book A",
|
||||
},
|
||||
"books",
|
||||
)
|
||||
self.assertEqual(record["issued"], {"year": 2025, "month": 3, "day": 9})
|
||||
self.assertEqual(record["relations"]["is_part_of"], "Book A")
|
||||
|
||||
|
||||
class LiteratureMergeTests(unittest.TestCase):
|
||||
def test_merges_same_doi_and_preserves_sources(self) -> None:
|
||||
result = merge_publications([
|
||||
("paper_server", {"id": "p1", "doi": "10.1/ABC", "title": "Title", "abstract": "A"}),
|
||||
("materials_library", {"id": "m1", "doi": "https://doi.org/10.1/abc", "title": "Title"}),
|
||||
])
|
||||
self.assertEqual(len(result["publications"]), 1)
|
||||
self.assertEqual(
|
||||
{source["backend"] for source in result["publications"][0]["sources"]},
|
||||
{"paper_server", "materials_library"},
|
||||
)
|
||||
|
||||
def test_does_not_merge_chapter_with_book(self) -> None:
|
||||
result = merge_publications([
|
||||
("source", {"title": "Hydration", "type": "book", "first_author": "Li", "year": 2024}),
|
||||
("source", {"title": "Hydration", "type": "book_chapter", "first_author": "Li", "year": 2024}),
|
||||
])
|
||||
self.assertEqual(len(result["publications"]), 2)
|
||||
|
||||
def test_keeps_standard_revisions_and_patent_family_members(self) -> None:
|
||||
result = merge_publications([
|
||||
("standards", {"title": "Method", "type": "standard", "standard_number": "GB/T 1", "publisher": "SAC", "year": 2020}),
|
||||
("standards", {"title": "Method", "type": "standard", "standard_number": "GB/T 1", "publisher": "SAC", "year": 2025}),
|
||||
("patents", {"title": "Binder", "type": "patent", "patent_publication_number": "CN100A", "patent_family": "F1"}),
|
||||
("patents", {"title": "Binder", "type": "patent", "patent_publication_number": "US100B", "patent_family": "F1"}),
|
||||
])
|
||||
self.assertEqual(len(result["publications"]), 4)
|
||||
|
||||
def test_marks_similar_titles_as_possible_duplicates(self) -> None:
|
||||
result = merge_publications([
|
||||
("a", {"title": "Hydration of low carbon cement systems", "type": "article", "year": 2024}),
|
||||
("b", {"title": "Hydration of low-carbon cement system", "type": "article", "year": 2024}),
|
||||
])
|
||||
self.assertEqual(len(result["publications"]), 2)
|
||||
self.assertEqual(len(result["possible_duplicates"]), 1)
|
||||
|
||||
def test_cli_writes_output(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
source = root / "source.json"
|
||||
output = root / "output.json"
|
||||
source.write_text(json.dumps([{"title": "A", "doi": "10.1/a"}]), encoding="utf-8")
|
||||
self.assertEqual(main(["--input", f"paper_server={source}", "--output", str(output)]), 0)
|
||||
self.assertEqual(len(json.loads(output.read_text(encoding="utf-8"))["publications"]), 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
"""document_search API 客户端 helper。
|
||||
"""内部材料知识库的 host-side 客户端。
|
||||
|
||||
base_url / api_key 走 env:
|
||||
DOCUMENT_SEARCH_URL 默认 https://ai.ctc-zc.com:8100/api
|
||||
|
|
@ -42,7 +42,7 @@ def _api_key() -> str:
|
|||
key = os.environ.get("DOCUMENT_SEARCH_API_KEY", "").strip()
|
||||
if not key:
|
||||
raise RuntimeError(
|
||||
"DOCUMENT_SEARCH_API_KEY env 未设置 —— 配置后再调 documents skill"
|
||||
"DOCUMENT_SEARCH_API_KEY env 未设置 —— 配置后再使用 literature skill 的内部材料库来源"
|
||||
)
|
||||
return key
|
||||
|
||||
|
|
@ -9,7 +9,7 @@ from concurrent.futures import ThreadPoolExecutor
|
|||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from skills.documents import client as doc_client
|
||||
from . import document_client as doc_client
|
||||
|
||||
from .base import Tool
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ from .base import Tool
|
|||
from .output import compact_tool_output, format_timeout_result
|
||||
|
||||
_SENSITIVE_PATTERNS = ("API_KEY", "TOKEN", "SECRET", "PASSWORD", "PRIVATE_KEY")
|
||||
# 刻意放行的例外:research skill 在 sandbox 里直连 paper_server,这把只读文献库 key
|
||||
# 刻意放行的例外:literature skill 在 sandbox 里直连 paper_server,这把只读文献库 key
|
||||
# 的消费方就是沙盒代码本身;低价值、可随时在 paper_server admin 撤销。
|
||||
_ENV_ALLOWLIST = frozenset({"PAPER_SERVER_API_KEY"})
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue