81 lines
3.0 KiB
Python
81 lines
3.0 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""对话助手 RAG 引用语料编译脚本(issue #133 / PRD 5.4 引用溯源)。
|
||
|
||
扫描行业模板知识库文档(``templates/*/rag-kb/documents/*.md``),抽取
|
||
「文档标题 + 章节 + 章节摘要」编译为 ``web/chat/citations.json``,
|
||
供增强版对话页(assistant.html)做前端检索与**引用溯源展示**。
|
||
|
||
与 rag-kb 内核的关系:``core/rag-kb`` 是完整检索管线(暂无 HTTP 服务),
|
||
本脚本只把文档资产转成前端可 fetch 的静态语料(Demo 级),
|
||
引用条目带「文档名 + 章节」,对齐 PRD 5.4「强制引用溯源」的展示语义。
|
||
|
||
用法(仓库根目录下):
|
||
python web/chat/scripts/build_citations.py
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import glob
|
||
import json
|
||
import os
|
||
import re
|
||
import sys
|
||
|
||
_REPO_ROOT = os.path.dirname(
|
||
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
)
|
||
_DOC_GLOB = os.path.join(_REPO_ROOT, "templates", "*", "rag-kb", "documents", "*.md")
|
||
_OUT_PATH = os.path.join(_REPO_ROOT, "web", "chat", "citations.json")
|
||
|
||
#: 每个章节摘要的最大字符数(控制语料体积,前端检索只看要点)
|
||
SNIPPET_MAX_CHARS = 160
|
||
|
||
|
||
def extract_sections(path: str) -> dict:
|
||
"""从一份 Markdown 文档抽取标题与章节摘要。"""
|
||
with open(path, encoding="utf-8") as f:
|
||
text = f.read()
|
||
rel = os.path.relpath(path, _REPO_ROOT).replace(os.sep, "/")
|
||
title_match = re.search(r"^#\s+(.+)$", text, flags=re.M)
|
||
title = title_match.group(1).strip() if title_match else os.path.basename(path)
|
||
|
||
sections = []
|
||
# 按 ## 章节切分;无章节时整篇作为一节
|
||
parts = re.split(r"^##\s+(.+)$", text, flags=re.M)
|
||
if len(parts) >= 3:
|
||
for i in range(1, len(parts), 2):
|
||
heading = parts[i].strip()
|
||
body = parts[i + 1] if i + 1 < len(parts) else ""
|
||
snippet = re.sub(r"\s+", " ", body).strip()[:SNIPPET_MAX_CHARS]
|
||
if snippet:
|
||
sections.append({"heading": heading, "snippet": snippet})
|
||
else:
|
||
snippet = re.sub(r"\s+", " ", text).strip()[:SNIPPET_MAX_CHARS]
|
||
if snippet:
|
||
sections.append({"heading": "全文", "snippet": snippet})
|
||
|
||
return {"title": title, "source": rel, "sections": sections}
|
||
|
||
|
||
def main() -> int:
|
||
docs = []
|
||
for path in sorted(glob.glob(_DOC_GLOB)):
|
||
try:
|
||
docs.append(extract_sections(path))
|
||
print(f"[OK] {os.path.basename(path)} "
|
||
f"({len(docs[-1]['sections'])} 章节)")
|
||
except Exception as exc: # noqa: BLE001 - 聚合全部失败
|
||
print(f"[FAIL] {path}: {exc}")
|
||
return 1
|
||
if not docs:
|
||
print("未找到任何知识库文档")
|
||
return 1
|
||
with open(_OUT_PATH, "w", encoding="utf-8") as f:
|
||
json.dump({"schema": "iAOP-chat-citations-v1", "documents": docs},
|
||
f, ensure_ascii=False, indent=2)
|
||
print(f"共 {len(docs)} 份文档 -> {os.path.relpath(_OUT_PATH, _REPO_ROOT)}")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|