union_kb_ingest 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -13
- package/config/config.yaml +2 -2
- package/ingest.py +35 -33
- package/normalizer.py +956 -54
- package/package.json +2 -3
- package/parser.py +401 -1
- package/prompts/generate_kb_items.md +4 -4
- package/prompts//347/237/245/350/257/206/345/272/223/345/273/272/347/253/213/350/247/204/350/214/203.md +170 -0
- package/requirements.txt +1 -1
- package/schemas.py +27 -2
- package/splitter.py +20 -0
- package/validator.py +122 -7
- package/writer.py +4 -1
- package/ArkKickidcService.java +0 -578
- package/drafts/.gitkeep +0 -1
- package/prompts//350/201/224/345/220/210/350/277/220/347/273/264/347/237/245/350/257/206/345/272/223/345/273/272/347/253/213/350/247/204/350/214/203.md +0 -272
- /package/{approved → result}/.gitkeep +0 -0
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
2. 通过 Docling slim 的离线文本解析能力生成统一 Markdown 中间格式。
|
|
9
9
|
3. 按章节、场景、规则、指标等粒度切割。
|
|
10
10
|
4. 可选调用大模型,把内容整理为项目知识库规范要求的 Markdown 文件。
|
|
11
|
-
5.
|
|
11
|
+
5. 默认生成可直接交给知识库项目使用的 `status: active` Markdown 文件。
|
|
12
12
|
|
|
13
13
|
启用大模型时,工具会把 `prompts/联合运维知识库建立规范.md` 作为格式和质量约束放入提示词,要求模型依据原文语义判断业务场景、模块、角色、标签和风险等级。代码中的启发式生成只作为未启用大模型或调用失败时的兜底,不使用预设业务关键词去指导大模型输出。
|
|
14
14
|
|
|
@@ -37,13 +37,13 @@ python -m pip install -r requirements.txt
|
|
|
37
37
|
input/
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
生成知识库文件:
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
43
|
python ingest.py draft
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
如果 `
|
|
46
|
+
如果 `result/` 中已有生成文件,命令会先询问是否覆盖。选择 `y` 后会清空 `result/` 中已有生成文件,再重新生成;选择其他内容会直接退出,避免多次生成结果相互影响。
|
|
47
47
|
|
|
48
48
|
只解析为中间 Markdown:
|
|
49
49
|
|
|
@@ -51,25 +51,23 @@ python ingest.py draft
|
|
|
51
51
|
python ingest.py parse
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
校验生成结果:
|
|
55
55
|
|
|
56
56
|
```bash
|
|
57
57
|
python ingest.py validate
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
默认目录为 `input/`、`parsed/` 和 `result/`。只有需要处理其他目录时,才使用 `--input` 或 `--output` 覆盖。
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
python ingest.py promote
|
|
64
|
-
```
|
|
62
|
+
`draft` 默认按 `config/config.yaml` 的 `draft.max_chars` 控制单次送入模型的原文长度,并额外提供文档目录和相邻片段摘要作为辅助上下文。这样可以降低私有模型单轮负载,同时尽量保留前后章节关系。命令行仍可用 `--max-chars` 临时覆盖。
|
|
65
63
|
|
|
66
|
-
|
|
64
|
+
每条知识库文件会写入分类画像元数据:`category`、`subcategory`、`category_keywords` 和 `related_items`。这些字段优先来自源文件一级标题、首页标题、章节目录、文件名、当前小类正文和关联小类语义,用于标识知识大类、小类、关键词和条目间关系。后续 RAG 入库和检索时,应把这些字段写入向量库 metadata,并用于分类过滤、查询路由或重排加权,降低不同场景之间因为相似词命中而串场的概率。
|
|
67
65
|
|
|
68
|
-
`
|
|
66
|
+
生成结果会按原始输入遍历顺序写入 `source_order`,并用 `000001-...md` 这样的文件名前缀保持目录排序与原文从上到下的顺序一致。页码只写入 Front Matter 的 `source_pages`/`source_trace` 和正文 `## 5. 来源依据`,不会进入正文 `## 1. 核心内容` 到 `## 4. 关联能力`。
|
|
69
67
|
|
|
70
68
|
## 大模型配置
|
|
71
69
|
|
|
72
|
-
|
|
70
|
+
默认不强制调用大模型,会使用启发式模板生成知识库文件。
|
|
73
71
|
|
|
74
72
|
如果要启用大模型整理,修改 `config/config.yaml`:
|
|
75
73
|
|
|
@@ -98,10 +96,10 @@ export KB_LLM_API_KEY="your-zhipu-api-key"
|
|
|
98
96
|
export KB_LLM_MODEL="glm-4.7"
|
|
99
97
|
```
|
|
100
98
|
|
|
101
|
-
工具通过
|
|
99
|
+
工具通过 Z.AI 新版 Python SDK 调用中文智谱开放平台 GLM,依赖固定为 `zai-sdk==0.2.2`,客户端固定使用官方中文写法 `from zai import ZhipuAiClient`,`base_url` 使用 `https://open.bigmodel.cn/api/paas/v4/`。工具不再包含旧 `zhipuai` SDK、国际版 `ZaiClient` 或 OpenAI 调用路径,也不 import 项目 `src` 代码。
|
|
102
100
|
|
|
103
101
|
## 与线上项目的关系
|
|
104
102
|
|
|
105
|
-
这个工具只产出符合规范的 `*.md`
|
|
103
|
+
这个工具只产出符合规范的 `*.md` 文件到 `result/`,后续由线上知识库加载流程处理。
|
|
106
104
|
|
|
107
105
|
建议线上打包时排除整个 `tools/kb_ingest` 目录。
|
package/config/config.yaml
CHANGED
package/ingest.py
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import argparse
|
|
5
|
-
import shutil
|
|
6
5
|
import sys
|
|
7
6
|
from pathlib import Path
|
|
8
7
|
from typing import List
|
|
@@ -41,20 +40,19 @@ def cmd_parse(args) -> int:
|
|
|
41
40
|
def cmd_draft(args) -> int:
|
|
42
41
|
input_path = Path(args.input)
|
|
43
42
|
output_dir = Path(args.output)
|
|
44
|
-
approved_dir = Path(args.approved_dir)
|
|
45
|
-
result_dir = Path(args.result_dir)
|
|
46
43
|
|
|
47
44
|
existing = _list_effective_files(output_dir)
|
|
48
|
-
if existing and not _confirm_overwrite(output_dir,
|
|
45
|
+
if existing and not _confirm_overwrite(output_dir, existing):
|
|
49
46
|
print("aborted. existing files were kept.")
|
|
50
47
|
return 0
|
|
51
48
|
|
|
52
49
|
if existing:
|
|
53
|
-
_clear_generated_files(output_dir
|
|
50
|
+
_clear_generated_files(output_dir)
|
|
54
51
|
|
|
55
52
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
56
53
|
|
|
57
54
|
total_items = 0
|
|
55
|
+
source_order = 0
|
|
58
56
|
draft_config = get_draft_config()
|
|
59
57
|
max_chars = args.max_chars or draft_config.max_chars
|
|
60
58
|
files = iter_input_files(input_path)
|
|
@@ -67,7 +65,11 @@ def cmd_draft(args) -> int:
|
|
|
67
65
|
outline_max_sections=draft_config.outline_max_sections,
|
|
68
66
|
)
|
|
69
67
|
for block in blocks:
|
|
70
|
-
for item in normalize_block(block, status=
|
|
68
|
+
for item in normalize_block(block, status=args.status):
|
|
69
|
+
source_order += 1
|
|
70
|
+
item.source_order = source_order
|
|
71
|
+
item.source_pages = sorted(set(block.pages))
|
|
72
|
+
item.source_trace = _source_trace(block)
|
|
71
73
|
write_item(item, output_dir)
|
|
72
74
|
total_items += 1
|
|
73
75
|
print(f"drafted: {path} blocks={len(blocks)}")
|
|
@@ -86,15 +88,11 @@ def _list_effective_files(path: Path) -> list[Path]:
|
|
|
86
88
|
|
|
87
89
|
def _confirm_overwrite(
|
|
88
90
|
output_dir: Path,
|
|
89
|
-
approved_dir: Path,
|
|
90
|
-
result_dir: Path,
|
|
91
91
|
existing: list[Path],
|
|
92
92
|
) -> bool:
|
|
93
93
|
print(f"found {len(existing)} existing file(s) in {output_dir}.")
|
|
94
94
|
print("Continuing will delete existing generated files under:")
|
|
95
95
|
print(f"- {output_dir}")
|
|
96
|
-
print(f"- {approved_dir}")
|
|
97
|
-
print(f"- {result_dir}")
|
|
98
96
|
answer = input("Overwrite and continue? [y/N]: ").strip().lower()
|
|
99
97
|
return answer in {"y", "yes"}
|
|
100
98
|
|
|
@@ -120,6 +118,13 @@ def _attach_block_context(
|
|
|
120
118
|
parts = []
|
|
121
119
|
if outline:
|
|
122
120
|
parts.append(f"文档章节目录:\n{outline}")
|
|
121
|
+
if block.category or block.subcategory or block.category_keywords:
|
|
122
|
+
parts.append(
|
|
123
|
+
"知识分类:\n"
|
|
124
|
+
f"大类标题:{block.category}\n"
|
|
125
|
+
f"小类标题:{block.subcategory}\n"
|
|
126
|
+
f"关键词:{', '.join(block.category_keywords)}"
|
|
127
|
+
)
|
|
123
128
|
if idx > 0:
|
|
124
129
|
parts.append(
|
|
125
130
|
"上一片段摘要:\n"
|
|
@@ -139,6 +144,16 @@ def _attach_block_context(
|
|
|
139
144
|
pages=block.pages,
|
|
140
145
|
order=block.order,
|
|
141
146
|
context="\n\n".join(parts),
|
|
147
|
+
category=block.category,
|
|
148
|
+
category_description=block.category_description,
|
|
149
|
+
category_keywords=block.category_keywords,
|
|
150
|
+
source_doc_description=block.source_doc_description,
|
|
151
|
+
subcategory=block.subcategory,
|
|
152
|
+
subcategory_description=block.subcategory_description,
|
|
153
|
+
category_path=block.category_path,
|
|
154
|
+
related_categories=block.related_categories,
|
|
155
|
+
relation_notes=block.relation_notes,
|
|
156
|
+
related_items=block.related_items,
|
|
142
157
|
))
|
|
143
158
|
return output
|
|
144
159
|
|
|
@@ -167,6 +182,13 @@ def _compact_context_text(text: str, limit: int) -> str:
|
|
|
167
182
|
return compact[:limit].rstrip() + "..."
|
|
168
183
|
|
|
169
184
|
|
|
185
|
+
def _source_trace(block: ParsedBlock) -> str:
|
|
186
|
+
parts = [f"section={block.source_section}"]
|
|
187
|
+
if block.pages:
|
|
188
|
+
parts.append(f"pages={','.join(map(str, sorted(set(block.pages))))}")
|
|
189
|
+
return "; ".join(parts)
|
|
190
|
+
|
|
191
|
+
|
|
170
192
|
def cmd_validate(args) -> int:
|
|
171
193
|
issues = validate_dir(Path(args.input))
|
|
172
194
|
for issue in issues:
|
|
@@ -175,20 +197,6 @@ def cmd_validate(args) -> int:
|
|
|
175
197
|
return 1 if any(i.level == "error" for i in issues) else 0
|
|
176
198
|
|
|
177
199
|
|
|
178
|
-
def cmd_promote(args) -> int:
|
|
179
|
-
input_dir = Path(args.input)
|
|
180
|
-
result_dir = Path(args.result_dir)
|
|
181
|
-
result_dir.mkdir(parents=True, exist_ok=True)
|
|
182
|
-
count = 0
|
|
183
|
-
for path in sorted(input_dir.rglob("*.md")):
|
|
184
|
-
target = result_dir / path.name
|
|
185
|
-
shutil.copy2(path, target)
|
|
186
|
-
count += 1
|
|
187
|
-
print(f"promoted: {path} -> {target}")
|
|
188
|
-
print(f"done. promoted={count}")
|
|
189
|
-
return 0
|
|
190
|
-
|
|
191
|
-
|
|
192
200
|
def build_parser() -> argparse.ArgumentParser:
|
|
193
201
|
parser = argparse.ArgumentParser(description="Offline document-to-knowledge Markdown generator.")
|
|
194
202
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
@@ -200,21 +208,15 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
200
208
|
|
|
201
209
|
draft_cmd = sub.add_parser("draft", help="Generate draft knowledge files.")
|
|
202
210
|
draft_cmd.add_argument("--input", default=str(CURRENT_DIR / "input"))
|
|
203
|
-
draft_cmd.add_argument("--output", default=str(CURRENT_DIR / "
|
|
204
|
-
draft_cmd.add_argument("--
|
|
205
|
-
draft_cmd.add_argument("--result-dir", default=str(CURRENT_DIR / "result"))
|
|
211
|
+
draft_cmd.add_argument("--output", default=str(CURRENT_DIR / "result"))
|
|
212
|
+
draft_cmd.add_argument("--status", default="active", choices=["draft", "active"])
|
|
206
213
|
draft_cmd.add_argument("--max-chars", type=int, default=None)
|
|
207
214
|
draft_cmd.set_defaults(func=cmd_draft)
|
|
208
215
|
|
|
209
216
|
validate_cmd = sub.add_parser("validate", help="Validate generated Markdown files.")
|
|
210
|
-
validate_cmd.add_argument("--input", default=str(CURRENT_DIR / "
|
|
217
|
+
validate_cmd.add_argument("--input", default=str(CURRENT_DIR / "result"))
|
|
211
218
|
validate_cmd.set_defaults(func=cmd_validate)
|
|
212
219
|
|
|
213
|
-
promote_cmd = sub.add_parser("promote", help="Copy reviewed files to result.")
|
|
214
|
-
promote_cmd.add_argument("--input", default=str(CURRENT_DIR / "approved"))
|
|
215
|
-
promote_cmd.add_argument("--result-dir", default=str(CURRENT_DIR / "result"))
|
|
216
|
-
promote_cmd.set_defaults(func=cmd_promote)
|
|
217
|
-
|
|
218
220
|
return parser
|
|
219
221
|
|
|
220
222
|
|