union_kb_ingest 1.0.3 → 1.0.5

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
@@ -61,7 +61,7 @@ python ingest.py validate
61
61
 
62
62
  `draft` 默认按 `config/config.yaml` 的 `draft.max_chars` 控制单次送入模型的原文长度,并额外提供文档目录和相邻片段摘要作为辅助上下文。这样可以降低私有模型单轮负载,同时尽量保留前后章节关系。命令行仍可用 `--max-chars` 临时覆盖。
63
63
 
64
- 每条知识库文件会写入分类画像元数据:`category`、`category_description` 和 `category_keywords`。这些字段优先来自源文件一级标题、首页标题、章节目录和文件名,用于标识一个批次/业务场景的大类。后续 RAG 入库和检索时,应把这些字段写入向量库 metadata,并用于分类过滤、查询路由或重排加权,降低不同场景之间因为相似词命中而串场的概率。
64
+ 每条知识库文件会写入分类画像元数据:`category`、`subcategory`、`category_keywords` 和 `related_items`。这些字段优先来自源文件一级标题、首页标题、章节目录、文件名、当前小类正文和关联小类语义,用于标识知识大类、小类、关键词和条目间关系。后续 RAG 入库和检索时,应把这些字段写入向量库 metadata,并用于分类过滤、查询路由或重排加权,降低不同场景之间因为相似词命中而串场的概率。
65
65
 
66
66
  生成结果会按原始输入遍历顺序写入 `source_order`,并用 `000001-...md` 这样的文件名前缀保持目录排序与原文从上到下的顺序一致。页码只写入 Front Matter 的 `source_pages`/`source_trace` 和正文 `## 5. 来源依据`,不会进入正文 `## 1. 核心内容` 到 `## 4. 关联能力`。
67
67
 
package/app_config.py CHANGED
@@ -15,6 +15,7 @@ DEFAULT_CONFIG_PATH = CURRENT_DIR / "config" / "config.yaml"
15
15
 
16
16
  @dataclass(frozen=True)
17
17
  class LlmConfig:
18
+ """LLM 调用配置,支持配置文件和环境变量覆盖。"""
18
19
  enabled: bool = False
19
20
  base_url: str = ""
20
21
  api_key: str = ""
@@ -26,6 +27,7 @@ class LlmConfig:
26
27
 
27
28
  @dataclass(frozen=True)
28
29
  class DraftConfig:
30
+ """草稿生成阶段的切分和上下文配置。"""
29
31
  max_chars: int = 3600
30
32
  context_chars: int = 800
31
33
  outline_max_sections: int = 40
@@ -33,6 +35,7 @@ class DraftConfig:
33
35
 
34
36
  @lru_cache(maxsize=1)
35
37
  def get_llm_config() -> LlmConfig:
38
+ """读取并合并 LLM 配置。"""
36
39
  raw = _read_config().get("llm", {})
37
40
  if not isinstance(raw, dict):
38
41
  raw = {}
@@ -50,6 +53,7 @@ def get_llm_config() -> LlmConfig:
50
53
 
51
54
  @lru_cache(maxsize=1)
52
55
  def get_draft_config() -> DraftConfig:
56
+ """读取并合并草稿生成配置。"""
53
57
  raw = _read_config().get("draft", {})
54
58
  if not isinstance(raw, dict):
55
59
  raw = {}
@@ -62,6 +66,7 @@ def get_draft_config() -> DraftConfig:
62
66
 
63
67
 
64
68
  def _read_config() -> Dict[str, Any]:
69
+ """读取 YAML 配置文件并返回字典。"""
65
70
  path = Path(os.environ.get("KB_INGEST_CONFIG", DEFAULT_CONFIG_PATH))
66
71
  if not path.exists():
67
72
  return {}
@@ -70,6 +75,7 @@ def _read_config() -> Dict[str, Any]:
70
75
 
71
76
 
72
77
  def _env_bool(name: str, default: bool) -> bool:
78
+ """读取布尔环境变量并回退到默认值。"""
73
79
  value = os.environ.get(name)
74
80
  if value is None:
75
81
  return default
@@ -77,6 +83,7 @@ def _env_bool(name: str, default: bool) -> bool:
77
83
 
78
84
 
79
85
  def _env_int(name: str, value: Any, default: int) -> int:
86
+ """读取整数环境变量并回退到默认值。"""
80
87
  raw = os.environ.get(name, value)
81
88
  try:
82
89
  return int(raw)
@@ -85,6 +92,7 @@ def _env_int(name: str, value: Any, default: int) -> int:
85
92
 
86
93
 
87
94
  def _env_float(name: str, value: Any, default: float) -> float:
95
+ """读取浮点环境变量并回退到默认值。"""
88
96
  raw = os.environ.get(name, value)
89
97
  try:
90
98
  return float(raw)
@@ -93,6 +101,7 @@ def _env_float(name: str, value: Any, default: float) -> float:
93
101
 
94
102
 
95
103
  def _as_bool(value: Any, default: bool) -> bool:
104
+ """把配置值转换为布尔值。"""
96
105
  if isinstance(value, bool):
97
106
  return value
98
107
  if isinstance(value, str):
@@ -1,9 +1,9 @@
1
1
  llm:
2
- enabled: false
2
+ enabled: true
3
3
  timeout_seconds: 120
4
4
  max_tokens: 8192
5
5
  temperature: 0.1
6
- api_key: "your-zhipu-api-key"
6
+ api_key: "15f066c4509845038027ea5746524af5.w4CLSC6ODiKVC1wK"
7
7
  model: "GLM-4.7-Flash"
8
8
  base_url: "https://open.bigmodel.cn/api/paas/v4/"
9
9
 
package/ingest.py CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
 
4
4
  import argparse
5
5
  import sys
6
+ from dataclasses import replace
6
7
  from pathlib import Path
7
8
  from typing import List
8
9
 
@@ -23,6 +24,7 @@ IGNORED_EXISTING_FILES = {".gitkeep", ".DS_Store"}
23
24
 
24
25
 
25
26
  def cmd_parse(args) -> int:
27
+ """执行解析子命令。"""
26
28
  input_path = Path(args.input)
27
29
  output_dir = Path(args.output)
28
30
  output_dir.mkdir(parents=True, exist_ok=True)
@@ -38,6 +40,7 @@ def cmd_parse(args) -> int:
38
40
 
39
41
 
40
42
  def cmd_draft(args) -> int:
43
+ """执行草稿生成子命令。"""
41
44
  input_path = Path(args.input)
42
45
  output_dir = Path(args.output)
43
46
 
@@ -78,6 +81,7 @@ def cmd_draft(args) -> int:
78
81
 
79
82
 
80
83
  def _list_effective_files(path: Path) -> list[Path]:
84
+ """列出目录下需要考虑的已有文件。"""
81
85
  if not path.exists():
82
86
  return []
83
87
  return sorted(
@@ -90,6 +94,7 @@ def _confirm_overwrite(
90
94
  output_dir: Path,
91
95
  existing: list[Path],
92
96
  ) -> bool:
97
+ """询问用户是否覆盖已有生成文件。"""
93
98
  print(f"found {len(existing)} existing file(s) in {output_dir}.")
94
99
  print("Continuing will delete existing generated files under:")
95
100
  print(f"- {output_dir}")
@@ -98,6 +103,7 @@ def _confirm_overwrite(
98
103
 
99
104
 
100
105
  def _clear_generated_files(*dirs: Path) -> None:
106
+ """删除指定目录下的已有生成文件。"""
101
107
  for directory in dirs:
102
108
  for path in _list_effective_files(directory):
103
109
  path.unlink()
@@ -109,6 +115,7 @@ def _attach_block_context(
109
115
  context_chars: int,
110
116
  outline_max_sections: int,
111
117
  ) -> List[ParsedBlock]:
118
+ """为片段附加目录和邻近片段上下文。"""
112
119
  if context_chars <= 0:
113
120
  return blocks
114
121
 
@@ -118,11 +125,11 @@ def _attach_block_context(
118
125
  parts = []
119
126
  if outline:
120
127
  parts.append(f"文档章节目录:\n{outline}")
121
- if block.category_description:
128
+ if block.category or block.subcategory or block.category_keywords:
122
129
  parts.append(
123
- "知识大类说明:\n"
124
- f"大类:{block.category}\n"
125
- f"说明:{block.category_description}\n"
130
+ "知识分类:\n"
131
+ f"大类标题:{block.category}\n"
132
+ f"小类标题:{block.subcategory}\n"
126
133
  f"关键词:{', '.join(block.category_keywords)}"
127
134
  )
128
135
  if idx > 0:
@@ -137,21 +144,15 @@ def _attach_block_context(
137
144
  f"章节:{blocks[idx + 1].source_section}\n"
138
145
  f"{_compact_context_text(blocks[idx + 1].content, context_chars // 2)}"
139
146
  )
140
- output.append(ParsedBlock(
141
- source_doc=block.source_doc,
142
- source_section=block.source_section,
143
- content=block.content,
144
- pages=block.pages,
145
- order=block.order,
147
+ output.append(replace(
148
+ block,
146
149
  context="\n\n".join(parts),
147
- category=block.category,
148
- category_description=block.category_description,
149
- category_keywords=block.category_keywords,
150
150
  ))
151
151
  return output
152
152
 
153
153
 
154
154
  def _document_outline(blocks: List[ParsedBlock], max_sections: int) -> str:
155
+ """生成文档片段目录摘要。"""
155
156
  sections = []
156
157
  seen = set()
157
158
  for block in blocks:
@@ -169,6 +170,7 @@ def _document_outline(blocks: List[ParsedBlock], max_sections: int) -> str:
169
170
 
170
171
 
171
172
  def _compact_context_text(text: str, limit: int) -> str:
173
+ """压缩上下文文本到指定长度。"""
172
174
  compact = " ".join(text.split())
173
175
  if limit <= 0 or len(compact) <= limit:
174
176
  return compact
@@ -176,6 +178,7 @@ def _compact_context_text(text: str, limit: int) -> str:
176
178
 
177
179
 
178
180
  def _source_trace(block: ParsedBlock) -> str:
181
+ """生成来源章节和页码追踪信息。"""
179
182
  parts = [f"section={block.source_section}"]
180
183
  if block.pages:
181
184
  parts.append(f"pages={','.join(map(str, sorted(set(block.pages))))}")
@@ -183,6 +186,7 @@ def _source_trace(block: ParsedBlock) -> str:
183
186
 
184
187
 
185
188
  def cmd_validate(args) -> int:
189
+ """执行校验子命令。"""
186
190
  issues = validate_dir(Path(args.input))
187
191
  for issue in issues:
188
192
  print(f"{issue.level}: {issue.path}: {issue.message}")
@@ -191,6 +195,7 @@ def cmd_validate(args) -> int:
191
195
 
192
196
 
193
197
  def build_parser() -> argparse.ArgumentParser:
198
+ """构建命令行参数解析器。"""
194
199
  parser = argparse.ArgumentParser(description="Offline document-to-knowledge Markdown generator.")
195
200
  sub = parser.add_subparsers(dest="command", required=True)
196
201
 
@@ -214,6 +219,7 @@ def build_parser() -> argparse.ArgumentParser:
214
219
 
215
220
 
216
221
  def main() -> int:
222
+ """命令行入口。"""
217
223
  parser = build_parser()
218
224
  args = parser.parse_args()
219
225
  return args.func(args)