Under the counter
A small agent loop, with every exit written down.
FastAPI + LangGraph backend, Qdrant for retrieval, Gemini as the model, Next.js for this page. The model decides what to look up; code decides anything that has to be exactly right.
1 · The loop
POST /api/chat
agent node
tools node
give_up node
2 · Retrieval
Data. 453 furniture products from Amazon Berkeley Objects (real names, materials, sizes, photos), deduplicated across colour variants. Prices, stock and UK eligibility are generated deterministically from the product id. Vendor lines like “free returns” are dropped so the five policy files are the only source of policy.
Chunking. One document per product (short, chunking would split facts that belong together). Policies split by section heading, each chunk prefixed with “Document > Section” so it stands on its own: 30 chunks.
Two vectors per point. Dense: bge-small-en-v1.5 (384 dimensions, runs locally on CPU via ONNX). Sparse: BM25, with IDF computed by Qdrant. Dense catches “cozy reading chair”; BM25 catches “jute” and “queen”. Both candidate lists are merged with Reciprocal Rank Fusion.
Retrieval eval · 80 shopper queries, no model in the loop
| Setup | hit@1 | hit@5 | MRR | ms |
|---|---|---|---|---|
| dense | 88% | 98% | 0.91 | 31 |
| sparse | 89% | 99% | 0.94 | 26.9 |
| hybrid | 91% | 100% | 0.95 | 30.4 |
| dense + category filter | 88% | 98% | 0.92 | 15 |
| sparse + category filter | 89% | 99% | 0.94 | 32.4 |
| hybrid + category filter | 91% | 100% | 0.95 | 42.1 |
Each query was written by an LLM from one product’s description, without brand or name words. Hit@5 = that product is in the top 5. Other products can also fit a query, so these are lower bounds.
3 · Tools: what the model may not do itself
| search_products | Hybrid search over 453 products with price, category, country and stock filters. | Filters run inside Qdrant, so a $900 sofa never reaches the model when the budget is $500. |
| get_product_details | Size in inches and cm, weight, materials, stock, shipping countries. | Unit conversion in code, not in the model's head. |
| search_store_policies | Top 3 policy sections (shipping, VAT, returns, warranty, care, payments). | The answer cites the section it came from. |
| quote_shipping | Exact shipping cost, delivery time and UK VAT note for a basket. | Pricing rules are business logic. The model never adds numbers up. |
| lookup_order | Order status, only when order id and email both match. | Same reply for wrong email and unknown order, so ids can't be probed. |
| handoff_to_human | Creates a ticket for complaints, damage, warranty, trade orders. | Some decisions should not be automated. |
4 · Eval
5 · Prompt versions
6 · Not built (yet)
Listing Studio
Photo → facts → copy → check → repair
1 · Read the photo
2 · Write from facts
3 · Check, repair
4 · Look-alikes