Skip to content

Commit 5dd9f58

Browse files
authored
Merge pull request #9 from Aucannot/codex/review-skill-implementation-for-ai-paper-summary
feat: add paper-reading workflow and subcategories to ai-auto-note-publisher
2 parents ddedf7d + f98d6c7 commit 5dd9f58

6 files changed

Lines changed: 241 additions & 17 deletions

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ python3 skills/ai-auto-note-publisher/scripts/create_ai_auto_note.py \
5656
--repo-root .
5757
```
5858

59-
可选:如果你要覆盖自动分类,可传 `--subcategory`(例如 `rag``agents``general`)。
59+
可选:如果你要覆盖自动分类,可传 `--subcategory`(例如 `rag``agents``paper-reading``productivity``general`)。
60+
61+
论文场景中,脚本支持从 `arXiv:2501.12345` 这类文本自动规范化为 `https://arxiv.org/abs/2501.12345`
6062

6163
生成并提交后,如需自动推送 PR 到本仓库(需要已安装并登录 `gh`):
6264

skills/ai-auto-note-publisher/SKILL.md

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: ai-auto-note-publisher
3-
description: 自动把学习对话沉淀成博客笔记并提交到当前仓库。当用户提到“学习总结/自动归档/上传笔记/ai auto note”时使用。统一写入 categories: ["ai-auto-note", "<auto-subcategory>"],并按子目录自动归类。
3+
description: 自动把学习对话沉淀成博客笔记并提交到当前仓库。当用户提到“学习总结/自动归档/上传笔记/ai auto note/论文共读/论文总结”时使用。统一写入 categories: ["ai-auto-note", "<auto-subcategory>"],并按子目录自动归类。
44
---
55

66
# AI Auto Note Publisher
@@ -9,23 +9,27 @@ description: 自动把学习对话沉淀成博客笔记并提交到当前仓库
99

1010
## 何时使用
1111

12-
当用户希望把与 AI 学习的结论自动沉淀成文档,并上传到仓库时。
12+
当用户希望把与 AI 学习(尤其是论文共读)的结论自动沉淀成文档,并上传到仓库时。
1313

1414
## 输出规范
1515

1616
- 路径:`_posts/ai-auto-note/<subcategory>/YYYY-MM-DD-title.md`
1717
- Front matter 固定:
1818
- `categories: ["ai-auto-note", "<subcategory>"]`
19-
- `tags: ["ai-auto-note", "<subcategory>"]`
19+
- `tags`: 至少包含 `"ai-auto-note"``<subcategory>`
2020
- 正文结构至少包含:
2121
- `## 背景`
2222
- `## 结论速记`
2323
- `## 后续行动`
24+
- 若子分类是 `paper-reading`,额外包含:
25+
- `## 论文信息`
26+
- `## 方法与实验要点`
2427

2528
## 快速流程
2629

27-
1. 准备学习记录文本(例如 `tmp/notes.txt`)。
28-
2. 运行脚本生成文章:
30+
1. 先和用户完成论文/主题讨论,确认有稳定结论。
31+
2. 准备学习记录文本(例如 `tmp/notes.txt`)。
32+
3. 运行脚本生成文章:
2933

3034
```bash
3135
python3 skills/ai-auto-note-publisher/scripts/create_ai_auto_note.py \
@@ -37,22 +41,26 @@ python3 skills/ai-auto-note-publisher/scripts/create_ai_auto_note.py \
3741
python3 skills/ai-auto-note-publisher/scripts/create_ai_auto_note.py \
3842
--title "你的标题" \
3943
--input tmp/notes.txt \
40-
--subcategory rag \
44+
--subcategory paper-reading \
4145
--repo-root .
4246
```
4347

44-
3. 检查生成内容是否符合语义;必要时微调标题与要点
45-
4. 提交到仓库。
46-
5. 推送分支并自动创建 PR(需 gh CLI 已登录)。
48+
4. 检查生成内容是否符合语义;若是论文笔记,补全“核心任务/主要贡献”等占位字段
49+
5. 提交到仓库。
50+
6. 推送分支并自动创建 PR(需 gh CLI 已登录)。
4751

4852
## 子分类策略
4953

50-
脚本会按关键词自动分类(如 `llm``prompting``rag``agents` 等),并优先做英文关键词边界匹配(避免 `evaluation` 误命中等情况)。
54+
脚本会按关键词自动分类(如 `llm``prompting``rag``agents``paper-reading``productivity` 等),并优先做英文关键词边界匹配(避免 `evaluation` 误命中等情况)。
5155

5256
如未命中关键词,默认分类为 `general`
5357

5458
详细映射见:`references/subcategory-rules.md`
5559

60+
标签策略:脚本会去重并在输入稀疏时补充稳定兜底标签(如 `paper``reading-notes`)。
61+
62+
脚本会自动从原文提取论文标题(如 `Title:` / `论文标题:` / `《...》`)与 arXiv 链接(支持 `https://arxiv.org/...``arXiv:2501.12345`);未识别时保留“待补充”。
63+
5664
## 自动推送 PR 到本仓库
5765

5866
准备 PR 描述文件(例如 `tmp/pr-body.md`)后执行:

skills/ai-auto-note-publisher/references/subcategory-rules.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,18 @@
1111
- `training`: finetune, sft, rlhf, 训练, 蒸馏
1212
- `evaluation`: benchmark, eval, 评测, 幻觉, hallucination
1313
- `deployment`: 部署, serving, latency, 吞吐, inference
14+
- `paper-reading`: paper, 论文, arxiv, method, 实验, ablation, baseline, sota
15+
- `productivity`: 复盘, 学习计划, todo, 行动项, 习惯, 时间管理
1416
- 默认: `general`
1517

1618
## 调整建议
1719

1820
- 如果某个主题常出现误分类,优先新增关键词而不是新增大量新类。
1921
- 英文关键词建议使用“完整词”匹配(如 `eval` 只匹配独立单词),减少误命中。
2022
- 保持二级分类数量可控(建议 < 12)。
23+
24+
25+
## 元信息提取补充
26+
27+
- arXiv 链接支持两种来源:完整 URL(`https://arxiv.org/abs/...`)和编号格式(`arXiv:2501.12345`)。
28+
- 若识别到编号格式,生成文章时会规范化为 `https://arxiv.org/abs/<id>`

skills/ai-auto-note-publisher/scripts/create_ai_auto_note.py

Lines changed: 124 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,17 @@
1818
"training": ["finetune", "sft", "rlhf", "训练", "蒸馏"],
1919
"evaluation": ["benchmark", "eval", "评测", "幻觉", "hallucination"],
2020
"deployment": ["部署", "serving", "latency", "吞吐", "inference"],
21+
"paper-reading": [
22+
"paper",
23+
"论文",
24+
"arxiv",
25+
"method",
26+
"实验",
27+
"ablation",
28+
"baseline",
29+
"sota",
30+
],
31+
"productivity": ["复盘", "学习计划", "todo", "行动项", "习惯", "时间管理"],
2132
}
2233

2334
VALID_SUBCATEGORIES = set(SUBCATEGORY_KEYWORDS) | {"general"}
@@ -38,8 +49,30 @@
3849
"inference",
3950
"latency",
4051
"serving",
52+
"paper",
53+
"arxiv",
54+
"method",
55+
"ablation",
56+
"baseline",
57+
"sota",
58+
"todo",
4159
}
4260

61+
METADATA_PREFIXES = (
62+
"title:",
63+
"title:",
64+
"paper title:",
65+
"paper title:",
66+
"论文标题:",
67+
"论文标题:",
68+
"链接:",
69+
"链接:",
70+
"link:",
71+
"link:",
72+
"arxiv:",
73+
"arxiv:",
74+
)
75+
4376

4477
def slugify(text: str) -> str:
4578
slug = re.sub(r"[^a-zA-Z0-9\u4e00-\u9fff\s-]", "", text).strip().lower()
@@ -78,11 +111,24 @@ def classify_subcategory(text: str) -> str:
78111
return best if scores[best] > 0 else "general"
79112

80113

114+
def _is_metadata_line(line: str) -> bool:
115+
lower = line.strip().lower()
116+
if lower.startswith(METADATA_PREFIXES):
117+
return True
118+
119+
# Skip pure arXiv URL lines, but keep explanatory sentences that merely contain a URL.
120+
pure_arxiv_url = re.fullmatch(
121+
r"https?://arxiv\.org/(?:abs|pdf)/\d{4}\.\d{4,5}(?:v\d+)?/?",
122+
lower,
123+
)
124+
return pure_arxiv_url is not None
125+
126+
81127
def summarize_points(content: str, max_points: int = 6) -> list[str]:
82128
lines = [ln.strip(" -\t") for ln in content.splitlines() if ln.strip()]
83129
points = []
84130
for line in lines:
85-
if len(line) < 8:
131+
if len(line) < 8 or _is_metadata_line(line):
86132
continue
87133
points.append(line)
88134
if len(points) >= max_points:
@@ -121,6 +167,8 @@ def _collect_tags(subcategory: str, source_text: str) -> list[str]:
121167
"evaluation": ["eval", "benchmark", "评测", "hallucination"],
122168
"training": ["sft", "rlhf", "finetune", "训练", "蒸馏"],
123169
"deployment": ["inference", "serving", "部署", "latency"],
170+
"paper-reading": ["paper", "arxiv", "method", "实验", "ablation"],
171+
"productivity": ["复盘", "行动项", "计划", "时间管理", "学习方法"],
124172
}
125173

126174
for tag, markers in topic_tags.items():
@@ -129,7 +177,58 @@ def _collect_tags(subcategory: str, source_text: str) -> list[str]:
129177
if any(_keyword_hit(normalized_text, marker) for marker in markers):
130178
tags.append(tag)
131179

132-
return tags[:5]
180+
# Deduplicate while preserving order.
181+
deduped_tags = list(dict.fromkeys(tags))
182+
183+
# Add stable fallback tags to keep tags informative even on sparse inputs.
184+
fallback_tags = {
185+
"paper-reading": ["paper", "reading-notes"],
186+
"productivity": ["learning-method", "action-items"],
187+
}
188+
for fallback in fallback_tags.get(subcategory, ["learning-notes"]):
189+
if fallback not in deduped_tags:
190+
deduped_tags.append(fallback)
191+
if len(deduped_tags) >= 3:
192+
break
193+
194+
return deduped_tags[:5]
195+
196+
197+
def _extract_field_value(line: str, field: str) -> str | None:
198+
pattern = rf"^\s*{re.escape(field)}\s*[::]\s*(.+?)\s*$"
199+
match = re.match(pattern, line, flags=re.IGNORECASE)
200+
if match:
201+
value = match.group(1).strip()
202+
return value or "待补充"
203+
return None
204+
205+
206+
def _extract_paper_title(source_text: str) -> str:
207+
lines = [ln.strip() for ln in source_text.splitlines() if ln.strip()]
208+
title_fields = ("title", "paper title", "论文标题")
209+
for line in lines:
210+
for field in title_fields:
211+
value = _extract_field_value(line, field)
212+
if value is not None:
213+
return value
214+
215+
quoted = re.search(r"[《\"]([^》\"]{8,160})[》\"]", source_text)
216+
if quoted:
217+
return quoted.group(1).strip()
218+
219+
return "待补充"
220+
221+
222+
def _extract_arxiv_link(source_text: str) -> str:
223+
url_match = re.search(r"https?://arxiv\.org/(?:abs|pdf)/\d{4}\.\d{4,5}(?:v\d+)?", source_text, re.IGNORECASE)
224+
if url_match:
225+
return url_match.group(0)
226+
227+
id_match = re.search(r"arxiv\s*[::]\s*(\d{4}\.\d{4,5}(?:v\d+)?)", source_text, re.IGNORECASE)
228+
if id_match:
229+
return f"https://arxiv.org/abs/{id_match.group(1)}"
230+
231+
return "待补充"
133232

134233

135234
def build_post(
@@ -147,9 +246,29 @@ def build_post(
147246
"",
148247
"本篇为学习对话自动沉淀的笔记,保留结论和可复用要点。",
149248
"",
249+
]
250+
251+
if subcategory == "paper-reading":
252+
paper_title = _extract_paper_title(source_text)
253+
arxiv_link = _extract_arxiv_link(source_text)
254+
body.extend(
255+
[
256+
"## 论文信息",
257+
"",
258+
f"- 论文标题:{paper_title}",
259+
"- 核心任务:待补充",
260+
"- 主要贡献:待补充",
261+
f"- 链接:{arxiv_link}",
262+
"",
263+
"## 方法与实验要点",
264+
"",
265+
]
266+
)
267+
268+
body.extend([
150269
"## 结论速记",
151270
"",
152-
]
271+
])
153272
body.extend([f"- {point}" for point in bullets])
154273
body.extend(
155274
[
@@ -167,7 +286,7 @@ def build_post(
167286
f'title: "{_yaml_escaped(title)}"',
168287
f"date: {date.isoformat()} 09:00:00 +0800",
169288
f'categories: ["{CATEGORY}", "{subcategory}"]',
170-
"tags: [" + ", ".join(f'"{tag}"' for tag in tags) + "]",
289+
"tags: [" + ", ".join(f'\"{tag}\"' for tag in tags) + "]",
171290
"---",
172291
"",
173292
]
@@ -197,7 +316,7 @@ def main() -> None:
197316
parser.add_argument(
198317
"--subcategory",
199318
default=None,
200-
help="Force specific subcategory (e.g. rag/agents/general)",
319+
help="Force specific subcategory (e.g. rag/agents/paper-reading/productivity/general)",
201320
)
202321

203322
args = parser.parse_args()
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import sys
2+
from pathlib import Path
3+
4+
SCRIPT_DIR = Path(__file__).resolve().parent
5+
if str(SCRIPT_DIR) not in sys.path:
6+
sys.path.insert(0, str(SCRIPT_DIR))
7+
8+
import datetime as dt
9+
import unittest
10+
11+
from create_ai_auto_note import (
12+
_extract_arxiv_link,
13+
_extract_paper_title,
14+
build_post,
15+
classify_subcategory,
16+
summarize_points,
17+
)
18+
19+
20+
class AutoNoteTests(unittest.TestCase):
21+
def test_classify_paper_reading(self):
22+
text = "这篇paper在arxiv上发布,包含ablation实验和baseline对比"
23+
self.assertEqual(classify_subcategory(text), "paper-reading")
24+
25+
def test_classify_productivity(self):
26+
text = "今天复盘学习计划,明确todo和行动项"
27+
self.assertEqual(classify_subcategory(text), "productivity")
28+
29+
def test_extract_paper_metadata(self):
30+
text = "Title: Test-Time Scaling\n链接:https://arxiv.org/abs/2501.12345"
31+
self.assertEqual(_extract_paper_title(text), "Test-Time Scaling")
32+
self.assertEqual(_extract_arxiv_link(text), "https://arxiv.org/abs/2501.12345")
33+
34+
35+
def test_extract_arxiv_id_format(self):
36+
text = "该论文编号 arXiv:2501.12345v2"
37+
self.assertEqual(_extract_arxiv_link(text), "https://arxiv.org/abs/2501.12345v2")
38+
39+
def test_extract_paper_title_with_fullwidth_colon(self):
40+
text = "论文标题:使用 CoT 增强推理能力"
41+
self.assertEqual(_extract_paper_title(text), "使用 CoT 增强推理能力")
42+
43+
def test_build_post_paper_reading_contains_sections(self):
44+
source = """Title: Test-Time Scaling
45+
我们讨论了method与ablation结果。
46+
链接:https://arxiv.org/abs/2501.12345
47+
下一步复现实验。"""
48+
_, post = build_post(
49+
title="论文测试",
50+
source_text=source,
51+
date=dt.date(2026, 3, 16),
52+
forced_subcategory="paper-reading",
53+
)
54+
self.assertIn("## 论文信息", post)
55+
self.assertIn("- 论文标题:Test-Time Scaling", post)
56+
self.assertIn("- 链接:https://arxiv.org/abs/2501.12345", post)
57+
self.assertIn("## 方法与实验要点", post)
58+
# metadata lines should not be duplicated as bullets in summary
59+
self.assertNotIn("- Title: Test-Time Scaling", post)
60+
self.assertNotIn("- 链接:https://arxiv.org/abs/2501.12345", post.split("## 结论速记", 1)[1])
61+
62+
def test_summarize_points_keeps_explanatory_line_with_arxiv_url(self):
63+
source = """我们参考了 https://arxiv.org/abs/2501.12345 并总结了关键实验结论。"""
64+
points = summarize_points(source)
65+
self.assertEqual(points, ["我们参考了 https://arxiv.org/abs/2501.12345 并总结了关键实验结论。"])
66+
67+
68+
def test_build_post_includes_fallback_tags_for_sparse_input(self):
69+
_, post = build_post(
70+
title="简短记录",
71+
source_text="仅有一行简短内容。",
72+
date=dt.date(2026, 3, 16),
73+
forced_subcategory="paper-reading",
74+
)
75+
self.assertIn('tags: ["ai-auto-note", "paper-reading", "paper"', post)
76+
def test_summarize_points_skips_metadata_lines(self):
77+
source = """Title: A
78+
链接:https://arxiv.org/abs/2501.12345
79+
这是有效总结内容,长度足够。"""
80+
points = summarize_points(source)
81+
self.assertEqual(points, ["这是有效总结内容,长度足够。"])
82+
83+
84+
if __name__ == "__main__":
85+
unittest.main()

skills/ai-auto-note/SKILL.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ description: "Summarize completed AI learning conversations into publishable Jek
99

1010
Convert finalized learning conversations into structured, publication-ready notes under `_posts/`, with consistent metadata and stable categorization.
1111

12+
> For repository automation and PR creation, prefer using `skills/ai-auto-note-publisher` scripts after drafting content.
13+
1214
## Workflow
1315

1416
1. Confirm the source material is conclusion-ready.
1517
2. Extract key conclusions, evidence, and next actions.
1618
3. Map the note to one subcategory using `references/subcategory-taxonomy.md`.
1719
4. Generate a new post file from `assets/post-template.md`.
18-
5. Save the file to `_posts/YYYY-MM-DD-ai-auto-note-<slug>.md`.
20+
5. Save the file to `_posts/ai-auto-note/<subcategory>/YYYY-MM-DD-<slug>.md`.
1921
6. Keep `categories` in this exact order:
2022
- `ai-auto-note`
2123
- `<subcategory>`

0 commit comments

Comments
 (0)