llama-index-node-parser-xberg 1.0.0rc37__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.
- llama_index_node_parser_xberg-1.0.0rc37/.gitignore +330 -0
- llama_index_node_parser_xberg-1.0.0rc37/PKG-INFO +161 -0
- llama_index_node_parser_xberg-1.0.0rc37/README.md +131 -0
- llama_index_node_parser_xberg-1.0.0rc37/llama_index/node_parser/xberg/__init__.py +5 -0
- llama_index_node_parser_xberg-1.0.0rc37/llama_index/node_parser/xberg/base.py +170 -0
- llama_index_node_parser_xberg-1.0.0rc37/pyproject.toml +66 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# Build artifacts
|
|
2
|
+
target/
|
|
3
|
+
build/
|
|
4
|
+
dist/
|
|
5
|
+
.polylint/
|
|
6
|
+
# Ad-hoc PDF/image debug binaries
|
|
7
|
+
/check_pdf_oxide
|
|
8
|
+
/check_*
|
|
9
|
+
# Hand-authored WASM JS distribution files (intentionally committed)
|
|
10
|
+
!crates/xberg-wasm/dist/index.js
|
|
11
|
+
!crates/xberg-wasm/dist/extraction/files.js
|
|
12
|
+
!crates/xberg-wasm/dist/ocr/enabler.js
|
|
13
|
+
.lycheecache
|
|
14
|
+
dist-musl/
|
|
15
|
+
*.so
|
|
16
|
+
*.dylib
|
|
17
|
+
*.dll
|
|
18
|
+
*.pdb
|
|
19
|
+
*.node
|
|
20
|
+
*.whl
|
|
21
|
+
*.tar.gz
|
|
22
|
+
*.gem
|
|
23
|
+
*.jar
|
|
24
|
+
wheels/
|
|
25
|
+
**/bin/
|
|
26
|
+
!**/src/bin/
|
|
27
|
+
**/obj/
|
|
28
|
+
|
|
29
|
+
# Rust
|
|
30
|
+
*.rs.bk
|
|
31
|
+
*.profraw
|
|
32
|
+
rust-coverage.lcov
|
|
33
|
+
rust_out
|
|
34
|
+
|
|
35
|
+
# Python
|
|
36
|
+
*.py[cod]
|
|
37
|
+
*$py.class
|
|
38
|
+
*.egg-info/
|
|
39
|
+
__pycache__/
|
|
40
|
+
.venv/
|
|
41
|
+
.mypy_cache/
|
|
42
|
+
.ruff_cache/
|
|
43
|
+
.pytest_cache/
|
|
44
|
+
.hypothesis/
|
|
45
|
+
.coverage
|
|
46
|
+
.coverage*
|
|
47
|
+
.e2e-logs/
|
|
48
|
+
htmlcov/
|
|
49
|
+
share/python-wheels/
|
|
50
|
+
prompt_template.egg-info/
|
|
51
|
+
packages/python/xberg/xberg-cli
|
|
52
|
+
|
|
53
|
+
# Node.js / WASM
|
|
54
|
+
node_modules/
|
|
55
|
+
*.tsbuildinfo
|
|
56
|
+
npm-debug.log*
|
|
57
|
+
pnpm-debug.log*
|
|
58
|
+
yarn-debug.log*
|
|
59
|
+
yarn-error.log*
|
|
60
|
+
# NAPI-RS generated files (index.js and index.d.ts are committed for TypeScript type resolution)
|
|
61
|
+
crates/xberg-node/*.tgz
|
|
62
|
+
# ORT dylibs are copied to crates/xberg-node/ at build time and included in npm publish
|
|
63
|
+
# despite the global *.dylib/*.so/*.dll rules below
|
|
64
|
+
!crates/xberg-node/*.dylib
|
|
65
|
+
!crates/xberg-node/*.so
|
|
66
|
+
!crates/xberg-node/*.dll
|
|
67
|
+
.wrangler/
|
|
68
|
+
.pnpm-store/
|
|
69
|
+
|
|
70
|
+
# Ruby
|
|
71
|
+
packages/ruby/.bundle/
|
|
72
|
+
packages/ruby/vendor/
|
|
73
|
+
!packages/ruby/vendor/rb-sys/
|
|
74
|
+
!vendor/rb-sys/build/
|
|
75
|
+
!vendor/rb-sys/build/**
|
|
76
|
+
*.Cache
|
|
77
|
+
.yardoc/
|
|
78
|
+
|
|
79
|
+
# PHP
|
|
80
|
+
vendor/
|
|
81
|
+
!crates/xberg-candle-ocr/src/vendor/
|
|
82
|
+
php-xberg.ini
|
|
83
|
+
packages/php/php-xberg.ini
|
|
84
|
+
packages/php/tests.xml
|
|
85
|
+
packages/php/var/
|
|
86
|
+
.php-cs-fixer.cache
|
|
87
|
+
|
|
88
|
+
# Swift / SwiftPM / Xcode
|
|
89
|
+
.build/
|
|
90
|
+
.swiftpm/
|
|
91
|
+
# Legacy SwiftPM resolved-packages dir — anchored to packages/swift/ so it
|
|
92
|
+
# does NOT match our top-level packages/ directory on macOS case-insensitive
|
|
93
|
+
# filesystems (where the bare `Packages/` pattern previously matched and
|
|
94
|
+
# silently dropped packages/dart/pubspec.yaml + 9 other scaffold files from
|
|
95
|
+
# the index during regen).
|
|
96
|
+
packages/swift/**/Packages/
|
|
97
|
+
Package.resolved
|
|
98
|
+
*.xcodeproj/
|
|
99
|
+
*.xcworkspace/
|
|
100
|
+
DerivedData/
|
|
101
|
+
|
|
102
|
+
# Java / C# / .NET
|
|
103
|
+
*.suo
|
|
104
|
+
*.user
|
|
105
|
+
*.cscfg
|
|
106
|
+
*.cobertura.xml
|
|
107
|
+
TestResults/
|
|
108
|
+
|
|
109
|
+
# Generated test apps (e2e-generator --mode published)
|
|
110
|
+
test_apps/
|
|
111
|
+
|
|
112
|
+
# Elixir
|
|
113
|
+
.nox/
|
|
114
|
+
|
|
115
|
+
# Go
|
|
116
|
+
packages/go/v*/install
|
|
117
|
+
|
|
118
|
+
# C FFI
|
|
119
|
+
crates/xberg-ffi/tests/c/test_*
|
|
120
|
+
!crates/xberg-ffi/tests/c/test_*.c
|
|
121
|
+
crates/xberg-ffi/tests/c/*.o
|
|
122
|
+
crates/xberg-ffi/tests/c/*.dSYM/
|
|
123
|
+
e2e/c/test_*
|
|
124
|
+
!e2e/c/test_*.c
|
|
125
|
+
e2e/c/*.o
|
|
126
|
+
e2e/c/*.dSYM/
|
|
127
|
+
|
|
128
|
+
# Generated pkg-config
|
|
129
|
+
*.pc
|
|
130
|
+
|
|
131
|
+
# Coverage
|
|
132
|
+
coverage/
|
|
133
|
+
coverage.info
|
|
134
|
+
coverage.json
|
|
135
|
+
coverage.lcov
|
|
136
|
+
coverage.out
|
|
137
|
+
coverage.xml
|
|
138
|
+
packages/*/.coverage
|
|
139
|
+
packages/*/coverage.json
|
|
140
|
+
packages/*/coverage.out
|
|
141
|
+
packages/*/htmlcov/
|
|
142
|
+
packages/*/test-results/
|
|
143
|
+
packages/*/TestResults/
|
|
144
|
+
|
|
145
|
+
# IDE / Editor
|
|
146
|
+
.idea/
|
|
147
|
+
*.iml
|
|
148
|
+
.vscode/
|
|
149
|
+
.run/
|
|
150
|
+
.DS_store
|
|
151
|
+
|
|
152
|
+
# CI / Tools
|
|
153
|
+
.cache/
|
|
154
|
+
.tmp/
|
|
155
|
+
*temp/
|
|
156
|
+
*.temp
|
|
157
|
+
*.tmp
|
|
158
|
+
*.log
|
|
159
|
+
**/logs/
|
|
160
|
+
**/test_output.log
|
|
161
|
+
.mvn/
|
|
162
|
+
.tox/
|
|
163
|
+
.artifacts/
|
|
164
|
+
.remote-cache/
|
|
165
|
+
.rumdl_cache/
|
|
166
|
+
docker-logs/
|
|
167
|
+
|
|
168
|
+
# Benchmarks
|
|
169
|
+
benchmark-results/
|
|
170
|
+
benchmark_results.json
|
|
171
|
+
benchmarks/results/
|
|
172
|
+
!benchmarks/baselines/
|
|
173
|
+
aggregated-results/
|
|
174
|
+
results/
|
|
175
|
+
reports/
|
|
176
|
+
tests/benchmarks/results/
|
|
177
|
+
profiling-results/
|
|
178
|
+
profiling-results-*/
|
|
179
|
+
|
|
180
|
+
# Local perf-iteration artifacts (flamegraphs/ is committed; intermediate outputs are not)
|
|
181
|
+
profile.json.gz
|
|
182
|
+
profile.pb.gz
|
|
183
|
+
*.heap
|
|
184
|
+
bench/
|
|
185
|
+
|
|
186
|
+
# Docs
|
|
187
|
+
docs/_build/
|
|
188
|
+
docs/build/
|
|
189
|
+
docs/benchmarks/charts/
|
|
190
|
+
site/
|
|
191
|
+
|
|
192
|
+
# Elixir build artifacts (lock files MUST stay tracked — see CLAUDE.md
|
|
193
|
+
# task-runner / dependency-awareness rules and the brotli/alloc-no-stdlib
|
|
194
|
+
# regression that bit CI run 27492590406 because the NIF lock wasn't
|
|
195
|
+
# committed and the resolver picked a fresh broken combo).
|
|
196
|
+
packages/elixir/_build/
|
|
197
|
+
packages/elixir/deps/
|
|
198
|
+
e2e/elixir/_build/
|
|
199
|
+
e2e/elixir/deps/
|
|
200
|
+
e2e/elixir/config/
|
|
201
|
+
test_documents/_build/
|
|
202
|
+
test_documents/deps/
|
|
203
|
+
|
|
204
|
+
# Maven plugin artifacts
|
|
205
|
+
*.versionsBackup
|
|
206
|
+
|
|
207
|
+
# Gradle (Kotlin e2e)
|
|
208
|
+
e2e/kotlin/.gradle/
|
|
209
|
+
e2e/kotlin/build/
|
|
210
|
+
e2e/kotlin_android/.gradle/
|
|
211
|
+
e2e/kotlin_android/build/
|
|
212
|
+
|
|
213
|
+
# Ruby native build artifacts
|
|
214
|
+
packages/ruby/lib/*.bundle
|
|
215
|
+
packages/ruby/tmp/
|
|
216
|
+
|
|
217
|
+
# Tests
|
|
218
|
+
test_report.json
|
|
219
|
+
tests/e2e/logs/
|
|
220
|
+
tests/e2e/test_report.json
|
|
221
|
+
temp-test-*
|
|
222
|
+
packages/csharp/test_html_debug.cs
|
|
223
|
+
# CI renders this from runtime.json.template at publish time; template is the source of truth.
|
|
224
|
+
packages/csharp/Xberg/runtime.json
|
|
225
|
+
|
|
226
|
+
# AI / MCP / Agent config
|
|
227
|
+
.claude/
|
|
228
|
+
.codex/
|
|
229
|
+
.gemini/
|
|
230
|
+
.xberg/
|
|
231
|
+
.mcp.json
|
|
232
|
+
# Alef-generated agent skills (mirrors of .ai-rulez/skills; .claude/.codex covered above)
|
|
233
|
+
.agents/skills/
|
|
234
|
+
.github/skills/
|
|
235
|
+
.playwright-mcp/
|
|
236
|
+
.fastembed_cache/
|
|
237
|
+
*/.fastembed_cache/
|
|
238
|
+
AGENTS.md
|
|
239
|
+
CLAUDE.md
|
|
240
|
+
GEMINI.md
|
|
241
|
+
.cursorrules
|
|
242
|
+
.windsurfrules
|
|
243
|
+
[Tt][Oo][Dd][Oo]*
|
|
244
|
+
|
|
245
|
+
# Git worktrees
|
|
246
|
+
.worktrees/
|
|
247
|
+
|
|
248
|
+
# Misc
|
|
249
|
+
.env
|
|
250
|
+
output.txt
|
|
251
|
+
examples.txt
|
|
252
|
+
requirements.txt
|
|
253
|
+
docker-compose.yaml
|
|
254
|
+
/tmp/xberg-docker-test-results.json
|
|
255
|
+
tools/benchmark-harness/datasets/
|
|
256
|
+
tools/benchmark-harness/.fixtures-pdf-subset/
|
|
257
|
+
tools/benchmark-harness/vendored/docling/md/iso_21111_10.md
|
|
258
|
+
visualizations/
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# Additional generated artifacts
|
|
262
|
+
.alef/
|
|
263
|
+
*.pyd
|
|
264
|
+
.gems/
|
|
265
|
+
vendor/bundle/
|
|
266
|
+
*.h.bak
|
|
267
|
+
*.test
|
|
268
|
+
*.class
|
|
269
|
+
bin/
|
|
270
|
+
obj/
|
|
271
|
+
*.nupkg
|
|
272
|
+
pkg/
|
|
273
|
+
|
|
274
|
+
# Dart's alef-generated install-time downloader must ship in the pub.dev package,
|
|
275
|
+
# so it is exempt from the blanket bin/ ignore above.
|
|
276
|
+
!packages/dart/bin/
|
|
277
|
+
!packages/dart/bin/download_libs.dart
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# Local dev artifacts
|
|
281
|
+
docs-site/public/demo-dev.html
|
|
282
|
+
docs-site/dist/
|
|
283
|
+
docs-site/.astro/
|
|
284
|
+
demo-loaded.png
|
|
285
|
+
scratch/
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
.php-cs-fixer.*
|
|
289
|
+
|
|
290
|
+
e2e/zig/.zig-cache/
|
|
291
|
+
|
|
292
|
+
# Swift e2e build artifacts (Swift Package Manager)
|
|
293
|
+
e2e/swift/.build/
|
|
294
|
+
e2e/swift/.swiftpm/
|
|
295
|
+
e2e/swift/Package.resolved
|
|
296
|
+
e2e/swift_e2e/.build/
|
|
297
|
+
e2e/swift_e2e/.swiftpm/
|
|
298
|
+
e2e/swift_e2e/Package.resolved
|
|
299
|
+
|
|
300
|
+
# Dart e2e build artifacts (lock file tracked for reproducibility)
|
|
301
|
+
e2e/dart/.dart_tool/
|
|
302
|
+
e2e/dart/build/
|
|
303
|
+
|
|
304
|
+
# Dart FRB codegen cache (incremental build optimization)
|
|
305
|
+
packages/dart/rust/.frb_codegen_hash
|
|
306
|
+
|
|
307
|
+
# Go e2e build artifacts
|
|
308
|
+
e2e/go/vendor/
|
|
309
|
+
|
|
310
|
+
erl_crash.dump
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
.gradle/
|
|
314
|
+
|
|
315
|
+
packages/go/.lib/
|
|
316
|
+
|
|
317
|
+
.build/
|
|
318
|
+
|
|
319
|
+
# basemind code-map index (local tooling)
|
|
320
|
+
.basemind/
|
|
321
|
+
|
|
322
|
+
.bench-rc32/
|
|
323
|
+
.private-bench/
|
|
324
|
+
build-output-musl-node/
|
|
325
|
+
|
|
326
|
+
poly.local.toml
|
|
327
|
+
|
|
328
|
+
# BEGIN ai-rulez (DO NOT EDIT - managed by ai-rulez)
|
|
329
|
+
.ai-rulez/.generated-manifest.json
|
|
330
|
+
# END ai-rulez
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: llama-index-node-parser-xberg
|
|
3
|
+
Version: 1.0.0rc37
|
|
4
|
+
Summary: Structure-aware LlamaIndex node parser that turns xberg native chunks and elements into nodes
|
|
5
|
+
Project-URL: Homepage, https://github.com/xberg-io/xberg/tree/main/integrations/python/llama-index/node_parsers/llama-index-node-parser-xberg
|
|
6
|
+
Project-URL: Issues, https://github.com/xberg-io/xberg/issues
|
|
7
|
+
Project-URL: Repository, https://github.com/xberg-io/xberg/tree/main/integrations/python/llama-index/node_parsers/llama-index-node-parser-xberg
|
|
8
|
+
Author-email: xberg <contact@xberg.io>
|
|
9
|
+
Maintainer-email: xberg <contact@xberg.io>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Keywords: ai,chunking,document-structure,llamaindex,llm,node-parser,rag,xberg
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
24
|
+
Classifier: Topic :: Text Processing :: General
|
|
25
|
+
Classifier: Topic :: Text Processing :: Indexing
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Requires-Dist: llama-index-core<0.15,>=0.14.23
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# LlamaIndex Node Parser Xberg
|
|
32
|
+
|
|
33
|
+
<div align="center" style="display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin: 20px 0;">
|
|
34
|
+
<a href="https://pypi.org/project/llama-index-node-parser-xberg/">
|
|
35
|
+
<img src="https://img.shields.io/pypi/v/llama-index-node-parser-xberg?label=Node%20Parser&color=007ec6" alt="Node Parser">
|
|
36
|
+
</a>
|
|
37
|
+
<a href="https://pypi.org/project/xberg/">
|
|
38
|
+
<img src="https://img.shields.io/pypi/v/xberg?label=Xberg&color=007ec6" alt="Xberg">
|
|
39
|
+
</a>
|
|
40
|
+
<a href="https://github.com/xberg-io/llama-index-xberg/blob/main/LICENSE">
|
|
41
|
+
<img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License">
|
|
42
|
+
</a>
|
|
43
|
+
<a href="https://docs.xberg.io">
|
|
44
|
+
<img src="https://img.shields.io/badge/docs-xberg.io-blue" alt="Documentation">
|
|
45
|
+
</a>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<img width="3384" height="573" alt="Xberg Banner" src="https://github.com/user-attachments/assets/1b6c6ad7-3b6d-4171-b1c9-f2026cc9deb8" />
|
|
49
|
+
|
|
50
|
+
<div align="center" style="margin-top: 20px;">
|
|
51
|
+
<a href="https://discord.gg/xt9WY3GnKR">
|
|
52
|
+
<img height="22" src="https://img.shields.io/badge/Discord-Join%20our%20community-7289da?logo=discord&logoColor=white" alt="Discord">
|
|
53
|
+
</a>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
Structure-aware LlamaIndex node parser for xberg-extracted documents. It turns
|
|
57
|
+
xberg's native **chunks** into nodes, and falls back to structural **elements**
|
|
58
|
+
when chunks are absent.
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install llama-index-node-parser-xberg
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Requires `llama-index-core>=0.14.23,<0.15`. This package does not depend on
|
|
67
|
+
`xberg` directly — `xberg` is a dependency of the reader
|
|
68
|
+
(`llama-index-readers-xberg`), which produces the documents this parser splits.
|
|
69
|
+
|
|
70
|
+
## Prerequisites
|
|
71
|
+
|
|
72
|
+
> **This parser requires documents with `_xberg_chunks` or `_xberg_elements`
|
|
73
|
+
> metadata.** These are produced by `XbergReader`. Prefer native chunking; use
|
|
74
|
+
> element-based extraction when you want one node per structural element.
|
|
75
|
+
> Documents carrying neither pass through unchanged with a warning.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from xberg import ChunkingConfig, ExtractionConfig
|
|
79
|
+
from llama_index.readers.xberg import XbergReader
|
|
80
|
+
|
|
81
|
+
# Preferred: native semantic chunks with heading path and page span.
|
|
82
|
+
reader = XbergReader(
|
|
83
|
+
extraction_config=ExtractionConfig(chunking=ChunkingConfig(max_characters=1000, overlap=200))
|
|
84
|
+
)
|
|
85
|
+
documents = reader.load_data("report.pdf")
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Features
|
|
89
|
+
|
|
90
|
+
- Chunk-aware splitting — each xberg native chunk becomes a node, carrying `chunk_type`, `heading_path`, and page span
|
|
91
|
+
- Element fallback — when no chunks are present, headings, paragraphs, tables, and code blocks each become a node
|
|
92
|
+
- Source and prev/next relationships tracked via `NodeRelationship`
|
|
93
|
+
- Graceful degradation — documents without chunk or element metadata pass through with a warning
|
|
94
|
+
- Composes with other transformations (e.g., `SentenceSplitter`)
|
|
95
|
+
- Async support via `aget_nodes_from_documents`
|
|
96
|
+
- Serialization support (`to_dict` / `from_dict`)
|
|
97
|
+
|
|
98
|
+
## Usage
|
|
99
|
+
|
|
100
|
+
### Basic
|
|
101
|
+
|
|
102
|
+
Full reader-to-nodes flow:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from xberg import ChunkingConfig, ExtractionConfig
|
|
106
|
+
from llama_index.readers.xberg import XbergReader
|
|
107
|
+
from llama_index.node_parser.xberg import XbergNodeParser
|
|
108
|
+
|
|
109
|
+
reader = XbergReader(
|
|
110
|
+
extraction_config=ExtractionConfig(chunking=ChunkingConfig(max_characters=1000, overlap=200))
|
|
111
|
+
)
|
|
112
|
+
documents = reader.load_data("report.pdf")
|
|
113
|
+
|
|
114
|
+
parser = XbergNodeParser()
|
|
115
|
+
nodes = parser.get_nodes_from_documents(documents)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### IngestionPipeline
|
|
119
|
+
|
|
120
|
+
Chain with `SentenceSplitter` to further split any oversized nodes:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from llama_index.core.ingestion import IngestionPipeline
|
|
124
|
+
from llama_index.core.node_parser import SentenceSplitter
|
|
125
|
+
|
|
126
|
+
pipeline = IngestionPipeline(
|
|
127
|
+
transformations=[
|
|
128
|
+
XbergNodeParser(),
|
|
129
|
+
SentenceSplitter(chunk_size=512), # Further split large nodes
|
|
130
|
+
]
|
|
131
|
+
)
|
|
132
|
+
nodes = pipeline.run(documents=documents)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### VectorStoreIndex
|
|
136
|
+
|
|
137
|
+
Using the `transformations` parameter:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from llama_index.core import VectorStoreIndex
|
|
141
|
+
|
|
142
|
+
index = VectorStoreIndex.from_documents(
|
|
143
|
+
documents,
|
|
144
|
+
transformations=[XbergNodeParser()],
|
|
145
|
+
)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Async
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
nodes = await parser.aget_nodes_from_documents(documents)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Behavior Notes
|
|
155
|
+
|
|
156
|
+
- Chunks take priority over elements. When a document carries both
|
|
157
|
+
`_xberg_chunks` and `_xberg_elements`, the parser splits on chunks.
|
|
158
|
+
- Documents without either metadata key pass through unchanged with a warning.
|
|
159
|
+
This is intentional — silently falling back would hide that you are not
|
|
160
|
+
getting structure-aware splitting.
|
|
161
|
+
- Empty or whitespace-only chunks and elements are automatically skipped.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# LlamaIndex Node Parser Xberg
|
|
2
|
+
|
|
3
|
+
<div align="center" style="display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin: 20px 0;">
|
|
4
|
+
<a href="https://pypi.org/project/llama-index-node-parser-xberg/">
|
|
5
|
+
<img src="https://img.shields.io/pypi/v/llama-index-node-parser-xberg?label=Node%20Parser&color=007ec6" alt="Node Parser">
|
|
6
|
+
</a>
|
|
7
|
+
<a href="https://pypi.org/project/xberg/">
|
|
8
|
+
<img src="https://img.shields.io/pypi/v/xberg?label=Xberg&color=007ec6" alt="Xberg">
|
|
9
|
+
</a>
|
|
10
|
+
<a href="https://github.com/xberg-io/llama-index-xberg/blob/main/LICENSE">
|
|
11
|
+
<img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License">
|
|
12
|
+
</a>
|
|
13
|
+
<a href="https://docs.xberg.io">
|
|
14
|
+
<img src="https://img.shields.io/badge/docs-xberg.io-blue" alt="Documentation">
|
|
15
|
+
</a>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<img width="3384" height="573" alt="Xberg Banner" src="https://github.com/user-attachments/assets/1b6c6ad7-3b6d-4171-b1c9-f2026cc9deb8" />
|
|
19
|
+
|
|
20
|
+
<div align="center" style="margin-top: 20px;">
|
|
21
|
+
<a href="https://discord.gg/xt9WY3GnKR">
|
|
22
|
+
<img height="22" src="https://img.shields.io/badge/Discord-Join%20our%20community-7289da?logo=discord&logoColor=white" alt="Discord">
|
|
23
|
+
</a>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
Structure-aware LlamaIndex node parser for xberg-extracted documents. It turns
|
|
27
|
+
xberg's native **chunks** into nodes, and falls back to structural **elements**
|
|
28
|
+
when chunks are absent.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install llama-index-node-parser-xberg
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Requires `llama-index-core>=0.14.23,<0.15`. This package does not depend on
|
|
37
|
+
`xberg` directly — `xberg` is a dependency of the reader
|
|
38
|
+
(`llama-index-readers-xberg`), which produces the documents this parser splits.
|
|
39
|
+
|
|
40
|
+
## Prerequisites
|
|
41
|
+
|
|
42
|
+
> **This parser requires documents with `_xberg_chunks` or `_xberg_elements`
|
|
43
|
+
> metadata.** These are produced by `XbergReader`. Prefer native chunking; use
|
|
44
|
+
> element-based extraction when you want one node per structural element.
|
|
45
|
+
> Documents carrying neither pass through unchanged with a warning.
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from xberg import ChunkingConfig, ExtractionConfig
|
|
49
|
+
from llama_index.readers.xberg import XbergReader
|
|
50
|
+
|
|
51
|
+
# Preferred: native semantic chunks with heading path and page span.
|
|
52
|
+
reader = XbergReader(
|
|
53
|
+
extraction_config=ExtractionConfig(chunking=ChunkingConfig(max_characters=1000, overlap=200))
|
|
54
|
+
)
|
|
55
|
+
documents = reader.load_data("report.pdf")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Features
|
|
59
|
+
|
|
60
|
+
- Chunk-aware splitting — each xberg native chunk becomes a node, carrying `chunk_type`, `heading_path`, and page span
|
|
61
|
+
- Element fallback — when no chunks are present, headings, paragraphs, tables, and code blocks each become a node
|
|
62
|
+
- Source and prev/next relationships tracked via `NodeRelationship`
|
|
63
|
+
- Graceful degradation — documents without chunk or element metadata pass through with a warning
|
|
64
|
+
- Composes with other transformations (e.g., `SentenceSplitter`)
|
|
65
|
+
- Async support via `aget_nodes_from_documents`
|
|
66
|
+
- Serialization support (`to_dict` / `from_dict`)
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
### Basic
|
|
71
|
+
|
|
72
|
+
Full reader-to-nodes flow:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from xberg import ChunkingConfig, ExtractionConfig
|
|
76
|
+
from llama_index.readers.xberg import XbergReader
|
|
77
|
+
from llama_index.node_parser.xberg import XbergNodeParser
|
|
78
|
+
|
|
79
|
+
reader = XbergReader(
|
|
80
|
+
extraction_config=ExtractionConfig(chunking=ChunkingConfig(max_characters=1000, overlap=200))
|
|
81
|
+
)
|
|
82
|
+
documents = reader.load_data("report.pdf")
|
|
83
|
+
|
|
84
|
+
parser = XbergNodeParser()
|
|
85
|
+
nodes = parser.get_nodes_from_documents(documents)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### IngestionPipeline
|
|
89
|
+
|
|
90
|
+
Chain with `SentenceSplitter` to further split any oversized nodes:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from llama_index.core.ingestion import IngestionPipeline
|
|
94
|
+
from llama_index.core.node_parser import SentenceSplitter
|
|
95
|
+
|
|
96
|
+
pipeline = IngestionPipeline(
|
|
97
|
+
transformations=[
|
|
98
|
+
XbergNodeParser(),
|
|
99
|
+
SentenceSplitter(chunk_size=512), # Further split large nodes
|
|
100
|
+
]
|
|
101
|
+
)
|
|
102
|
+
nodes = pipeline.run(documents=documents)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### VectorStoreIndex
|
|
106
|
+
|
|
107
|
+
Using the `transformations` parameter:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from llama_index.core import VectorStoreIndex
|
|
111
|
+
|
|
112
|
+
index = VectorStoreIndex.from_documents(
|
|
113
|
+
documents,
|
|
114
|
+
transformations=[XbergNodeParser()],
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Async
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
nodes = await parser.aget_nodes_from_documents(documents)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Behavior Notes
|
|
125
|
+
|
|
126
|
+
- Chunks take priority over elements. When a document carries both
|
|
127
|
+
`_xberg_chunks` and `_xberg_elements`, the parser splits on chunks.
|
|
128
|
+
- Documents without either metadata key pass through unchanged with a warning.
|
|
129
|
+
This is intentional — silently falling back would hide that you are not
|
|
130
|
+
getting structure-aware splitting.
|
|
131
|
+
- Empty or whitespace-only chunks and elements are automatically skipped.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Structure-aware node parser for xberg-extracted documents."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
if sys.version_info >= (3, 12):
|
|
9
|
+
from typing import override
|
|
10
|
+
else:
|
|
11
|
+
from typing_extensions import override
|
|
12
|
+
|
|
13
|
+
from llama_index.core.node_parser import NodeParser
|
|
14
|
+
from llama_index.core.schema import BaseNode, Document, NodeRelationship, TextNode
|
|
15
|
+
from llama_index.core.utils import get_tqdm_iterable
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
_ELEMENT_METADATA_KEYS = ("element_type", "page_number", "element_index")
|
|
20
|
+
_CHUNK_METADATA_KEYS = (
|
|
21
|
+
"chunk_type",
|
|
22
|
+
"heading_path",
|
|
23
|
+
"page_number",
|
|
24
|
+
"first_page",
|
|
25
|
+
"last_page",
|
|
26
|
+
"chunk_index",
|
|
27
|
+
"total_chunks",
|
|
28
|
+
"token_count",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
_FORWARDED_KEYS = ("_xberg_chunks", "_xberg_elements")
|
|
32
|
+
|
|
33
|
+
_MISSING_ELEMENTS_WARNING = (
|
|
34
|
+
"Document %s has no '_xberg_chunks' or '_xberg_elements' metadata. "
|
|
35
|
+
"Passing through unchanged. Use XbergReader with "
|
|
36
|
+
"ExtractionConfig(chunking=ChunkingConfig(...)) for native chunk nodes, or "
|
|
37
|
+
"ExtractionConfig(result_format='element_based') for element nodes."
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class XbergNodeParser(NodeParser):
|
|
42
|
+
"""Structure-aware node parser for xberg-extracted documents.
|
|
43
|
+
|
|
44
|
+
Turns xberg's output into individual ``TextNode`` objects, preserving
|
|
45
|
+
document structure through the RAG pipeline. It prefers xberg's native
|
|
46
|
+
**chunks** (``_xberg_chunks``) — semantic splits carrying heading path and
|
|
47
|
+
page span — and falls back to structural **elements** (``_xberg_elements``)
|
|
48
|
+
when chunks are absent.
|
|
49
|
+
|
|
50
|
+
Produce chunk-bearing documents with ``XbergReader`` configured with
|
|
51
|
+
``ExtractionConfig(chunking=ChunkingConfig(...))``; element-bearing
|
|
52
|
+
documents with ``result_format="element_based"``. Documents carrying
|
|
53
|
+
neither pass through unchanged with a warning.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def class_name(cls) -> str:
|
|
58
|
+
"""Return the unique class identifier for serialisation."""
|
|
59
|
+
return "XbergNodeParser"
|
|
60
|
+
|
|
61
|
+
@override
|
|
62
|
+
def _parse_nodes(
|
|
63
|
+
self,
|
|
64
|
+
nodes: Sequence[BaseNode],
|
|
65
|
+
show_progress: bool = False,
|
|
66
|
+
**kwargs: Any,
|
|
67
|
+
) -> list[BaseNode]:
|
|
68
|
+
output: list[BaseNode] = []
|
|
69
|
+
nodes_with_progress = get_tqdm_iterable(nodes, show_progress, "Parsing nodes")
|
|
70
|
+
|
|
71
|
+
for node in nodes_with_progress:
|
|
72
|
+
chunks = node.metadata.get("_xberg_chunks")
|
|
73
|
+
if isinstance(chunks, list) and chunks:
|
|
74
|
+
output.extend(self._nodes_from_chunks(node, chunks))
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
elements = node.metadata.get("_xberg_elements")
|
|
78
|
+
if isinstance(elements, list) and elements:
|
|
79
|
+
output.extend(self._nodes_from_elements(node, elements))
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
logger.warning(_MISSING_ELEMENTS_WARNING, node.node_id)
|
|
83
|
+
output.append(node)
|
|
84
|
+
|
|
85
|
+
return output
|
|
86
|
+
|
|
87
|
+
def _new_text_node(self, text: str, index: int, source: BaseNode, metadata: dict[str, Any]) -> TextNode:
|
|
88
|
+
"""Build a child ``TextNode`` inheriting formatting from its source."""
|
|
89
|
+
return TextNode(
|
|
90
|
+
text=text,
|
|
91
|
+
id_=self.id_func(index, source),
|
|
92
|
+
metadata=metadata,
|
|
93
|
+
excluded_llm_metadata_keys=list(source.excluded_llm_metadata_keys),
|
|
94
|
+
metadata_separator=source.metadata_separator,
|
|
95
|
+
metadata_template=source.metadata_template,
|
|
96
|
+
text_template=source.text_template,
|
|
97
|
+
relationships={NodeRelationship.SOURCE: source.as_related_node_info()},
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def _nodes_from_chunks(self, node: BaseNode, chunks: list[dict[str, Any]]) -> list[TextNode]:
|
|
101
|
+
"""Split a chunk-bearing Document into one TextNode per non-empty chunk."""
|
|
102
|
+
excluded_embed = list(node.excluded_embed_metadata_keys) + list(_CHUNK_METADATA_KEYS)
|
|
103
|
+
result: list[TextNode] = []
|
|
104
|
+
idx = 0
|
|
105
|
+
for chunk in chunks:
|
|
106
|
+
text = chunk.get("content", "")
|
|
107
|
+
if not text.strip():
|
|
108
|
+
continue
|
|
109
|
+
meta = chunk.get("metadata", {})
|
|
110
|
+
text_node = self._new_text_node(
|
|
111
|
+
text,
|
|
112
|
+
idx,
|
|
113
|
+
node,
|
|
114
|
+
{
|
|
115
|
+
"chunk_type": chunk.get("chunk_type", "unknown"),
|
|
116
|
+
"heading_path": meta.get("heading_path", []),
|
|
117
|
+
"page_number": meta.get("first_page"),
|
|
118
|
+
"first_page": meta.get("first_page"),
|
|
119
|
+
"last_page": meta.get("last_page"),
|
|
120
|
+
"chunk_index": meta.get("chunk_index"),
|
|
121
|
+
"total_chunks": meta.get("total_chunks"),
|
|
122
|
+
"token_count": meta.get("token_count"),
|
|
123
|
+
},
|
|
124
|
+
)
|
|
125
|
+
text_node.excluded_embed_metadata_keys = excluded_embed
|
|
126
|
+
result.append(text_node)
|
|
127
|
+
idx += 1
|
|
128
|
+
return result
|
|
129
|
+
|
|
130
|
+
def _nodes_from_elements(self, node: BaseNode, elements: list[dict[str, Any]]) -> list[TextNode]:
|
|
131
|
+
"""Split an element-bearing Document into one TextNode per non-empty element."""
|
|
132
|
+
excluded_embed = list(node.excluded_embed_metadata_keys) + list(_ELEMENT_METADATA_KEYS)
|
|
133
|
+
result: list[TextNode] = []
|
|
134
|
+
idx = 0
|
|
135
|
+
for el in elements:
|
|
136
|
+
text = el.get("text", "")
|
|
137
|
+
if not text.strip():
|
|
138
|
+
continue
|
|
139
|
+
el_meta = el.get("metadata", {})
|
|
140
|
+
text_node = self._new_text_node(
|
|
141
|
+
text,
|
|
142
|
+
idx,
|
|
143
|
+
node,
|
|
144
|
+
{
|
|
145
|
+
"element_type": el.get("element_type", "unknown"),
|
|
146
|
+
"page_number": el_meta.get("page_number"),
|
|
147
|
+
"element_index": el_meta.get("element_index"),
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
text_node.excluded_embed_metadata_keys = excluded_embed
|
|
151
|
+
result.append(text_node)
|
|
152
|
+
idx += 1
|
|
153
|
+
return result
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def _strip_forwarded_metadata(nodes: list[BaseNode]) -> list[BaseNode]:
|
|
157
|
+
"""Remove reader forwarding keys from child TextNodes only.
|
|
158
|
+
|
|
159
|
+
Passthrough documents keep their metadata untouched.
|
|
160
|
+
"""
|
|
161
|
+
for node in nodes:
|
|
162
|
+
if node.source_node is not None:
|
|
163
|
+
for key in _FORWARDED_KEYS:
|
|
164
|
+
node.metadata.pop(key, None)
|
|
165
|
+
return nodes
|
|
166
|
+
|
|
167
|
+
@override
|
|
168
|
+
def _postprocess_parsed_nodes(self, nodes: list[BaseNode], parent_doc_map: dict[str, Document]) -> list[BaseNode]:
|
|
169
|
+
nodes = super()._postprocess_parsed_nodes(nodes, parent_doc_map)
|
|
170
|
+
return self._strip_forwarded_metadata(nodes)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
build-backend = "hatchling.build"
|
|
3
|
+
requires = ["hatchling"]
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "llama-index-node-parser-xberg"
|
|
7
|
+
version = "1.0.0rc37"
|
|
8
|
+
description = "Structure-aware LlamaIndex node parser that turns xberg native chunks and elements into nodes"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
keywords = ["ai", "chunking", "document-structure", "xberg", "llamaindex", "llm", "node-parser", "rag"]
|
|
11
|
+
license = "MIT"
|
|
12
|
+
maintainers = [{ name = "xberg", email = "contact@xberg.io" }]
|
|
13
|
+
authors = [{ name = "xberg", email = "contact@xberg.io" }]
|
|
14
|
+
requires-python = ">=3.10"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Programming Language :: Python :: 3.14",
|
|
27
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
28
|
+
"Topic :: Text Processing :: General",
|
|
29
|
+
"Topic :: Text Processing :: Indexing",
|
|
30
|
+
"Typing :: Typed",
|
|
31
|
+
]
|
|
32
|
+
dependencies = ["llama-index-core>=0.14.23,<0.15"]
|
|
33
|
+
urls.Homepage = "https://github.com/xberg-io/xberg/tree/main/integrations/python/llama-index/node_parsers/llama-index-node-parser-xberg"
|
|
34
|
+
urls.Issues = "https://github.com/xberg-io/xberg/issues"
|
|
35
|
+
urls.Repository = "https://github.com/xberg-io/xberg/tree/main/integrations/python/llama-index/node_parsers/llama-index-node-parser-xberg"
|
|
36
|
+
|
|
37
|
+
[dependency-groups]
|
|
38
|
+
dev = ["pytest>=9", "pytest-asyncio>=0.24", "pytest-cov>=6"]
|
|
39
|
+
|
|
40
|
+
[tool.hatch]
|
|
41
|
+
build.targets.sdist.include = ["llama_index/"]
|
|
42
|
+
build.targets.wheel.include = ["llama_index/"]
|
|
43
|
+
|
|
44
|
+
[tool.mypy]
|
|
45
|
+
disallow_untyped_defs = true
|
|
46
|
+
exclude = ["_static", "build", "examples", "notebooks", "venv"]
|
|
47
|
+
ignore_missing_imports = true
|
|
48
|
+
python_version = "3.10"
|
|
49
|
+
|
|
50
|
+
[tool.pytest]
|
|
51
|
+
ini_options.testpaths = ["tests"]
|
|
52
|
+
ini_options.asyncio_mode = "auto"
|
|
53
|
+
|
|
54
|
+
[tool.coverage]
|
|
55
|
+
run.source = ["llama_index/node_parser/xberg"]
|
|
56
|
+
report.fail_under = 80
|
|
57
|
+
|
|
58
|
+
[tool.llamahub]
|
|
59
|
+
contains_example = false
|
|
60
|
+
import_path = "llama_index.node_parser.xberg"
|
|
61
|
+
class_authors.XbergNodeParser = "v-tan"
|
|
62
|
+
|
|
63
|
+
[tool.uv]
|
|
64
|
+
# xberg ships as a pre-release (1.0.0rcN) until the 1.0.0 GA; persist prerelease
|
|
65
|
+
# resolution so the lockfile is authoritative and `uv run` does not re-resolve. ~keep
|
|
66
|
+
prerelease = "allow"
|