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 CHANGED
@@ -8,7 +8,7 @@
8
8
  2. 通过 Docling slim 的离线文本解析能力生成统一 Markdown 中间格式。
9
9
  3. 按章节、场景、规则、指标等粒度切割。
10
10
  4. 可选调用大模型,把内容整理为项目知识库规范要求的 Markdown 文件。
11
- 5. 默认生成 `status: draft` 草稿,不进入现有 RAG 检索。
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
- 如果 `drafts/` 中已有草稿文件,命令会先询问是否覆盖。选择 `y` 后会清空 `drafts/`、`approved/` 和 `result/` 中已有生成文件,再重新生成;选择其他内容会直接退出,避免多次生成结果相互影响。
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
- ```bash
63
- python ingest.py promote
64
- ```
62
+ `draft` 默认按 `config/config.yaml` 的 `draft.max_chars` 控制单次送入模型的原文长度,并额外提供文档目录和相邻片段摘要作为辅助上下文。这样可以降低私有模型单轮负载,同时尽量保留前后章节关系。命令行仍可用 `--max-chars` 临时覆盖。
65
63
 
66
- 默认目录为 `input/`、`parsed/`、`drafts/`、`approved/` 和 `result/`。只有需要处理其他目录时,才使用 `--input`、`--output` `--result-dir` 覆盖。
64
+ 每条知识库文件会写入分类画像元数据:`category`、`subcategory`、`category_keywords` 和 `related_items`。这些字段优先来自源文件一级标题、首页标题、章节目录、文件名、当前小类正文和关联小类语义,用于标识知识大类、小类、关键词和条目间关系。后续 RAG 入库和检索时,应把这些字段写入向量库 metadata,并用于分类过滤、查询路由或重排加权,降低不同场景之间因为相似词命中而串场的概率。
67
65
 
68
- `draft` 默认按 `config/config.yaml` 的 `draft.max_chars` 控制单次送入模型的原文长度,并额外提供文档目录和相邻片段摘要作为辅助上下文。这样可以降低私有模型单轮负载,同时尽量保留前后章节关系。命令行仍可用 `--max-chars` 临时覆盖。
66
+ 生成结果会按原始输入遍历顺序写入 `source_order`,并用 `000001-...md` 这样的文件名前缀保持目录排序与原文从上到下的顺序一致。页码只写入 Front Matter 的 `source_pages`/`source_trace` 和正文 `## 5. 来源依据`,不会进入正文 `## 1. 核心内容` 到 `## 4. 关联能力`。
69
67
 
70
68
  ## 大模型配置
71
69
 
72
- 默认不强制调用大模型,会使用启发式模板生成 `draft` 文件。
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
- 工具通过 ZhipuAI SDK 调用 GLM,不再包含 OpenAI 调用路径。`base_url` 使用 ZhipuAI SDK 的服务根地址即可。工具不 import 项目 `src` 代码。
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` 文件。确认无误后,人工把 `status: draft` 改为 `active`,再放入 `result/`,后续由线上知识库加载流程处理。
103
+ 这个工具只产出符合规范的 `*.md` 文件到 `result/`,后续由线上知识库加载流程处理。
106
104
 
107
105
  建议线上打包时排除整个 `tools/kb_ingest` 目录。
@@ -1,9 +1,9 @@
1
1
  llm:
2
- enabled: true
2
+ enabled: false
3
3
  timeout_seconds: 120
4
4
  max_tokens: 8192
5
5
  temperature: 0.1
6
- api_key: "15f066c4509845038027ea5746524af5.w4CLSC6ODiKVC1wK"
6
+ api_key: ""
7
7
  model: "GLM-4.7-Flash"
8
8
  base_url: "https://open.bigmodel.cn/api/paas/v4/"
9
9
 
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, approved_dir, result_dir, existing):
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, approved_dir, result_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="draft"):
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 / "drafts"))
204
- draft_cmd.add_argument("--approved-dir", default=str(CURRENT_DIR / "approved"))
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 / "drafts"))
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