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.
@@ -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,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.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"