structured-data-extraction 0.1.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.
- structured_data_extraction-0.1.0/LICENSE +21 -0
- structured_data_extraction-0.1.0/PKG-INFO +234 -0
- structured_data_extraction-0.1.0/README.md +202 -0
- structured_data_extraction-0.1.0/pyproject.toml +64 -0
- structured_data_extraction-0.1.0/setup.cfg +4 -0
- structured_data_extraction-0.1.0/src/structured_data_extraction/__init__.py +8 -0
- structured_data_extraction-0.1.0/src/structured_data_extraction/classify.py +148 -0
- structured_data_extraction-0.1.0/src/structured_data_extraction/config.py +62 -0
- structured_data_extraction-0.1.0/src/structured_data_extraction/convert.py +165 -0
- structured_data_extraction-0.1.0/src/structured_data_extraction/embed.py +270 -0
- structured_data_extraction-0.1.0/src/structured_data_extraction/extract.py +441 -0
- structured_data_extraction-0.1.0/src/structured_data_extraction/features.py +160 -0
- structured_data_extraction-0.1.0/src/structured_data_extraction/filter_records.py +133 -0
- structured_data_extraction-0.1.0/src/structured_data_extraction/main.py +329 -0
- structured_data_extraction-0.1.0/src/structured_data_extraction/matching.py +153 -0
- structured_data_extraction-0.1.0/src/structured_data_extraction/models/section_classifier/docling_cats.joblib +0 -0
- structured_data_extraction-0.1.0/src/structured_data_extraction/models/section_classifier/rf_classifier.joblib +0 -0
- structured_data_extraction-0.1.0/src/structured_data_extraction/models/section_classifier/tfidf_char_vectorizer.joblib +0 -0
- structured_data_extraction-0.1.0/src/structured_data_extraction/models/section_classifier/tfidf_vectorizer.joblib +0 -0
- structured_data_extraction-0.1.0/src/structured_data_extraction/paths.py +34 -0
- structured_data_extraction-0.1.0/src/structured_data_extraction/rater.py +188 -0
- structured_data_extraction-0.1.0/src/structured_data_extraction/records.py +71 -0
- structured_data_extraction-0.1.0/src/structured_data_extraction/search.py +110 -0
- structured_data_extraction-0.1.0/src/structured_data_extraction/tables.py +406 -0
- structured_data_extraction-0.1.0/src/structured_data_extraction/templates/extraction.j2 +51 -0
- structured_data_extraction-0.1.0/src/structured_data_extraction/templates/schema_discovery.j2 +91 -0
- structured_data_extraction-0.1.0/src/structured_data_extraction/vocab.py +47 -0
- structured_data_extraction-0.1.0/src/structured_data_extraction.egg-info/PKG-INFO +234 -0
- structured_data_extraction-0.1.0/src/structured_data_extraction.egg-info/SOURCES.txt +40 -0
- structured_data_extraction-0.1.0/src/structured_data_extraction.egg-info/dependency_links.txt +1 -0
- structured_data_extraction-0.1.0/src/structured_data_extraction.egg-info/entry_points.txt +11 -0
- structured_data_extraction-0.1.0/src/structured_data_extraction.egg-info/requires.txt +21 -0
- structured_data_extraction-0.1.0/src/structured_data_extraction.egg-info/top_level.txt +1 -0
- structured_data_extraction-0.1.0/tests/test_convert.py +66 -0
- structured_data_extraction-0.1.0/tests/test_embed.py +209 -0
- structured_data_extraction-0.1.0/tests/test_extract.py +113 -0
- structured_data_extraction-0.1.0/tests/test_features.py +54 -0
- structured_data_extraction-0.1.0/tests/test_matching.py +137 -0
- structured_data_extraction-0.1.0/tests/test_rater.py +171 -0
- structured_data_extraction-0.1.0/tests/test_records.py +45 -0
- structured_data_extraction-0.1.0/tests/test_search.py +80 -0
- structured_data_extraction-0.1.0/tests/test_tables.py +95 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dimitrios
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: structured-data-extraction
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pipeline for extracting structured data (tables, sections, key/value fields) from PDF documents using docling, embeddings, and LLMs.
|
|
5
|
+
Author-email: Dimitrios Avellas <dimitrisavellas8@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://gitlab.indiscale.com/caosdb/src/structured-data-extraction
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: docling==2.82.0
|
|
12
|
+
Requires-Dist: docling-core==2.70.2
|
|
13
|
+
Requires-Dist: docling-parse==5.6.1
|
|
14
|
+
Requires-Dist: docling-ibm-models==3.13.0
|
|
15
|
+
Requires-Dist: yake
|
|
16
|
+
Requires-Dist: sentence-transformers
|
|
17
|
+
Requires-Dist: numpy
|
|
18
|
+
Requires-Dist: pandas
|
|
19
|
+
Requires-Dist: pydantic
|
|
20
|
+
Requires-Dist: instructor
|
|
21
|
+
Requires-Dist: openai
|
|
22
|
+
Requires-Dist: jinja2
|
|
23
|
+
Requires-Dist: scikit-learn==1.8.0
|
|
24
|
+
Requires-Dist: joblib
|
|
25
|
+
Requires-Dist: pymupdf
|
|
26
|
+
Requires-Dist: einops
|
|
27
|
+
Requires-Dist: rapidfuzz
|
|
28
|
+
Requires-Dist: python-dotenv
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# Structured Data Extraction
|
|
34
|
+
|
|
35
|
+
Pull user-specified fields out of arbitrary PDFs and return them as structured JSON with provenance.
|
|
36
|
+
|
|
37
|
+
It's **domain-agnostic**: nothing domain-specific is checked in. The vocabulary, acronyms, and field shapes are all derived from each document at runtime, so the same pipeline handles a chip datasheet or an earnings report without changes.
|
|
38
|
+
|
|
39
|
+
## How it works
|
|
40
|
+
|
|
41
|
+
Give it a PDF and a list of keys. It converts the PDF to text, learns the document's vocabulary, splits it into typed sections, and embeds them. Then it finds the sections and rows closest to your keys, cleans up any messy tables among them, and asks an LLM to read just those passages and return structured values.
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
PDF
|
|
45
|
+
└─► convert (docling) → markdown + items.json
|
|
46
|
+
└─► vocab (YAKE) → vocab.json
|
|
47
|
+
└─► classify (RandomForest) → sections.json
|
|
48
|
+
└─► tables (complexity score) → tables.json
|
|
49
|
+
└─► embed (sentence-transformers) → *_embeddings.npy + row_map
|
|
50
|
+
└─► search (cosine similarity) → matches.json
|
|
51
|
+
└─► refine matched tables (vision LLM, lazy) → cleaned HTML
|
|
52
|
+
└─► extract (schema + LLM) → output/<stem>_extraction_<model>.json
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Each step writes its output to disk and is skipped on a re-run if the file already exists, so iterating on a prompt does not re-run docling.
|
|
56
|
+
|
|
57
|
+
## Why this design
|
|
58
|
+
|
|
59
|
+
- Vocabulary is learned per document. `vocab.py` runs YAKE keyword extraction on each PDF instead of using a fixed word list, and a one-word key like `gain` is expanded with the document's related terms (`gain error`, `gain drift`) before search.
|
|
60
|
+
- Search is semantic. "CMRR" and "common-mode rejection ratio" share no letters but sit close in embedding space, so cosine similarity over sentence-transformer embeddings finds the passage where string matching would miss it.
|
|
61
|
+
- Table rows are embedded one at a time, each with its group header prepended, so a row reads like `Gain Error > G=1 | 0.01 | 0.04 %` instead of a bare `0.01 | 0.04 %`.
|
|
62
|
+
- Vision runs lazily. The vision LLM only cleans up a table if that table matched a key, so cost tracks what you asked for rather than document size.
|
|
63
|
+
- The extraction LLM is constrained. `extract.py` discovers a per-key schema, builds a Pydantic model from it, and hands it to [Instructor](https://python.useinstructor.com/), which validates the output instead of accepting free text.
|
|
64
|
+
- Running several models is optional. Extraction fans out across the configured LLMs in parallel; records are de-duplicated (table-sourced ones preferred) and `rater.py` scores their agreement and attaches a confidence to each record. With one model, every record is confidence 1.0.
|
|
65
|
+
|
|
66
|
+
## Pipeline at a glance
|
|
67
|
+
|
|
68
|
+
| Step | File | Job |
|
|
69
|
+
|------|------|-----|
|
|
70
|
+
| Convert | `convert.py` | Docling to markdown + `items.json` (label, page, bbox). OCR off, table model in FAST mode. |
|
|
71
|
+
| Vocab | `vocab.py` | Unsupervised domain vocabulary (YAKE, up to 4-grams). |
|
|
72
|
+
| Classify | `classify.py` | Pre-trained RandomForest tags each item as header/list/table/text/title. |
|
|
73
|
+
| Tables | `tables.py` | Score table complexity; rebuild symbol-only tables before embedding and matched tables after search (vision LLM, text-LLM fallback). |
|
|
74
|
+
| Embed | `embed.py` | SentenceTransformer over sections and table rows. nomic task prefixes (`search_document:` / `search_query:`). Sections stacked before rows. |
|
|
75
|
+
| Search | `search.py` | Cosine similarity vs the section+row matrix; hits above `SIMILARITY_THRESHOLD` (capped at `SEARCH_TOP_K`). |
|
|
76
|
+
| Extract | `extract.py` | Schema discovery, on-the-fly Pydantic model, Instructor extraction with retries, dedup, provenance. |
|
|
77
|
+
| Rate | `rater.py` | Cluster records into facts across models, attach a consensus confidence, score agreement (Cohen's kappa); write `fused.json` (every fact, confidence-sorted per key) and `rating_report.json`. |
|
|
78
|
+
| Package | `records.py` | Wrap `fused.json` into one provenance-stamped record per document (a metadata envelope plus the extracted fields). |
|
|
79
|
+
|
|
80
|
+
Supporting: `matching.py` (same-fact equivalence), `features.py` (classifier feature vector), `paths.py` (cache filename conventions), `config.py` (loads `.env`, derives paths, auto-selects CUDA/MPS/CPU).
|
|
81
|
+
|
|
82
|
+
## Design notes
|
|
83
|
+
|
|
84
|
+
Each Python file has a module-level docstring explaining what it does and the non-obvious design decisions behind it. Start with the docstring at the top of the file you want to understand.
|
|
85
|
+
|
|
86
|
+
## Installation
|
|
87
|
+
|
|
88
|
+
**Requirements:** Python 3.11+, plus an OpenAI-compatible LLM endpoint (the defaults assume a local [Ollama](https://ollama.com)). The first run also downloads the embedding model from Hugging Face.
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# 1. create and activate a virtual environment
|
|
92
|
+
python3 -m venv venv
|
|
93
|
+
source venv/bin/activate
|
|
94
|
+
|
|
95
|
+
# 2. install from PyPI
|
|
96
|
+
pip install structured-data-extraction
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
To modify the pipeline itself, install an editable checkout instead:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
git clone <repo-url> && cd structured-data-extraction
|
|
103
|
+
pip install -e .
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Create a `.env` in the directory you run the tool from.** It is required: the package reads these values on startup and will not run without them. It searches the working directory and its parents, and real shell environment variables override the file. Copy this block and edit the values:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# Provider: any OpenAI-compatible endpoint (Ollama, OpenAI, ...)
|
|
110
|
+
LLM_BASE_URL=http://localhost:11434/v1
|
|
111
|
+
LLM_API_KEY=ollama
|
|
112
|
+
|
|
113
|
+
# Models
|
|
114
|
+
SCHEMA_MODEL=glm-5.1:cloud
|
|
115
|
+
EXTRACTION_MODELS=gemma4:31b-cloud # comma-separated for parallel voting
|
|
116
|
+
VISION_MODEL=kimi-k2.7-code:cloud # must have vision abilities
|
|
117
|
+
EMBEDDING_MODEL=nomic-ai/nomic-embed-text-v1.5 # HugginFace model ID (downloaded locally on the first run)
|
|
118
|
+
|
|
119
|
+
# Tuning
|
|
120
|
+
EMBED_BATCH_SIZE=4 # small so long tables don't OOM a local GPU; raise on bigger hardware
|
|
121
|
+
MAX_LLM_RETRIES=2 # per-model retry budget on extraction failures
|
|
122
|
+
EXTRACTION_WORKERS=4 # parallel workers when running multiple extraction models
|
|
123
|
+
|
|
124
|
+
# Pipeline tuning = OPTIONAL: omit this whole block and the defaults below apply.
|
|
125
|
+
VOCAB_TOP_N=50 # YAKE vocab terms kept per document
|
|
126
|
+
VOCAB_MAX_NGRAM=4 # longest phrase YAKE treats as one term
|
|
127
|
+
SIMILARITY_THRESHOLD=0.55 # cosine floor for a section/row to count as a match
|
|
128
|
+
SEARCH_TOP_K=25 # max matches kept per key (above the threshold)
|
|
129
|
+
VISION_COMPLEXITY_THRESHOLD=0.15 # min table complexity worth sending to the vision model
|
|
130
|
+
VISION_PARALLELISM=4 # parallel vision calls when refining matched tables
|
|
131
|
+
EXPAND_KEYS=true # expand each key with the document's own related vocab before search
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Notes:
|
|
135
|
+
- `EXTRACTION_MODELS` is a comma-separated list. List several to cross-check agreement, or one to skip the agreement step. The first model is the "primary", used wherever a single model is needed (such as text-based table cleanup).
|
|
136
|
+
- For a non-Ollama provider, set `LLM_BASE_URL` / `LLM_API_KEY` accordingly (for OpenAI: `https://api.openai.com/v1` and `sk-...`).
|
|
137
|
+
- Shell environment variables override the file. Make sure every named model actually exists on your provider, or that step retries and then fails.
|
|
138
|
+
- The **pipeline tuning** block is optional: every value falls back to the default shown above if it's absent from `.env`. They are the knobs for recall vs. cost — e.g. lower `SIMILARITY_THRESHOLD` or raise `SEARCH_TOP_K` to retrieve more (and feed the LLM more), raise `VISION_COMPLEXITY_THRESHOLD` to send fewer tables to the (slow) vision model.
|
|
139
|
+
|
|
140
|
+
## Usage
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# one PDF
|
|
144
|
+
structured-extract pdf/1167fc.pdf --keys gain CMRR "supply voltage" #when key is more than one word then it should be inside ""
|
|
145
|
+
|
|
146
|
+
# a whole directory
|
|
147
|
+
structured-extract pdf/ --keys revenue "net income"
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Wrap multi-word keys in quotes. If you omit `--keys`, it prompts for a comma-separated list. A full run writes per-model extractions to `output/<stem>_extraction_<model>.json`, then fuses them into `fused.json` (every fact, sorted by confidence) plus `rating_report.json`, and wraps `fused.json` into the provenance-stamped `records.json`.
|
|
152
|
+
|
|
153
|
+
A multi-model run then ends by printing the confidence-tier histogram and offering to keep a subset of tiers — pass `--levels` (e.g. `--levels 1.0 0.75`, or `all`) to choose up front, or omit it for an interactive prompt. Single-model runs skip this step, since every record is confidence 1.0.
|
|
154
|
+
|
|
155
|
+
**Re-run a single stage.** Every step is also available as its own console script. Run from the same working directory as the full pipeline:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
# early stages (operate on files from the previous step)
|
|
159
|
+
structured-extract-convert --input pdf/example.pdf # PDF to markdown
|
|
160
|
+
structured-extract-vocab --input markdown/example.md # domain vocabulary
|
|
161
|
+
structured-extract-classify --stem example # section classification
|
|
162
|
+
structured-extract-embed --stem example --keys gain CMRR # embed sections + keys
|
|
163
|
+
|
|
164
|
+
# retrieval and extraction
|
|
165
|
+
structured-extract-search --stem example --original-keys gain CMRR # semantic search
|
|
166
|
+
structured-extract-extract --stem example --keys gain CMRR # LLM extraction
|
|
167
|
+
|
|
168
|
+
# post-processing (no --stem flag, they read output dir)
|
|
169
|
+
structured-extract-rate # fuse models, score agreement
|
|
170
|
+
structured-extract-records # wrap fused.json into records.json
|
|
171
|
+
structured-extract-filter --levels 1.0 0.75 # keep only selected confidence tiers
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Confidence is `models-agreeing / total-models`. `filter_records` reads `records.json` and keeps any subset of the confidence tiers present in the data — pass `--levels` (e.g. `1.0 0.75`, or `all`), or omit it for an interactive menu. Needs more than one model to be meaningful.
|
|
175
|
+
|
|
176
|
+
## Outputs
|
|
177
|
+
|
|
178
|
+
Every stage caches its result under `intermediate/` and is skipped on a re-run if the file already exists; the finished deliverables land in `output/`.
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
intermediate/<stem>_items.json docling items (label, page, bbox)
|
|
182
|
+
<stem>_vocab.json YAKE vocabulary
|
|
183
|
+
<stem>_sections.json classified content stream
|
|
184
|
+
<stem>_tables.json tables + complexity scores
|
|
185
|
+
<stem>_*_embeddings.npy section / key vectors
|
|
186
|
+
<stem>_matches.json search hits per key
|
|
187
|
+
<stem>_run.json run manifest (models, timings)
|
|
188
|
+
|
|
189
|
+
output/<stem>_extraction_<model>.json one file per extraction model
|
|
190
|
+
fused.json every fact, confidence-sorted per key ← the deliverable
|
|
191
|
+
records.json fused.json wrapped in a provenance envelope, one per document
|
|
192
|
+
rating_report.json agreement scoring + pairwise Cohen's kappa
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
A `records.json` entry pairs a provenance header with the extracted fields. Each field is a list of records sorted by confidence (`models-agreeing / total-models`), so the top record per key is the one the most models agreed on:
|
|
196
|
+
|
|
197
|
+
```jsonc
|
|
198
|
+
{
|
|
199
|
+
"0150200051": {
|
|
200
|
+
"type": "ExtractionResult",
|
|
201
|
+
"provenance": {
|
|
202
|
+
"source_file": "0150200051.pdf",
|
|
203
|
+
"document_type": "cable connector product datasheet",
|
|
204
|
+
"models": { "schema_discovery": "glm-5.1:cloud", "vision": "kimi-k2.7-code:cloud",
|
|
205
|
+
"embedding": "nomic-ai/nomic-embed-text-v1.5" },
|
|
206
|
+
"user_keys": ["product family", "cable length", "Voltage", "net weight"]
|
|
207
|
+
},
|
|
208
|
+
"product_family": [
|
|
209
|
+
{ "confidence": 1.0, "models": ["deepseek-v4-flash:cloud", "gemma4:31b-cloud",
|
|
210
|
+
"gpt-oss:120b-cloud", "minimax-m2.5:cloud"],
|
|
211
|
+
"value": "Cable", "metadata": { "section_type": "text", "specified": true } },
|
|
212
|
+
{ "confidence": 0.25, "models": ["deepseek-v4-flash:cloud"],
|
|
213
|
+
"value": "Cable Series", "metadata": { "section_type": "text", "specified": true } }
|
|
214
|
+
],
|
|
215
|
+
"voltage": [
|
|
216
|
+
{ "confidence": 1.0, "models": ["deepseek-v4-flash:cloud", "gemma4:31b-cloud",
|
|
217
|
+
"gpt-oss:120b-cloud", "minimax-m2.5:cloud"],
|
|
218
|
+
"qualifier": "Maximum", "value": 60.0, "value_unit": "V AC",
|
|
219
|
+
"metadata": { "section_type": "text", "specified": true } }
|
|
220
|
+
]
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Testing
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
pip install -e ".[dev]"
|
|
229
|
+
pytest
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## License
|
|
233
|
+
|
|
234
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Structured Data Extraction
|
|
2
|
+
|
|
3
|
+
Pull user-specified fields out of arbitrary PDFs and return them as structured JSON with provenance.
|
|
4
|
+
|
|
5
|
+
It's **domain-agnostic**: nothing domain-specific is checked in. The vocabulary, acronyms, and field shapes are all derived from each document at runtime, so the same pipeline handles a chip datasheet or an earnings report without changes.
|
|
6
|
+
|
|
7
|
+
## How it works
|
|
8
|
+
|
|
9
|
+
Give it a PDF and a list of keys. It converts the PDF to text, learns the document's vocabulary, splits it into typed sections, and embeds them. Then it finds the sections and rows closest to your keys, cleans up any messy tables among them, and asks an LLM to read just those passages and return structured values.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
PDF
|
|
13
|
+
└─► convert (docling) → markdown + items.json
|
|
14
|
+
└─► vocab (YAKE) → vocab.json
|
|
15
|
+
└─► classify (RandomForest) → sections.json
|
|
16
|
+
└─► tables (complexity score) → tables.json
|
|
17
|
+
└─► embed (sentence-transformers) → *_embeddings.npy + row_map
|
|
18
|
+
└─► search (cosine similarity) → matches.json
|
|
19
|
+
└─► refine matched tables (vision LLM, lazy) → cleaned HTML
|
|
20
|
+
└─► extract (schema + LLM) → output/<stem>_extraction_<model>.json
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Each step writes its output to disk and is skipped on a re-run if the file already exists, so iterating on a prompt does not re-run docling.
|
|
24
|
+
|
|
25
|
+
## Why this design
|
|
26
|
+
|
|
27
|
+
- Vocabulary is learned per document. `vocab.py` runs YAKE keyword extraction on each PDF instead of using a fixed word list, and a one-word key like `gain` is expanded with the document's related terms (`gain error`, `gain drift`) before search.
|
|
28
|
+
- Search is semantic. "CMRR" and "common-mode rejection ratio" share no letters but sit close in embedding space, so cosine similarity over sentence-transformer embeddings finds the passage where string matching would miss it.
|
|
29
|
+
- Table rows are embedded one at a time, each with its group header prepended, so a row reads like `Gain Error > G=1 | 0.01 | 0.04 %` instead of a bare `0.01 | 0.04 %`.
|
|
30
|
+
- Vision runs lazily. The vision LLM only cleans up a table if that table matched a key, so cost tracks what you asked for rather than document size.
|
|
31
|
+
- The extraction LLM is constrained. `extract.py` discovers a per-key schema, builds a Pydantic model from it, and hands it to [Instructor](https://python.useinstructor.com/), which validates the output instead of accepting free text.
|
|
32
|
+
- Running several models is optional. Extraction fans out across the configured LLMs in parallel; records are de-duplicated (table-sourced ones preferred) and `rater.py` scores their agreement and attaches a confidence to each record. With one model, every record is confidence 1.0.
|
|
33
|
+
|
|
34
|
+
## Pipeline at a glance
|
|
35
|
+
|
|
36
|
+
| Step | File | Job |
|
|
37
|
+
|------|------|-----|
|
|
38
|
+
| Convert | `convert.py` | Docling to markdown + `items.json` (label, page, bbox). OCR off, table model in FAST mode. |
|
|
39
|
+
| Vocab | `vocab.py` | Unsupervised domain vocabulary (YAKE, up to 4-grams). |
|
|
40
|
+
| Classify | `classify.py` | Pre-trained RandomForest tags each item as header/list/table/text/title. |
|
|
41
|
+
| Tables | `tables.py` | Score table complexity; rebuild symbol-only tables before embedding and matched tables after search (vision LLM, text-LLM fallback). |
|
|
42
|
+
| Embed | `embed.py` | SentenceTransformer over sections and table rows. nomic task prefixes (`search_document:` / `search_query:`). Sections stacked before rows. |
|
|
43
|
+
| Search | `search.py` | Cosine similarity vs the section+row matrix; hits above `SIMILARITY_THRESHOLD` (capped at `SEARCH_TOP_K`). |
|
|
44
|
+
| Extract | `extract.py` | Schema discovery, on-the-fly Pydantic model, Instructor extraction with retries, dedup, provenance. |
|
|
45
|
+
| Rate | `rater.py` | Cluster records into facts across models, attach a consensus confidence, score agreement (Cohen's kappa); write `fused.json` (every fact, confidence-sorted per key) and `rating_report.json`. |
|
|
46
|
+
| Package | `records.py` | Wrap `fused.json` into one provenance-stamped record per document (a metadata envelope plus the extracted fields). |
|
|
47
|
+
|
|
48
|
+
Supporting: `matching.py` (same-fact equivalence), `features.py` (classifier feature vector), `paths.py` (cache filename conventions), `config.py` (loads `.env`, derives paths, auto-selects CUDA/MPS/CPU).
|
|
49
|
+
|
|
50
|
+
## Design notes
|
|
51
|
+
|
|
52
|
+
Each Python file has a module-level docstring explaining what it does and the non-obvious design decisions behind it. Start with the docstring at the top of the file you want to understand.
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
**Requirements:** Python 3.11+, plus an OpenAI-compatible LLM endpoint (the defaults assume a local [Ollama](https://ollama.com)). The first run also downloads the embedding model from Hugging Face.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# 1. create and activate a virtual environment
|
|
60
|
+
python3 -m venv venv
|
|
61
|
+
source venv/bin/activate
|
|
62
|
+
|
|
63
|
+
# 2. install from PyPI
|
|
64
|
+
pip install structured-data-extraction
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
To modify the pipeline itself, install an editable checkout instead:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
git clone <repo-url> && cd structured-data-extraction
|
|
71
|
+
pip install -e .
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Create a `.env` in the directory you run the tool from.** It is required: the package reads these values on startup and will not run without them. It searches the working directory and its parents, and real shell environment variables override the file. Copy this block and edit the values:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# Provider: any OpenAI-compatible endpoint (Ollama, OpenAI, ...)
|
|
78
|
+
LLM_BASE_URL=http://localhost:11434/v1
|
|
79
|
+
LLM_API_KEY=ollama
|
|
80
|
+
|
|
81
|
+
# Models
|
|
82
|
+
SCHEMA_MODEL=glm-5.1:cloud
|
|
83
|
+
EXTRACTION_MODELS=gemma4:31b-cloud # comma-separated for parallel voting
|
|
84
|
+
VISION_MODEL=kimi-k2.7-code:cloud # must have vision abilities
|
|
85
|
+
EMBEDDING_MODEL=nomic-ai/nomic-embed-text-v1.5 # HugginFace model ID (downloaded locally on the first run)
|
|
86
|
+
|
|
87
|
+
# Tuning
|
|
88
|
+
EMBED_BATCH_SIZE=4 # small so long tables don't OOM a local GPU; raise on bigger hardware
|
|
89
|
+
MAX_LLM_RETRIES=2 # per-model retry budget on extraction failures
|
|
90
|
+
EXTRACTION_WORKERS=4 # parallel workers when running multiple extraction models
|
|
91
|
+
|
|
92
|
+
# Pipeline tuning = OPTIONAL: omit this whole block and the defaults below apply.
|
|
93
|
+
VOCAB_TOP_N=50 # YAKE vocab terms kept per document
|
|
94
|
+
VOCAB_MAX_NGRAM=4 # longest phrase YAKE treats as one term
|
|
95
|
+
SIMILARITY_THRESHOLD=0.55 # cosine floor for a section/row to count as a match
|
|
96
|
+
SEARCH_TOP_K=25 # max matches kept per key (above the threshold)
|
|
97
|
+
VISION_COMPLEXITY_THRESHOLD=0.15 # min table complexity worth sending to the vision model
|
|
98
|
+
VISION_PARALLELISM=4 # parallel vision calls when refining matched tables
|
|
99
|
+
EXPAND_KEYS=true # expand each key with the document's own related vocab before search
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Notes:
|
|
103
|
+
- `EXTRACTION_MODELS` is a comma-separated list. List several to cross-check agreement, or one to skip the agreement step. The first model is the "primary", used wherever a single model is needed (such as text-based table cleanup).
|
|
104
|
+
- For a non-Ollama provider, set `LLM_BASE_URL` / `LLM_API_KEY` accordingly (for OpenAI: `https://api.openai.com/v1` and `sk-...`).
|
|
105
|
+
- Shell environment variables override the file. Make sure every named model actually exists on your provider, or that step retries and then fails.
|
|
106
|
+
- The **pipeline tuning** block is optional: every value falls back to the default shown above if it's absent from `.env`. They are the knobs for recall vs. cost — e.g. lower `SIMILARITY_THRESHOLD` or raise `SEARCH_TOP_K` to retrieve more (and feed the LLM more), raise `VISION_COMPLEXITY_THRESHOLD` to send fewer tables to the (slow) vision model.
|
|
107
|
+
|
|
108
|
+
## Usage
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# one PDF
|
|
112
|
+
structured-extract pdf/1167fc.pdf --keys gain CMRR "supply voltage" #when key is more than one word then it should be inside ""
|
|
113
|
+
|
|
114
|
+
# a whole directory
|
|
115
|
+
structured-extract pdf/ --keys revenue "net income"
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Wrap multi-word keys in quotes. If you omit `--keys`, it prompts for a comma-separated list. A full run writes per-model extractions to `output/<stem>_extraction_<model>.json`, then fuses them into `fused.json` (every fact, sorted by confidence) plus `rating_report.json`, and wraps `fused.json` into the provenance-stamped `records.json`.
|
|
120
|
+
|
|
121
|
+
A multi-model run then ends by printing the confidence-tier histogram and offering to keep a subset of tiers — pass `--levels` (e.g. `--levels 1.0 0.75`, or `all`) to choose up front, or omit it for an interactive prompt. Single-model runs skip this step, since every record is confidence 1.0.
|
|
122
|
+
|
|
123
|
+
**Re-run a single stage.** Every step is also available as its own console script. Run from the same working directory as the full pipeline:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
# early stages (operate on files from the previous step)
|
|
127
|
+
structured-extract-convert --input pdf/example.pdf # PDF to markdown
|
|
128
|
+
structured-extract-vocab --input markdown/example.md # domain vocabulary
|
|
129
|
+
structured-extract-classify --stem example # section classification
|
|
130
|
+
structured-extract-embed --stem example --keys gain CMRR # embed sections + keys
|
|
131
|
+
|
|
132
|
+
# retrieval and extraction
|
|
133
|
+
structured-extract-search --stem example --original-keys gain CMRR # semantic search
|
|
134
|
+
structured-extract-extract --stem example --keys gain CMRR # LLM extraction
|
|
135
|
+
|
|
136
|
+
# post-processing (no --stem flag, they read output dir)
|
|
137
|
+
structured-extract-rate # fuse models, score agreement
|
|
138
|
+
structured-extract-records # wrap fused.json into records.json
|
|
139
|
+
structured-extract-filter --levels 1.0 0.75 # keep only selected confidence tiers
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Confidence is `models-agreeing / total-models`. `filter_records` reads `records.json` and keeps any subset of the confidence tiers present in the data — pass `--levels` (e.g. `1.0 0.75`, or `all`), or omit it for an interactive menu. Needs more than one model to be meaningful.
|
|
143
|
+
|
|
144
|
+
## Outputs
|
|
145
|
+
|
|
146
|
+
Every stage caches its result under `intermediate/` and is skipped on a re-run if the file already exists; the finished deliverables land in `output/`.
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
intermediate/<stem>_items.json docling items (label, page, bbox)
|
|
150
|
+
<stem>_vocab.json YAKE vocabulary
|
|
151
|
+
<stem>_sections.json classified content stream
|
|
152
|
+
<stem>_tables.json tables + complexity scores
|
|
153
|
+
<stem>_*_embeddings.npy section / key vectors
|
|
154
|
+
<stem>_matches.json search hits per key
|
|
155
|
+
<stem>_run.json run manifest (models, timings)
|
|
156
|
+
|
|
157
|
+
output/<stem>_extraction_<model>.json one file per extraction model
|
|
158
|
+
fused.json every fact, confidence-sorted per key ← the deliverable
|
|
159
|
+
records.json fused.json wrapped in a provenance envelope, one per document
|
|
160
|
+
rating_report.json agreement scoring + pairwise Cohen's kappa
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
A `records.json` entry pairs a provenance header with the extracted fields. Each field is a list of records sorted by confidence (`models-agreeing / total-models`), so the top record per key is the one the most models agreed on:
|
|
164
|
+
|
|
165
|
+
```jsonc
|
|
166
|
+
{
|
|
167
|
+
"0150200051": {
|
|
168
|
+
"type": "ExtractionResult",
|
|
169
|
+
"provenance": {
|
|
170
|
+
"source_file": "0150200051.pdf",
|
|
171
|
+
"document_type": "cable connector product datasheet",
|
|
172
|
+
"models": { "schema_discovery": "glm-5.1:cloud", "vision": "kimi-k2.7-code:cloud",
|
|
173
|
+
"embedding": "nomic-ai/nomic-embed-text-v1.5" },
|
|
174
|
+
"user_keys": ["product family", "cable length", "Voltage", "net weight"]
|
|
175
|
+
},
|
|
176
|
+
"product_family": [
|
|
177
|
+
{ "confidence": 1.0, "models": ["deepseek-v4-flash:cloud", "gemma4:31b-cloud",
|
|
178
|
+
"gpt-oss:120b-cloud", "minimax-m2.5:cloud"],
|
|
179
|
+
"value": "Cable", "metadata": { "section_type": "text", "specified": true } },
|
|
180
|
+
{ "confidence": 0.25, "models": ["deepseek-v4-flash:cloud"],
|
|
181
|
+
"value": "Cable Series", "metadata": { "section_type": "text", "specified": true } }
|
|
182
|
+
],
|
|
183
|
+
"voltage": [
|
|
184
|
+
{ "confidence": 1.0, "models": ["deepseek-v4-flash:cloud", "gemma4:31b-cloud",
|
|
185
|
+
"gpt-oss:120b-cloud", "minimax-m2.5:cloud"],
|
|
186
|
+
"qualifier": "Maximum", "value": 60.0, "value_unit": "V AC",
|
|
187
|
+
"metadata": { "section_type": "text", "specified": true } }
|
|
188
|
+
]
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Testing
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
pip install -e ".[dev]"
|
|
197
|
+
pytest
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## License
|
|
201
|
+
|
|
202
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0.3"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "structured-data-extraction"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Pipeline for extracting structured data (tables, sections, key/value fields) from PDF documents using docling, embeddings, and LLMs."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Dimitrios Avellas", email = "dimitrisavellas8@gmail.com" }]
|
|
14
|
+
|
|
15
|
+
# Mirrors requirements.txt.
|
|
16
|
+
dependencies = [
|
|
17
|
+
"docling==2.82.0",
|
|
18
|
+
"docling-core==2.70.2",
|
|
19
|
+
"docling-parse==5.6.1",
|
|
20
|
+
"docling-ibm-models==3.13.0",
|
|
21
|
+
"yake",
|
|
22
|
+
"sentence-transformers",
|
|
23
|
+
"numpy",
|
|
24
|
+
"pandas",
|
|
25
|
+
"pydantic",
|
|
26
|
+
"instructor",
|
|
27
|
+
"openai",
|
|
28
|
+
"jinja2",
|
|
29
|
+
"scikit-learn==1.8.0",
|
|
30
|
+
"joblib",
|
|
31
|
+
"pymupdf",
|
|
32
|
+
|
|
33
|
+
"einops",
|
|
34
|
+
"rapidfuzz",
|
|
35
|
+
"python-dotenv",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
dev = ["pytest"]
|
|
40
|
+
|
|
41
|
+
[project.scripts]
|
|
42
|
+
structured-extract = "structured_data_extraction.main:main"
|
|
43
|
+
structured-extract-convert = "structured_data_extraction.convert:main"
|
|
44
|
+
structured-extract-vocab = "structured_data_extraction.vocab:main"
|
|
45
|
+
structured-extract-classify = "structured_data_extraction.classify:main"
|
|
46
|
+
structured-extract-embed = "structured_data_extraction.embed:main"
|
|
47
|
+
structured-extract-search = "structured_data_extraction.search:main"
|
|
48
|
+
structured-extract-extract = "structured_data_extraction.extract:main"
|
|
49
|
+
structured-extract-rate = "structured_data_extraction.rater:main"
|
|
50
|
+
structured-extract-records = "structured_data_extraction.records:main"
|
|
51
|
+
structured-extract-filter = "structured_data_extraction.filter_records:main"
|
|
52
|
+
|
|
53
|
+
[project.urls]
|
|
54
|
+
Repository = "https://gitlab.indiscale.com/caosdb/src/structured-data-extraction"
|
|
55
|
+
|
|
56
|
+
[tool.setuptools.packages.find]
|
|
57
|
+
where = ["src"]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
[tool.setuptools.package-data]
|
|
61
|
+
structured_data_extraction = ["templates/*.j2", "models/section_classifier/*"]
|
|
62
|
+
|
|
63
|
+
[tool.pytest.ini_options]
|
|
64
|
+
testpaths = ["tests"]
|