← All projects

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

Try Style Maitri ↗Source on GitHub ↗

  • 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 catalogue
  • 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

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.

  1. Shopper message

    WebSocket /chat/stream or REST, via a Next.js 15 frontend

  2. FastAPI backend

    Supabase RS256 JWT auth, Postgres-backed session store

  3. LangGraph router

    Groq llama-3.1-8b-instant classifies intent; a code-level guard forces deterministic transitions after search/compare

  4. Tool node

    search · compare · filter · outfit-bundle · clarify · respond

  5. 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

  6. Groq response generation + grounding check

    strips any claim not backed by an actually-retrieved item

  7. 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

  1. 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.

  2. 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.

  3. 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.

  4. 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.