AI automation · extraction with a review gate

ai-product-extractor

Messy listings become structured rows. The interesting part is not the extraction — it is the row that extracts cleanly and is still wrong, and what the pipeline does about it.

Public · synthetic demo · offline
The problem

"It extracted a price" is not "it extracted the right price."

A rule-based parser reads a listing, finds a currency symbol followed by digits, and returns a number. It looks like a success. On a listing that says "was $119 now $79.95" it returns 119 — confidently, silently, and wrong by $39.05. Nothing in the output says anything is amiss. That single row is the whole reason this project exists.

Input

Ten deliberately messy listings

L001  Anker PowerCore 10000 Portable Charger — $25.99. 10000mAh. In stock.
L004  Instant Pot Duo 7-in-1 pressure cooker, 6 Quart. was $119 now $79.95!
L006  handmade oak cutting board, ~30x20cm, no price listed, contact for availability
L007  샤오미 미밴드 39,900원 재고있음
L009  USB-C cable 2m — $8

Mixed currencies (USD/KRW), HTML noise, sale prices, missing prices,
Korean stock wording. Ten rows, one target schema.
The money shot

Actual output of pipeline.py on the committed listings

Rows in10
Sent to a human2
Auto-passed8
Errors in auto-passed rows0
RowWhat the gate sawDecision
L004 — "was $119 now $79.95"two distinct prices in the source; a rule-based pick is a coin flipprice_ambiguous → review
L006 — "no price listed"no price to extract at allprice_missing → review
the other 8 rowsone unambiguous price, currency and availability resolvedauto-pass

Checked against the committed ground truth, the 8 auto-passed rows contain zero errors in price, currency, or availability. The one row a naive pipeline would have shipped wrong is exactly the one that got flagged. Nothing else was flagged — the gate does not cry on rows it can actually judge.

Naive vs gated

Same extractor, same data, different honesty

No gateWith the gate
Rows shipped as final108
Wrong values shipped silently1  (L004: $119 instead of $79.95)0
Rows a human must look atunknown — nothing says which2, named, with the reason
False alarms0
Field coverage

What this backend fills, and what it openly does not

FieldFilledCoverage
name10 / 10100%
availability10 / 10100%
price9 / 1090%
currency9 / 1090%
brand0 / 100% — the offline backend does not do this
category0 / 100% — the offline backend does not do this

This is the distinction the project is built on. A wrong value is a per-row alarm and goes to a human. A field the backend structurally cannot fill is a capability gap — it is printed as 0% in every run rather than flagged on all ten rows, because a checker that fires on everything is as useless as one that never fires. Brand and category are what the LLM backend is for; the offline path is the deterministic baseline that runs anywhere, with no key.

How it's verified

Runs offline, twice, with the same result.

Every listing is validated against a Pydantic schema before it reaches SQLite, and rows upsert on a stable business key. Running the pipeline twice leaves the products table at 10 rows with 2 still marked needs_review — no duplication, no drift. The unit suite covers currency parsing, HTML stripping, the missing-price path, the was/now ambiguity path, and — deliberately — that an unambiguous row is not flagged: 6 tests, all passing. No API key is required for any of it.

Honest limitations
  • Ten synthetic listings are a demonstration of the method, not a benchmark. Real accuracy depends entirely on the client's source text.
  • The offline backend extracts no brand and no category. Seven of the eight auto-passed rows do have a brand stated in the source that this path leaves empty — the coverage report says so on every run rather than letting it pass unnoticed.
  • The gate catches ambiguity and absence, not plausibility. A single, clean, wrong number in the source will still pass.
  • The LLM backend is implemented and documented but is not measured here, because running it requires an API key. No numbers are claimed for it.