md-rag-core 0.0.1__tar.gz
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.
- md_rag_core-0.0.1/.gitignore +58 -0
- md_rag_core-0.0.1/PKG-INFO +18 -0
- md_rag_core-0.0.1/README.md +0 -0
- md_rag_core-0.0.1/pyproject.toml +63 -0
- md_rag_core-0.0.1/src/md_rag_core/__init__.py +1 -0
- md_rag_core-0.0.1/src/md_rag_core/chunker/__init__.py +25 -0
- md_rag_core-0.0.1/src/md_rag_core/chunker/aggregators/__init__.py +0 -0
- md_rag_core-0.0.1/src/md_rag_core/chunker/aggregators/base.py +194 -0
- md_rag_core-0.0.1/src/md_rag_core/chunker/aggregators/code_aggregator.py +241 -0
- md_rag_core-0.0.1/src/md_rag_core/chunker/aggregators/image_aggregator.py +95 -0
- md_rag_core-0.0.1/src/md_rag_core/chunker/aggregators/list_aggregator.py +227 -0
- md_rag_core-0.0.1/src/md_rag_core/chunker/aggregators/math_aggregator.py +90 -0
- md_rag_core-0.0.1/src/md_rag_core/chunker/aggregators/section_aggregator.py +154 -0
- md_rag_core-0.0.1/src/md_rag_core/chunker/aggregators/table_aggregator.py +308 -0
- md_rag_core-0.0.1/src/md_rag_core/chunker/aggregators/text_aggregator.py +95 -0
- md_rag_core-0.0.1/src/md_rag_core/chunker/builder.py +131 -0
- md_rag_core-0.0.1/src/md_rag_core/chunker/models.py +240 -0
- md_rag_core-0.0.1/src/md_rag_core/chunker/post_processor.py +319 -0
- md_rag_core-0.0.1/src/md_rag_core/chunker/section_splitter.py +83 -0
- md_rag_core-0.0.1/src/md_rag_core/parser/__init__.py +69 -0
- md_rag_core-0.0.1/src/md_rag_core/parser/handlers/__init__.py +65 -0
- md_rag_core-0.0.1/src/md_rag_core/parser/handlers/base.py +79 -0
- md_rag_core-0.0.1/src/md_rag_core/parser/handlers/code_handler.py +82 -0
- md_rag_core-0.0.1/src/md_rag_core/parser/handlers/heading_handler.py +114 -0
- md_rag_core-0.0.1/src/md_rag_core/parser/handlers/html_table_handler.py +157 -0
- md_rag_core-0.0.1/src/md_rag_core/parser/handlers/image_handler.py +67 -0
- md_rag_core-0.0.1/src/md_rag_core/parser/handlers/list_handler.py +167 -0
- md_rag_core-0.0.1/src/md_rag_core/parser/handlers/math_handler.py +195 -0
- md_rag_core-0.0.1/src/md_rag_core/parser/handlers/md_table_handler.py +186 -0
- md_rag_core-0.0.1/src/md_rag_core/parser/inline_parser.py +197 -0
- md_rag_core-0.0.1/src/md_rag_core/parser/models.py +188 -0
- md_rag_core-0.0.1/src/md_rag_core/parser/parser.py +156 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Compiled class file
|
|
2
|
+
*.class
|
|
3
|
+
|
|
4
|
+
# Log file
|
|
5
|
+
*.log
|
|
6
|
+
|
|
7
|
+
# BlueJ files
|
|
8
|
+
*.ctxt
|
|
9
|
+
|
|
10
|
+
*.ini
|
|
11
|
+
|
|
12
|
+
# Mobile Tools for Java (J2ME)
|
|
13
|
+
.mtj.tmp/
|
|
14
|
+
|
|
15
|
+
# Package Files #
|
|
16
|
+
*.jar
|
|
17
|
+
*.war
|
|
18
|
+
|
|
19
|
+
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
|
20
|
+
hs_err_pid*
|
|
21
|
+
|
|
22
|
+
# IDEA or eclipse files #
|
|
23
|
+
.settings/
|
|
24
|
+
.project
|
|
25
|
+
.classpath
|
|
26
|
+
.factorypath
|
|
27
|
+
.idea/
|
|
28
|
+
*.iml
|
|
29
|
+
|
|
30
|
+
# mvn target
|
|
31
|
+
target/
|
|
32
|
+
.mvn/
|
|
33
|
+
|
|
34
|
+
# log files
|
|
35
|
+
logs/
|
|
36
|
+
|
|
37
|
+
# npm files
|
|
38
|
+
node_modules/
|
|
39
|
+
rebel.xml
|
|
40
|
+
|
|
41
|
+
# mac os
|
|
42
|
+
.DS_Store
|
|
43
|
+
|
|
44
|
+
docker-compose/
|
|
45
|
+
finalshell/
|
|
46
|
+
__pycache__/
|
|
47
|
+
nlb-nginx/
|
|
48
|
+
build/
|
|
49
|
+
dist/
|
|
50
|
+
.pytest_cache/
|
|
51
|
+
/royal_tsx/
|
|
52
|
+
deploy_python/
|
|
53
|
+
inis-*/
|
|
54
|
+
logs/
|
|
55
|
+
*.c
|
|
56
|
+
*.key
|
|
57
|
+
*.pem
|
|
58
|
+
nacos-data
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: md-rag-core
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A core library for Markdown-aware chunking, semantic structuring, vectorization, and RAG retrieval.
|
|
5
|
+
Author-email: ferdinand <gongjpa@gmail.com>
|
|
6
|
+
License-Expression: Apache-2.0 OR MIT
|
|
7
|
+
Keywords: RAG,chunking,markdown,retrieval,semantic
|
|
8
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Provides-Extra: test
|
|
18
|
+
Requires-Dist: pytest>=9.0.2; extra == 'test'
|
|
File without changes
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "md-rag-core"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "A core library for Markdown-aware chunking, semantic structuring, vectorization, and RAG retrieval."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = []
|
|
8
|
+
|
|
9
|
+
# 协议信息
|
|
10
|
+
license = "Apache-2.0 OR MIT"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "ferdinand", email = "gongjpa@gmail.com" },
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
# 可以到网站https://pypi.org/classifiers/查找合适的classifiers
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: Apache Software License",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
# 关键字方便快速检索
|
|
28
|
+
keywords = [
|
|
29
|
+
"markdown", "semantic", "RAG", "retrieval", "chunking"
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
test = [
|
|
34
|
+
"pytest>=9.0.2",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[build-system]
|
|
38
|
+
requires = ["hatchling"]
|
|
39
|
+
build-backend = "hatchling.build"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# === 关键配置: 指定包的位置 ===
|
|
43
|
+
# 告诉 hatchling,真正得包代码在 src 目录下
|
|
44
|
+
[tool.hatch.build.targets.wheel]
|
|
45
|
+
packages = ["src/md_rag_core"]
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.sdist]
|
|
48
|
+
# 明确指定 sdist 只包含 src 目录,排除 test 目录等其他根目录文件
|
|
49
|
+
include = [
|
|
50
|
+
"src",
|
|
51
|
+
# 建议也包含 README 和 LICENSE,否则 pip 安装有时会发出警告
|
|
52
|
+
"README.md",
|
|
53
|
+
"pyproject.toml"
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[tool.hatch.version]
|
|
57
|
+
path = "src/md_rag_core/__init__.py"
|
|
58
|
+
|
|
59
|
+
[[tool.uv.index]]
|
|
60
|
+
name = "testpypi"
|
|
61
|
+
url = "https://test.pypi.org/simple/"
|
|
62
|
+
publish-url = "https://test.pypi.org/legacy/"
|
|
63
|
+
explicit = true
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.1"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
chunker/aggregators/__init__.py — 聚合器包导出
|
|
3
|
+
|
|
4
|
+
统一导出所有 Aggregator 类,方便外部直接导入。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .aggregators.base import BaseAggregator
|
|
8
|
+
from .aggregators.text_aggregator import TextAggregator
|
|
9
|
+
from .aggregators.code_aggregator import CodeAggregator
|
|
10
|
+
from .aggregators.table_aggregator import TableAggregator
|
|
11
|
+
from .aggregators.math_aggregator import MathAggregator
|
|
12
|
+
from .aggregators.list_aggregator import ListAggregator
|
|
13
|
+
from .aggregators.image_aggregator import ImageAggregator
|
|
14
|
+
from .aggregators.section_aggregator import SectionAggregator
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"BaseAggregator",
|
|
18
|
+
"TextAggregator",
|
|
19
|
+
"CodeAggregator",
|
|
20
|
+
"TableAggregator",
|
|
21
|
+
"MathAggregator",
|
|
22
|
+
"ListAggregator",
|
|
23
|
+
"ImageAggregator",
|
|
24
|
+
"SectionAggregator",
|
|
25
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
chunker/aggregators/base.py — 聚合器抽象基类
|
|
3
|
+
|
|
4
|
+
定义所有类型感知聚合器(Aggregator)的统一接口。
|
|
5
|
+
|
|
6
|
+
设计思路
|
|
7
|
+
--------
|
|
8
|
+
与 Block 解析层的 Handler 模式类似,Chunk 层的聚合也采用"策略模式":
|
|
9
|
+
每种 Block 类型对应一个独立的 Aggregator,负责将该类型的 Block 组合成 Chunk。
|
|
10
|
+
|
|
11
|
+
主协调器(SectionAggregator)按类型将 Block 分流给对应的 Aggregator,
|
|
12
|
+
每个 Aggregator 只关心自己负责的类型,互不干扰,便于独立测试和扩展。
|
|
13
|
+
|
|
14
|
+
Aggregator 的职责
|
|
15
|
+
-----------------
|
|
16
|
+
1. 接收一组同类 Block(及其位置上下文)
|
|
17
|
+
2. 按本类型的语义聚合规则,生成一个或多个原始 Chunk(尚未注入面包屑)
|
|
18
|
+
3. 返回 Chunk 列表
|
|
19
|
+
|
|
20
|
+
注意:Aggregator 不负责注入标题面包屑,也不负责 token 大小的最终校正,
|
|
21
|
+
这两项工作由后处理器(PostProcessor)统一完成。
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from abc import ABC, abstractmethod
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
from ...chunker.models import Chunk, ChunkConfig, Section
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class BaseAggregator(ABC):
|
|
33
|
+
"""
|
|
34
|
+
聚合器抽象基类。
|
|
35
|
+
|
|
36
|
+
子类需实现 aggregate() 方法,将 Section 内的一组 Block 组合为 Chunk 列表。
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, config: ChunkConfig) -> None:
|
|
40
|
+
"""
|
|
41
|
+
Args:
|
|
42
|
+
config : 全局拆分配置,控制 token 上限、上下文长度等参数
|
|
43
|
+
"""
|
|
44
|
+
self.config = config
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def aggregate(
|
|
48
|
+
self,
|
|
49
|
+
section: Section,
|
|
50
|
+
blocks: list[dict[str, Any]],
|
|
51
|
+
all_section_blocks: list[dict[str, Any]],
|
|
52
|
+
) -> list[Chunk]:
|
|
53
|
+
"""
|
|
54
|
+
将一批 Block 聚合为 Chunk 列表。
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
section : 当前 Block 所属的 Section(用于获取 title_path)
|
|
58
|
+
blocks : 本次需要聚合的 Block 列表(已按类型筛选)
|
|
59
|
+
all_section_blocks : Section 内的全部 Block(用于查找上下文:
|
|
60
|
+
代码/表格/公式需要取前后的 text Block)
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
生成的 Chunk 列表(已设置 content、chunk_type、source_blocks、token_count,
|
|
64
|
+
但尚未注入 heading_prefix)
|
|
65
|
+
"""
|
|
66
|
+
...
|
|
67
|
+
|
|
68
|
+
# ------------------------------------------------------------------
|
|
69
|
+
# 工具方法(供子类共享)
|
|
70
|
+
# ------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
def _find_prev_text_block(
|
|
73
|
+
self,
|
|
74
|
+
target_block: dict[str, Any],
|
|
75
|
+
all_blocks: list[dict[str, Any]],
|
|
76
|
+
max_tokens: int,
|
|
77
|
+
) -> dict[str, Any] | None:
|
|
78
|
+
"""
|
|
79
|
+
在 all_blocks 中找到 target_block 之前最近的一个 text Block,
|
|
80
|
+
且其 token 数不超过 max_tokens。
|
|
81
|
+
|
|
82
|
+
用于代码/表格/公式聚合时绑定"前置说明段落"。
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
target_block : 目标 Block(代码/表格/公式)
|
|
86
|
+
all_blocks : Section 内的完整 Block 列表
|
|
87
|
+
max_tokens : 上下文 token 上限
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
符合条件的 text Block,或 None(不存在时)
|
|
91
|
+
"""
|
|
92
|
+
from ...chunker.models import estimate_tokens
|
|
93
|
+
|
|
94
|
+
target_idx = self._find_block_position(target_block, all_blocks)
|
|
95
|
+
if target_idx <= 0:
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
# 向前扫描,找第一个 text 类型的 Block
|
|
99
|
+
for i in range(target_idx - 1, -1, -1):
|
|
100
|
+
b = all_blocks[i]
|
|
101
|
+
if b.get("type") == "text":
|
|
102
|
+
if estimate_tokens(b.get("text", "")) <= max_tokens:
|
|
103
|
+
return b
|
|
104
|
+
# 超过 token 限制:截取前 max_tokens 对应的字符数
|
|
105
|
+
return self._truncate_block(b, max_tokens)
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
def _find_next_text_block(
|
|
109
|
+
self,
|
|
110
|
+
target_block: dict[str, Any],
|
|
111
|
+
all_blocks: list[dict[str, Any]],
|
|
112
|
+
max_tokens: int,
|
|
113
|
+
) -> dict[str, Any] | None:
|
|
114
|
+
"""
|
|
115
|
+
在 all_blocks 中找到 target_block 之后最近的一个 text Block,
|
|
116
|
+
且其 token 数不超过 max_tokens。
|
|
117
|
+
|
|
118
|
+
用于代码/表格/公式聚合时绑定"后置补充段落"。
|
|
119
|
+
"""
|
|
120
|
+
from ...chunker.models import estimate_tokens
|
|
121
|
+
|
|
122
|
+
target_idx = self._find_block_position(target_block, all_blocks)
|
|
123
|
+
if target_idx < 0 or target_idx >= len(all_blocks) - 1:
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
for i in range(target_idx + 1, len(all_blocks)):
|
|
127
|
+
b = all_blocks[i]
|
|
128
|
+
if b.get("type") == "text":
|
|
129
|
+
if estimate_tokens(b.get("text", "")) <= max_tokens:
|
|
130
|
+
return b
|
|
131
|
+
return self._truncate_block(b, max_tokens)
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def _find_block_position(
|
|
136
|
+
target: dict[str, Any],
|
|
137
|
+
block_list: list[dict[str, Any]],
|
|
138
|
+
) -> int:
|
|
139
|
+
"""
|
|
140
|
+
在 block_list 中定位 target Block 的索引。
|
|
141
|
+
|
|
142
|
+
优先通过 _block_index 字段(全局原始索引)匹配,
|
|
143
|
+
以应对 Block 字典被复制或部分修改的情况。
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
target : 目标 Block
|
|
147
|
+
block_list : 待搜索的列表
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
目标在列表中的位置索引,未找到返回 -1
|
|
151
|
+
"""
|
|
152
|
+
target_global_idx = target.get("_block_index")
|
|
153
|
+
if target_global_idx is not None:
|
|
154
|
+
for i, b in enumerate(block_list):
|
|
155
|
+
if b.get("_block_index") == target_global_idx:
|
|
156
|
+
return i
|
|
157
|
+
# 降级:按对象 id 匹配
|
|
158
|
+
for i, b in enumerate(block_list):
|
|
159
|
+
if b is target:
|
|
160
|
+
return i
|
|
161
|
+
return -1
|
|
162
|
+
|
|
163
|
+
@staticmethod
|
|
164
|
+
def _truncate_block(block: dict[str, Any], max_tokens: int) -> dict[str, Any]:
|
|
165
|
+
"""
|
|
166
|
+
返回一个 text Block 的截断副本,使其 token 数不超过 max_tokens。
|
|
167
|
+
|
|
168
|
+
截断在字符层面进行(chars ≈ tokens * 1.5),
|
|
169
|
+
尽量在句子末尾(句号/换行)处截断,保证截断后语义相对完整。
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
block : 原始 text Block
|
|
173
|
+
max_tokens : 截断后的 token 上限
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
截断后的 Block 副本(不修改原始 Block)
|
|
177
|
+
"""
|
|
178
|
+
max_chars = int(max_tokens * 1.5)
|
|
179
|
+
text = block.get("text", "")
|
|
180
|
+
if len(text) <= max_chars:
|
|
181
|
+
return block
|
|
182
|
+
|
|
183
|
+
# 在 max_chars 附近找最近的句子边界(句号、问号、感叹号、换行)
|
|
184
|
+
truncated = text[:max_chars]
|
|
185
|
+
for sep in ("。", "\n", ".", "!", "?", ";"):
|
|
186
|
+
pos = truncated.rfind(sep)
|
|
187
|
+
if pos > max_chars // 2: # 避免截断点太靠前
|
|
188
|
+
truncated = truncated[: pos + 1]
|
|
189
|
+
break
|
|
190
|
+
|
|
191
|
+
result = dict(block)
|
|
192
|
+
result["text"] = truncated
|
|
193
|
+
result["_truncated"] = True
|
|
194
|
+
return result
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""
|
|
2
|
+
chunker/aggregators/code_aggregator.py — 代码块聚合器
|
|
3
|
+
|
|
4
|
+
负责将代码 Block 与其上下文绑定,生成语义完整的 Chunk。
|
|
5
|
+
|
|
6
|
+
问题背景
|
|
7
|
+
--------
|
|
8
|
+
孤立的代码块向量无法表达"这段代码是干什么用的"。
|
|
9
|
+
用户提问"怎么初始化连接池"时,代码块本身可能不含"连接池"这个词,
|
|
10
|
+
无法被向量检索命中。
|
|
11
|
+
|
|
12
|
+
绑定策略
|
|
13
|
+
--------
|
|
14
|
+
前置上下文(≤ code_context_max token)
|
|
15
|
+
+ 代码块完整内容
|
|
16
|
+
+ 后置上下文(≤ code_context_max token,可选)
|
|
17
|
+
→ 一个 code 类 Chunk
|
|
18
|
+
|
|
19
|
+
超大代码块处理
|
|
20
|
+
--------------
|
|
21
|
+
当代码块本身 token 数超过 code_token_max 时,按以下规则拆分:
|
|
22
|
+
1. 按连续空行(≥2 个空行,通常是函数/类之间的间隔)拆分为逻辑段
|
|
23
|
+
2. 每段保留代码语言标识,前置上下文只附加到第一段
|
|
24
|
+
3. 相邻段之间保留 overlap_token_size 个 token 的重叠
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import re
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
from ...chunker.models import Chunk, ChunkType, Section, estimate_tokens
|
|
33
|
+
from ...chunker.aggregators.base import BaseAggregator
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CodeAggregator(BaseAggregator):
|
|
37
|
+
"""代码块聚合器,将代码 Block 与前后上下文绑定成 Chunk。"""
|
|
38
|
+
|
|
39
|
+
def aggregate(
|
|
40
|
+
self,
|
|
41
|
+
section: Section,
|
|
42
|
+
blocks: list[dict[str, Any]],
|
|
43
|
+
all_section_blocks: list[dict[str, Any]],
|
|
44
|
+
) -> list[Chunk]:
|
|
45
|
+
"""
|
|
46
|
+
每个代码 Block 独立处理,绑定前后最近的 text Block 作为上下文。
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
section : 当前章节(提供 title_path)
|
|
50
|
+
blocks : 经过类型筛选的 code Block 列表
|
|
51
|
+
all_section_blocks : 章节内全部 Block(用于查找前后上下文)
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
code 类 Chunk 列表
|
|
55
|
+
"""
|
|
56
|
+
chunks: list[Chunk] = []
|
|
57
|
+
|
|
58
|
+
for code_block in blocks:
|
|
59
|
+
# 查找前置说明段落
|
|
60
|
+
prev_text = self._find_prev_text_block(
|
|
61
|
+
code_block, all_section_blocks, self.config.code_context_max
|
|
62
|
+
)
|
|
63
|
+
# 查找后置补充说明
|
|
64
|
+
next_text = self._find_next_text_block(
|
|
65
|
+
code_block, all_section_blocks, self.config.code_context_max
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
code_text = code_block.get("text", "")
|
|
69
|
+
code_tokens = estimate_tokens(code_text)
|
|
70
|
+
language = code_block.get("meta", {}).get("language", "")
|
|
71
|
+
|
|
72
|
+
if code_tokens <= self.config.code_token_max:
|
|
73
|
+
# 正常大小的代码块:直接绑定上下文成一个 Chunk
|
|
74
|
+
chunks.append(
|
|
75
|
+
self._make_code_chunk(
|
|
76
|
+
code_block=code_block,
|
|
77
|
+
prev_text_block=prev_text,
|
|
78
|
+
next_text_block=next_text,
|
|
79
|
+
section=section,
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
else:
|
|
83
|
+
# 超大代码块:按逻辑边界拆分后逐段处理
|
|
84
|
+
sub_chunks = self._split_large_code(
|
|
85
|
+
code_block=code_block,
|
|
86
|
+
prev_text_block=prev_text,
|
|
87
|
+
language=language,
|
|
88
|
+
section=section,
|
|
89
|
+
)
|
|
90
|
+
chunks.extend(sub_chunks)
|
|
91
|
+
|
|
92
|
+
return chunks
|
|
93
|
+
|
|
94
|
+
def _make_code_chunk(
|
|
95
|
+
self,
|
|
96
|
+
code_block: dict[str, Any],
|
|
97
|
+
prev_text_block: dict[str, Any] | None,
|
|
98
|
+
next_text_block: dict[str, Any] | None,
|
|
99
|
+
section: Section,
|
|
100
|
+
) -> Chunk:
|
|
101
|
+
"""
|
|
102
|
+
将代码块与前后上下文拼接为一个 Chunk。
|
|
103
|
+
|
|
104
|
+
拼接格式:
|
|
105
|
+
[前置说明](可选)
|
|
106
|
+
[空行]
|
|
107
|
+
[代码内容]
|
|
108
|
+
[空行]
|
|
109
|
+
[后置说明](可选)
|
|
110
|
+
"""
|
|
111
|
+
parts: list[str] = []
|
|
112
|
+
source_blocks: list[dict[str, Any]] = []
|
|
113
|
+
|
|
114
|
+
if prev_text_block:
|
|
115
|
+
parts.append(prev_text_block.get("text", ""))
|
|
116
|
+
source_blocks.append(prev_text_block)
|
|
117
|
+
|
|
118
|
+
parts.append(code_block.get("text", ""))
|
|
119
|
+
source_blocks.append(code_block)
|
|
120
|
+
|
|
121
|
+
if next_text_block:
|
|
122
|
+
parts.append(next_text_block.get("text", ""))
|
|
123
|
+
source_blocks.append(next_text_block)
|
|
124
|
+
|
|
125
|
+
content = "\n\n".join(parts)
|
|
126
|
+
return Chunk(
|
|
127
|
+
content=content,
|
|
128
|
+
chunk_type=ChunkType.CODE,
|
|
129
|
+
title_path=list(section.title_path),
|
|
130
|
+
source_blocks=source_blocks,
|
|
131
|
+
token_count=estimate_tokens(content),
|
|
132
|
+
metadata={"language": code_block.get("meta", {}).get("language", "")},
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def _split_large_code(
|
|
136
|
+
self,
|
|
137
|
+
code_block: dict[str, Any],
|
|
138
|
+
prev_text_block: dict[str, Any] | None,
|
|
139
|
+
language: str,
|
|
140
|
+
section: Section,
|
|
141
|
+
) -> list[Chunk]:
|
|
142
|
+
"""
|
|
143
|
+
将超大代码块按逻辑边界(连续空行)拆分为多个 Chunk。
|
|
144
|
+
|
|
145
|
+
拆分规则:
|
|
146
|
+
- 以连续 2 个或以上空行作为逻辑段的分隔点(通常是函数/类之间)
|
|
147
|
+
- 每段保留语言标识(代码围栏格式)
|
|
148
|
+
- 前置上下文只附加到第一段,避免上下文在每段重复,稀释语义
|
|
149
|
+
- 相邻段之间保留重叠:后一段包含前一段最后 overlap_token_size 个 token 的内容
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
code_block : 原始代码 Block
|
|
153
|
+
prev_text_block : 前置上下文(可为 None)
|
|
154
|
+
language : 代码语言标识
|
|
155
|
+
section : 当前章节
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
拆分后的 Chunk 列表
|
|
159
|
+
"""
|
|
160
|
+
code_text = code_block.get("text", "")
|
|
161
|
+
|
|
162
|
+
# 按连续空行切分代码段
|
|
163
|
+
# re.split 以 2 个以上连续换行为分隔符
|
|
164
|
+
raw_segments = re.split(r"\n{2,}", code_text)
|
|
165
|
+
# 过滤掉空段
|
|
166
|
+
segments = [s.strip() for s in raw_segments if s.strip()]
|
|
167
|
+
|
|
168
|
+
if len(segments) <= 1:
|
|
169
|
+
# 无法找到逻辑边界,强制按字符数切割(兜底策略)
|
|
170
|
+
segments = self._force_split_by_size(code_text)
|
|
171
|
+
|
|
172
|
+
chunks: list[Chunk] = []
|
|
173
|
+
overlap_text = "" # 前一段的重叠尾部
|
|
174
|
+
|
|
175
|
+
for seg_idx, segment in enumerate(segments):
|
|
176
|
+
# 在重叠文本前加上语言标识围栏(便于向量模型识别代码类型)
|
|
177
|
+
if language:
|
|
178
|
+
seg_with_lang = f"```{language}\n{overlap_text}{segment}\n```"
|
|
179
|
+
else:
|
|
180
|
+
seg_with_lang = f"```\n{overlap_text}{segment}\n```"
|
|
181
|
+
|
|
182
|
+
parts: list[str] = []
|
|
183
|
+
source_blocks: list[dict[str, Any]] = [code_block]
|
|
184
|
+
|
|
185
|
+
# 前置上下文只附加到第一个子段
|
|
186
|
+
if seg_idx == 0 and prev_text_block:
|
|
187
|
+
parts.append(prev_text_block.get("text", ""))
|
|
188
|
+
source_blocks.insert(0, prev_text_block)
|
|
189
|
+
|
|
190
|
+
parts.append(seg_with_lang)
|
|
191
|
+
content = "\n\n".join(parts)
|
|
192
|
+
|
|
193
|
+
chunks.append(Chunk(
|
|
194
|
+
content=content,
|
|
195
|
+
chunk_type=ChunkType.CODE,
|
|
196
|
+
title_path=list(section.title_path),
|
|
197
|
+
source_blocks=source_blocks,
|
|
198
|
+
token_count=estimate_tokens(content),
|
|
199
|
+
metadata={
|
|
200
|
+
"language": language,
|
|
201
|
+
"segment_index": seg_idx,
|
|
202
|
+
"total_segments": len(segments),
|
|
203
|
+
},
|
|
204
|
+
))
|
|
205
|
+
|
|
206
|
+
# 计算下一段的重叠内容:取当前段末尾 overlap_token_size 对应的字符
|
|
207
|
+
overlap_chars = int(self.config.overlap_token_size * 1.5)
|
|
208
|
+
overlap_text = segment[-overlap_chars:] if len(segment) > overlap_chars else segment
|
|
209
|
+
# 重叠文本加注释标记,让向量模型知道这是延续内容
|
|
210
|
+
overlap_text = f"# ... (续上段)\n{overlap_text}\n\n"
|
|
211
|
+
|
|
212
|
+
return chunks
|
|
213
|
+
|
|
214
|
+
def _force_split_by_size(self, code_text: str) -> list[str]:
|
|
215
|
+
"""
|
|
216
|
+
兜底策略:在无法找到逻辑边界时,按固定字符数强制切割代码。
|
|
217
|
+
|
|
218
|
+
切割时尽量在换行处切割,避免在代码行中间截断。
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
code_text : 完整代码文本
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
切割后的代码片段列表
|
|
225
|
+
"""
|
|
226
|
+
max_chars = int(self.config.code_token_max * 1.5)
|
|
227
|
+
segments = []
|
|
228
|
+
remaining = code_text
|
|
229
|
+
|
|
230
|
+
while len(remaining) > max_chars:
|
|
231
|
+
# 在 max_chars 附近找最近的换行处
|
|
232
|
+
cut_pos = remaining.rfind("\n", 0, max_chars)
|
|
233
|
+
if cut_pos <= 0:
|
|
234
|
+
cut_pos = max_chars # 实在找不到换行,强制截断
|
|
235
|
+
segments.append(remaining[:cut_pos])
|
|
236
|
+
remaining = remaining[cut_pos:].lstrip("\n")
|
|
237
|
+
|
|
238
|
+
if remaining:
|
|
239
|
+
segments.append(remaining)
|
|
240
|
+
|
|
241
|
+
return segments
|