CHANGELOG.md
Rama main · réplica de lectura del repositorio original.
# CHANGELOG
All notable changes to py-idp are documented here. Versions follow
[Semantic Versioning](https://semver.org/). The first number is bumped
on breaking API changes; the second on backward-compatible features;
the third on bugfixes.
## [Unreleased] — user-facing errors, retries, and FastAPI handlers
### Added
* **Structured error envelope for every API error.** New `idp.errors` module
provides a typed hierarchy (`IDPError` and subclasses), `error_envelope()`,
`format_error()`, `classify_status()`, `is_transient()`, a `wrap()`
decorator and a `safe_call()` context manager. Each error carries a stable
machine-readable `code`, a `request_id`, a user-facing `hint` and a
`docs_url` anchor. See `USER_ERRORS.md` for the full catalog.
* **Typed exceptions from the LLM backends.** Backend failures now raise
`BackendUnavailableError` / `TimeoutError_` instead of leaking raw
`httpx` exceptions, with the original error preserved in `__cause__`.
* **`BatchItemResult.error_code` and `.transient`**, so batch callers can
route failures without parsing message strings.
* **`tests/test_error_surface.py`** covering the classification helpers, the
envelope builders, the wrappers, the FastAPI exception handlers and two
end-to-end checks of the envelope shape.
### Changed
* **BREAKING (API): error responses no longer use FastAPI's `{"detail": ...}`
shape.** Every error now returns `{"error": {"code", "message", "type",
"request_id", "hint", "docs_url"}}`. Clients that read `body["detail"]`
must switch to `body["error"]["message"]` (or switch on
`body["error"]["code"]`). This includes routing 404s and HTTP exceptions,
which previously returned FastAPI's default body.
### Fixed
* `idp.errors` no longer binds the name `TimeoutError` at module level. The
alias shadowed the builtin for the whole module, so `is_transient()` was
testing against the py-idp error instead of the real `TimeoutError`, and a
plain `TimeoutError("timed out")` was classified as non-transient and
never retried.
* `src/idp/batch.py` called an undefined `log` in six places (the module
logger is `_log`), raising `NameError` on every error path.
## [0.6.2] — 2026-10-23 — PyYAML is now a core dependency
Packaging follow-up. No API changes.
### Fixed
* **PyYAML was optional, but v0.6.1 made it mandatory.** Once the bundled
templates became the default fallback path (v0.6.1), every default install
parses YAML frontmatter on server startup — but PyYAML was never in
`[project.dependencies]`. A clean `pip install py-idp==0.6.1` followed by
`TemplateRegistry.load_with_fallback(...)` raised:
```text
ImportError: idp.templates requires PyYAML. Install it with:
pip install PyYAML (or pip install py-idp[templates]).
```
Found by installing the published v0.6.1 wheel into a clean virtualenv and
exercising the user-facing path. Local tests, wheel-content assertions, and
all five CI Python-version jobs had passed — none of them ran in an
environment without PyYAML already present.
PyYAML is now a core dependency. The import stays lazy so `import idp`
remains fast and never hard-fails.
* **The error message pointed at a nonexistent extra.** It recommended
`pip install py-idp[templates]`, but no `templates = [...]` entry was ever
declared in `pyproject.toml`, so the suggested command could not work.
Declared it as a thin alias onto PyYAML so the message and any existing
install docs keep working.
* **`load_with_fallback` degrades instead of crashing.** If PyYAML is
genuinely unavailable (stripped-down or vendored install), the bundled-parse
attempt now warns and returns an empty registry rather than propagating
`ImportError` and taking down server startup.
### Added
* Four tests in `tests/test_template_packaging.py`: PyYAML is in core
dependencies, the `[templates]` extra is actually declared, the fallback
degrades to empty when the yaml import raises, and the guard does not
break the normal path.
## [0.6.1] — 2026-10-23 — ship the bundled document templates
Packaging fix. No API changes.
### Fixed
* **Sample templates never shipped in the package.** The `.md` templates
lived at the repo root in `templates/`, outside `src/`, so
`[tool.setuptools.packages.find] where = ["src"]` never picked them
up. Verified against the actual artifacts: **0 template files** in
the v0.6.0 wheel and sdist, and the same for every prior release back
to at least v0.3.1. Anyone who ran `pip install py-idp` and pointed
the server at the default `./templates` got an **empty
`TemplateRegistry`**, despite the README advertising sample templates
for `invoice` / `contract` / `bank_statement` (and, since v0.5,
`w2` / `id_card` / `medical_claim`).
The templates now live in `src/idp/templates_pkg/` and are declared as
package data, so they are present in both the wheel and the sdist.
`TemplateRegistry.load_with_fallback()` prefers a user-supplied
directory when it holds `.md` files and otherwise falls back to the
packaged set; `api.py` startup uses it, so the server gets a working
registry out of the box.
`bundled_template_dir()` resolves the packaged copy via
`importlib.resources` and returns `None` on older Python or zip-safe
loaders rather than raising.
### Added
* `tests/test_template_packaging.py` — 10 tests, including two that
inspect the **built wheel and sdist** to assert all six `.md` files are
present. That is the assertion which would have caught the bug.
### Changed
* `IDP_TEMPLATE_DIR` pointing at an empty or missing directory now
serves the bundled defaults instead of nothing. A populated user
directory still wins outright. Call `TemplateRegistry.load()`
directly (rather than `load_with_fallback()`) if you want a strictly
empty registry.
## [0.6.0] — 2026-10-23 — Unlimited-OCR backend
Adds `baidu/Unlimited-OCR` (Baidu, arXiv 2606.23050) as a parallel
self-hosted OCR backend. Differentiation over Nanonets-OCR2-3B: 32K
context, constant KV cache via Reference Sliding Window Attention (R-SWA),
and a native `infer_multi()` API for dozens-of-pages-in-one-forward-pass.
MIT-licensed. Expected 5-10× wall-time improvement on ≥10-page documents,
plus cross-page reasoning (e.g. "see appendix A" actually reads the
appendix).
### Added
* **`UnlimitedOcrBackend`** — `src/idp/llm/unlimited_ocr.py`. Mirrors
`NanonetsVLBackend` shape: gated behind `IDP_ENABLE_UNLIMITED_OCR=1`,
lazy load, multimodal, platform-aware. Uses the model's native
`model.infer()` (single image, gundam mode) and `model.infer_multi()`
(multi-page, base mode) entry points. When `Message.images_b64` carries
more than one image, `_infer_multi` is called; otherwise `_infer_single`.
`<|det|>region_type [bbox]<|/det|>` markup is stripped from the output
so the downstream JSON extraction pipeline sees clean text.
* **Wired into `get_backend("unlimited-ocr")` and `list_backends()`** —
same gated-registration pattern as `nanonets`. Reuses the existing
`[hf-vlm]` extra (no new pip dependencies).
* **`tests/test_unlimited_ocr_backend.py`** — 28 tests covering gating,
lazy load, image extraction, infer vs infer_multi routing, marker
stripping, device resolution, dtype validation, 4-bit quantization,
recursive output-file discovery, and the helpful ImportError when
torch/transformers are missing.
* **`tests/test_version_consistency.py`** — 4 tests pinning
`pyproject.toml`, `src/idp/__init__.py`, and `CITATION.cff` to the same
version string. Prevents the drift that shipped v0.5.0 with
`idp.__version__ == "0.4.0"`.
### Fixed
* **`_resolve_dtype` raised a bare `KeyError`** on an unrecognized
`dtype=` value. Now raises `ValueError` naming the accepted choices,
matching the clear-message pattern used elsewhere in the file.
* **`_read_output_text` used a non-recursive glob**, so a model version
writing per-page output into a subdirectory would silently return
empty text (the pipeline then extracted nothing, with no error). Now
uses `rglob()` with an `is_file()` guard.
* **`_infer_text_only` renamed to `_infer_synthetic_placeholder`.** The
model has no text-only entry point — every documented path requires an
image — so the old name implied behavior that does not exist.
* **Version drift** — `src/idp/__init__.py` sat at `0.4.0` while
`pyproject.toml` was at `0.5.0`, so `pip install py-idp==0.5.0` then
`idp.__version__` reported the wrong number. `CITATION.cff` was staler
still (`0.3.1.post1`). All three now read `0.6.0`.
### Reuses
* `[hf-vlm]` extra (no new pip dependencies; the Unlimited-OCR
`trust_remote_code` model and Nanonets share identical deps: `torch`,
`transformers`, `accelerate`, `safetensors`, `Pillow`,
`huggingface-hub`)
* Existing `idp.llm.Backend` ABC and `is_multimodal` contract
* `Message.images_b64: list[str]` field for image attach
* `_safe_json` downstream parser (works on the marker-stripped text)
### Security
Unlimited-OCR ships **custom code** (DeepSeek-OCR-2 base + R-SWA decoder)
loaded with `trust_remote_code=True`. This is standard practice for
cutting-edge open-weights models but does mean the `modeling_*.py`
files from the repo are executed in your Python process. Review the
upstream code if this is a concern; users who prefer not to run the
custom code can deploy via the SGLang-server path and use a plain
OpenAI-compat client instead (separate backend, out of scope for v0.6).
## [0.5.0] — Unreleased — multi-track enhancements
Four independent feature tracks landed in one PR, each with its own
tests and a backward-compatible contract. Headline: **"more backends,
more doc types, async, eval CI gate."**
### Added
* **5 international LLM provider backends** — `src/idp/llm/international.py`.
`Groq`, `Mistral`, `Together AI`, `Cohere`, and `Fireworks` join the
existing `OpenAI` / `Anthropic` / `Ollama` line-up. Each is registered
via a `INTERNATIONAL_PROVIDER_PRESETS` dict (mirroring the
`CHINA_PROVIDER_PRESETS` pattern) and resolved through
`get_backend("intl:groq" | "intl:mistral" | "intl:together" |
"intl:cohere" | "intl:fireworks")`. All speak the OpenAI
Chat-Completions protocol, so they share `OpenAICompatBackend` under
the hood. Multimodal variants are picked automatically when you
pass `multimodal=True`. Total registered backends: **23** (was 18).
Tested via `tests/test_international_providers.py` (6 tests).
* **`list_backends()` registry helper** — `src/idp/llm/backend.py`.
Returns a list of `{"name", "family", "auth"}` dicts covering every
registered backend. Gated backends (`slowmock`, `nanonets`) are
surfaced only when their `IDP_ENABLE_*` env var is set. Useful for
CLI help screens, config validation, and tests that assert the
registry's coverage. Re-exported from `idp.llm`.
Tested via `tests/test_backend_registry_helpers.py` (10 tests).
* **`Backend.__repr__`** — `src/idp/llm/backend.py`. Every backend now
prints as `<ClassName name='...'>`, making logs and debugger
sessions far less opaque. Subclasses can override for richer
state (model, base_url, key presence).
* **3 new built-in document types** — `src/idp/core/schemas.py`:
- `W2` (US IRS wage and tax statement) with `W2LineItem`
- `IDCard` (driver's license / passport / state ID)
- `MedicalClaim` (CMS-1500 / UB-04) with `MedicalClaimLineItem`
All three ship with corresponding Markdown templates in
`templates/w2.md`, `templates/id_card.md`, `templates/medical_claim.md`
(frontmatter + field descriptions + "Common mistakes" section, same
shape as the existing `invoice.md` / `contract.md` / `bank_statement.md`).
Registered in `SCHEMA_REGISTRY` so `Pipeline(schema="W2")`,
`extract_one("w2.pdf", schema="W2")`, and template auto-discovery
all work out of the box.
Tested via `tests/test_new_schemas.py` (11 tests).
* **Async pipeline layer** — `src/idp/pipeline/async_pipeline.py`.
New `AsyncPipeline` class wraps the existing `Pipeline` so you can
`await apipe.arun(doc)` and `await apipe.arun_many(docs)`. All
existing backends transparently support async via a new
`Backend.acomplete()` method that defaults to
`asyncio.to_thread(self.complete, req)` — so zero risk to
existing code, and backends with a native async client
(`openai.AsyncOpenAI`, `anthropic.AsyncAnthropic`) can override
later for better performance.
* **Async batch helper** — `src/idp/batch.py::process_batch_async`.
Same surface as the existing sync `process_batch` but with a
bounded `concurrency=` semaphore (default 4), an optional
`on_progress(done, total)` callback, and structured error capture
in `BatchItemResult.success` / `.latency_s` fields.
Tested via `tests/test_async_pipeline.py` (14 tests).
* **Per-field accuracy report in eval harness** — `src/idp/eval/runner.py`.
`run_dataset()` now additionally returns `per_field_accuracy`:
a dict of `{field_name: {"match": int, "total": int, "accuracy": float}}`
aggregated across all cases × all strategies. Backward-compatible —
existing `rows` / `detail` / `dataset` / `strategies` keys are
preserved. Tested via `tests/test_eval_regression.py` (12 tests).
* **Regression-threshold gate for CI** — `src/idp/eval/runner.py`.
New `check_regression(results, thresholds)` returns a list of
failing threshold descriptions, plus a `verify_against_thresholds`
convenience wrapper that raises `EvalRegressionError`. Threshold
grammar: `per_strategy.<name>.<metric>` and
`per_field_accuracy.<field>.<metric>`. Use this in CI: empty
list = green, non-empty = fail the build.
* **`format_report(results)`** — `src/idp/eval/runner.py`.
Plain-text pretty-printer for eval results, suitable for
`print()` in CLI output or test assertions. No external table
library.
### Changed
* `BatchItemResult` gained two new optional fields: `success: bool`
and `latency_s: float`, kept consistent with the existing `ok` /
`elapsed_seconds` via a `__post_init__` reconciler. Sync callers
that construct `BatchItemResult(path=..., result=..., ...)` still
work — the new fields default and reconcile transparently.
### Test coverage
* **+53 new tests** (6 + 10 + 11 + 12 + 14) across 5 new test files.
* **All 984 existing tests still pass** (2 skipped — pre-existing
`test_enhance_benchmark.py` requires the `pytest-benchmark` plugin
which is not in the dev environment).
* `python -c "import idp"` still works.
## [0.4.0] — 2026-09-21 — v0.4 plan complete
The v0.4 plan recorded in `docs/ROADMAP_v0.4.md` ships in this release.
Five PRs landed (#37, #38, #39, #40, #41), each independently tested
and reviewable. Headline: **"much easier setup, still need accuracy."**
### Added
* **`TieredBackend` (v0.4 P2)** — `src/idp/llm/tiered.py`. A drop-in
`Backend` subclass that routes every `complete()` call to a cheap
local model first and only escalates to an expensive model when
the cheap response fails JSON validation or has per-field
confidence below `ambiguity_threshold` (default 0.7, per the D2
spec). Raises `TieredBackendExhaustedError` with both raw outputs
attached if both tiers fail. Existing `Pipeline` callers see no
change — assign `Pipeline(backend=TieredBackend(...))` and go.
Tested via `tests/test_tiered.py` (14 tests).
* **`OllamaBackend` (v0.4 P2)** — `src/idp/llm/ollama.py`. Thin
`Backend` subclass over the local Ollama HTTP API at
`http://localhost:11434`. Uses `httpx` (already a core dep),
no `ollama` SDK required. Honors `OLLAMA_MODEL` and `OLLAMA_HOST`
env vars; `is_multimodal` is True for vision-capable tags.
`get_backend("ollama")` now resolves to this class instead of
`OpenAICompatBackend` (Ollama speaks its own `/api/chat` shape).
Tested via `tests/test_ollama_backend.py` (15 tests, all using a
mocked `httpx.MockTransport`).
* **`examples/06_tiered_pipeline.py`** — the headline v0.4 cost win
example: `TieredBackend(OllamaBackend(), AnthropicBackend())` in
one `Pipeline` setup.
* **`tests/test_tiered.py`** (14 tests) — covers routing-to-cheap,
escalation on low-confidence / parse-failure, threshold-boundary
behaviour, both-tiers-exhausted error contract, pipeline
configuration and defensive constructor validation.
* **`tests/test_ollama_backend.py`** (15 tests) — covers the wire
shape, response parsing, error surfaces, env-var resolution, and
multimodal tag detection — all against a mocked `httpx.MockTransport`.
* **Example notebooks (v0.4 P1)** — `examples/notebooks/`. Three
executed Jupyter notebooks walk through py-idp end-to-end with
the in-tree `MockBackend` (no API key required):
- `01_pipeline_minimal.ipynb` (5-min tour, 11 cells)
- `02_hitl_loop.ipynb` (HITL feedback loop, 11 cells)
- `03_batch.ipynb` (30-doc batch + DataFrame + checkpoint
resume, 19 cells — also covers the v0.4 direction D3
`BatchItemResult` workflow)
Each notebook ships with real output cells so GitHub renders
numbers, not blank blocks. `pyproject.toml` adds a `[notebooks]`
extra (`nbformat` + `nbconvert` + `ipykernel` + `pandas`) for users
who want to re-execute them. Ruff config excludes `*.ipynb` (cells
have import quirks that don't fit module-level lint).
* **`idp hitl triage` (v0.4 P3 + D1)** — `src/idp/hitl/triage.py`.
A pure module (no Streamlit) exposing `triage(storage, *,
min_reviews=3, threshold=0.6) -> TriageReport`. Walks reviewed
`StoredResult`s, partitions fields into `.systematic_errors`
(correction rate ≥ threshold AND n ≥ min_reviews) and
`.insufficient` (the honest "needs N more reviews" bucket).
CLI: `idp triage --storage <path> [--min-reviews 3] [--threshold
0.6] [--output json|md]`. Streamlit "Triage" page reachable from
the sidebar in the existing `idp serve` UI. Tested via
`tests/test_triage.py` (21 tests).
* **HITL UX polish (v0.4 P3 + C3)** — `src/idp/hitl/app.py`. Additive:
the review-queue view gains a side-by-side field view (model
output | human-edited with pre-populated widget), a bulk-accept
button at ≥ 0.9 confidence (calls `save_review` with unchanged
extraction on high-confidence fields), a skip button (non-persistent
close), a per-document confidence histogram (`st.bar_chart`),
and a keyboard-shortcut help expander. The Triage page is
reachable from the sidebar. All existing review behaviour
unchanged. Tested via 6 new AppTest smoke tests.
* **CORD baseline eval (v0.4 P4 + C2)** — `src/idp/eval/datasets/cord/`.
30 synthetic-but-faithful receipts (matching the CORD schema and
license — CC-BY-2020, the `cord2020` paper), `manifest.json` with
ground-truth extraction per doc, verbatim `LICENSE.txt` and
`ATTRIBUTION.md`. New CLI helper `tests/eval_cord/update_baseline_doc.py`
regenerates `docs/eval/BASELINE.md` (the per-field precision/recall/F1
table). Slow-marked `tests/eval_cord/test_eval_real_dataset.py`
runs against a real backend when `IDP_USER_ID=ollama|anthropic|openai`
is set. MockBackend baseline is intentionally near zero (no real
model calls happen) — the doc gives users a starting point to
re-run against their own backend.
* **Template-version migration (v0.4 P5 + C4)** — `src/idp/migrate.py`.
Exposes `migrate_template_version(storage, template_name, *,
from_version, to_version, pipeline_factory, output_path=None,
dry_run=True) -> MigrationReport`. Reuses `process_batch` plumbing.
Default is dry-run (does not mutate the source schema); `--commit`
flag persists new extractions as fresh v2 `StoredResult`s while v1
records stay intact. Per-field diff (`added` / `removed` /
`changed` / `unchanged`). CLI: `idp migrate-template --storage
<back> --template-name <name> --from-version <v1> --to-version
<v2> [--dry-run] [--commit] [--output FILE]`. Tested via
`tests/test_migrate_template.py` (10 unit tests + 1 slow e2e test
against CORD fixtures).
* **`pyproject.toml` `[notebooks]` extra** — `nbformat>=5.10`,
`nbconvert>=7.0`, `ipykernel>=6.0`, `pandas>=2.1`. Install with
`pip install py-idp[notebooks]` to re-execute the example
notebooks.
* **`pyproject.toml` `slow` pytest marker** — registered for tests
that require a real backend (CORD baseline, CORD migration e2e).
Skipped in default CI; run with `pytest -m slow`.
* **Breaking: `get_backend("ollama")` now returns `OllamaBackend`** —
previously returned `OpenAICompatBackend` (Ollama was treated as
OpenAI-compatible). Ollama's native `/api/chat` shape is materially
different, so v0.4 routes it through the dedicated class. Users
who relied on the OpenAI-compat behaviour should switch to
`get_backend("compat")` with `OLLAMA_OPENAI_COMPAT=true` (or
whatever env var the OpenAI-compatible server expects). Safe
because v0.3.x was never published to PyPI.
### Not in v0.4 (explicit non-goals, deferred to v0.5)
* Ollama streaming response handling (`stream=true`).
* Per-field confidence escalation (only the whole-response
acceptance test is in v0.4).
* Rule-based / regex pre-pass as a third tier ahead of the cheap
LLM.
* Tiered caching (re-run the cheap tier vs serve from cache) — for
v0.4 compose TieredBackend with the existing
`idp.reliability.CachingBackend` instead.
* Per-template triage report (today: one triage report across all
schemas).
* LLM-based confidence overrides (triage uses stored `confidence`
as-is; no inference).
* Automatic field-correction (no `save_review` writes happen from
the triage tool).
* Statistical significance testing on triage (threshold-based, not
p-value).
* Full-feedback-loop-aware migration (v0.4 is raw-extraction only).
* Per-template baselines (each schema gets its own baseline
numbers).
* Automated regression gating on the CORD baseline (drift
threshold TBD).
* The original ~1000 real CORD receipts (manifest format is
forward-compatible; a v0.5 PR can swap them in).
* Per-template CORD baseline coverage (multi-class document types
— FUNSD forms, CUAD contracts — all deferred).
## [0.3.8] — 2026-09-17 — coverage push to 92%, PIL 10+ bug fix
### Fixed
* **`NanonetsVLBackend._preprocess` crashed on PIL 10+** — used
`image.LANCZOS` which was removed from PIL Image instances in v10.
Result: `_extract_images` returned `[]` for any image with a long
edge > `max_image_side` (silently dropped, no error surfaced).
Fixed by trying `Image.Resampling.LANCZOS` first, then `Image.LANCZOS`
(older PIL), falling back to `Image.BICUBIC`. Found via the new
`_preprocess_resize_when_too_large` test.
### Added
* **`hitl/app.py` smoke coverage: 48% → 87%** — 12 tests in
`tests/test_hitl_app_smoke.py` driving Streamlit's `AppTest`
harness through sidebar metrics, queue/history/About pages, and
the page navigation paths.
* **`llm/nanonets.py` mock coverage: 61% → 64%** — 21 tests pinning
`_build_prompt`, `_extract_images`, `_clean_json`, `_preprocess`,
`_require_deps`, and the `complete()` text-only vs multimodal
path dispatch.
* **Property-based tests** for `_perturb_json`, `_omit_fields`,
`_empty_schema` — 13 new tests using Hypothesis.
* **CI coverage gate** — `.github/workflows/tests.yml` now runs
`pytest --cov-fail-under=90`, so dropping coverage below 90%
fails CI.
* **CHANGELOG link in README** — added link in all 3 READMEs
(English + 繁中 + 简中) so users can find release notes.
### Tests
* 792 → 836 (+44 new). Total coverage 90% → 92%.
## [0.3.7] — 2026-09-17 — coverage push to 90%, HITL app smoke tests
### Fixed
* **`_stub()` real bug** — the previous audit's defensive guard against
malformed schemas (e.g. `{"type": "object", "properties": 0}`) was
bypassed by the `t == "object"` clause. Result: assertion failure
in normal mode, `AttributeError` under `python -O` (asserts stripped).
Combined the predicates so the object branch only runs when
`properties` is actually a dict. **Verified under `-O`.**
### Added
* **`idp.console` (public path for `pretty_print_result`)** — moved
out of `idp._util` (private-by-convention) into a new public module
with a real docstring and type hints. Old `idp._util` import keeps
working (re-export shim). `idp.pretty_print_result` exposed at top
level.
* **`tests/test_hitl_app_smoke.py`** — 6 tests using Streamlit's
`AppTest` harness to smoke-test the HITL UI module
(`hitl/app.py` 0% → 48%). The data logic is already 100% via
`tests/test_hitl_review.py`.
### Changed
* Updated 6 example scripts to import `pretty_print_result` from
`idp.console` (the new public path).
* README + CHANGELOG + pyproject version bumps 0.3.6 → 0.3.7.
### Tests
* 738 → 792 (+54 new). Total coverage 88% → 90%.
Per-file coverage gains:
| file | before | after | delta |
|------|--------|-------|-------|
| eval/metrics.py | 76% | 90% | +14 |
| classify/classifier.py | 84% | 100% | +16 |
| rl/online.py | 85% | 91% | +6 |
| api.py | 92% | 93% | +1 |
| storage/sql.py | 87% | 88% | +1 |
| pipeline/pipeline.py | 92% | 99% | +7 |
| hitl/app.py | 0% | 48% | +48 |
| cli_sources.py | 93% | 98% | +5 (PR #24) |
## [0.3.6] — 2026-09-16 — `idp batch` CLI, batch module home
### Added
* **`idp batch` CLI** — new command for running the pipeline over
many documents. Replaces the previous pattern of looping
`idp run` or calling `process_batch` directly. Example:
```bash
idp batch /mnt/inbox/ --backend ollama --schema Invoice \
--output out.jsonl --report report.json --dlq dlq.jsonl
```
Sources can be paths, directories (recursive scan for *.pdf /
*.png / *.jpg / *.tiff / *.txt), or `@<file>` (path list).
Produces per-doc JSONL, an aggregate summary (throughput,
p50/p95 latency, error histogram), and a dead-letter queue for
failed docs.
* **`idp.process_batch` and `idp.BatchItemResult` exposed at top
level** — `from idp import process_batch` works directly. The
helper was already there; now it's discoverable at the top
of the package.
* **`idp.cli_sources` module** — small path-collection helper
(`collect_paths`) used by the `idp batch` CLI, exposed for
scripts that want the same path-discovery behavior without
going through Typer.
### Changed
* **`idp.llm.nanonets_batch` → `idp.batch`** — the existing
`process_batch` helper moved to its proper home. The old path
is kept as a 12-line re-export shim
(`from idp.llm.nanonets_batch import process_batch` still
works). The old name was misleading — the helper works with
any backend, not just Nanonets.
* **CI ruff strictness** — `.github/workflows/tests.yml` and the
local ruff now agree on F841 (unused local) and I001
(import-order). The batch CLI's file-handle code was
restructured to satisfy F841 without a `del` workaround.
### Tests
* 654 → 665 (+11): `tests/test_cli_batch.py` covers
`collect_paths` (file / dir / @file / dedup / errors) and the
full CLI surface (`--help`, `--dry-run`, output files,
missing-source exit codes).
## [0.3.5] — 2026-09-15 — Tier-A roadmap fixes, Mermaid CI guard
### Fixed
* **`datetime.utcnow()` deprecation** — `src/idp/storage/sql.py`
migrated from `datetime.utcnow()` (deprecated in Python 3.12+)
to `datetime.now(timezone.utc)`. The naive ISO 8601 wire format
is preserved. Adds a regression test (`tests/test_storage_no_deprecation_warnings.py`)
that pins the fix and would fail if anyone reverts it. **0
`datetime.utcnow` deprecations from our code in CI.**
* **`count_corrections` reported all fields as "corrected" for
unreviewed results** — `src/idp/hitl/review.py` (extracted from
`src/idp/hitl/app.py` as part of A3) now returns 0 when
`reviewed_extraction is None`. The previous behavior was
`n_corrections == len(extraction)`, which is nonsense for an
un-reviewed result. Caught by `test_count_corrections_zero_when_no_review`.
* **HITL data-handling logic now unit-testable** — `idp.hitl.app`
was 0% covered because its logic was intertwined with the
Streamlit UI. Extracted into `idp.hitl/review.py` (pure, no
Streamlit import). The pure module is at **100% coverage** with
18 tests. The Streamlit app now imports the helpers; this is
the right factoring for a future FastAPI replacement UI.
### Added
* **SQLite + Postgres dialect parity tests** — `tests/test_sql_dialect.py`
(23 tests) covers `_parse_url` for both schemes, `_connect`
mocked for both, `_strip_postgres_only` for the syntax it actually
handles (DO `$` blocks, CREATE TYPE, review_status ENUM), and
schema bootstrap. Documents the current scope of
`_strip_postgres_only` (it does NOT strip RETURNING / ON CONFLICT
— those would require a real Postgres to verify safely).
* **CLI test coverage 83% → 97%** — `tests/test_cli.py` now exercises
the actual `discover-schema` flow (not just `--help`), the
`--storage` URL vs path heuristic in `rl-update`, and the
`--synthetic without --fixtures` error in `rl-eval`. 6 new tests.
* **`@mermaid-js/mermaid-cli` validation in CI** — new workflow
`.github/workflows/validate-mermaid.yml` runs `mmdc` on every
Mermaid block in the 3 READMEs and fails the build on parse
errors. This prevents the exact class of bug we shipped in
PR #15 (unquoted braces in node labels were mis-parsed as
diamond-shape openers, breaking the entire diagram). Renders
are also uploaded as artifacts for visual review.
### Tests
* 654 tests pass (was 613), +41 new
* mypy clean across **61 source files** (was 60)
* ruff clean
* 0 `datetime.utcnow` deprecations from our code (verified with
`pytest -W error::DeprecationWarning`)
## [0.3.4] — 2026-09-14 — Tier-1 test coverage, branch protection, CI bots
### Added
* **CLI test coverage** — `tests/test_cli.py` (17 tests) covers every
command in `idp.pipeline.cli`: `run`, `schemas`, `providers`,
`discover-schema`, `eval`, `rl-update`, `rl-eval`, `serve`. Brings
the CLI from 0% to 83% line coverage. Tests use Typer's CliRunner
so we exercise the actual CLI surface without subprocess overhead.
* **`InProcessQueue` test coverage** — `tests/test_queue.py` (12 tests)
covers job submit/status/list, worker run/failure/recovery, and
start/stop idempotency. Brings `queue/jobs.py` from 0% to 97%.
* **Eval harness test coverage** — `tests/test_eval_runner.py` (11
tests) covers dataset loading, schema validation, multiple
strategies, missing-doc skipping, and end-to-end runs against the
bundled invoices dataset. Brings `eval/runner.py` from 0% to 96%.
* **Migration audit test coverage** — `tests/test_migrate_audit.py`
(6 tests) covers empty DB, all-compatible rows, the headline
case (v0.1-OK but v0.2-fails), mixed DBs, output-file writing, and
schema-name skipping. Brings `migrate_audit.py` from 0% to 76%.
### Bots and branch protection
* **Branch protection on `main`** — required status checks enforced,
linear history required, force-pushes disabled, branch deletion
disabled, conversation resolution required. Enforced for admins too,
so even direct pushes by the owner need a PR review.
* **CodeQL workflow** (`.github/workflows/codeql.yml`) — weekly
security scan + per-PR scan using GitHub's default security-and-
quality query pack. Runs only on Python (no JS/TS to analyze).
* **CI matrix expanded** — `tests.yml` now runs on Python 3.10 / 3.11
/ 3.12 / 3.13 / 3.14 (was 3.10–3.12). Matches `pyproject.toml`'s
`python = ">=3.10"` declaration and includes the latest stable.
* **CI build-artifact guard** — `tests.yml` runs a shell check at the
start of every CI job that fails the build if `site/` or other
build dirs are tracked. Mirrors the local `.githooks/pre-commit`
hook so PRs from forks (where the local hook isn't installed) get
caught too.
* **Concurrency** — `.github/workflows/tests.yml` cancels in-progress
runs for the same branch when a new commit is pushed. Saves CI
minutes.
### Tests
* 46 new tests added across 4 new files. Coverage of the 4
user-facing modules that previously had 0% is now between 76% and
97%.
## [0.3.3] — 2026-09-14 — Templates wire to LLM, structured error envelope, README translations
### Added
* **Template body now reaches the LLM** — `Pipeline(template=...)`
threads the template's Markdown body into every extraction call's
user message. Field descriptions, worked examples, and
common-mistakes notes from your `templates/*.md` files are prepended
to the prompt before the JSON Schema. The body is capped at ~2000
tokens to protect the 16k-context Nanonets-OCR2 budget, and is
included in **every** chunk call (not just the first), so multi-chunk
documents keep full template context throughout.
* **`Pipeline(template="name")` + `set_template_registry(registry)`**
— resolve the template by name at run() time. Hot-reload works
transparently when the registry was loaded with `watch=True`. Missing
template name → logs a warning and proceeds without one.
* **Template provenance on `Document` and `PipelineResult`** — the
matched template's name and version are recorded on
`Document.template_name` / `Document.template_version` and surfaced
in `PipelineResult` + `to_dict()` for audit. Answers "which template
version produced this row?" without re-running.
* **Structured error envelope** — every `IDPError` subtype now carries
a stable machine-readable `code` (`IDP-RATE-001`, `IDP-PARSE-001`,
`IDP-SCHEMA-001`, `IDP-BACKEND-001`, `IDP-STORE-001`,
`IDP-CONF-001`, `IDP-TMPL-001`, `IDP-TMPL-404`) and an
`http_status`. The API's per-subtype exception handlers emit a
uniform JSON shape: `{"error": {"code", "message", "type",
"request_id", "details?"}}`. `RateLimitedError` adds a
`Retry-After: 60` header.
* **`X-Request-ID` middleware** — every request gets a UUID (or
echoes the caller's header). Surfaces in the response header AND
every error envelope so client/server logs can be correlated.
* **API endpoints for templates**:
* `GET /templates` — list summary (name, schema, version, patterns)
* `GET /templates/{name}` — full template including body
* `POST /extract` is now template-aware: when `schema_name` is
omitted, the server picks a template by filename + MIME and uses
its body as LLM context. The matched template is returned as
`template_used` in the response.
* **Configuration** — `IDP_TEMPLATE_DIR` (default `./templates`),
`IDP_TEMPLATE_RELOAD` (default `false`; dev mode only).
* **README translations** — Traditional Chinese (`README.zh-TW.md`)
and Simplified Chinese (`README.zh-CN.md`) at full feature parity
with the English version. Language switcher at the top of each
README.
### Tests
* 12 new tests in `tests/test_template_to_llm.py` cover: template
body reaching the LLM, template body in every chunk, hot-reload
between run() calls, provenance on `Document` and `PipelineResult`,
Pipeline API (`template=Template`, `template="name"` with
registry), token cap on huge bodies, missing-template fallback.
## [0.3.2] — 2026-09-10 — Reliability, performance, observability, schema discovery
### Added
* **`Pipeline(retry=...)`** — `RetryingBackend` wraps any Backend with
exponential-backoff retries on transient errors. Defaults: 4 attempts,
1s → 2s → 4s → 8s with ±20% jitter, capped at 30s. Auth and bad-request
errors fail fast. See `idp.reliability.RetryConfig`.
* **`Pipeline(cache=...)`** — `ExtractionCache` is a disk-backed SQLite
cache keyed by sha256 of `(schema_name, backend_name, request)`. Same
input → no LLM call. Default location `~/.cache/idp/extract.db`.
`cache.stats()` returns entries, total_hits, per-schema breakdown.
See `idp.reliability.ExtractionCache`.
* **`Pipeline(cache=True, retry=True)` composes** — cache is applied
AFTER retry so cached hits skip the retry loop entirely.
* **`process_batch(checkpoint=...)`** — `CheckpointStore` is an
append-only JSONL ledger of processed paths. Re-running with the same
checkpoint path skips already-done paths (idempotent). The
`record()` method holds an `flock` across write+flush+fsync for
crash-safe single-line appends. `archive_at_start=True` rotates the
ledger between runs (timestamped backup file). See `idp.checkpoint`.
* **`discover_schema` — hint grounding** — `DiscoveryResult.hint_grounding`
is a new dict with `hint_tokens`, `schema_fields`, `grounded`
(list of `(hint_token, schema_field, "exact"|"fuzzy")` triples),
`ungrounded`, and `grounding_score` (0.0 = none of your hint tokens
appear, 1.0 = perfect match). When the score is below 0.5, a
warning is logged listing the ungrounded hint tokens. Doesn't fix
wrong field names — makes the wrongness observable so you know to
verify.
* **`@lru_cache`-cached schema serialization** for `_build_messages()`
in `extract.py`. The JSON Schema dump for a Pydantic class is
cached per-class; only fields the LLM needs to produce output
(type/description/items/enum/format/required) are sent, dropping
`$defs`, `additionalProperties`, and other verbose fields. Measured
~75-80% reduction on built-in schemas (Invoice 785 → 169 tokens).
For a 20-field custom schema, ~21K tokens of schema overhead per
13-chunk document is paid ONCE instead of N times.
* **Token-aware text truncation** (`_truncate_to_tokens`) replaces
`text[:N_CHARS]` with `tiktoken`-based exact-token truncation.
Avoids silent budget overshoot on dense text (numbers, currency,
base64) where chars ~= tokens.
* **RAM optimizations**: lazy streamlit import in `idp.hitl.app`
(saves ~20 MB of protobuf etc. when importing the package without
running the UI), `JsonFileStorage` in-memory cache (saves ~40 MB of
re-parsing on long batch reads), and intermediate-string cleanup
in `extract()`.
### Changed
* `MockBackend` reports `is_multimodal=True` so it works in any
multimodal pipeline without code changes.
* CLI defaults to Nanonets backend when `IDP_ENABLE_NANONETS=1`;
explicit CLI error if not set.
### Fixed
* `_stub()` in `llm.backend` previously crashed on malformed JSON
Schema inputs like `{'properties': 0}` (int instead of dict). Now
defensive: falls back to returning the raw schema when no
recognizable structure is found. Caught by Hypothesis property
tests.
* `MockBackend` can now be passed directly to `Pipeline(schema=...)`
via `Pipeline(backend="mock")` — was returning `None` for `is_multimodal`.
## [0.3.1] — 2026-09-10 — Discoverability & pre-existing-bug pass
### Added
* `CITATION.cff` — GitHub now shows a "Cite this repository" button and
surfaces py-idp in academic dependency graphs.
* `docs/SECURITY.md` — supported-versions table, private reporting
channel, scope, and a 72 h / 14 d SLA for critical issues. Surfaces
the GitHub Security tab and enables private vulnerability reports.
* `.github/ISSUE_TEMPLATE/{bug_report,feature_request,question}.yml`
— structured intake; first-time contributors get the right fields
on the first try.
### Changed
* README badge row now links to live targets: CI to the Actions page,
PyPI/downloads to the package stats, stars to the stargazers page,
plus a `Cite this repository` badge for `CITATION.cff`.
* New README sections: **Security**, **Citing**, **Contributing** —
each links to the corresponding repo file.
* pyproject keywords expanded to cover `rag`, `multimodal`,
`information-extraction`, `intelligent-document-processing` so
`pip search` and GitHub topic graph pick the project up.
### Fixed
* `tiktoken` is now a real `[project] dependency` (was promised by
the README but never declared; fresh installs and CI failed with
`ModuleNotFoundError` on `idp.chunker.TokenChunker`).
* New `[pdf-render]` extra (`pdf2image`, `Pillow`) for the multimodal
path (`PdfPagesParser`, `NanonetsVLBackend`); declared in
`pyproject.toml`, installed by CI.
* `.github/workflows/tests.yml` install line now uses
`.[dev,eval,api,pdf-render]` so `test_security_adversarial.py`
collects cleanly.
* Six test files no longer hard-code `/Users/hermes/py-idp/...` as
the repo path; they derive `REPO` from `Path(__file__)`. Tests now
pass on any machine, not just the author's.
* `PolicyCache.flush_now()` race with the background flusher: a
caller that flushes synchronously and then reads the policy file
can no longer observe a stray `.tmp` because the flusher re-checks
`_dirty.is_set()` inside the lock and skips no-op writes.
## [0.3.0] — 2026-09-02 — Self-hosted VLM backend
### Added
* **`NanonetsVLBackend`** in `idp.llm.nanonets` — self-hosted,
offline OCR + extraction via `nanonets/Nanonets-OCR2-3B` (Qwen2.5-VL
fine-tune on Hugging Face). No API key, no cloud egress. ~7 GB
weights cached at `~/.cache/huggingface/hub/`. Runs on Apple
Silicon (MPS) or CUDA. Lazy model load on first `.complete()` call.
Configurable `device`, `dtype`, `max_image_side`, `load_in_4bit`.
* **`[hf-vlm]` extra** in `pyproject.toml`:
`torch`, `transformers>=4.45`, `accelerate`, `safetensors`,
`Pillow`, `huggingface-hub`.
* **Gated by `IDP_ENABLE_NANONETS=1`** to avoid surprise ~7 GB
downloads. `get_backend("nanonets")` raises a clear error if the
gate is off or `[hf-vlm]` is not installed.
## [0.2.0] — 2026-09-01 — Production hardening
### Added
* **`idp.errors`**: a typed exception hierarchy (`IDPError` base +
`ConfigurationError`, `RateLimitedError`, `BackendUnavailableError`,
`StorageError`, `DocumentParseError`, `SchemaValidationError`).
Catch `IDPError` for framework-level handling without swallowing
unrelated exceptions.
* **`idp.metrics`**: lightweight in-process metrics with thread-safe
counters, gauges, histograms, and Prometheus text-format export
at `GET /metrics`.
* **`idp.config`**: validated `Settings` loaded from env. All
variables are `IDP_`-prefixed. Bad values raise `ConfigurationError`
at startup so misconfigured deployments fail fast instead of at the
first request.
* **`idp.ratelimit`**: per-key + global sliding-window rate limiter
(1-minute windows). Raises `RateLimitedError` (→ HTTP 429) when
exceeded.
* **`idp.api`**: production-hardened FastAPI app replacing the
`examples/api.py` demo:
- `/healthz` (liveness), `/readyz` (readiness), `/version`,
`/metrics`, `/extract`
- Auth via `X-API-Key` header **or** `Authorization: Bearer …`
- `IDP_MAX_UPLOAD_BYTES` enforced via both `Content-Length` header
and actual stream read (defends against missing/lying headers)
- Structured request logging with metrics
- CORS support via `IDP_CORS_ORIGINS` (comma-separated)
* **`idp._logging`**: `LOG_LEVEL` + `LOG_FORMAT=human|json`
configuration helpers.
### Changed
* **Invoice** schema: `invoice_number`, `vendor_name`, `total_amount`
are now **required**. Partial extraction still works for everything
else.
* **Contract** schema: `title` is now **required**.
* **BankStatement** schema: `account_holder` is now **required**.
* **Document.from_path()** now raises `IsADirectoryError` for
directories and `OSError` for non-regular files (FIFOs, sockets,
devices) instead of silently returning an empty Document.
* Bumped to v0.2.0.
### Fixed
* **`_stub` crashed on malformed `enum` values** (dict/set/bool/empty
list). Caught by Hypothesis property test. Now falls back to `None`
on any non-list enum.
## [0.1.0] — 2026-08
Initial release. Six-stage pipeline (parse → classify → extract →
assess → validate → HITL), mock + OpenAI-compatible + Anthropic
backends, in-process queue, JSON + SQLite + in-memory storage,
human-in-the-loop Streamlit UI, reinforcement-learning policy from
review feedback.