The five LCBO Vintages bottles you’d love this Saturday — picked by an agent that learns from you.
Every two weeks, LCBO drops ~100 new wines. mySommelier builds a taste fingerprint from the wines you’ve loved, ranks the catalogue against it, lets you talk to a sommelier agent that uses tools to refine the picks in plain English, and updates itself every time you rate a wine. The more you use it, the sharper the picks.
Live: my-sommelier-rinalabs.vercel.app
Status: shipping. See BACKLOG.md for what’s next.
The taste fingerprint is not static. Every interaction updates it.
- Rate a pick (Loved it / Wasn’t for me / Not for me) →
profiles.profile_vectorrecomputes as the average of your loved-wine embeddings minus 0.3 × the average of your disliked-wine embeddings, normalised. The fingerprint card and the next picks shift on the same render. - Tell the chat about a wine you tried ("I had Henry of Pelham Cab Merlot and loved it") → the agent searches LCBO’s full catalogue, imports the bottle, embeds it, adds it to your loved list, and recomputes your profile.
- Sugar ceiling adapts → if your loved wines all sit under 3 g/L, the ranker auto-filters anything above ~5 g/L from future picks. If you start loving off-dry Rieslings, the ceiling rises with you.
- Skip a pick ("Not for me, didn’t even try it") → counts as a soft negative signal, pushes the profile away from that style.
By the third or fourth Saturday, the ranker is markedly more precise than the cold-start it had after onboarding.
- Next.js 16 (App Router) + TypeScript + Tailwind v4
- Supabase — Postgres +
pgvector+ Auth + Storage. Drizzle ORM owns the schema. - Gemini via Vercel AI SDK
gemini-embedding-001— wine and profile embeddings (3072-dim)gemini-2.5-flash— “why this match” lines + the refinement chat agent
- Vercel — hosting + cron (Saturday-morning email send) + preview deploys
- Resend — transactional email send (free tier, ~100/day)
- Data source:
lcbo.com/graphql— LCBO’s own Adobe Commerce / Magento GraphQL. Per-release category UIDs let us sync the current Vintages drop directly.
[1] Onboarding (free text)
"I love Tawse Chardonnay, Malivoire Gamay, Paco & Lola Albariño"
│
▼
Gemini extracts grapes/regions/wines
│
▼
For each named wine → search lcbo.com/graphql → import → embed
│
▼
profiles.profile_vector = avg of those wine embeddings
[2] Picks page (/p/{slug})
profile_vector × wines.embedding
│
▼
Top-5 by cosine similarity, MMR diversity,
sugar ceiling derived from your loved wines,
excluding wines you’ve already tried
│
▼
Each pick: name, varietal, style, sugar, alcohol, "why this match"
rationale generated by gemini-2.5-flash
[3] Refinement chat (below the picks)
Tools the agent can call:
• rerank — narrow by origin/varietal/price/style
• searchByVibe — free-text descriptors (lighter, mineral, bolder)
• findSimilar — anchor on a SKU + modifier
• explainPick — deep explanation of one wine
• logWine — log any LCBO wine the user has tried,
searches the full catalogue, updates profile
[4] Feedback loop — the system learns every time
Rate any pick (loved / not for me / skip)
OR tell the chat "I tried X and loved it"
│
▼
profile_vector recomputes:
avg(loved embeddings) − 0.3 × avg(disliked embeddings)
│
▼
Sugar ceiling re-derives from updated loved-wine sugar levels
│
▼
Fingerprint card, picks, and Saturday email all reflect the
new profile starting on the next render. No manual refresh.
[5] Saturday email (Vercel cron, idempotent)
8 AM, 10 AM, 12 PM, 2 PM ET — discovers latest Vintages release,
skips runs where the drop isn’t live yet, sends each subscriber
exactly one email per release.
The same topPicksForProfile() function powers the page, the chat tools, and the email — one ranker, three surfaces.
Three layers, each catching a different failure mode:
bun run evals:data # Data quality — catalogue + embeddings (run first)
bun run evals # Retrieval — NDCG/Recall over four labeled personas
TRACE_LOG=1 bun dev # Trace capture — JSONL into evals/traces/ for L2 reviewHamel-style L1 assertions, retrieval metrics with rule-based ground-truth judges, and trace files for human + LLM-as-judge review. Full framework writeup in evals/README.md — taxonomy, the four behavioral categories, how to add a persona.
# 1. Clone and install
git clone https://github.com/rinaLabs/mySommelier.git
cd mySommelier
bun install
# 2. Configure environment
cp .env.example .env.local
# Fill in:
# GOOGLE_GENERATIVE_AI_API_KEY — aistudio.google.com
# NEXT_PUBLIC_SUPABASE_URL ┐
# NEXT_PUBLIC_SUPABASE_ANON_KEY │ supabase.com → Settings → API
# SUPABASE_SERVICE_ROLE_KEY ┘
# DATABASE_URL — Supabase Settings → Database → Transaction pooler
# DIRECT_URL — Supabase Settings → Database → Session pooler
# RESEND_API_KEY — resend.com (only needed for the email feature)
# CRON_SECRET — any random 32+ char string
# In the Supabase SQL editor, run: CREATE EXTENSION IF NOT EXISTS vector;
# 3. Push the schema
bun run db:push
# 4. Sync the current LCBO Vintages release + embed
bun run db:sync
bun run db:embed
# 5. Start the dev server
bun run devOpen http://localhost:3000, click Build my profile, fill the form. You’ll land at /p/{your-slug} with picks, fingerprint, Your Wines list, and the refinement chat.
| Script | What it does |
|---|---|
bun run dev |
Next.js dev server |
bun run build |
Production build |
bun run typecheck |
tsc --noEmit |
bun run lint |
ESLint |
bun run db:push |
Apply Drizzle schema to Supabase |
bun run db:sync |
Pull the latest Vintages release into the wines table |
bun run db:embed |
Embed any wines without an embedding |
bun run db:seed |
Seed a demo profile (optional, for testing without onboarding) |
bun run evals |
Run the eval suite |