querdex 0.1.0__tar.gz → 0.2.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.
- {querdex-0.1.0 → querdex-0.2.0}/.gitignore +1 -0
- {querdex-0.1.0 → querdex-0.2.0}/PKG-INFO +72 -2
- {querdex-0.1.0 → querdex-0.2.0}/README.md +70 -0
- {querdex-0.1.0 → querdex-0.2.0}/pyproject.toml +2 -2
- {querdex-0.1.0 → querdex-0.2.0}/scripts/run_release_gate.py +3 -3
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/__init__.py +1 -1
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/cli.py +36 -0
- querdex-0.2.0/src/querdex/extraction/__init__.py +29 -0
- querdex-0.2.0/src/querdex/extraction/aligner.py +81 -0
- querdex-0.2.0/src/querdex/extraction/chunker.py +108 -0
- querdex-0.2.0/src/querdex/extraction/extractor.py +291 -0
- querdex-0.2.0/src/querdex/extraction/models.py +89 -0
- querdex-0.2.0/src/querdex/extraction/visualize.py +175 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/ingestion/parsers/sqlite_parser.py +8 -2
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/ingestion/parsers/url_parser.py +53 -4
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/services/engine.py +21 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/storage/graph_store.py +16 -6
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/storage/sqlite_store.py +36 -1
- querdex-0.2.0/tests/test_extraction.py +245 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/test_ingestion_additional.py +5 -15
- {querdex-0.1.0 → querdex-0.2.0}/uv.lock +1 -1
- {querdex-0.1.0 → querdex-0.2.0}/.env.example +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/.github/workflows/ci.yml +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/.github/workflows/publish.yml +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/Makefile +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/infra/Dockerfile +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/infra/ENVIRONMENTS.md +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/infra/README.md +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/infra/docker-compose.yml +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/scripts/dev_index_and_query.sh +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/adaptive/__init__.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/adaptive/updater.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/evaluation/__init__.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/evaluation/harness.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/evaluation/metrics.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/indexing/__init__.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/indexing/coordinator.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/indexing/diff_reindex.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/indexing/entity_extractor.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/indexing/entity_map_updater.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/indexing/graph_builder.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/indexing/quality.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/indexing/tree_builder.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/ingestion/__init__.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/ingestion/base.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/ingestion/orchestrator.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/ingestion/parsers/__init__.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/ingestion/parsers/audio_video_parser.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/ingestion/parsers/code_parser.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/ingestion/parsers/csv_parser.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/ingestion/parsers/docx_parser.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/ingestion/parsers/html_parser.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/ingestion/parsers/markdown_parser.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/ingestion/parsers/ocr.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/ingestion/parsers/pdf_parser.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/ingestion/parsers/text_parser.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/llm/__init__.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/llm/anthropic_client.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/llm/client.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/llm/fake_client.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/llm/openai_client.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/ops/__init__.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/ops/health.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/ops/observability.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/ops/retry.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/query/__init__.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/query/analyzer.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/query/answering.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/query/graph_walker.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/query/multi_doc.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/query/router.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/query/tiered_search.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/schemas/__init__.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/schemas/models.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/services/__init__.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/services/interfaces.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/storage/__init__.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/utils/__init__.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/utils/llm_validation.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/utils/query_cluster.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/src/querdex/utils/tree_ops.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/fixtures/eval/baseline_cases.json +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/fixtures/eval/kpi_baseline.json +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/fixtures/golden/parser_manifest.json +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/fixtures/golden/sample.csv +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/fixtures/golden/sample.html +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/fixtures/golden/sample.md +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/fixtures/golden/sample.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/fixtures/golden/sample.txt +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/test_adaptive_updater.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/test_diff_reindex.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/test_docx_integration.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/test_entity_map_updater.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/test_evaluation_harness.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/test_graph_builder.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/test_graph_store.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/test_ingestion.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/test_llm_integration.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/test_metrics_harness.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/test_ocr_provider.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/test_parser_golden.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/test_pdf_integration.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/test_pdf_parser.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/test_pipeline.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/test_query_analyzer.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/test_query_graph_multi.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/test_reindex.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/test_routing_suite.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/test_schemas.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/test_store.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/test_tree_builder.py +0 -0
- {querdex-0.1.0 → querdex-0.2.0}/tests/test_tree_quality.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: querdex
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Reasoning-first document intelligence system
|
|
5
|
-
Author-email:
|
|
5
|
+
Author-email: Animay Tiwari <animaytiwari@outlook.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Keywords: document,indexing,llm,rag,retrieval
|
|
8
8
|
Classifier: Development Status :: 3 - Alpha
|
|
@@ -32,6 +32,12 @@ Description-Content-Type: text/markdown
|
|
|
32
32
|
|
|
33
33
|
# Querdex
|
|
34
34
|
|
|
35
|
+
[](https://pypi.org/project/querdex/)
|
|
36
|
+
[](https://pypistats.org/packages/querdex)
|
|
37
|
+
[](https://pypi.org/project/querdex/)
|
|
38
|
+
[](https://github.com/its-animay/querdex/actions/workflows/ci.yml)
|
|
39
|
+
[](https://opensource.org/licenses/MIT)
|
|
40
|
+
|
|
35
41
|
**Reasoning-first document intelligence system.**
|
|
36
42
|
|
|
37
43
|
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 +50,7 @@ Querdex indexes any document into a hierarchical tree, then uses a two-tier LLM
|
|
|
44
50
|
- [Installation](#installation)
|
|
45
51
|
- [Quick Start (CLI)](#quick-start-cli)
|
|
46
52
|
- [LLM Setup](#llm-setup)
|
|
53
|
+
- [Structured Extraction](#structured-extraction)
|
|
47
54
|
- [CLI Reference](#cli-reference)
|
|
48
55
|
- [Python API](#python-api)
|
|
49
56
|
- [Supported File Types](#supported-file-types)
|
|
@@ -214,6 +221,54 @@ export QUERDEX_LLM_TIER2_MODEL=gpt-4o # powerful
|
|
|
214
221
|
|
|
215
222
|
---
|
|
216
223
|
|
|
224
|
+
## Structured Extraction
|
|
225
|
+
|
|
226
|
+
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.
|
|
227
|
+
|
|
228
|
+
The schema is defined **by example**, not by code. Describe what you want and (optionally) show one or two examples:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
querdex extract --doc-id demo \
|
|
232
|
+
--prompt "Extract revenue figures and executive names" \
|
|
233
|
+
--examples examples.json \
|
|
234
|
+
--html review.html
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
`examples.json`:
|
|
238
|
+
```json
|
|
239
|
+
[
|
|
240
|
+
{
|
|
241
|
+
"text": "Alice Chen reported revenue of $5M in Q1.",
|
|
242
|
+
"extractions": [
|
|
243
|
+
{"extraction_class": "metric", "extraction_text": "revenue of $5M", "attributes": {"period": "Q1"}},
|
|
244
|
+
{"extraction_class": "person", "extraction_text": "Alice Chen"}
|
|
245
|
+
]
|
|
246
|
+
}
|
|
247
|
+
]
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
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.
|
|
251
|
+
|
|
252
|
+
`--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.
|
|
253
|
+
|
|
254
|
+
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.
|
|
255
|
+
|
|
256
|
+
Python API:
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
from querdex.extraction import ExtractionTask, ExtractionExample, ExampleExtraction
|
|
260
|
+
|
|
261
|
+
task = ExtractionTask(
|
|
262
|
+
description="Extract revenue figures and executive names",
|
|
263
|
+
examples=[...],
|
|
264
|
+
)
|
|
265
|
+
run = engine.extract_document("demo", task, passes=1)
|
|
266
|
+
for e in run.extractions:
|
|
267
|
+
print(e.extraction_class, repr(e.extraction_text), e.section_id, e.char_start, e.alignment)
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
217
272
|
## CLI Reference
|
|
218
273
|
|
|
219
274
|
```
|
|
@@ -224,6 +279,7 @@ querdex [--db PATH] <command> [options]
|
|
|
224
279
|
|---------|-------------|
|
|
225
280
|
| `index <file>` | Index a document. Auto-detects format from extension. |
|
|
226
281
|
| `query` | Query an indexed document. |
|
|
282
|
+
| `extract` | Run schema-by-example structured extraction over an indexed document. |
|
|
227
283
|
| `delete` | Remove a document and all its data from the store. |
|
|
228
284
|
|
|
229
285
|
### `index`
|
|
@@ -249,6 +305,20 @@ querdex query --doc-id ID --query TEXT [--session-id ID]
|
|
|
249
305
|
| `--query` | required | Natural language question |
|
|
250
306
|
| `--session-id` | none | Enables multi-turn context (pass same ID across turns) |
|
|
251
307
|
|
|
308
|
+
### `extract`
|
|
309
|
+
|
|
310
|
+
```
|
|
311
|
+
querdex extract --doc-id ID --prompt TEXT [--examples FILE] [--passes N] [--html FILE]
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
| Argument | Default | Description |
|
|
315
|
+
|----------|---------|-------------|
|
|
316
|
+
| `--doc-id` | required | Document to extract from |
|
|
317
|
+
| `--prompt` | required | Natural language description of what to extract |
|
|
318
|
+
| `--examples` | none | JSON file with few-shot examples (defines the output schema) |
|
|
319
|
+
| `--passes` | `1` | Extraction passes; more passes improve recall |
|
|
320
|
+
| `--html` | none | Write an interactive HTML review page to this path |
|
|
321
|
+
|
|
252
322
|
### `delete`
|
|
253
323
|
|
|
254
324
|
```
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Querdex
|
|
2
2
|
|
|
3
|
+
[](https://pypi.org/project/querdex/)
|
|
4
|
+
[](https://pypistats.org/packages/querdex)
|
|
5
|
+
[](https://pypi.org/project/querdex/)
|
|
6
|
+
[](https://github.com/its-animay/querdex/actions/workflows/ci.yml)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
|
|
3
9
|
**Reasoning-first document intelligence system.**
|
|
4
10
|
|
|
5
11
|
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 +18,7 @@ Querdex indexes any document into a hierarchical tree, then uses a two-tier LLM
|
|
|
12
18
|
- [Installation](#installation)
|
|
13
19
|
- [Quick Start (CLI)](#quick-start-cli)
|
|
14
20
|
- [LLM Setup](#llm-setup)
|
|
21
|
+
- [Structured Extraction](#structured-extraction)
|
|
15
22
|
- [CLI Reference](#cli-reference)
|
|
16
23
|
- [Python API](#python-api)
|
|
17
24
|
- [Supported File Types](#supported-file-types)
|
|
@@ -182,6 +189,54 @@ export QUERDEX_LLM_TIER2_MODEL=gpt-4o # powerful
|
|
|
182
189
|
|
|
183
190
|
---
|
|
184
191
|
|
|
192
|
+
## Structured Extraction
|
|
193
|
+
|
|
194
|
+
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.
|
|
195
|
+
|
|
196
|
+
The schema is defined **by example**, not by code. Describe what you want and (optionally) show one or two examples:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
querdex extract --doc-id demo \
|
|
200
|
+
--prompt "Extract revenue figures and executive names" \
|
|
201
|
+
--examples examples.json \
|
|
202
|
+
--html review.html
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`examples.json`:
|
|
206
|
+
```json
|
|
207
|
+
[
|
|
208
|
+
{
|
|
209
|
+
"text": "Alice Chen reported revenue of $5M in Q1.",
|
|
210
|
+
"extractions": [
|
|
211
|
+
{"extraction_class": "metric", "extraction_text": "revenue of $5M", "attributes": {"period": "Q1"}},
|
|
212
|
+
{"extraction_class": "person", "extraction_text": "Alice Chen"}
|
|
213
|
+
]
|
|
214
|
+
}
|
|
215
|
+
]
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
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.
|
|
219
|
+
|
|
220
|
+
`--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.
|
|
221
|
+
|
|
222
|
+
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.
|
|
223
|
+
|
|
224
|
+
Python API:
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
from querdex.extraction import ExtractionTask, ExtractionExample, ExampleExtraction
|
|
228
|
+
|
|
229
|
+
task = ExtractionTask(
|
|
230
|
+
description="Extract revenue figures and executive names",
|
|
231
|
+
examples=[...],
|
|
232
|
+
)
|
|
233
|
+
run = engine.extract_document("demo", task, passes=1)
|
|
234
|
+
for e in run.extractions:
|
|
235
|
+
print(e.extraction_class, repr(e.extraction_text), e.section_id, e.char_start, e.alignment)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
185
240
|
## CLI Reference
|
|
186
241
|
|
|
187
242
|
```
|
|
@@ -192,6 +247,7 @@ querdex [--db PATH] <command> [options]
|
|
|
192
247
|
|---------|-------------|
|
|
193
248
|
| `index <file>` | Index a document. Auto-detects format from extension. |
|
|
194
249
|
| `query` | Query an indexed document. |
|
|
250
|
+
| `extract` | Run schema-by-example structured extraction over an indexed document. |
|
|
195
251
|
| `delete` | Remove a document and all its data from the store. |
|
|
196
252
|
|
|
197
253
|
### `index`
|
|
@@ -217,6 +273,20 @@ querdex query --doc-id ID --query TEXT [--session-id ID]
|
|
|
217
273
|
| `--query` | required | Natural language question |
|
|
218
274
|
| `--session-id` | none | Enables multi-turn context (pass same ID across turns) |
|
|
219
275
|
|
|
276
|
+
### `extract`
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
querdex extract --doc-id ID --prompt TEXT [--examples FILE] [--passes N] [--html FILE]
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
| Argument | Default | Description |
|
|
283
|
+
|----------|---------|-------------|
|
|
284
|
+
| `--doc-id` | required | Document to extract from |
|
|
285
|
+
| `--prompt` | required | Natural language description of what to extract |
|
|
286
|
+
| `--examples` | none | JSON file with few-shot examples (defines the output schema) |
|
|
287
|
+
| `--passes` | `1` | Extraction passes; more passes improve recall |
|
|
288
|
+
| `--html` | none | Write an interactive HTML review page to this path |
|
|
289
|
+
|
|
220
290
|
### `delete`
|
|
221
291
|
|
|
222
292
|
```
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "querdex"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
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 = [{ name = "
|
|
8
|
+
authors = [{ name = "Animay Tiwari", email = "animaytiwari@outlook.com" }]
|
|
9
9
|
keywords = ["document", "indexing", "retrieval", "llm", "rag"]
|
|
10
10
|
classifiers = [
|
|
11
11
|
"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
|