querdex 0.1.0__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {querdex-0.1.0 → querdex-0.2.1}/.gitignore +1 -0
- {querdex-0.1.0 → querdex-0.2.1}/PKG-INFO +74 -2
- {querdex-0.1.0 → querdex-0.2.1}/README.md +71 -0
- {querdex-0.1.0 → querdex-0.2.1}/pyproject.toml +5 -2
- {querdex-0.1.0 → querdex-0.2.1}/scripts/run_release_gate.py +3 -3
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/__init__.py +1 -1
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/cli.py +36 -0
- querdex-0.2.1/src/querdex/extraction/__init__.py +29 -0
- querdex-0.2.1/src/querdex/extraction/aligner.py +81 -0
- querdex-0.2.1/src/querdex/extraction/chunker.py +108 -0
- querdex-0.2.1/src/querdex/extraction/extractor.py +291 -0
- querdex-0.2.1/src/querdex/extraction/models.py +89 -0
- querdex-0.2.1/src/querdex/extraction/visualize.py +175 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/ingestion/parsers/sqlite_parser.py +8 -2
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/ingestion/parsers/url_parser.py +53 -4
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/services/engine.py +21 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/storage/graph_store.py +16 -6
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/storage/sqlite_store.py +36 -1
- querdex-0.2.1/tests/test_extraction.py +245 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/test_ingestion_additional.py +5 -15
- {querdex-0.1.0 → querdex-0.2.1}/uv.lock +1 -1
- {querdex-0.1.0 → querdex-0.2.1}/.env.example +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/.github/workflows/ci.yml +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/.github/workflows/publish.yml +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/Makefile +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/infra/Dockerfile +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/infra/ENVIRONMENTS.md +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/infra/README.md +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/infra/docker-compose.yml +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/scripts/dev_index_and_query.sh +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/adaptive/__init__.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/adaptive/updater.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/evaluation/__init__.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/evaluation/harness.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/evaluation/metrics.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/indexing/__init__.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/indexing/coordinator.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/indexing/diff_reindex.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/indexing/entity_extractor.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/indexing/entity_map_updater.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/indexing/graph_builder.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/indexing/quality.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/indexing/tree_builder.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/ingestion/__init__.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/ingestion/base.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/ingestion/orchestrator.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/ingestion/parsers/__init__.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/ingestion/parsers/audio_video_parser.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/ingestion/parsers/code_parser.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/ingestion/parsers/csv_parser.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/ingestion/parsers/docx_parser.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/ingestion/parsers/html_parser.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/ingestion/parsers/markdown_parser.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/ingestion/parsers/ocr.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/ingestion/parsers/pdf_parser.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/ingestion/parsers/text_parser.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/llm/__init__.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/llm/anthropic_client.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/llm/client.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/llm/fake_client.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/llm/openai_client.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/ops/__init__.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/ops/health.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/ops/observability.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/ops/retry.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/query/__init__.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/query/analyzer.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/query/answering.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/query/graph_walker.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/query/multi_doc.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/query/router.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/query/tiered_search.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/schemas/__init__.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/schemas/models.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/services/__init__.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/services/interfaces.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/storage/__init__.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/utils/__init__.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/utils/llm_validation.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/utils/query_cluster.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/src/querdex/utils/tree_ops.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/fixtures/eval/baseline_cases.json +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/fixtures/eval/kpi_baseline.json +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/fixtures/golden/parser_manifest.json +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/fixtures/golden/sample.csv +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/fixtures/golden/sample.html +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/fixtures/golden/sample.md +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/fixtures/golden/sample.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/fixtures/golden/sample.txt +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/test_adaptive_updater.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/test_diff_reindex.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/test_docx_integration.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/test_entity_map_updater.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/test_evaluation_harness.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/test_graph_builder.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/test_graph_store.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/test_ingestion.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/test_llm_integration.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/test_metrics_harness.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/test_ocr_provider.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/test_parser_golden.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/test_pdf_integration.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/test_pdf_parser.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/test_pipeline.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/test_query_analyzer.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/test_query_graph_multi.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/test_reindex.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/test_routing_suite.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/test_schemas.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/test_store.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/test_tree_builder.py +0 -0
- {querdex-0.1.0 → querdex-0.2.1}/tests/test_tree_quality.py +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: querdex
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Reasoning-first document intelligence system
|
|
5
|
-
Author
|
|
5
|
+
Author: Animay Tiwari
|
|
6
|
+
Author-email: Animay Tiwari <animaytiwari@outlook.com>
|
|
6
7
|
License: MIT
|
|
7
8
|
Keywords: document,indexing,llm,rag,retrieval
|
|
8
9
|
Classifier: Development Status :: 3 - Alpha
|
|
@@ -32,6 +33,13 @@ Description-Content-Type: text/markdown
|
|
|
32
33
|
|
|
33
34
|
# Querdex
|
|
34
35
|
|
|
36
|
+
[](https://pypi.org/project/querdex/)
|
|
37
|
+
[](https://pepy.tech/projects/querdex)
|
|
38
|
+
[](https://pypistats.org/packages/querdex)
|
|
39
|
+
[](https://pypi.org/project/querdex/)
|
|
40
|
+
[](https://github.com/its-animay/querdex/actions/workflows/ci.yml)
|
|
41
|
+
[](https://opensource.org/licenses/MIT)
|
|
42
|
+
|
|
35
43
|
**Reasoning-first document intelligence system.**
|
|
36
44
|
|
|
37
45
|
Querdex indexes any document into a hierarchical tree, then uses a two-tier LLM search to answer questions with cited sources. It works without an LLM (keyword heuristics), and optionally plugs in Anthropic or OpenAI for higher-quality results.
|
|
@@ -44,6 +52,7 @@ Querdex indexes any document into a hierarchical tree, then uses a two-tier LLM
|
|
|
44
52
|
- [Installation](#installation)
|
|
45
53
|
- [Quick Start (CLI)](#quick-start-cli)
|
|
46
54
|
- [LLM Setup](#llm-setup)
|
|
55
|
+
- [Structured Extraction](#structured-extraction)
|
|
47
56
|
- [CLI Reference](#cli-reference)
|
|
48
57
|
- [Python API](#python-api)
|
|
49
58
|
- [Supported File Types](#supported-file-types)
|
|
@@ -214,6 +223,54 @@ export QUERDEX_LLM_TIER2_MODEL=gpt-4o # powerful
|
|
|
214
223
|
|
|
215
224
|
---
|
|
216
225
|
|
|
226
|
+
## Structured Extraction
|
|
227
|
+
|
|
228
|
+
Pull structured facts out of any indexed document — with **source grounding**: every extraction carries the exact section, page, and character span it came from, so nothing is silently hallucinated.
|
|
229
|
+
|
|
230
|
+
The schema is defined **by example**, not by code. Describe what you want and (optionally) show one or two examples:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
querdex extract --doc-id demo \
|
|
234
|
+
--prompt "Extract revenue figures and executive names" \
|
|
235
|
+
--examples examples.json \
|
|
236
|
+
--html review.html
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`examples.json`:
|
|
240
|
+
```json
|
|
241
|
+
[
|
|
242
|
+
{
|
|
243
|
+
"text": "Alice Chen reported revenue of $5M in Q1.",
|
|
244
|
+
"extractions": [
|
|
245
|
+
{"extraction_class": "metric", "extraction_text": "revenue of $5M", "attributes": {"period": "Q1"}},
|
|
246
|
+
{"extraction_class": "person", "extraction_text": "Alice Chen"}
|
|
247
|
+
]
|
|
248
|
+
}
|
|
249
|
+
]
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
The classes and attribute keys in your examples define the output schema. Every result is aligned back to the source text (exact → fuzzy matching); model output that cannot be located is kept but flagged `unaligned` so you can review it instead of trusting it.
|
|
253
|
+
|
|
254
|
+
`--html` writes a self-contained review page: the full document with color-coded highlights per extraction class, toggleable legend, attribute tooltips, and a click-to-jump list of all extractions.
|
|
255
|
+
|
|
256
|
+
Long documents are chunked and processed in parallel; use `--passes 2` to trade extra LLM calls for higher recall. Without an LLM configured, extraction degrades to literal matching of your example texts.
|
|
257
|
+
|
|
258
|
+
Python API:
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
from querdex.extraction import ExtractionTask, ExtractionExample, ExampleExtraction
|
|
262
|
+
|
|
263
|
+
task = ExtractionTask(
|
|
264
|
+
description="Extract revenue figures and executive names",
|
|
265
|
+
examples=[...],
|
|
266
|
+
)
|
|
267
|
+
run = engine.extract_document("demo", task, passes=1)
|
|
268
|
+
for e in run.extractions:
|
|
269
|
+
print(e.extraction_class, repr(e.extraction_text), e.section_id, e.char_start, e.alignment)
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
217
274
|
## CLI Reference
|
|
218
275
|
|
|
219
276
|
```
|
|
@@ -224,6 +281,7 @@ querdex [--db PATH] <command> [options]
|
|
|
224
281
|
|---------|-------------|
|
|
225
282
|
| `index <file>` | Index a document. Auto-detects format from extension. |
|
|
226
283
|
| `query` | Query an indexed document. |
|
|
284
|
+
| `extract` | Run schema-by-example structured extraction over an indexed document. |
|
|
227
285
|
| `delete` | Remove a document and all its data from the store. |
|
|
228
286
|
|
|
229
287
|
### `index`
|
|
@@ -249,6 +307,20 @@ querdex query --doc-id ID --query TEXT [--session-id ID]
|
|
|
249
307
|
| `--query` | required | Natural language question |
|
|
250
308
|
| `--session-id` | none | Enables multi-turn context (pass same ID across turns) |
|
|
251
309
|
|
|
310
|
+
### `extract`
|
|
311
|
+
|
|
312
|
+
```
|
|
313
|
+
querdex extract --doc-id ID --prompt TEXT [--examples FILE] [--passes N] [--html FILE]
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
| Argument | Default | Description |
|
|
317
|
+
|----------|---------|-------------|
|
|
318
|
+
| `--doc-id` | required | Document to extract from |
|
|
319
|
+
| `--prompt` | required | Natural language description of what to extract |
|
|
320
|
+
| `--examples` | none | JSON file with few-shot examples (defines the output schema) |
|
|
321
|
+
| `--passes` | `1` | Extraction passes; more passes improve recall |
|
|
322
|
+
| `--html` | none | Write an interactive HTML review page to this path |
|
|
323
|
+
|
|
252
324
|
### `delete`
|
|
253
325
|
|
|
254
326
|
```
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Querdex
|
|
2
2
|
|
|
3
|
+
[](https://pypi.org/project/querdex/)
|
|
4
|
+
[](https://pepy.tech/projects/querdex)
|
|
5
|
+
[](https://pypistats.org/packages/querdex)
|
|
6
|
+
[](https://pypi.org/project/querdex/)
|
|
7
|
+
[](https://github.com/its-animay/querdex/actions/workflows/ci.yml)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
|
|
3
10
|
**Reasoning-first document intelligence system.**
|
|
4
11
|
|
|
5
12
|
Querdex indexes any document into a hierarchical tree, then uses a two-tier LLM search to answer questions with cited sources. It works without an LLM (keyword heuristics), and optionally plugs in Anthropic or OpenAI for higher-quality results.
|
|
@@ -12,6 +19,7 @@ Querdex indexes any document into a hierarchical tree, then uses a two-tier LLM
|
|
|
12
19
|
- [Installation](#installation)
|
|
13
20
|
- [Quick Start (CLI)](#quick-start-cli)
|
|
14
21
|
- [LLM Setup](#llm-setup)
|
|
22
|
+
- [Structured Extraction](#structured-extraction)
|
|
15
23
|
- [CLI Reference](#cli-reference)
|
|
16
24
|
- [Python API](#python-api)
|
|
17
25
|
- [Supported File Types](#supported-file-types)
|
|
@@ -182,6 +190,54 @@ export QUERDEX_LLM_TIER2_MODEL=gpt-4o # powerful
|
|
|
182
190
|
|
|
183
191
|
---
|
|
184
192
|
|
|
193
|
+
## Structured Extraction
|
|
194
|
+
|
|
195
|
+
Pull structured facts out of any indexed document — with **source grounding**: every extraction carries the exact section, page, and character span it came from, so nothing is silently hallucinated.
|
|
196
|
+
|
|
197
|
+
The schema is defined **by example**, not by code. Describe what you want and (optionally) show one or two examples:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
querdex extract --doc-id demo \
|
|
201
|
+
--prompt "Extract revenue figures and executive names" \
|
|
202
|
+
--examples examples.json \
|
|
203
|
+
--html review.html
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`examples.json`:
|
|
207
|
+
```json
|
|
208
|
+
[
|
|
209
|
+
{
|
|
210
|
+
"text": "Alice Chen reported revenue of $5M in Q1.",
|
|
211
|
+
"extractions": [
|
|
212
|
+
{"extraction_class": "metric", "extraction_text": "revenue of $5M", "attributes": {"period": "Q1"}},
|
|
213
|
+
{"extraction_class": "person", "extraction_text": "Alice Chen"}
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
]
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The classes and attribute keys in your examples define the output schema. Every result is aligned back to the source text (exact → fuzzy matching); model output that cannot be located is kept but flagged `unaligned` so you can review it instead of trusting it.
|
|
220
|
+
|
|
221
|
+
`--html` writes a self-contained review page: the full document with color-coded highlights per extraction class, toggleable legend, attribute tooltips, and a click-to-jump list of all extractions.
|
|
222
|
+
|
|
223
|
+
Long documents are chunked and processed in parallel; use `--passes 2` to trade extra LLM calls for higher recall. Without an LLM configured, extraction degrades to literal matching of your example texts.
|
|
224
|
+
|
|
225
|
+
Python API:
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
from querdex.extraction import ExtractionTask, ExtractionExample, ExampleExtraction
|
|
229
|
+
|
|
230
|
+
task = ExtractionTask(
|
|
231
|
+
description="Extract revenue figures and executive names",
|
|
232
|
+
examples=[...],
|
|
233
|
+
)
|
|
234
|
+
run = engine.extract_document("demo", task, passes=1)
|
|
235
|
+
for e in run.extractions:
|
|
236
|
+
print(e.extraction_class, repr(e.extraction_text), e.section_id, e.char_start, e.alignment)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
185
241
|
## CLI Reference
|
|
186
242
|
|
|
187
243
|
```
|
|
@@ -192,6 +248,7 @@ querdex [--db PATH] <command> [options]
|
|
|
192
248
|
|---------|-------------|
|
|
193
249
|
| `index <file>` | Index a document. Auto-detects format from extension. |
|
|
194
250
|
| `query` | Query an indexed document. |
|
|
251
|
+
| `extract` | Run schema-by-example structured extraction over an indexed document. |
|
|
195
252
|
| `delete` | Remove a document and all its data from the store. |
|
|
196
253
|
|
|
197
254
|
### `index`
|
|
@@ -217,6 +274,20 @@ querdex query --doc-id ID --query TEXT [--session-id ID]
|
|
|
217
274
|
| `--query` | required | Natural language question |
|
|
218
275
|
| `--session-id` | none | Enables multi-turn context (pass same ID across turns) |
|
|
219
276
|
|
|
277
|
+
### `extract`
|
|
278
|
+
|
|
279
|
+
```
|
|
280
|
+
querdex extract --doc-id ID --prompt TEXT [--examples FILE] [--passes N] [--html FILE]
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
| Argument | Default | Description |
|
|
284
|
+
|----------|---------|-------------|
|
|
285
|
+
| `--doc-id` | required | Document to extract from |
|
|
286
|
+
| `--prompt` | required | Natural language description of what to extract |
|
|
287
|
+
| `--examples` | none | JSON file with few-shot examples (defines the output schema) |
|
|
288
|
+
| `--passes` | `1` | Extraction passes; more passes improve recall |
|
|
289
|
+
| `--html` | none | Write an interactive HTML review page to this path |
|
|
290
|
+
|
|
220
291
|
### `delete`
|
|
221
292
|
|
|
222
293
|
```
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "querdex"
|
|
3
|
-
version = "0.1
|
|
3
|
+
version = "0.2.1"
|
|
4
4
|
description = "Reasoning-first document intelligence system"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
7
7
|
license = { text = "MIT" }
|
|
8
|
-
authors = [
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Animay Tiwari" },
|
|
10
|
+
{ name = "Animay Tiwari", email = "animaytiwari@outlook.com" },
|
|
11
|
+
]
|
|
9
12
|
keywords = ["document", "indexing", "retrieval", "llm", "rag"]
|
|
10
13
|
classifiers = [
|
|
11
14
|
"Development Status :: 3 - Alpha",
|
|
@@ -5,12 +5,12 @@ import asyncio
|
|
|
5
5
|
import json
|
|
6
6
|
import sys
|
|
7
7
|
|
|
8
|
-
from
|
|
9
|
-
from
|
|
8
|
+
from querdex.evaluation import EvaluationHarness, KPIDashboardBuilder, RetrievalMetricsHarness
|
|
9
|
+
from querdex.services import build_engine
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def _build_parser() -> argparse.ArgumentParser:
|
|
13
|
-
parser = argparse.ArgumentParser(description="Run
|
|
13
|
+
parser = argparse.ArgumentParser(description="Run querdex evaluation and release gate checks.")
|
|
14
14
|
parser.add_argument("--db", required=True, help="SQLite database path")
|
|
15
15
|
parser.add_argument("--doc-path", required=True, help="Path to document to index for the benchmark run")
|
|
16
16
|
parser.add_argument("--doc-id", required=True, help="Document id used for benchmark queries")
|
|
@@ -2,8 +2,10 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import asyncio
|
|
5
|
+
import json
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
|
|
8
|
+
from querdex.extraction import ExtractionExample, ExtractionTask, render_extraction_html
|
|
7
9
|
from querdex.services import build_engine
|
|
8
10
|
|
|
9
11
|
|
|
@@ -25,6 +27,13 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
25
27
|
delete_parser = subparsers.add_parser("delete", help="Delete an indexed document")
|
|
26
28
|
delete_parser.add_argument("--doc-id", required=True, help="Document id to delete")
|
|
27
29
|
|
|
30
|
+
extract_parser = subparsers.add_parser("extract", help="Run structured extraction over an indexed document")
|
|
31
|
+
extract_parser.add_argument("--doc-id", required=True, help="Document id to extract from")
|
|
32
|
+
extract_parser.add_argument("--prompt", required=True, help="Description of what to extract")
|
|
33
|
+
extract_parser.add_argument("--examples", default=None, help="Path to JSON file with few-shot examples")
|
|
34
|
+
extract_parser.add_argument("--passes", type=int, default=1, help="Extraction passes (more passes, more recall)")
|
|
35
|
+
extract_parser.add_argument("--html", default=None, help="Write an interactive HTML review page to this path")
|
|
36
|
+
|
|
28
37
|
return parser
|
|
29
38
|
|
|
30
39
|
|
|
@@ -52,6 +61,33 @@ def main() -> None:
|
|
|
52
61
|
engine.store.delete_document(args.doc_id)
|
|
53
62
|
print(f"Deleted doc_id={args.doc_id}")
|
|
54
63
|
|
|
64
|
+
elif args.command == "extract":
|
|
65
|
+
examples = []
|
|
66
|
+
if args.examples:
|
|
67
|
+
raw_examples = json.loads(Path(args.examples).read_text(encoding="utf-8"))
|
|
68
|
+
examples = [ExtractionExample.model_validate(item) for item in raw_examples]
|
|
69
|
+
task = ExtractionTask(description=args.prompt, examples=examples)
|
|
70
|
+
run = engine.extract_document(args.doc_id, task, passes=args.passes)
|
|
71
|
+
stats = run.stats
|
|
72
|
+
print(
|
|
73
|
+
f"Run {run.run_id}: {len(run.extractions)} extractions "
|
|
74
|
+
f"(exact={stats.exact_count} fuzzy={stats.fuzzy_count} unaligned={stats.unaligned_count}) "
|
|
75
|
+
f"in {stats.latency_ms}ms across {stats.chunk_count} chunks"
|
|
76
|
+
)
|
|
77
|
+
for extraction in run.extractions[:20]:
|
|
78
|
+
location = (
|
|
79
|
+
f"p{extraction.page_number} {extraction.section_id}"
|
|
80
|
+
if extraction.section_id
|
|
81
|
+
else "ungrounded"
|
|
82
|
+
)
|
|
83
|
+
print(f" [{extraction.extraction_class}] {extraction.extraction_text!r} ({location})")
|
|
84
|
+
if len(run.extractions) > 20:
|
|
85
|
+
print(f" ... and {len(run.extractions) - 20} more")
|
|
86
|
+
if args.html:
|
|
87
|
+
sections = engine.store.sections_for_doc(args.doc_id)
|
|
88
|
+
Path(args.html).write_text(render_extraction_html(run, sections), encoding="utf-8")
|
|
89
|
+
print(f"HTML review written to {args.html}")
|
|
90
|
+
|
|
55
91
|
finally:
|
|
56
92
|
engine.store.close()
|
|
57
93
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from querdex.extraction.aligner import align
|
|
2
|
+
from querdex.extraction.chunker import ChunkPiece, TextChunk, chunk_sections
|
|
3
|
+
from querdex.extraction.extractor import StructuredExtractor
|
|
4
|
+
from querdex.extraction.models import (
|
|
5
|
+
AlignmentStatus,
|
|
6
|
+
ExampleExtraction,
|
|
7
|
+
Extraction,
|
|
8
|
+
ExtractionExample,
|
|
9
|
+
ExtractionRun,
|
|
10
|
+
ExtractionStats,
|
|
11
|
+
ExtractionTask,
|
|
12
|
+
)
|
|
13
|
+
from querdex.extraction.visualize import render_extraction_html
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"AlignmentStatus",
|
|
17
|
+
"ChunkPiece",
|
|
18
|
+
"ExampleExtraction",
|
|
19
|
+
"Extraction",
|
|
20
|
+
"ExtractionExample",
|
|
21
|
+
"ExtractionRun",
|
|
22
|
+
"ExtractionStats",
|
|
23
|
+
"ExtractionTask",
|
|
24
|
+
"StructuredExtractor",
|
|
25
|
+
"TextChunk",
|
|
26
|
+
"align",
|
|
27
|
+
"chunk_sections",
|
|
28
|
+
"render_extraction_html",
|
|
29
|
+
]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from difflib import SequenceMatcher
|
|
4
|
+
|
|
5
|
+
from querdex.extraction.models import AlignmentStatus
|
|
6
|
+
|
|
7
|
+
_MIN_FUZZY_RATIO = 0.75
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _normalize(text: str) -> tuple[str, list[int]]:
|
|
11
|
+
"""Lowercase and collapse whitespace, keeping a map back to original indices.
|
|
12
|
+
|
|
13
|
+
``index_map[i]`` is the index in ``text`` of normalized character ``i``,
|
|
14
|
+
which lets fuzzy matches report offsets in the original string.
|
|
15
|
+
"""
|
|
16
|
+
normalized_chars: list[str] = []
|
|
17
|
+
index_map: list[int] = []
|
|
18
|
+
prev_space = True # drop leading whitespace
|
|
19
|
+
for i, ch in enumerate(text):
|
|
20
|
+
if ch.isspace():
|
|
21
|
+
if prev_space:
|
|
22
|
+
continue
|
|
23
|
+
normalized_chars.append(" ")
|
|
24
|
+
index_map.append(i)
|
|
25
|
+
prev_space = True
|
|
26
|
+
else:
|
|
27
|
+
normalized_chars.append(ch.lower())
|
|
28
|
+
index_map.append(i)
|
|
29
|
+
prev_space = False
|
|
30
|
+
if normalized_chars and normalized_chars[-1] == " ":
|
|
31
|
+
normalized_chars.pop()
|
|
32
|
+
index_map.pop()
|
|
33
|
+
return "".join(normalized_chars), index_map
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def align(needle: str, haystack: str) -> tuple[int, int, AlignmentStatus] | None:
|
|
37
|
+
"""Locate needle inside haystack, returning (start, end, status) in haystack coordinates.
|
|
38
|
+
|
|
39
|
+
Tries progressively looser strategies: exact, case-insensitive,
|
|
40
|
+
whitespace-normalized, then fuzzy via difflib. Returns None when no
|
|
41
|
+
sufficiently similar region exists - callers should treat that
|
|
42
|
+
extraction as unverified model output.
|
|
43
|
+
"""
|
|
44
|
+
needle = needle.strip()
|
|
45
|
+
if not needle:
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
idx = haystack.find(needle)
|
|
49
|
+
if idx >= 0:
|
|
50
|
+
return idx, idx + len(needle), "exact"
|
|
51
|
+
|
|
52
|
+
idx = haystack.lower().find(needle.lower())
|
|
53
|
+
if idx >= 0:
|
|
54
|
+
return idx, idx + len(needle), "exact"
|
|
55
|
+
|
|
56
|
+
norm_haystack, index_map = _normalize(haystack)
|
|
57
|
+
norm_needle, _ = _normalize(needle)
|
|
58
|
+
if not norm_needle or not norm_haystack:
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
idx = norm_haystack.find(norm_needle)
|
|
62
|
+
if idx >= 0:
|
|
63
|
+
start = index_map[idx]
|
|
64
|
+
end = index_map[idx + len(norm_needle) - 1] + 1
|
|
65
|
+
return start, end, "fuzzy"
|
|
66
|
+
|
|
67
|
+
matcher = SequenceMatcher(a=norm_haystack, b=norm_needle, autojunk=False)
|
|
68
|
+
match = matcher.find_longest_match(0, len(norm_haystack), 0, len(norm_needle))
|
|
69
|
+
if match.size == 0:
|
|
70
|
+
return None
|
|
71
|
+
window_start = max(0, match.a - match.b)
|
|
72
|
+
window_end = min(len(norm_haystack), window_start + len(norm_needle))
|
|
73
|
+
if window_end <= window_start:
|
|
74
|
+
return None
|
|
75
|
+
window = norm_haystack[window_start:window_end]
|
|
76
|
+
ratio = SequenceMatcher(a=window, b=norm_needle, autojunk=False).ratio()
|
|
77
|
+
if ratio < _MIN_FUZZY_RATIO:
|
|
78
|
+
return None
|
|
79
|
+
start = index_map[window_start]
|
|
80
|
+
end = index_map[window_end - 1] + 1
|
|
81
|
+
return start, end, "fuzzy"
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
|
|
6
|
+
from querdex.schemas import Section
|
|
7
|
+
|
|
8
|
+
_SENTENCE_END_RE = re.compile(r"[.!?]\s")
|
|
9
|
+
_PIECE_SEPARATOR = "\n\n"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class ChunkPiece:
|
|
14
|
+
"""A contiguous region of a chunk mapped back to its source section."""
|
|
15
|
+
|
|
16
|
+
section_id: str
|
|
17
|
+
page_number: int
|
|
18
|
+
chunk_offset: int
|
|
19
|
+
section_offset: int
|
|
20
|
+
length: int
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class TextChunk:
|
|
25
|
+
chunk_id: str
|
|
26
|
+
text: str
|
|
27
|
+
pieces: list[ChunkPiece] = field(default_factory=list)
|
|
28
|
+
|
|
29
|
+
def locate(self, char_start: int) -> ChunkPiece | None:
|
|
30
|
+
"""Return the piece containing char_start (chunk coordinates)."""
|
|
31
|
+
for piece in self.pieces:
|
|
32
|
+
if piece.chunk_offset <= char_start < piece.chunk_offset + piece.length:
|
|
33
|
+
return piece
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _split_long_text(text: str, max_chars: int) -> list[tuple[int, str]]:
|
|
38
|
+
"""Split text into (section_offset, piece) tuples of at most max_chars.
|
|
39
|
+
|
|
40
|
+
Cuts prefer sentence boundaries, then word boundaries, so extractions
|
|
41
|
+
are unlikely to straddle a chunk edge.
|
|
42
|
+
"""
|
|
43
|
+
pieces: list[tuple[int, str]] = []
|
|
44
|
+
start = 0
|
|
45
|
+
while start < len(text):
|
|
46
|
+
if len(text) - start <= max_chars:
|
|
47
|
+
pieces.append((start, text[start:]))
|
|
48
|
+
break
|
|
49
|
+
window = text[start : start + max_chars]
|
|
50
|
+
cut = max((m.end() for m in _SENTENCE_END_RE.finditer(window)), default=0)
|
|
51
|
+
if cut < max_chars // 2:
|
|
52
|
+
space = window.rfind(" ")
|
|
53
|
+
cut = space + 1 if space > max_chars // 2 else max_chars
|
|
54
|
+
pieces.append((start, text[start : start + cut]))
|
|
55
|
+
start += cut
|
|
56
|
+
return pieces
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def chunk_sections(sections: list[Section], *, max_chars: int = 4000) -> list[TextChunk]:
|
|
60
|
+
"""Group section contents into chunks of at most max_chars.
|
|
61
|
+
|
|
62
|
+
Consecutive small sections are packed together; oversized sections are
|
|
63
|
+
split at sentence boundaries. Every region of every chunk maps back to
|
|
64
|
+
its source section and offset so extractions can be grounded precisely.
|
|
65
|
+
"""
|
|
66
|
+
chunks: list[TextChunk] = []
|
|
67
|
+
current_parts: list[str] = []
|
|
68
|
+
current_pieces: list[ChunkPiece] = []
|
|
69
|
+
current_len = 0
|
|
70
|
+
|
|
71
|
+
def flush() -> None:
|
|
72
|
+
nonlocal current_parts, current_pieces, current_len
|
|
73
|
+
if current_parts:
|
|
74
|
+
chunks.append(
|
|
75
|
+
TextChunk(
|
|
76
|
+
chunk_id=f"chunk_{len(chunks) + 1:04d}",
|
|
77
|
+
text="".join(current_parts),
|
|
78
|
+
pieces=current_pieces,
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
current_parts = []
|
|
82
|
+
current_pieces = []
|
|
83
|
+
current_len = 0
|
|
84
|
+
|
|
85
|
+
def add_piece(section: Section, section_offset: int, piece_text: str) -> None:
|
|
86
|
+
nonlocal current_len
|
|
87
|
+
if current_parts:
|
|
88
|
+
current_parts.append(_PIECE_SEPARATOR)
|
|
89
|
+
current_len += len(_PIECE_SEPARATOR)
|
|
90
|
+
current_pieces.append(
|
|
91
|
+
ChunkPiece(
|
|
92
|
+
section_id=section.section_id,
|
|
93
|
+
page_number=section.page_number,
|
|
94
|
+
chunk_offset=current_len,
|
|
95
|
+
section_offset=section_offset,
|
|
96
|
+
length=len(piece_text),
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
current_parts.append(piece_text)
|
|
100
|
+
current_len += len(piece_text)
|
|
101
|
+
|
|
102
|
+
for section in sections:
|
|
103
|
+
for section_offset, piece_text in _split_long_text(section.content, max_chars):
|
|
104
|
+
if current_len and current_len + len(_PIECE_SEPARATOR) + len(piece_text) > max_chars:
|
|
105
|
+
flush()
|
|
106
|
+
add_piece(section, section_offset, piece_text)
|
|
107
|
+
flush()
|
|
108
|
+
return chunks
|