The Wine Recommender is a conversational agent that guides a user through a structured dialogue to recommend wines from a curated dataset. It captures preferences across four dimensions — colour, alcohol level, country, price — then optionally refines on blend and taste profile.
Its defining feature is a constraint-relaxation fallback: when no wine satisfies all of a user's stated preferences, the engine systematically loosens the least-critical constraint until the feasible set is non-empty — and reports exactly which constraints it relaxed.
User: "I want a strong red from Spain"
→ Color=Red, ABV=14-15%, Country=Spain
→ Strict filter: 0 matches
→ Relax ABV constraint → matches found
Bot: "Here are some wines. Note: we relaxed the alcohol-level constraint."
The recommender operates over a curated dataset of wines with 20 attributes each: ABV, winery, vintage, country, region, colour, blend, grape types, ratings, price, body / tannins / sweetness / acidity scores, tasting notes, and food pairings.
The raw file contains a handful of malformed rows (out-of-range ABV, zero price). The analysis script filters these explicitly and logs every drop — no silent data loss.
| Metric | Value |
|---|---|
| Raw rows | 103 |
| Dropped (ABV outside 5–20%) | 6 |
| Dropped (non-positive price) | 6 |
| Clean wines | 91 |
| Countries covered | 5 (Italy, France, Spain, Moldova, Argentina) |
| Wineries | 87 |
| Price range | $10.99 – $69.99 (median $29.83) |
| ABV range | 5.0 – 16.5% (mean 13.1%) |
| Colour split | Rosé 33 · Red 23 · Sparkling 18 · White 17 |
Across all 256 (colour × ABV × country × price) preference combinations:
| Filtering mode | Combinations matched | Hit rate |
|---|---|---|
| Strict (all constraints) | 70 / 256 | 27.3% |
| With constraint relaxation | 168 / 256 | 65.6% |
The relaxation fallback recovers 98 combinations that strict filtering would leave empty — more than doubling the effective coverage of the catalogue. Reproduce with:
python analyze_dataset.py┌─────────────┐ POST /conversation ┌──────────────────────┐
│ Browser │ ──────────────────────────► │ Flask app (app.py) │
│ (static/) │ ◄────────────────────────── │ session per user_id │
└─────────────┘ JSON {message,options} └──────────┬───────────┘
│
┌───────────▼────────────┐
│ WineRecommender │
│ · slot-filling FSM │
│ · free-text regex parse │
│ · strict filter │
│ · relaxation fallback │
└──────────────────────────┘
Key design elements:
- Finite state machine — four
initial_stepsslots (Color, AlcoholLevel, Country, PriceRange), then tworefinement_steps(Blend, Wine Tastes). Each slot is validated before the FSM transitions. - Session isolation — each
user_idgets an independentWineRecommenderinstance with its own conversation state, stored server-side in asessionsdict. - Free-text parsing — regex interpretation of natural input:
"under $30","less than 13%","strong"→14-15%,"light"→11-12%,"medium"→12-13%. - Constraint relaxation — ordered fallback (
fallback_order = ["AlcoholLevel", "PriceRange"]): the engine drops the ABV constraint first, then widens the price band, reporting each relaxation back to the user. - Colour-aware refinement — the Blend options are dynamically filtered to the blends actually present for the chosen wine colour.
git clone https://github.com/Raeus1901/wine_bot.git
cd wine_bot
pip install -r requirements.txt
python app.py
# → http://localhost:5001API example:
curl -X POST "http://localhost:5001/conversation?user_id=demo" \
-H "Content-Type: application/json" \
-d '{"message": "I want a strong red from Spain"}'Endpoints:
| Route | Method | Purpose |
|---|---|---|
/ |
GET | Serve the chat UI (static/index.html) |
/wine_recommender_endpoint |
POST | Stateless greeting on page load |
/conversation?user_id=<id> |
POST | Main stateful dialogue turn |
/reset?user_id=<id> |
POST | Clear a user's session |
This is a side project, but its core mechanics map onto patterns used in quantitative finance:
- Constraint relaxation = optimization under active constraints. When a mandate
(sector cap + tracking-error limit + turnover budget) admits no feasible portfolio, a
practitioner slackens the least-binding constraint first. The recommender's
fallback_orderis exactly this: an explicit, ordered relaxation hierarchy with transparent reporting of what was dropped. - Slot-filling FSM = structured preference elicitation. Capturing a client's risk tolerance, horizon, and liquidity needs through validated, sequential questioning is the same dialogue pattern as capturing colour / ABV / country / price.
- Hit-rate metric = feasible-region diagnostics. The 27.3% → 65.6% jump quantifies how much the fallback widens the feasible set — the recommender analogue of measuring what fraction of mandates are feasible before vs. after slackening active constraints.
The transferable skill is translating a fuzzy human objective into a structured, relaxable constraint problem with explicit fallback logic and measurable coverage.
| Layer | Technology |
|---|---|
| Backend | Flask 2.3.2, Werkzeug 2.3.3 |
| Data | pandas 1.5.3, numpy 1.24.3 |
| Frontend | Vanilla HTML / CSS / JS (static/) |
| WSGI server | Gunicorn 20.1.0 (Procfile included) |
- Dataset size. 91 clean wines is a demonstration dataset; production use would require a larger, regularly-refreshed catalogue.
- Data quality. The raw file has ~12% malformed rows (bad ABV, zero price); these are
filtered explicitly in
analyze_dataset.pywith every drop logged. - Parsing. Free-text interpretation is regex-based, not a learned NLU model — robust for supported phrasings, brittle outside them.
- Recommendation logic. Filtering is rule-based; there is no collaborative-filtering or learned ranking. A natural extension would be a learned ranker over the feasible set.
- Price-range relaxation. The price-band fallback expects a
$min-$maxtoken format; the ABV relaxation is the primary working fallback path.
Jean Treves — M.A. Quantitative Methods in the Social Sciences, Columbia University LinkedIn • GitHub
Master thesis (FinBERT × SARIMAX): finbert-sarimax-energy-forecasting