union_kb_ingest 1.0.0

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/parser.py ADDED
@@ -0,0 +1,287 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ import shutil
5
+ import subprocess
6
+ import tempfile
7
+ from pathlib import Path
8
+ from typing import List
9
+
10
+ from schemas import ParsedBlock, ParsedDocument
11
+
12
+
13
+ SUPPORTED_EXTENSIONS = {".pdf", ".docx", ".doc", ".md", ".markdown", ".txt"}
14
+
15
+
16
+ def iter_input_files(input_path: Path) -> List[Path]:
17
+ if input_path.is_file():
18
+ return [input_path] if input_path.suffix.lower() in SUPPORTED_EXTENSIONS else []
19
+ files = [
20
+ p for p in input_path.rglob("*")
21
+ if p.is_file() and p.suffix.lower() in SUPPORTED_EXTENSIONS
22
+ ]
23
+ return sorted(files)
24
+
25
+
26
+ def parse_document(path: Path) -> ParsedDocument:
27
+ suffix = path.suffix.lower()
28
+ if suffix == ".pdf":
29
+ markdown = _parse_pdf_text(path)
30
+ elif suffix == ".docx":
31
+ markdown = _parse_docx(path)
32
+ elif suffix == ".doc":
33
+ markdown = _parse_legacy_doc(path)
34
+ elif suffix in {".md", ".markdown", ".txt"}:
35
+ markdown = path.read_text(encoding="utf-8")
36
+ else:
37
+ raise ValueError(f"Unsupported file type: {path}")
38
+
39
+ blocks = _markdown_to_blocks(path.name, markdown)
40
+ return ParsedDocument(
41
+ source_path=path,
42
+ source_doc=path.name,
43
+ markdown=markdown,
44
+ blocks=blocks,
45
+ )
46
+
47
+
48
+ def _parse_pdf_text(path: Path) -> str:
49
+ """Extract embedded PDF text without OCR, layout models, or remote artifacts."""
50
+ from docling_parse.pdf_parser import DoclingPdfParser
51
+ from docling_parse.pdf_parsers import DecodePageConfig
52
+
53
+ parser = DoclingPdfParser(loglevel="fatal")
54
+ document = parser.load(path_or_stream=path)
55
+ if document is None:
56
+ raise RuntimeError(f"docling-parse could not load PDF: {path}")
57
+
58
+ try:
59
+ config = DecodePageConfig()
60
+ config.keep_char_cells = True
61
+ config.keep_shapes = False
62
+ config.keep_bitmaps = False
63
+ config.create_word_cells = False
64
+ config.create_line_cells = True
65
+ config.enforce_same_font = True
66
+
67
+ lines: List[str] = []
68
+ for page_no in range(1, document.number_of_pages() + 1):
69
+ page = document.get_page(page_no, config=config)
70
+ lines.extend(cell.text.strip() for cell in page.textline_cells if cell.text.strip())
71
+ return _compact_pdf_lines(lines)
72
+ finally:
73
+ document.unload()
74
+
75
+
76
+ def _compact_pdf_lines(lines: List[str]) -> str:
77
+ cleaned = [_normalize_pdf_line(line) for line in lines]
78
+ cleaned = [line for line in cleaned if line and not _is_noise_line(line)]
79
+
80
+ output: List[str] = []
81
+ paragraph = ""
82
+ for line in cleaned:
83
+ heading = _heading_markdown(line)
84
+ if heading:
85
+ if paragraph:
86
+ output.append(paragraph)
87
+ paragraph = ""
88
+ output.append(heading)
89
+ continue
90
+
91
+ if _should_keep_as_own_line(line):
92
+ if paragraph:
93
+ output.append(paragraph)
94
+ paragraph = ""
95
+ output.append(line)
96
+ continue
97
+
98
+ paragraph = _join_text(paragraph, line)
99
+
100
+ if paragraph:
101
+ output.append(paragraph)
102
+
103
+ return _separate_embedded_headings("\n\n".join(output).strip())
104
+
105
+
106
+ def _normalize_pdf_line(line: str) -> str:
107
+ line = line.replace("\u00a0", " ")
108
+ line = line.replace("网联清算有限公司", "")
109
+ line = re.sub(r"[ \t]+", " ", line)
110
+ return line.strip()
111
+
112
+
113
+ def _is_noise_line(line: str) -> bool:
114
+ if re.fullmatch(r"[-—–_·•\s]+", line):
115
+ return True
116
+ if re.fullmatch(r"\d{1,4}", line):
117
+ return True
118
+ if re.fullmatch(r"第\s*\d{1,4}\s*页(?:\s*/\s*共\s*\d{1,4}\s*页)?", line):
119
+ return True
120
+ if re.fullmatch(r"Page\s+\d{1,4}(?:\s+of\s+\d{1,4})?", line, re.I):
121
+ return True
122
+ if "目 录" in line or "目录" in line and "." * 3 in line:
123
+ return True
124
+ if line.count(".") >= 20:
125
+ return True
126
+ if re.search(r"[\x00-\x08\x0b-\x1f\x7f]", line):
127
+ return True
128
+ return False
129
+
130
+
131
+ def _heading_markdown(line: str) -> str:
132
+ if line.startswith("#"):
133
+ return line
134
+
135
+ match = re.match(r"^(\d+(?:\.\d+){1,4})[、..\s]*(.{2,100})$", line)
136
+ if match:
137
+ title = match.group(2).strip()
138
+ if not re.match(r"^[\u4e00-\u9fffA-Za-z]", title):
139
+ return ""
140
+ depth = 2 + min(match.group(1).count("."), 2)
141
+ return f"{'#' * depth} {line}"
142
+
143
+ match = re.match(r"^(第[一二三四五六七八九十百千万]+[章节条])[、..\s]*(.{2,100})$", line)
144
+ if match:
145
+ return f"## {line}"
146
+
147
+ if re.match(r"^附\s*录\s*[A-ZA-Z]\s*.{2,100}$", line):
148
+ return f"## {line}"
149
+
150
+ return ""
151
+
152
+
153
+ def _separate_embedded_headings(text: str) -> str:
154
+ preface = text.find("前 言本指引")
155
+ if preface > 0:
156
+ text = text[preface:]
157
+
158
+ def split_numeric(match: re.Match) -> str:
159
+ start = match.start()
160
+ prev = text[max(0, start - 8):start]
161
+ if prev.endswith(("参见", "详见", "参考")) or re.search(r"[Vv]$", prev):
162
+ return match.group(1)
163
+ if not prev.endswith(("指引", "说明", "文件", "策略", "评价", "内容", "范围")):
164
+ return match.group(1)
165
+ return f"\n\n{match.group(1)}"
166
+
167
+ text = re.sub(
168
+ r"(?<![\d.])(\d+\.\d+(?:\.\d+){0,3})(?=[\u4e00-\u9fffA-Za-z])",
169
+ split_numeric,
170
+ text,
171
+ )
172
+ text = re.sub(
173
+ r"(?<!\n)(附\s*录\s*[A-ZA-Z])(?=[\u4e00-\u9fffA-Za-z])",
174
+ r"\n\n\1",
175
+ text,
176
+ )
177
+ text = re.sub(r"\n{3,}", "\n\n", text)
178
+ return text.strip()
179
+
180
+
181
+ def _should_keep_as_own_line(line: str) -> bool:
182
+ return bool(
183
+ re.match(r"^([((]?[一二三四五六七八九十]+[))]?|[A-Za-z]|\d+)[、..)]\s+", line)
184
+ or re.search(r"\s{2,}", line)
185
+ or "\t" in line
186
+ )
187
+
188
+
189
+ def _join_text(current: str, line: str) -> str:
190
+ if not current:
191
+ return line
192
+ if current.endswith(("。", ";", ":", ":", "?", "!", ".", ";", "?", "!")):
193
+ return f"{current}\n{line}"
194
+ if re.search(r"[A-Za-z0-9]$", current) and re.match(r"^[A-Za-z0-9]", line):
195
+ return f"{current} {line}"
196
+ return f"{current}{line}"
197
+
198
+
199
+ def _parse_docx(path: Path) -> str:
200
+ from docling.datamodel.base_models import InputFormat
201
+ from docling.document_converter import DocumentConverter, WordFormatOption
202
+
203
+ converter = DocumentConverter(
204
+ allowed_formats=[InputFormat.DOCX],
205
+ format_options={InputFormat.DOCX: WordFormatOption()},
206
+ )
207
+ result = converter.convert(str(path))
208
+ return result.document.export_to_markdown()
209
+
210
+
211
+ def _parse_legacy_doc(path: Path) -> str:
212
+ soffice = shutil.which("soffice") or shutil.which("libreoffice")
213
+ if not soffice:
214
+ raise RuntimeError(
215
+ "Legacy .doc parsing needs LibreOffice headless conversion. "
216
+ "Install LibreOffice and expose soffice in PATH, or convert the file to .docx first."
217
+ )
218
+
219
+ with tempfile.TemporaryDirectory(prefix="kb_ingest_doc_") as tmp:
220
+ tmp_dir = Path(tmp)
221
+ subprocess.run(
222
+ [
223
+ soffice,
224
+ "--headless",
225
+ "--convert-to",
226
+ "docx",
227
+ "--outdir",
228
+ str(tmp_dir),
229
+ str(path),
230
+ ],
231
+ check=True,
232
+ stdout=subprocess.PIPE,
233
+ stderr=subprocess.PIPE,
234
+ text=True,
235
+ )
236
+ converted = tmp_dir / f"{path.stem}.docx"
237
+ if not converted.exists():
238
+ candidates = list(tmp_dir.glob("*.docx"))
239
+ if not candidates:
240
+ raise RuntimeError(f"LibreOffice did not produce docx for {path}")
241
+ converted = candidates[0]
242
+ return _parse_docx(converted)
243
+
244
+
245
+ def _markdown_to_blocks(source_doc: str, markdown: str) -> List[ParsedBlock]:
246
+ normalized = _recover_headings(markdown)
247
+ pieces = re.split(r"\n(?=#{1,4}\s+)", normalized)
248
+ blocks: List[ParsedBlock] = []
249
+ for idx, piece in enumerate(p.strip() for p in pieces if p.strip()):
250
+ title = _first_heading(piece) or f"文档片段 {idx + 1}"
251
+ pages = [int(n) for n in re.findall(r"第\s*(\d+)\s*页", title)]
252
+ blocks.append(ParsedBlock(
253
+ source_doc=source_doc,
254
+ source_section=title,
255
+ content=piece,
256
+ pages=pages,
257
+ order=idx,
258
+ ))
259
+ if not blocks and normalized.strip():
260
+ blocks.append(ParsedBlock(source_doc=source_doc, source_section="全文", content=normalized.strip()))
261
+ return blocks
262
+
263
+
264
+ def _recover_headings(markdown: str) -> str:
265
+ lines = []
266
+ heading_pattern = re.compile(r"^(\d+(?:\.\d+){1,4}|第[一二三四五六七八九十百]+[章节条]|附\s*录\s*[A-ZA-Z])[\s、..]*(.{2,80})$")
267
+ for raw in markdown.splitlines():
268
+ line = raw.strip()
269
+ match = heading_pattern.match(line)
270
+ if match and not line.startswith("#"):
271
+ title = match.group(2).strip()
272
+ if not re.match(r"^[\u4e00-\u9fffA-Za-z]", title):
273
+ lines.append(raw)
274
+ continue
275
+ depth = 2 + min(match.group(1).count("."), 2)
276
+ lines.append(f"{'#' * depth} {line}")
277
+ else:
278
+ lines.append(raw)
279
+ return "\n".join(lines)
280
+
281
+
282
+ def _first_heading(text: str) -> str:
283
+ for line in text.splitlines():
284
+ match = re.match(r"^#{1,4}\s+(.+)$", line.strip())
285
+ if match:
286
+ return match.group(1).strip()
287
+ return ""
@@ -0,0 +1,27 @@
1
+ 你是联合运维知识库整理助手。
2
+
3
+ 请基于输入原文生成标准知识库条目,并严格参照 `prompts/联合运维知识库建立规范.md`。必须遵守:
4
+
5
+ 1. 只依据原文,不编造阈值、角色、日期、版本。
6
+ 2. 如果一个片段包含多个独立场景、规则、指标、处置策略,拆成多个 items。
7
+ 3. 每个 item 需要可独立检索、独立回答。
8
+ 4. 保留表格、阈值、比较符、单位、持续时间和适用对象。
9
+ 5. 输出严格 JSON,不要 Markdown 代码围栏。
10
+ 6. 不要依据预设业务关键词套写业务模块、角色、标签或风险等级,应根据原文语义判断;原文缺失时使用空数组或规范允许的默认值。
11
+
12
+ 输出格式:
13
+
14
+ {
15
+ "items": [
16
+ {
17
+ "title": "",
18
+ "doc_type": "scenario",
19
+ "business_modules": [],
20
+ "source_version": "",
21
+ "risk_level": "low",
22
+ "applicable_roles": [],
23
+ "tags": [],
24
+ "body": "# 标题\n\n## 1. 适用范围\n\n...\n\n## 7. 来源依据\n\n..."
25
+ }
26
+ ]
27
+ }
@@ -0,0 +1,272 @@
1
+ # 联合运维知识库建立规范
2
+
3
+ ## 1. 建设目标
4
+
5
+ 联合运维知识库用于支撑运维智能助手在一期阶段完成“只读问答 + 查询类 Function Call”的能力建设。知识库应帮助系统准确识别用户问题所属的业务模块、运行场景、风险等级、处置策略、可用查询能力和引用依据。
6
+
7
+ 一期知识库的核心目标如下:
8
+
9
+ 1. 支持 RAG 检索识别业务场景和业务模块。
10
+ 2. 支持大模型基于权威资料回答运维制度、指标阈值、处置策略、故障定级等问题。
11
+ 3. 支持大模型理解可用的查询类 Function Call,但不直接执行变更类动作。
12
+ 4. 支持检索结果可追溯到原始文档、章节和版本。
13
+ 5. 支持后续扩展到诊断类、操作类和审批类能力。
14
+
15
+ ## 2. 文档分类
16
+
17
+ 知识库文档按内容类型分为以下几类:
18
+
19
+ | 类型 | 编码前缀 | 说明 | 示例 |
20
+ | --- | --- | --- | --- |
21
+ | 业务模块 | `biz` | 描述业务范围、业务链路、参与方、上下游依赖 | 快捷支付、退款、付款 |
22
+ | 运行场景 | `scenario` | 描述生产运行风险、业务异常、关闭渠道、恢复等场景 | 成功率下降、耗时升高 |
23
+ | 处置策略 | `sop` | 描述不同角色在不同场景下应采取的动作 | 异常方排查、影响方降流 |
24
+ | 指标规则 | `metric` | 描述指标定义、阈值、计算口径 | 系统成功率、平均耗时 |
25
+ | 故障定级 | `severity` | 描述故障等级、触发条件、善后要求 | 轻微、一般、严重 |
26
+ | 变更管理 | `change` | 描述生产变更评估、通知、异常处置、质量评价 | 变更影响评估 |
27
+ | 函数说明 | `function` | 描述可调用函数的用途、参数、权限、返回值 | 查询告警、查询指标 |
28
+ | 评价规则 | `evaluation` | 描述周期评价对象、周期、指标、权重和定级 | 周/月/年评价 |
29
+
30
+ ## 3. 单篇知识文档格式
31
+
32
+ 每个知识条目建议使用 Markdown 文件保存,并使用 YAML Front Matter 描述元数据。正文结构应稳定,便于切分、检索和展示。
33
+
34
+ ```markdown
35
+ ---
36
+ kb_id: "scenario-netunion-runtime-risk-v1"
37
+ title: "网络支付清算平台生产运行风险场景"
38
+ doc_type: "scenario"
39
+ domain: "联合运维"
40
+ business_modules: ["签约", "解约", "快捷支付", "付款", "退款", "代收", "网关支付"]
41
+ source_doc: "网络支付清算平台联合运维运行实践指引V1.0.md"
42
+ source_version: "V1.0"
43
+ source_section: "5.2.1 存在运行风险"
44
+ effective_date: ""
45
+ owner: "联合运维知识库"
46
+ confidentiality: "内部"
47
+ risk_level: "low"
48
+ applicable_roles: ["网联平台", "异常方", "影响方", "成员单位"]
49
+ tags: ["生产运行", "风险", "系统成功率", "平均耗时", "带宽"]
50
+ status: "active"
51
+ ---
52
+
53
+ # 标题
54
+
55
+ ## 1. 适用范围
56
+
57
+ ## 2. 触发条件
58
+
59
+ ## 3. 处置策略
60
+
61
+ ## 4. 关联指标
62
+
63
+ ## 5. 关联函数
64
+
65
+ ## 6. 检索提示
66
+
67
+ ## 7. 来源依据
68
+ ```
69
+
70
+ ## 4. 元数据规范
71
+
72
+ | 字段 | 必填 | 说明 | 示例 |
73
+ | --- | --- | --- | --- |
74
+ | `kb_id` | 是 | 全局唯一 ID,建议使用英文、数字和短横线 | `severity-netunion-runtime-v1` |
75
+ | `title` | 是 | 知识条目标题 | `运行故障定级规则` |
76
+ | `doc_type` | 是 | 文档分类 | `severity` |
77
+ | `domain` | 是 | 所属领域 | `联合运维` |
78
+ | `business_modules` | 是 | 适用业务模块,可为空数组但字段必须存在 | `["快捷支付", "退款"]` |
79
+ | `source_doc` | 是 | 来源文档文件名 | `网络支付清算平台联合运维运行实践指引V1.0.md` |
80
+ | `source_version` | 是 | 来源版本 | `V1.0` |
81
+ | `source_section` | 是 | 来源章节 | `5.3 运行故障定级` |
82
+ | `effective_date` | 否 | 生效日期,未知可留空 | `2026-01-01` |
83
+ | `owner` | 是 | 维护责任方 | `联合运维知识库` |
84
+ | `confidentiality` | 是 | 密级 | `内部` |
85
+ | `risk_level` | 是 | 知识对应的操作风险 | `low` |
86
+ | `applicable_roles` | 是 | 适用角色 | `["异常方", "影响方"]` |
87
+ | `tags` | 是 | 检索标签 | `["故障", "定级"]` |
88
+ | `status` | 是 | 状态 | `active` |
89
+
90
+ `risk_level` 建议枚举:
91
+
92
+ | 值 | 含义 | 一期处理策略 |
93
+ | --- | --- | --- |
94
+ | `low` | 查询、解释、制度说明 | 可直接回答或调用只读函数 |
95
+ | `medium` | 诊断建议、影响判断 | 可回答,可建议查询,不自动变更 |
96
+ | `high` | 涉及降流、关停、重启、扩容、回滚 | 一期只给建议,不执行 |
97
+ | `critical` | 涉及生产高危或不可逆动作 | 必须转人工审批 |
98
+
99
+ ## 5. 正文内容规范
100
+
101
+ ### 5.1 适用范围
102
+
103
+ 说明该知识条目适用于哪些业务、系统、角色和场景。范围要明确,避免出现“所有情况均适用”这类模糊表述。
104
+
105
+ ### 5.2 触发条件
106
+
107
+ 凡是涉及阈值、时间、比例、数量的内容,必须结构化表达。建议使用表格。
108
+
109
+ 示例:
110
+
111
+ | 指标 | 大型单位 | 中型单位 | 小型单位 | 持续时间 | 是否决策指标 |
112
+ | --- | --- | --- | --- | --- | --- |
113
+ | 平均系统成功率 | `<99.99% 且系统失败笔数 >100` | `<99.99% 且系统失败笔数 >50` | `<99.99% 且系统失败笔数 >10` | `5min` | 是 |
114
+
115
+ ### 5.3 处置策略
116
+
117
+ 处置策略必须按角色拆分,至少区分:
118
+
119
+ 1. 平台方。
120
+ 2. 异常方。
121
+ 3. 影响方。
122
+ 4. 变更发起方。
123
+ 5. 变更影响方。
124
+
125
+ 动作应使用明确动词,例如“通知”“排查”“同步”“降流”“暂停发送”“灰度恢复”。涉及高风险动作时,必须标记“需人工确认”。
126
+
127
+ ### 5.4 关联指标
128
+
129
+ 指标必须引用统一指标口径。指标定义应尽量沉淀为独立 `metric` 条目,业务条目中只引用。
130
+
131
+ 常见指标包括:
132
+
133
+ 1. 系统请求数量。
134
+ 2. 系统失败数量。
135
+ 3. 业务失败数量。
136
+ 4. 系统成功率。
137
+ 5. 业务成功率。
138
+ 6. 平均耗时。
139
+ 7. 正常服务时间。
140
+ 8. 异常持续时间。
141
+
142
+ ### 5.5 关联函数
143
+
144
+ 一期只登记查询类函数。函数说明可以进入知识库,但执行权限由 Function Call 层控制。
145
+
146
+ 函数说明建议格式:
147
+
148
+ ```yaml
149
+ function_name: "query_runtime_metrics"
150
+ display_name: "查询运行指标"
151
+ function_type: "read"
152
+ description: "查询指定业务、机构、时间窗口内的系统成功率、业务成功率、平均耗时和交易量。"
153
+ risk_level: "low"
154
+ requires_confirmation: false
155
+ required_permissions: ["metrics:read"]
156
+ input_schema:
157
+ business_module: "业务模块,例如快捷支付、退款"
158
+ org_type: "单位分类,例如大型单位、中型单位、小型单位"
159
+ time_range: "查询时间范围"
160
+ output_schema:
161
+ system_success_rate: "系统成功率"
162
+ business_success_rate: "业务成功率"
163
+ avg_latency_ms: "平均耗时"
164
+ request_count: "系统请求数量"
165
+ failed_count: "系统失败数量"
166
+ ```
167
+
168
+ ### 5.6 检索提示
169
+
170
+ 为提升 RAG 命中率,每篇知识条目应维护自然语言检索提示,包括用户可能的问法。
171
+
172
+ 示例:
173
+
174
+ 1. “系统成功率低于多少算运行风险?”
175
+ 2. “平均耗时超过 2 秒要怎么处理?”
176
+ 3. “大型单位异常交易多少笔需要升级?”
177
+
178
+ ### 5.7 来源依据
179
+
180
+ 必须记录来源文档、版本和章节。若为人工总结,应标明“基于来源章节归纳”,避免将推理内容伪装成原文。
181
+
182
+ ## 6. 内容切分规范
183
+
184
+ 为了兼顾召回准确性和上下文完整性,建议按“一个知识点一条文档”的方式切分。
185
+
186
+ 切分原则:
187
+
188
+ 1. 一个场景一条,例如“存在运行风险”“出现业务异常-联合处置”“出现业务异常-关闭渠道”。
189
+ 2. 一个规则一条,例如“运行故障定级”“故障善后处置”。
190
+ 3. 一个函数一条,例如“查询运行指标函数说明”。
191
+ 4. 单条正文建议控制在 800 到 1500 个中文字符,复杂表格类条目可适当放宽。
192
+ 5. 不要把整份制度原文作为一个向量文档直接入库。
193
+ 6. 表格必须保留表头,避免阈值脱离适用对象。
194
+ 7. 涉及“或”“且”“大于等于”“小于”等逻辑关系时,必须保留原始逻辑。
195
+
196
+ ## 7. 命名规范
197
+
198
+ 文件名建议格式:
199
+
200
+ ```text
201
+ {doc_type}-{domain_or_system}-{topic}-v{version}.md
202
+ ```
203
+
204
+ 示例:
205
+
206
+ ```text
207
+ scenario-netunion-runtime-risk-v1.md
208
+ severity-netunion-runtime-classification-v1.md
209
+ function-netunion-query-runtime-metrics-v1.md
210
+ ```
211
+
212
+ 中文标题可以放在 `title` 字段中,文件名建议使用英文,便于程序处理和跨平台同步。
213
+
214
+ ## 8. RAG 入库字段建议
215
+
216
+ 入库时建议至少写入以下字段:
217
+
218
+ | 字段 | 来源 | 用途 |
219
+ | --- | --- | --- |
220
+ | `content` | Markdown 正文 | 向量检索 |
221
+ | `title` | Front Matter | 展示和关键词检索 |
222
+ | `doc_type` | Front Matter | 检索过滤 |
223
+ | `business_modules` | Front Matter | 业务模块过滤 |
224
+ | `tags` | Front Matter | 关键词召回 |
225
+ | `source_doc` | Front Matter | 溯源 |
226
+ | `source_section` | Front Matter | 溯源 |
227
+ | `risk_level` | Front Matter | 工具调用安全控制 |
228
+ | `applicable_roles` | Front Matter | 角色匹配 |
229
+ | `status` | Front Matter | 过滤废弃条目 |
230
+
231
+ ## 9. 质量校验规范
232
+
233
+ 每条知识入库前应检查:
234
+
235
+ 1. 是否有唯一 `kb_id`。
236
+ 2. 是否能追溯到来源文档和章节。
237
+ 3. 是否包含适用范围。
238
+ 4. 阈值是否保留单位、比较符和持续时间。
239
+ 5. 处置策略是否按角色拆分。
240
+ 6. 是否标注风险等级。
241
+ 7. 是否存在过期或冲突内容。
242
+ 8. 是否包含检索提示。
243
+ 9. 是否避免把高风险操作描述成可自动执行。
244
+ 10. 是否与已有知识条目重复或冲突。
245
+
246
+ ## 10. 一期推荐知识库目录
247
+
248
+ ```text
249
+ trainingDocs/
250
+ knowledgeBase/
251
+ 联合运维知识库建立规范.md
252
+ samples/
253
+ scenario-netunion-runtime-risk-v1.md
254
+ scenario-netunion-business-exception-joint-handling-v1.md
255
+ scenario-netunion-business-exception-channel-close-v1.md
256
+ severity-netunion-runtime-classification-v1.md
257
+ metric-netunion-runtime-indicators-v1.md
258
+ function-netunion-query-runtime-metrics-v1.md
259
+ ```
260
+
261
+ 一期建议优先沉淀以下条目:
262
+
263
+ 1. 业务范围与单位分类。
264
+ 2. 生产运行风险场景。
265
+ 3. 业务异常联合处置场景。
266
+ 4. 业务异常关闭渠道场景。
267
+ 5. 处置升级策略。
268
+ 6. 恢复策略。
269
+ 7. 运行故障定级。
270
+ 8. 故障善后处置。
271
+ 9. 运行指标定义。
272
+ 10. 查询类函数说明。
@@ -0,0 +1,9 @@
1
+ # Offline-only optional dependencies. Do not add these to the app runtime unless needed.
2
+ pyyaml>=6.0.1
3
+ zhipuai>=2.1.0
4
+ sniffio>=1.3.0
5
+
6
+ # Docling slim plus only file-format backends used by this offline tool.
7
+ # Do not install `docling` or `docling-slim[standard]` here: those pull OCR,
8
+ # layout/table ML models, torch/onnxruntime, and may try to download artifacts.
9
+ docling-slim[format-pdf-docling,format-docx,format-markdown]>=2.70.0; python_version >= "3.10"
package/schemas.py ADDED
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+ from typing import Dict, List
6
+
7
+
8
+ DOC_TYPES = {
9
+ "biz",
10
+ "scenario",
11
+ "sop",
12
+ "metric",
13
+ "severity",
14
+ "change",
15
+ "function",
16
+ "evaluation",
17
+ }
18
+
19
+
20
+ @dataclass
21
+ class ParsedBlock:
22
+ source_doc: str
23
+ source_section: str
24
+ content: str
25
+ pages: List[int] = field(default_factory=list)
26
+ order: int = 0
27
+
28
+
29
+ @dataclass
30
+ class ParsedDocument:
31
+ source_path: Path
32
+ source_doc: str
33
+ markdown: str
34
+ blocks: List[ParsedBlock]
35
+
36
+
37
+ @dataclass
38
+ class KnowledgeItem:
39
+ kb_id: str
40
+ title: str
41
+ doc_type: str
42
+ domain: str
43
+ business_modules: List[str]
44
+ source_doc: str
45
+ source_version: str
46
+ source_section: str
47
+ effective_date: str
48
+ owner: str
49
+ confidentiality: str
50
+ risk_level: str
51
+ applicable_roles: List[str]
52
+ tags: List[str]
53
+ status: str
54
+ body: str
55
+ review_status: str = "pending"
56
+ source_trace: str = ""
57
+
58
+ def metadata(self) -> Dict:
59
+ return {
60
+ "kb_id": self.kb_id,
61
+ "title": self.title,
62
+ "doc_type": self.doc_type,
63
+ "domain": self.domain,
64
+ "business_modules": self.business_modules,
65
+ "source_doc": self.source_doc,
66
+ "source_version": self.source_version,
67
+ "source_section": self.source_section,
68
+ "effective_date": self.effective_date,
69
+ "owner": self.owner,
70
+ "confidentiality": self.confidentiality,
71
+ "risk_level": self.risk_level,
72
+ "applicable_roles": self.applicable_roles,
73
+ "tags": self.tags,
74
+ "status": self.status,
75
+ "review_status": self.review_status,
76
+ "source_trace": self.source_trace,
77
+ }
78
+
79
+
80
+ @dataclass
81
+ class ValidationIssue:
82
+ path: Path
83
+ level: str
84
+ message: str
85
+