Style Maitri
A white-label AI shopping assistant for Indian fashion brands, stress-tested by sending 6 agents to role-play skeptical shoppers on the live site, shipped only after that audit's two trust-breaking bugs were found and fixed.
Last updated 2026-10-13 · 4 min read
Verified source
~~**Wave 19 correction (2026-07-31):** `agentic-shopping-assistant/reports/model_eval_20260731T060759Z.md:7` (gitignored/regenerated report, verified live: `n=378 all-fields-exact=94.4%`).~~ Both this row's number and its own `wave 20` caveat below confirmed it was never git-verifiable; the shipped site value is back to the git-committed `reports/final_scorecard_2026-07-12.txt` (commit `57e7e60`) figure, 93.8%/n=211. **UNVERIFIABLE by automated check, wave 20 (2026-07-31):** searched every git-tracked file in `agentic-shopping-assistant` (`git ls-files \| xargs grep`) for "94.4" or "378" in an eval-accuracy context — no committed artifact contains this figure. The eval reports directory is entirely gitignored (regenerated per-run, never committed); no test asserts an accuracy floor. This number can only be re-confirmed by a human re-running the eval locally, or by GG committing the report (or a summary of it) going forward — until then, the metric-freshness checker will report this ID `UNVERIFIABLE` every run, correctly, not a bug in the checker.
commit db4a6ed · measured 2026-07-12
Intent-parsing accuracy (all fields exact)Verified source
~~**Wave 19 correction (2026-07-31):** `agentic-shopping-assistant/data/processed/unified/catalogue.parquet` (gitignored, verified live via pandas: 112,425 rows, 42 unique stores).~~ The 112,425-item figure was never git-verifiable (see this row's own `wave 20` caveat below); the shipped site value reverted to the git-committed `reports/final_scorecard_2026-07-12.txt` (commit `57e7e60`) figure, 52,494 items/8 stores. The store *list* itself (which brands) did stay corrected to the 8-store roster below — that correction traces to the same committed report as the count, so it's independent of the reverted 42-store figure. **Partially verifiable from a committed artifact, wave 20 (2026-07-31):** the **42-store** count independently corroborated by a committed, live-run regression test — `agentic-shopping-assistant/tests/test_unified_index.py::test_store_roster_matches_expected` asserts `set(unified_df["store"].unique()) == _VALID_STORES` (42 entries) and would fail CI if the roster changed. The **112,425-item** count has NO committed source anywhere (`git ls-files \| xargs grep` for "112,425"/"112425" across the repo: no hits) — the parquet itself is gitignored and regenerated, and no test asserts a row-count floor. the metric-freshness checker currently reports this whole claim `UNVERIFIABLE` (its one cited path is the gitignored parquet); that's accurate for the item count but understates the store count, which a human could verify from the test file today. Not fixed in the checker itself — true per-token source attribution (as opposed to per-claim) is more machinery than this finding warranted; flagged here instead. **Open tension not resolved here:** the 42-store count has a real committed regression test behind it (unlike the item count), so it sits in a different evidentiary tier than the fully-gitignored item count — GG's 2026-08-05 revert treated both as one claim and reverted together; a future pass could split them if the store count alone is ever needed.
commit db4a6ed · measured 2026-07-12
Cross-store catalogueCited in provenance.md
`reports/deep_diagnosis_2026-07-12.md:3-9,125-138`
verified against source 2026-10-10
Adversarial live-site audit
The problem
Indian D2C fashion brands want a conversational shopping assistant, something a shopper can chat with the way they'd ask a helpful store clerk, but grounded in their own real catalogue: their actual prices, sizes, and stock, never invented ones. Style Maitri is built white-label so any brand can run it: one Docker container (a packaged, portable copy of the whole app) plus one YAML config file (a small settings file) per brand.
It's been demoed across 8 real Indian fashion catalogues: Snitch, Fashor, Virgio, Powerlook, Myntra, Flipkart, GlobalRepublic, and Libas, for 52,494 items in total. The core promise is that it can't hallucinate: it should never quote a price or describe a fabric that isn't actually in the catalogue it's serving.
How it works
A shopper's message enters a LangGraph agent loop (a graph-shaped state machine that lets an LLM take one step at a time, pick a tool, act, decide the next step) with six tools: search, compare, filter, outfit-bundle, clarify, and respond. An LLM router (Groq's llama-3.1-8b-instant) reads the message and classifies intent to pick which tool runs next. Because an LLM's judgment can drift or loop, a plain code-level guard sits underneath it and forces a deterministic next step after search or compare complete, the agent physically cannot wander in circles forever.
Once a tool decides it needs product data, retrieval runs as a hybrid: a dense FAISS embedding search (turns text into vectors and finds nearby meanings) running alongside a BM25 keyword search (classic exact-term matching), and the two ranked lists are merged with Reciprocal Rank Fusion, a simple, well-understood formula for combining two rankings without needing either one to "win."
Before anything is shown to the shopper, every generated response is checked against the items that were actually retrieved and stripped of any claim that isn't backed by real catalogue data. The same Docker image serves every brand; only the YAML config changes, which is what makes the white-label promise real rather than aspirational.
Architecture
One conversational loop, backed by a retrieval layer that runs two search strategies at once and a grounding check that runs after generation, not instead of it.
Shopper message
WebSocket /chat/stream or REST, via a Next.js 15 frontend
FastAPI backend
Supabase RS256 JWT auth, Postgres-backed session store
LangGraph router
Groq llama-3.1-8b-instant classifies intent; a code-level guard forces deterministic transitions after search/compare
Tool node
search · compare · filter · outfit-bundle · clarify · respond
Hybrid retrieval
fused via Reciprocal Rank Fusion (k=60)
FAISS dense search
IndexFlatIP, all-MiniLM-L6-v2, 384-dim
BM25 keyword search
BM25Okapi, exact-term matching
Groq response generation + grounding check
strips any claim not backed by an actually-retrieved item
Reply to shopper
6-turn Postgres-backed conversation memory carries context forward
Same Docker image serves every brand; only a per-brand YAML config changes. Deploy: Docker → Cloud Run (API), Vercel (frontend).
Key decisions, and why
Fuse two retrieval methods instead of picking one
A shopper might type a vague vibe ("something for a summer wedding") or an exact keyword (a color, a brand name, a price). Pure embedding search is good at the first and weak at the second; pure keyword search is the opposite. Running FAISS (dense) and BM25 (sparse) in parallel and fusing their rankings gets both kinds of query right instead of trading one off for the other.
A rule-based normalizer, not an LLM, to unify garment names
Eight catalogues describe the same kind of item eight different ways, "kurta", "kurti", "tunic top" might all mean the same thing to a shopper. Instead of asking an LLM to guess a mapping every time, a deterministic keyword/rule engine resolves every product title into one canonical taxonomy. It imports nothing from the LLM stack at all, which makes it auditable, free to run, and impossible to get inconsistent answers from on the same input twice.
An LLM router with a code guard, not a faster classifier cascade
An explicit experiment compared three routing designs: a pure LLM router (100% task-pass rate, ~2,100ms median latency, ~$0.10 per 1,000 queries), a pure DistilBERT classifier (75% pass rate, ~31ms, ~$0.05 per 1,000 queries, the reranker downstream still makes one LLM call), and a cascade of the two (94% pass rate, adjusted). Production kept the pure-LLM router, backstopped by the deterministic code guard that caps how many times the loop can iterate, correctness for the shopper mattered more than the cascade's latency and cost advantage.
A transparent formula for outfit-ranking boosts, not a black-box model
When the assistant suggests an outfit pairing, its coherence score gets boosted by real conversion signals (did shoppers actually like this pairing) through an explicit formula: final_score = coherence × (1 + 0.25 × positive_rate), gated so it only kicks in once at least 10 real signals exist (avoiding the cold-start problem of trusting noise). A brand's team can read that formula and know exactly why a pairing ranked where it did, a learned collaborative-filtering model couldn't offer that.
Results, the honest numbers
Verified source
~~**Wave 19 correction (2026-07-31):** `agentic-shopping-assistant/reports/model_eval_20260731T060759Z.md:7` (gitignored/regenerated report, verified live: `n=378 all-fields-exact=94.4%`).~~ Both this row's number and its own `wave 20` caveat below confirmed it was never git-verifiable; the shipped site value is back to the git-committed `reports/final_scorecard_2026-07-12.txt` (commit `57e7e60`) figure, 93.8%/n=211. **UNVERIFIABLE by automated check, wave 20 (2026-07-31):** searched every git-tracked file in `agentic-shopping-assistant` (`git ls-files \| xargs grep`) for "94.4" or "378" in an eval-accuracy context — no committed artifact contains this figure. The eval reports directory is entirely gitignored (regenerated per-run, never committed); no test asserts an accuracy floor. This number can only be re-confirmed by a human re-running the eval locally, or by GG committing the report (or a summary of it) going forward — until then, the metric-freshness checker will report this ID `UNVERIFIABLE` every run, correctly, not a bug in the checker.
commit db4a6ed · measured 2026-07-12
- Intent-parsing accuracy (all fields exact) · 100% on well-formed queries, 61% on the adversarial subset
Verified source
~~**Wave 19 correction (2026-07-31):** `agentic-shopping-assistant/data/processed/unified/catalogue.parquet` (gitignored, verified live via pandas: 112,425 rows, 42 unique stores).~~ The 112,425-item figure was never git-verifiable (see this row's own `wave 20` caveat below); the shipped site value reverted to the git-committed `reports/final_scorecard_2026-07-12.txt` (commit `57e7e60`) figure, 52,494 items/8 stores. The store *list* itself (which brands) did stay corrected to the 8-store roster below — that correction traces to the same committed report as the count, so it's independent of the reverted 42-store figure. **Partially verifiable from a committed artifact, wave 20 (2026-07-31):** the **42-store** count independently corroborated by a committed, live-run regression test — `agentic-shopping-assistant/tests/test_unified_index.py::test_store_roster_matches_expected` asserts `set(unified_df["store"].unique()) == _VALID_STORES` (42 entries) and would fail CI if the roster changed. The **112,425-item** count has NO committed source anywhere (`git ls-files \| xargs grep` for "112,425"/"112425" across the repo: no hits) — the parquet itself is gitignored and regenerated, and no test asserts a row-count floor. the metric-freshness checker currently reports this whole claim `UNVERIFIABLE` (its one cited path is the gitignored parquet); that's accurate for the item count but understates the store count, which a human could verify from the test file today. Not fixed in the checker itself — true per-token source attribution (as opposed to per-claim) is more machinery than this finding warranted; flagged here instead. **Open tension not resolved here:** the 42-store count has a real committed regression test behind it (unlike the item count), so it sits in a different evidentiary tier than the fully-gitignored item count — GG's 2026-08-05 revert treated both as one claim and reverted together; a future pass could split them if the store count alone is ever needed.
commit db4a6ed · measured 2026-07-12
- Cross-store catalogue
Cited in provenance.md
~~**Wave 19 correction (2026-07-31):** `agentic-shopping-assistant/reports/model_eval_20260731T060759Z.md:21-25` (gitignored/regenerated, verified live).~~ Both the pre- and post-correction numbers here trace only to gitignored reports (`model_eval_20260712T091248Z.md` and `model_eval_20260731T060759Z.md`, neither git-tracked) — with neither side verifiable, the 2026-08-05 revert kept the earlier baseline rather than swap one unverifiable number for another.
measured 2026-07-31
- Retrieval precision@5 · drops to 67% on adversarial queries (n=92)
Cited in provenance.md
`reports/deep_diagnosis_2026-07-12.md:3-9,125-138`
verified against source 2026-10-10
- Adversarial live-site audit · skeptical-shopper queries judged disappointing: 6 subagents role-playing wedding shoppers on the live production site found 2 trust-destroying bugs no offline eval had caught
6 agents went shopping as skeptics on the live site, and found what a 93.8%-accuracy eval set never could
The offline evals looked healthy: 93.8% intent accuracy, 96–99% retrieval precision on normal queries.
But those numbers only measure what the eval set happens to ask. To find out what a real, skeptical shopper would experience, 6 independent agents were sent to drive the actual live production site as wedding shoppers would, 32 messy, realistic queries, judged honestly rather than graded against a rubric written in advance.
15 of the 32 responses were judged disappointing, and two were worse than disappointing: a body-shape styling feature fabricated a confident claim about someone's body from a photo that had no person in it at all, and a bridal search ranked children's clothing above adult items in the results.
The most interesting finding was a consistency bug, not an accuracy bug. When the outfit-board tool can't find matching footwear, its code path correctly tells the shopper "we don't have footwear that matches." But the plain-search path handling the exact same missing-inventory situation invents a plausible-sounding recommendation instead, "pairs well with delicate ankle-strap sandals", when zero footwear items exist anywhere in the result. Two code paths, same underlying case, one honest and one confabulating. No offline metric was ever going to surface that, because the eval set doesn't know to compare one tool's honesty against another's. Only driving the live product like a real, skeptical user found it.
What this means if you need something similar
If your product's offline evals look healthy but you're not sure what a real, adversarial user would find, this is the audit pattern worth copying: agents role-playing skeptical real users on your actual live site, judged honestly rather than against a rubric you wrote yourself in advance.
Building something like this? I'm looking for Lead or Senior Applied AI roles, and I take on select AI/ML consulting engagements.
Related projects
- TriageIQ
Reads a new GitHub issue and tells you which component it belongs to, which already-solved issues look like it, and roughly how long it will take to close.
- Warmer
A word game you play once a day, where your only clue is how close each guess comes in meaning to the secret word.
- Multimodal Fashion Recommender
Recommends clothing from a photo of an outfit or a sentence describing one, and finds the same things either way.