INDIAN STANDARDS · BIS SERVICES · EVIDENCE-BACKED
Ask in plain English, Hindi or Telugu — or photograph the label.
MetrIQ answers only from verified BIS records, shows why each result matched,
and links the official BIS page behind every answer.
▶ Click to play the 40-second demo — every scene is the real app doing its job.
| VERIFIED INDIAN STANDARDS | RIGHT STANDARD IN TOP 5 | INVENTED STANDARD NUMBERS | LANGUAGES · EN · हिन्दी · తెలుగు |
Recall@5 over 149 queries derived from BIS's own product ↔ standard pairings
(backend/tests/data/eval_baseline.json). "0 invented" = zero standard numbers outside the
knowledge base across every adversarial and prompt-injection query.
Retrieved BIS information is the source of truth — not the language model.
natural language → query understanding → BIS knowledge retrieval
→ evidence ranking → grounded explanation
→ answer + evidence + official source + next steps
Every result is produced by deterministic code. A language model only explains what retrieval already found — it never picks a standard. MetrIQ never invents an Indian Standard number, a clause, a fee, a test or a compliance outcome, and it gives no PASS/FAIL verdict anywhere. When the evidence is not strong enough, it says so and abstains.
07 · ASK IN YOUR LANGUAGE
Hindi and Telugu in, the same verified evidence out
The knowledge base is never translated — standard numbers, schemes and URLs stay exactly as BIS publishes them.
flowchart LR
Q["Question<br/>EN · HI · TE"] --> L[Language layer<br/>deterministic]
P["Label photo"] --> O[Local OCR<br/>PP-OCRv3 · ONNX]
O --> D[Declarations<br/>regex + keywords]
L --> R[Retrieval engine<br/>weighted lexical · MatchReasons]
D --> R
R --> S[Verified BIS records<br/>505 standards · QCOs · certification · labs]
S --> W[Why this result?<br/>no model]
S --> E[Grounded explanation<br/>OpenRouter · guarded]
W --> A[Answer + evidence<br/>+ official source]
E --> A
style S fill:#eceffe,stroke:#2246ef,color:#17181b
style E stroke-dasharray: 4 4
| Layer | What it does | Model? |
|---|---|---|
Retrieval (app/retrieval/) |
Transparent weighted scoring over title / keywords / standard number / content; every point recorded as a MatchReason |
no |
Why this result? (app/product.py) |
Built from retrieval's own reasons — never generated | no |
Certification journey (app/certification_journey.py) |
Every step is a word-for-word quote from a verified record, with its URL | no |
Standard Passport (/standard/:id) |
Composes identity, currency, QCO status, clauses, route, labs | no |
Grounded answer (app/rag.py) |
Explains the retrieved records; a guard withholds any invented IS number, HUID, URL, fee or verdict | yes — replaceable |
# 1 · Backend (Python 3.11+)
cd backend
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
alembic upgrade head # needs PostgreSQL, DATABASE_URL default postgresql+psycopg:///metriq
uvicorn app.main:app --port 8000 # http://127.0.0.1:8000/docs
# 2 · Frontend
cd frontend
npm install
npm run dev # http://localhost:5173 (proxies /api → :8000)Grounded answers need an OpenRouter key in backend/.env (gitignored, server-side only):
OPENROUTER_API_KEY=sk-or-v1-...
OPENROUTER_MODEL=inclusionai/ling-3.0-flash-vl:free # copilot
OPENROUTER_GROUNDED_MODEL=inclusionai/ling-3.0-flash-vl:free # /ask + certificationWithout a key, everything deterministic still works — retrieval, Why this result,
the certification journey, the Passport, labs and OCR — and /ask falls back to
evidence-only text.
PostgreSQL setup (Arch Linux)
sudo pacman -S --needed postgresql
sudo -iu postgres initdb -D /var/lib/postgres/data
sudo systemctl enable --now postgresql
sudo -iu postgres createuser -s "$USER"
createdb metriq && createdb metriq_test # app database + test database
cd backend && ./.venv/bin/alembic upgrade headOnly saved inspections need the database; the /inspections endpoints return 503 until it is available.
Optional: local model (LM Studio) and vision key
LM Studio is used in exactly two places: the inspection pipeline's product-identification
fallback and Laboratory search's explain=true path. Load a small instruction model
(default qwen/qwen3-4b) and start its server on port 1234:
export LM_STUDIO_BASE_URL=http://127.0.0.1:1234/v1
export LM_STUDIO_MODEL=qwen/qwen3-4bVisual product understanding uses its own key, so its quota and outages are independent:
OPENROUTER_VISION_API_KEY=sk-or-v1-... # NOT the same as OPENROUTER_API_KEY
VISION_MODEL=inclusionai/ling-3.0-flash-vl:freeFree-tier guards (OPENROUTER_DAILY_LIMIT 45, OPENROUTER_MINUTE_LIMIT 15,
VISION_MAX_IMAGES, …) refuse a request locally before it reaches the network. A request is
sent only when you press a question — never on page load.
cd backend
./.venv/bin/python -m pytest -q # 395 tests (needs the metriq_test database)
./.venv/bin/python scripts/check_knowledge.py # knowledge-base validation + coverage
./.venv/bin/python scripts/eval_retrieval.py # offline retrieval eval vs committed baseline
./.venv/bin/python scripts/verify_snapshot.py # source drift report (never writes the KB)
cd ../frontend
npx tsc --noEmit && npm run buildModel-dependent tests use stubs — no live model call is made and no quota is spent.
| Retrieval baseline | |
|---|---|
| recall@1 | 85.6% |
| recall@5 | 98.1% |
| correct abstention | 24.4% |
| false match | 13.4% |
| 505 | verified Indian Standards, transcribed from BIS's "Products under Compulsory Certification" (Scheme I / II) with source URL and verification date |
| 1,537 | OCR'd clause records from 31 standards — citation-only, labelled as unverified OCR text, never used for retrieval |
| 29 | Quality Control Order rows — status comes from which BIS table a row is in, never from today's date |
| 1,205 | BIS LIMS laboratory records (245 labs · 83 cities) — 86 of 505 standards have a listed lab |
| 70 | certification, FAQ, hallmarking, Legal Metrology, consumer and general BIS records |
This is a focused, curated dataset — not complete BIS coverage. About 273 of the ~769 products notified under compulsory certification sit in Quality Control Orders outside the two BIS listing pages, and MetrIQ does not guess them.
API endpoints
| Endpoint | What it does | Model |
|---|---|---|
GET /health |
liveness | — |
GET/POST /search |
deterministic lexical retrieval over the BIS knowledge base | no |
POST /product-standard |
Product → candidate Indian Standard + "Why this result?" | no |
POST /ask |
grounded BIS Q&A in English / Hindi / Telugu (also serves Hallmarking) | OpenRouter |
POST /certification-guidance |
certification journey + optional grounded explanation (explain=false skips it) |
OpenRouter, optional |
POST /laboratory-search |
laboratories for a standard or product | LM Studio, optional |
GET /standard-passport/{record_id} · /standard-passport/lookup |
the Standard Passport for one standard (a number without a year lists every held edition) | no |
POST /product-context |
one server-derived context across all features | no |
POST /evidence-graph |
read-only graph of relationships already established | no |
POST /inspection/ocr · /inspection/analyze |
label photo(s) → OCR → declarations → product → standard | LM Studio fallback only |
POST /inspections · GET /inspections[/{id}] |
save / list / read immutable inspection records | no |
GET /inspections/{id}/report.pdf |
evidence-backed PDF built from the stored record | no |
POST /copilot/explain |
grounded explanation of a finished result | OpenRouter |
Label inspection pipeline
IMAGE → OCR REGION → DECLARATION → PRODUCT CLUE → PRODUCT → STANDARD CANDIDATE → BIS SOURCE
(id, bbox, (field, value, (text + its (KB product (IS 16102 (Part 1), (verified
confidence) status) OCR regions) description) why this result) record)
One package is a single photo (image, optional side) or several photos of the same
package (images + sides = FRONT / BACK / LEFT / RIGHT / TOP / BOTTOM / UNKNOWN). Each
photo is OCR'd separately and every value keeps the photo and region it came from; a photo
that cannot be read is reported as failed, and unphotographed sides as not uploaded — never
as missing.
Products and standards come only from data/knowledge/ through the same retrieval engine as
Product → Standard. A product match needs a product phrase of that record on the label; an
IS number printed on the label is one more signal, never proof on its own. Anything weaker is
REVIEW, with the reason. Declarations are DETECTED / UNCERTAIN / NOT_DETECTED in the
uploaded photos — never "legally missing".
app/escalation.py then states, deterministically, every point of the evidence chain the
photos could not establish (unreadable photo, product not identified, several candidates, no
verified standard, HUID, conflicting sides, …). It changes no result and produces no verdict.
Saved inspections are stored in PostgreSQL with their photos and are immutable — a database trigger rejects any update. History, a dashboard and a PDF report all read the stored record; nothing is recomputed.
curl -s -F "image=@samples/ocr-labels/synth_led-lamp.png" \
http://127.0.0.1:8000/inspection/analyze | jq '.product, [.standards[].standard_number]'Sample (samples/ocr-labels/) |
Identified as | Standard candidate |
|---|---|---|
synth_clean-declaration.png |
Roasted Bengal Gram | IS 18140:2023 |
synth_led-lamp.png |
Self-ballasted LED lamps | IS 16102 (Part 1) |
synth_electric-kettle.png |
Electric Kettles and Jugs | IS 367:1993 |
real_* (Wikimedia Commons) |
real-world labels, incl. hard cases | OCR stress tests |
Hallmarking & HUID — observed, never authenticated
MetrIQ can identify and explain observable hallmark/HUID evidence, but it does not authenticate a physical jewellery item's hallmark, HUID, jeweller registration or AHC status.
verification_statusis onlyNOT_VERIFIEDorNOT_DETECTED; there is no AUTHENTIC, VERIFIED or CERTIFIED state anywhere in the code.- The three BIS marks — BIS logo, purity/fineness, HUID — are each
DETECTED/NOT_DETECTED/UNCERTAIN/NOT_SUPPORTED. The BIS logo is alwaysNOT_SUPPORTED: it is a graphic and OCR reads text. - "Not detected" is about the photograph, never a finding that the article lacks the mark.
- Printed text like "HUID VERIFIED" is recorded as an untrusted claim and changes nothing.
- A HUID the user types is compared with the OCR text as a string only.
- Real verification is pointed to the BIS Care App, quoted from verified BIS records.
Testing laboratories
MetrIQ identifies laboratories from verified laboratory evidence. It does not independently establish a laboratory's current accreditation, scope, availability or operational status.
Search by standard, by a question naming a standard, or by product (which reuses
Product → Standard: "Where can I test an electric kettle?" → IS 367:1993 → the labs BIS LIMS
lists for it). A lab is relevant only because BIS lists it against that standard. Each
result carries a deterministic why (STANDARD_LISTED, PRODUCT_LISTED, NAME_MATCH,
CITY_MATCH), results are alphabetical, and validity is VALID_AT_SNAPSHOT /
EXPIRED_AT_SNAPSHOT / NOT_STATED. A different edition is a different standard, so
IS 14543 (2016) and (2024) labs are never merged. Addresses, phones, emails and NABL status
are not held and never supplied.
The snapshot (data/laboratories.json, retrieved 2026-09-19) is built by
backend/scripts/fetch_lims_laboratories.py; the app never calls LIMS at runtime.
Multilingual — English · Hindi · Telugu
The language of interaction changes, the source of truth does not.
query (any language) → script detection (no model) → known terms rewritten to canonical English
→ the SAME retrieval engine → the SAME verified records
→ grounded answer written in the user's language
/ask, /certification-guidance and /laboratory-search take language = auto / en /
hi / te. The alias table (app/language.py) only maps spellings of products and BIS terms
that exist in the knowledge base. Standard numbers, scheme names, document names and URLs are
reproduced exactly, never translated. If retrieval finds nothing, the assistant abstains in the
user's language. Hinglish / Tanglish work.
Certification journey
POST /certification-guidance answers "what certification process do I follow?". Nothing in
the journey is written by MetrIQ — each step is a word-for-word quote from a verified record
with its official URL. The scheme is read two independent ways (the BIS listing the standard
was transcribed from, and any certification record naming it); if they disagree, the conflict
is shown and MetrIQ picks neither.
| Status | Meaning |
|---|---|
VERIFIED |
one standard, a route established, every documented step present |
PARTIAL |
several candidates, a conflict, missing steps, or hallmarking |
INSUFFICIENT |
no verified record states a route |
It is guidance about the route for a product type — never a statement that a product or licence is certified. Fees, processing times and required documents are never stated; they live only in the BIS documents it links to.
Copilot, product context & evidence graph
Copilot — explains evidence the deterministic system produced; it never establishes
standards, compliance, lab status, certification applicability or HUID authenticity. After the
model replies, a deterministic guard replaces the text with MetrIQ's own sentence if it cites an
IS number, HUID or URL not in the context, claims authentication, states any PASS/FAIL or
"compliant" verdict, ranks or vouches for a laboratory, or states a fee not in the evidence. The
key stays on the server — there is no VITE_ variable for it.
Product context (app/product_context.py) — connects what each feature already
established into one view, with each feature AVAILABLE / NOT_AVAILABLE / NOT_APPLICABLE /
UNCERTAIN. It is a composition: no classifier, no ranking, no model. Conflicts are shown,
never resolved.
Evidence graph (app/evidence_graph.py) — a read-only projection of relationships already
established (OCR region → declaration → product → standard → requirement / certification /
laboratory / source). 11 node types, 8 edge types, each edge carrying an explanation taken from
the evidence. No model, no retrieval, no verdict vocabulary; nothing downstream reads it.
Retrieval scoring reference
Each query term is matched against a record's fields and the weights are summed
(RetrievalConfig in app/retrieval/engine.py): standard-number match 8 (12 if the year
also matches), title 4, keyword 3, category hint 2, document name 1.5, reference 1,
content mention 1.
Confidence of the top hit: high ≥ 7.5, medium ≥ 4.0, low ≥ 1.0, else none. If the
top hit covers less than ~⅓ of the query terms, confidence is capped at low. When nothing
matches: confidence: "none", abstained: true, empty results.
Inspection product identification keeps a result only when the label contains a multi-word product phrase of the record, so a single generic word ("water", "gram") can never pull in a standard.
Repository layout
backend/ Python · FastAPI
app/ retrieval/, rag.py, product.py, certification*.py, laboratory.py, lab_registry.py,
language.py, clauses.py, clause_groups.py, standard_currency.py, boundary.py,
ocr.py, declarations.py, pipeline.py, hallmark.py, copilot.py, openrouter.py, …
migrations/ Alembic (PostgreSQL)
scripts/ check_knowledge, eval_retrieval, verify_snapshot, build-time fetch_* tools
tests/ plain-Python runners, bridged to pytest
data/knowledge/ the knowledge base — one JSON file per category
data/laboratories.json BIS LIMS snapshot
samples/ocr-labels/ sample label images
frontend/ React 19 · TypeScript · Vite · Tailwind v4
docs/history/ phase-by-phase design decisions
BUILT FOR SMART INDIA HACKATHON · PROBLEM STATEMENT SIH26107
MetrIQ is a research prototype. It is not affiliated with or endorsed by the Bureau of Indian Standards,
and it does not make legal or compliance determinations.
Always confirm against the official BIS source it links.





