From 4b7a1dedfd4bd2f45d4012cb5576293007bb390a Mon Sep 17 00:00:00 2001 From: Murat Date: Tue, 3 Nov 2026 12:31:34 +0100 Subject: [PATCH] docs: add runnable LLM grounding example and animated demo Closes #31 Co-Authored-By: Claude Opus 5.5 --- README.md | 6 ++++-- docs/demo.svg | 25 ++++++++++++++++++++++ examples/llm/llm-response.json | 15 +++++++++++++ examples/llm/run.mjs | 39 ++++++++++++++++++++++++++++++++++ examples/llm/source.txt | 3 +++ 5 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 docs/demo.svg create mode 100644 examples/llm/llm-response.json create mode 100644 examples/llm/run.mjs create mode 100644 examples/llm/source.txt diff --git a/README.md b/README.md index f3a91ce..5724631 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ Turn local HTML — or any LLM's extraction output — into schema-valid JSON wh `local-only` · `deterministic replay` · `no browser` · `no external service` +grounded-json rejecting a hallucinated LLM field + Most extractors stop at JSON. That is where review starts. @@ -62,10 +64,10 @@ const receipt = ground({ }); receipt.coverage; // { grounded: 2, total: 3, invalid: 1, ratio: 0.67 } -receipt.issues; // [{ code: 'quote-not-found', pointer: '/color', ... }] +receipt.issues; // [{ code: 'quote-not-found', pointer: '/color', ... }] ``` -Ask your model to return a quote per field, then let the gate decide. With `strict: true`, the hallucinated value never reaches your database. +Ask your model to return a quote per field, then let the gate decide. A runnable version with a recorded LLM response lives in [`examples/llm`](./examples/llm) (`node examples/llm/run.mjs`). With `strict: true`, the hallucinated value never reaches your database. ## Install diff --git a/docs/demo.svg b/docs/demo.svg new file mode 100644 index 0000000..4948b75 --- /dev/null +++ b/docs/demo.svg @@ -0,0 +1,25 @@ + + + + + + grounded-json · LLM output vs. source + + $ node examples/llm/run.mjs + # LLM extracted 5 fields from a job posting + ✓ grounded /title "Senior Backend Engineer" + ✓ grounded /city "Karlsruhe" + ✓ grounded /salaryMin 68000 ← "68,000" · parse-number + ✓ grounded /germanLevel "B2" + ✗ REJECTED /remote "fully remote" quote-not-found + coverage 4/5 grounded · ratio 0.8 + The source says "hybrid". The hallucination never ships. + diff --git a/examples/llm/llm-response.json b/examples/llm/llm-response.json new file mode 100644 index 0000000..282ef7d --- /dev/null +++ b/examples/llm/llm-response.json @@ -0,0 +1,15 @@ +{ + "_note": "Recorded structured output from an LLM asked to return {value, quote} per field. 'remote' is hallucinated.", + "title": { + "value": "Senior Backend Engineer", + "quote": "Senior Backend Engineer" + }, + "city": { "value": "Karlsruhe", "quote": "Karlsruhe" }, + "salaryMin": { + "value": 68000, + "quote": "68,000", + "transforms": ["parse-number"] + }, + "remote": { "value": "fully remote", "quote": "fully remote" }, + "germanLevel": { "value": "B2", "quote": "B2" } +} diff --git a/examples/llm/run.mjs b/examples/llm/run.mjs new file mode 100644 index 0000000..3047cdd --- /dev/null +++ b/examples/llm/run.mjs @@ -0,0 +1,39 @@ +// Ground a recorded LLM extraction against its source. +// Run from the repo root after `npm run build:package`: +// node examples/llm/run.mjs +import { readFileSync } from 'node:fs'; +import { ground } from 'grounded-json'; +import { z } from 'zod'; + +const dir = new URL('./', import.meta.url); +const source = readFileSync(new URL('source.txt', dir), 'utf8'); +const { _note, ...fields } = JSON.parse( + readFileSync(new URL('llm-response.json', dir), 'utf8'), +); + +// Prompt pattern: "For every field return { value, quote } where quote is copied verbatim from the source." +const data = Object.fromEntries( + Object.entries(fields).map(([k, f]) => [k, f.value]), +); +const evidence = Object.entries(fields).map(([k, f]) => ({ + pointer: `/${k}`, + quote: f.quote, + ...(f.transforms && { transforms: f.transforms }), +})); + +const receipt = ground({ + data, + sources: [{ mediaType: 'text/plain', content: source }], + evidence, + schema: z.object({ + title: z.string(), + city: z.string(), + salaryMin: z.number(), + remote: z.string(), + germanLevel: z.string(), + }), +}); + +console.log('coverage:', receipt.coverage); +for (const issue of receipt.issues) + console.log(`REJECT ${issue.pointer}: ${issue.code}`); diff --git a/examples/llm/source.txt b/examples/llm/source.txt new file mode 100644 index 0000000..86bd6fe --- /dev/null +++ b/examples/llm/source.txt @@ -0,0 +1,3 @@ +Senior Backend Engineer (m/w/d) – Karlsruhe +Full-time, hybrid (3 days on site). Salary range: 68,000–82,000 EUR per year. +Stack: TypeScript, Node.js, PostgreSQL. German B2 required.