llama-index-node-parser-xberg 1.0.0__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.
@@ -0,0 +1,334 @@
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
+ !crates/xberg-libwpd/vendor/
83
+ # The vendored WordPerfect source archives are committed on purpose; re-include
84
+ # them past the global `*.tar.gz` rule above (a dir negation alone won't).
85
+ !crates/xberg-libwpd/vendor/*.tar.gz
86
+ php-xberg.ini
87
+ packages/php/php-xberg.ini
88
+ packages/php/tests.xml
89
+ packages/php/var/
90
+ .php-cs-fixer.cache
91
+
92
+ # Swift / SwiftPM / Xcode
93
+ .build/
94
+ .swiftpm/
95
+ # Legacy SwiftPM resolved-packages dir — anchored to packages/swift/ so it
96
+ # does NOT match our top-level packages/ directory on macOS case-insensitive
97
+ # filesystems (where the bare `Packages/` pattern previously matched and
98
+ # silently dropped packages/dart/pubspec.yaml + 9 other scaffold files from
99
+ # the index during regen).
100
+ packages/swift/**/Packages/
101
+ Package.resolved
102
+ *.xcodeproj/
103
+ *.xcworkspace/
104
+ DerivedData/
105
+
106
+ # Java / C# / .NET
107
+ *.suo
108
+ *.user
109
+ *.cscfg
110
+ *.cobertura.xml
111
+ TestResults/
112
+
113
+ # Generated test apps (e2e-generator --mode published)
114
+ test_apps/
115
+
116
+ # Elixir
117
+ .nox/
118
+
119
+ # Go
120
+ packages/go/v*/install
121
+
122
+ # C FFI
123
+ crates/xberg-ffi/tests/c/test_*
124
+ !crates/xberg-ffi/tests/c/test_*.c
125
+ crates/xberg-ffi/tests/c/*.o
126
+ crates/xberg-ffi/tests/c/*.dSYM/
127
+ e2e/c/test_*
128
+ !e2e/c/test_*.c
129
+ e2e/c/*.o
130
+ e2e/c/*.dSYM/
131
+
132
+ # Generated pkg-config
133
+ *.pc
134
+
135
+ # Coverage
136
+ coverage/
137
+ coverage.info
138
+ coverage.json
139
+ coverage.lcov
140
+ coverage.out
141
+ coverage.xml
142
+ packages/*/.coverage
143
+ packages/*/coverage.json
144
+ packages/*/coverage.out
145
+ packages/*/htmlcov/
146
+ packages/*/test-results/
147
+ packages/*/TestResults/
148
+
149
+ # IDE / Editor
150
+ .idea/
151
+ *.iml
152
+ .vscode/
153
+ .run/
154
+ .DS_store
155
+
156
+ # CI / Tools
157
+ .cache/
158
+ .tmp/
159
+ *temp/
160
+ *.temp
161
+ *.tmp
162
+ *.log
163
+ **/logs/
164
+ **/test_output.log
165
+ .mvn/
166
+ .tox/
167
+ .artifacts/
168
+ .remote-cache/
169
+ .rumdl_cache/
170
+ docker-logs/
171
+
172
+ # Benchmarks
173
+ benchmark-results/
174
+ benchmark_results.json
175
+ benchmarks/results/
176
+ !benchmarks/baselines/
177
+ aggregated-results/
178
+ results/
179
+ reports/
180
+ tests/benchmarks/results/
181
+ profiling-results/
182
+ profiling-results-*/
183
+
184
+ # Local perf-iteration artifacts (flamegraphs/ is committed; intermediate outputs are not)
185
+ profile.json.gz
186
+ profile.pb.gz
187
+ *.heap
188
+ bench/
189
+
190
+ # Docs
191
+ docs/_build/
192
+ docs/build/
193
+ docs/benchmarks/charts/
194
+ site/
195
+
196
+ # Elixir build artifacts (lock files MUST stay tracked — see CLAUDE.md
197
+ # task-runner / dependency-awareness rules and the brotli/alloc-no-stdlib
198
+ # regression that bit CI run 27492590406 because the NIF lock wasn't
199
+ # committed and the resolver picked a fresh broken combo).
200
+ packages/elixir/_build/
201
+ packages/elixir/deps/
202
+ e2e/elixir/_build/
203
+ e2e/elixir/deps/
204
+ e2e/elixir/config/
205
+ test_documents/_build/
206
+ test_documents/deps/
207
+
208
+ # Maven plugin artifacts
209
+ *.versionsBackup
210
+
211
+ # Gradle (Kotlin e2e)
212
+ e2e/kotlin/.gradle/
213
+ e2e/kotlin/build/
214
+ e2e/kotlin_android/.gradle/
215
+ e2e/kotlin_android/build/
216
+
217
+ # Ruby native build artifacts
218
+ packages/ruby/lib/*.bundle
219
+ packages/ruby/tmp/
220
+
221
+ # Tests
222
+ test_report.json
223
+ tests/e2e/logs/
224
+ tests/e2e/test_report.json
225
+ temp-test-*
226
+ packages/csharp/test_html_debug.cs
227
+ # CI renders this from runtime.json.template at publish time; template is the source of truth.
228
+ packages/csharp/Xberg/runtime.json
229
+
230
+ # AI / MCP / Agent config
231
+ .claude/
232
+ .codex/
233
+ .gemini/
234
+ .xberg/
235
+ .mcp.json
236
+ # Alef-generated agent skills (mirrors of .ai-rulez/skills; .claude/.codex covered above)
237
+ .agents/skills/
238
+ .github/skills/
239
+ .playwright-mcp/
240
+ .fastembed_cache/
241
+ */.fastembed_cache/
242
+ AGENTS.md
243
+ CLAUDE.md
244
+ GEMINI.md
245
+ .cursorrules
246
+ .windsurfrules
247
+ [Tt][Oo][Dd][Oo]*
248
+
249
+ # Git worktrees
250
+ .worktrees/
251
+
252
+ # Misc
253
+ .env
254
+ output.txt
255
+ examples.txt
256
+ requirements.txt
257
+ docker-compose.yaml
258
+ /tmp/xberg-docker-test-results.json
259
+ tools/benchmark-harness/datasets/
260
+ tools/benchmark-harness/.fixtures-pdf-subset/
261
+ tools/benchmark-harness/vendored/docling/md/iso_21111_10.md
262
+ visualizations/
263
+
264
+
265
+ # Additional generated artifacts
266
+ .alef/
267
+ *.pyd
268
+ .gems/
269
+ vendor/bundle/
270
+ *.h.bak
271
+ *.test
272
+ *.class
273
+ bin/
274
+ obj/
275
+ *.nupkg
276
+ pkg/
277
+
278
+ # Dart's alef-generated install-time downloader must ship in the pub.dev package,
279
+ # so it is exempt from the blanket bin/ ignore above.
280
+ !packages/dart/bin/
281
+ !packages/dart/bin/download_libs.dart
282
+
283
+
284
+ # Local dev artifacts
285
+ docs-site/public/demo-dev.html
286
+ docs-site/dist/
287
+ docs-site/.astro/
288
+ demo-loaded.png
289
+ scratch/
290
+
291
+
292
+ .php-cs-fixer.*
293
+
294
+ e2e/zig/.zig-cache/
295
+
296
+ # Swift e2e build artifacts (Swift Package Manager)
297
+ e2e/swift/.build/
298
+ e2e/swift/.swiftpm/
299
+ e2e/swift/Package.resolved
300
+ e2e/swift_e2e/.build/
301
+ e2e/swift_e2e/.swiftpm/
302
+ e2e/swift_e2e/Package.resolved
303
+
304
+ # Dart e2e build artifacts (lock file tracked for reproducibility)
305
+ e2e/dart/.dart_tool/
306
+ e2e/dart/build/
307
+
308
+ # Dart FRB codegen cache (incremental build optimization)
309
+ packages/dart/rust/.frb_codegen_hash
310
+
311
+ # Go e2e build artifacts
312
+ e2e/go/vendor/
313
+
314
+ erl_crash.dump
315
+
316
+
317
+ .gradle/
318
+
319
+ packages/go/.lib/
320
+
321
+ .build/
322
+
323
+ # basemind code-map index (local tooling)
324
+ .basemind/
325
+
326
+ .bench-rc32/
327
+ .private-bench/
328
+ build-output-musl-node/
329
+
330
+ poly.local.toml
331
+
332
+ # BEGIN ai-rulez (DO NOT EDIT - managed by ai-rulez)
333
+ .ai-rulez/.generated-manifest.json
334
+ # END ai-rulez
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.4
2
+ Name: llama-index-node-parser-xberg
3
+ Version: 1.0.0
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,5 @@
1
+ """XbergNodeParser — element-aware node parser for xberg-extracted documents."""
2
+
3
+ from llama_index.node_parser.xberg.base import XbergNodeParser
4
+
5
+ __all__ = ["XbergNodeParser"]
@@ -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.0"
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"