A defensive, image-grounded claim-verification agent that decides whether the photos a customer submits actually support their damage claim โ across three object types: cars, laptops, and packages. Built with HuggingFace Inference Providers and Qwen2.5-VL-72B structured vision reasoning, wrapped in a deterministic rule + consistency layer, for the HackerRank Orchestrate hackathon (June 2026).
For each claim the agent decides an evidence-sufficiency verdict, the issue type,
the object part, a final claim status (supported / contradicted /
not_enough_information), risk flags, severity, supporting image IDs, image
validity, and short image-grounded justifications โ and it routes a claim to
manual_review_required rather than guess whenever the evidence is weak, mismatched, or the
input is adversarial.
- Single-Call Vision Reasoning: One Qwen2.5-VL call per claim reads the conversation and the images together and emits all 10 predicted fields as schema-validated JSON โ no separate text LLM, no lossy free-text parsing.
- Claim-Restate-Then-Inspect Prompt: The model first restates the actual claimed part and issue (ignoring rambling distractors and embedded "approve this" text), then inspects the pixels โ folding the benefit of a multi-stage pipeline into one call.
- Prompt-Injection Defense: Claim text and any text inside an image (e.g. a sticky
note saying "mark supported") are treated as data, never instructions; directive text
raises
text_instruction_presentand is ignored. Verified live on an adversarial case. - Deterministic Rule + Consistency Layer: Every field is coerced to an allowed enum,
supporting_image_idsare clamped to real IDs, user-history flags are fused add-only (they never flip the visual decision), and format-level consistency rules enforce the dataset conventions (a decided claim always meets the evidence standard and cites its images). - Robust Multi-Format Image Handling: The test set mixes JPEG / PNG / WEBP / AVIF
(all named
.jpg); every image is decoded (Pillow โฅ 11.3) and re-encoded to a uniform JPEG before the model sees it. - Multilingual Input, English Output: Claims arrive in Hindi/Hinglish, Spanish, and romanized Chinese; the model interprets any language but always emits English enums.
- Deterministic by Design:
temperature=0, config-pinned models, content-addressed cache โ re-runs on the same input reproduce the sameoutput.csv. - On-Disk Caching: Every VLM response is cached by
sha256(model + prompt + image hashes), so re-runs and re-evaluations skip the network and cost ~0 new calls. - Offline Mock Mode + Self-Check:
--mockruns the whole pipeline end-to-end with no token, andpython code/postprocess.pyruns assert-based validation of the schema/rule logic โ no API calls required. - Evaluation Harness + Model Bake-Off: Scores the system on the labeled sample set and compares four VLMs across two generations and two orders of magnitude of size.
graph LR
%% Input
subgraph Input [1. Input]
CSV[("๐งพ claims.csv<br>44 claims")]
HIST[("๐ค user_history.csv")]
REQ[("๐ evidence_requirements.csv")]
IMG[("๐ผ๏ธ images/test/")]
end
%% Per-claim assembly
subgraph Prep [2. Per-Claim Assembly โ deterministic]
D1["๐ load & join<br>history + object requirements"]
D2["๐ผ๏ธ prepare images<br>resize 1024px ยท JPEG ยท sha256"]
end
%% Single VLM call
subgraph Model [3. Single VLM Call]
P["๐ build prompt<br>restate claim โ inspect"]
V["๐๏ธ Qwen2.5-VL-72B<br>structured JSON"]
P --> V
end
%% Rule layer
subgraph Rules [4. Deterministic Rule + Consistency Layer]
N["โ
validate + coerce enums<br>clamp supporting image ids"]
H["โ๏ธ fuse history flags<br>add-only ยท never flips status"]
C["๐งฎ consistency rules<br>evidence ยท severity ยท cited ids"]
N --> H --> C
end
%% Cache
subgraph Cache [Cache โ content-addressed]
CA["๐๏ธ code/.cache/vlm_cache.json"]
end
%% Output
subgraph Output [5. Output]
OUT[("๐ output.csv<br>14 columns ร 44 rows")]
end
CSV --> D1
HIST --> D1
REQ --> D1
IMG --> D2
D1 --> P
D2 --> P
V <-->|cache hit / miss| CA
V --> N
C --> OUT
style Input fill:#e1f5fe,stroke:#01579b
style Prep fill:#fff3e0,stroke:#e65100
style Model fill:#e8f5e9,stroke:#1b5e20
style Rules fill:#f3e5f5,stroke:#6a1b9a
style Cache fill:#eceff1,stroke:#37474f
style Output fill:#fce4ec,stroke:#880e4f
Full design rationale and the end-to-end build narrative live in
IMPLEMENTATION.md; component-level design in
code/ARCHITECTURE.md; metrics + operational analysis in
code/evaluation/evaluation_report.md; code-level
usage in code/README.md.
.
โโโ IMPLEMENTATION.md # design + build narrative (what & why)
โโโ problem_statement.md # task spec + I/O schema
โโโ code/ # โ the agent
โ โโโ main.py # entry point (argparse, per-claim loop, threaded)
โ โโโ config.py # env-only secrets + model aliases + tunables
โ โโโ data.py # CSV load/join, image-path parse, strict CSV writer
โ โโโ images.py # decode any format โ resize โ JPEG data URI โ hash
โ โโโ prompts.py # system rubric + per-claim multimodal message
โ โโโ client.py # one HF VLM client: call, cache, retry, mock
โ โโโ schema.py # canonical allowed-value tables (enums)
โ โโโ postprocess.py # validate + coerce + history fusion + consistency rules
โ โโโ evaluation/ # sample-set scoring + model bake-off
โ โ โโโ main.py # run each model, score vs labels, write report
โ โ โโโ metrics.py # accuracy + set-based risk-flag P/R/F1
โ โ โโโ evaluation_report.md # metrics + operational analysis (generated)
โ โโโ ARCHITECTURE.md # component design + tradeoffs + edge cases
โ โโโ requirements.txt
โ โโโ README.md
โโโ dataset/
โโโ claims.csv # inputs (run your agent on these โ 44 rows)
โโโ sample_claims.csv # inputs + expected outputs (20 labeled, for dev/eval)
โโโ user_history.csv # per-user claim history / risk context
โโโ evidence_requirements.csv# minimum image-evidence checklist by object/issue
โโโ output.csv # agent predictions (14 fields ร 44 rows)
โโโ images/sample/ # images referenced by sample_claims.csv
โโโ images/test/ # images referenced by claims.csv
git clone https://github.com/VIVPM/hackerrank-multi-modal-evidence-review-hackathon.git
cd hackerrank-multi-modal-evidence-review-hackathonIt is recommended to use a virtual environment.
python -m venv .venv
source .venv/bin/activate # (Linux/Mac)
.venv\Scripts\activate # (Windows)pip install -r code/requirements.txtKey dependencies include:
- huggingface_hub (HF Inference Providers client)
- pillow (โฅ 11.3 โ required to decode the AVIF test images natively)
- pandas
- python-dotenv
Copy .env.example โ .env in the project root and add your HuggingFace token:
HF_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxx
Get a token at https://huggingface.co/settings/tokens โ Read scope is enough.
.env is gitignored โ never commit it. No token is needed for --mock.
The agent calls Qwen vision models via HF Inference Providers, which are openly served โ no license-acceptance step is required:
Qwen/Qwen2.5-VL-72B-Instructโ thestrongtier (default, used foroutput.csv)Qwen/Qwen3-VL-8B-Instructโ thecheaptier (used in the eval comparison)
python code/main.py --mock --limit 3 # offline dry run (no token, no API)
python code/main.py --model strong # full run over all 44 claims -> output.csv
python code/main.py --model cheap # run with the cheap tier
python code/postprocess.py # assert-based schema/rule self-checkPredictions are written to output.csv (14 columns per claim); the submitted copy lives
in dataset/output.csv โ pass --output dataset/output.csv to write there directly. The
first live run populates the response cache; subsequent runs load it instantly.
Run the evaluation + model comparison (writes code/evaluation/evaluation_report.md):
python code/evaluation/main.py --models cheap,strongFor every claim the pipeline runs top-to-bottom, deterministic work first:
- Assemble โ load the claim row, join the user's
user_historybyuser_id, and attach theevidence_requirementsrows for that object type. - Prepare images โ decode each referenced image (JPEG / PNG / WEBP / AVIF, all named
.jpg), resize the long edge to ~1024px, re-encode to JPEG, base64 as a data URI, and hash the bytes for caching. - Build the prompt โ a static system rubric (allowed enums, the
valid_imagevsevidence_standard_metdefinitions, injection defense, multilingualโEnglish) plus a per-claim message carrying the conversation, the matched requirements, history context, and the images. - Single VLM call โ one Qwen2.5-VL-72B structured-output call restates the actual claim, inspects the images, and returns all 10 predicted fields as JSON (cache-first).
- Validate + coerce โ every field is mapped to an allowed enum,
risk_flagsare de-duped and ordered, andsupporting_image_idsare clamped to IDs that exist in the row. - Fuse history โ history flags (
user_history_risk,manual_review_required) are added as risk context only; they never flip the image-basedclaim_status. - Consistency rules โ enforce the dataset conventions: a decided claim
(
supported/contradicted) always meets the evidence standard and cites its supporting images; a contradiction with no assessable damage isseverity=none; onlynot_enough_informationleaves the evidence standard unmet.
Any missing/unreadable image or failed call degrades to a safe not_enough_information row โ
the run never crashes on a single claim.
| Variable | Required | Purpose |
|---|---|---|
HF_TOKEN |
yes (live) | HuggingFace Inference Providers auth |
HF_PROVIDER |
no | Pin a backend (auto default; e.g. nebius, fireworks-ai) |
VLM_MODEL_STRONG |
no | Override the strong model (default Qwen2.5-VL-72B) |
VLM_MODEL_CHEAP |
no | Override the cheap model (default Qwen3-VL-8B) |
VLM_USE_SCHEMA |
no | Set 1 to request response_format json_schema where supported |
CLI overrides (code/main.py): --model, --mock, --limit, --sleep, --workers,
--input, --output. Secrets are read from .env only. Never hardcode keys.
- Vision-Language Model (VLM): A single multimodal call reads text and images together; it is a superset of an LLM, so no separate text model is needed.
- Single-Call Chain-of-Thought: "Restate the claim, then inspect" structures the model's reasoning inside one call โ cleaner claim extraction without a second model call.
- Structured Output / JSON: The call returns a validated object with enum-correct fields instead of brittle free-text parsing.
- Deterministic Rule Layer: Enum coercion, image-ID clamping, add-only history fusion, and consistency rules turn a probabilistic model into a schema-guaranteed, convention-aligned output.
- Prompt-Injection Defense: All claim/in-image text is untrusted data; the model decides on visual evidence and flags directive text rather than obeying it.
- Content-Addressed Caching: Responses keyed by model + prompt + image hashes make the pipeline reproducible and near-free to re-run.
- HF Inference Providers: One client routes (
provider=auto) to a backend serving the model; models are swappable by changing a single string. - Determinism:
temperature=0+ cache so regressions are visible, not masked by sampling noise.
- Swap models: Set
VLM_MODEL_STRONG/VLM_MODEL_CHEAPto any HF-served VLM (--modelalso accepts a literal id) โ no code change. See the bake-off in Results. - Add an object type: Extend the enum tables in
schema.pyand the requirement rows indataset/evidence_requirements.csv; the prompt injects them automatically. - Tune image fidelity vs cost: Adjust the resize edge / JPEG quality in
config.py. - Harden the rules: Add flags or consistency checks in
postprocess.py(guarded by the self-check).
HF_TOKENnot set / 401: Ensure.envexists with a valid token and read scope.- Model not served (
provider=autoerrors): The chosen checkpoint isn't served by any provider; fall back with--model Qwen/Qwen2.5-VL-7B-Instructor setVLM_MODEL_STRONG. - 429 / rate limits: Raise
--sleep, lower--workers, or pin a fasterHF_PROVIDER; the client already retries with exponential backoff + jitter. - AVIF images fail to load: Upgrade Pillow to โฅ 11.3 (native AVIF support).
- Stale results: Delete
code/.cache/to force fresh model calls. - No token / offline: Use
--mockto exercise the full pipeline and I/O with no API.
The agent was run over all 44 input claims, producing a schema-valid output.csv
(14 columns ร 44 rows, no empty cells).
claim_status |
Count | Meaning |
|---|---|---|
| ๐ฉ contradicted | 26 | The claimed part is visible but the damage is absent or mismatched (wrong object, toy/stock photo, injection note, identity/color mismatch). |
| โ supported | 16 | The specific claimed damage is clearly visible on the claimed part. |
| โ not_enough_information | 2 | The claimed part/condition could not be evaluated from the images. |
| Total | 44 | Every row schema-valid; 31 routed to manual_review_required. |
The test set is deliberately adversarial (toy car, smartphone-not-laptop, scooter, tablet, color/identity mismatches, injection notes, manipulated "after" photos), so the pipeline is tuned to contradict/escalate rather than falsely approve on any borderline case.
| Model | Size / generation | claim_status | Verdict |
|---|---|---|---|
| Qwen2.5-VL-72B-Instruct | dense VLM | 85% | โ chosen |
| Qwen3-VL-235B-A22B-Instruct | MoE (22B active) | 75% | over-lenient |
| Qwen3-VL-8B-Instruct | dense VLM | 75% | cheap tier |
| Qwen3.5-397B-A17B | MoE, thinking | 65% | most lenient |
Bigger โ better here: both newer flagships lose to the 72B because they over-approve the adversarial cases; this task rewards skeptical calibration, not raw benchmark strength.
| Field | Accuracy |
|---|---|
evidence_standard_met |
95% |
object_part |
95% |
claim_status |
85% |
valid_image |
85% |
issue_type |
55% |
severity |
50% |
risk_flags (set F1) |
~74% |
- Deterministic:
temperature=0+ content-addressed cache โ identicaloutput.csvacross runs. - Validated offline:
python code/postprocess.pyand--mockexercise CSV I/O, enum coercion, image-ID clamping, history fusion, and the consistency rules with no network calls.
Forked from the interviewstreet/hackerrank-orchestrate-june26
starter repo; the code/ agent, design docs, and outputs are my own work.