eliel2003 commited on
Commit
6750912
Β·
verified Β·
1 Parent(s): 91dc83f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +360 -171
app.py CHANGED
@@ -1,9 +1,16 @@
1
  # ================================================================
2
  # ProSync AI β€” The Event Producer's Command Center
3
- # Handles both B2B (Corporate) and B2C (Private) events.
 
 
 
 
4
  # ================================================================
5
 
 
 
6
  import os
 
7
  import json
8
  import warnings
9
 
@@ -12,54 +19,64 @@ import pandas as pd
12
  import torch
13
  import gradio as gr
14
  from sentence_transformers import SentenceTransformer, util as st_util
15
- import spaces
16
 
17
  warnings.filterwarnings("ignore")
18
- os.environ["CUDA_VISIBLE_DEVICES"] = ""
19
- os.environ["TOKENIZERS_PARALLELISM"] = "false"
20
 
21
- # Required by HF GPU Space infrastructure β€” do not remove
 
22
  @spaces.GPU
23
- def dummy_gpu_function():
24
  pass
25
 
26
- # ── Config ────────────────────────────────────────────────────
27
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
 
28
  EMBED_MODEL_ID = "sentence-transformers/all-MiniLM-L6-v2"
29
 
30
- # Private events get a different (warmer, personal) prompt tone
 
 
 
 
 
 
 
 
 
 
31
 
32
- VENDOR_CATEGORIES = [
33
- "Catering", "AV_Technology", "Venue", "Security",
34
- "Photography_Video", "Entertainment", "Logistics",
35
- ]
36
  CATEGORY_EMOJI = {
37
- "Catering": "🍽️", "AV_Technology": "🎬", "Venue": "πŸ›οΈ",
38
- "Security": "πŸ›‘οΈ", "Photography_Video": "πŸ“·",
39
- "Entertainment": "🎭", "Logistics": "🚚",
40
- }
41
- ALLOC_RATIOS = {
42
- "Catering": 0.304, "Venue": 0.228, "AV_Technology": 0.175,
43
- "Entertainment": 0.104, "Photography_Video": 0.076,
44
- "Logistics": 0.057, "Security": 0.057,
45
  }
46
- CITIES = ["Beer Sheva", "Haifa", "Herzliya", "Jerusalem",
47
- "Netanya", "Petah Tikva", "Ramat Gan", "Tel Aviv"]
 
 
 
48
  SEASONS = ["Winter", "Spring", "Summer", "Fall"]
49
  EVENT_TYPES = [
50
- "Annual Conference", "Award Ceremony", "Bar/Bat Mitzvah", "Brand Activation",
51
- "Corporate Gala", "Family Reunion", "Investor Day", "Private Birthday",
52
- "Product Launch", "Team Building", "Tech Summit", "Trade Show",
 
53
  "Wedding", "Workshop Series",
54
  ]
 
55
  QUICK_STARTERS = [
56
  {
57
  "label": "πŸ™οΈ Tech Summit Β· Tel Aviv",
58
  "brief": "Large-scale tech summit β€” advanced AV, LED walls, live streaming, "
59
  "kosher catering for 400 guests, VIP executive security.",
60
  "city": "Tel Aviv", "season": "Summer", "budget": 250_000,
61
- "type": "Tech Summit", "guests": 400,
62
- "date": "October 15, 2026", "venue": "The Tel Aviv Convention Center",
63
  "notes": "Kosher catering required. VIP lounge for 30 executives.",
64
  },
65
  {
@@ -67,8 +84,7 @@ QUICK_STARTERS = [
67
  "brief": "Elegant annual corporate gala β€” plated fine dining, live band, "
68
  "professional photography and videography for 200 guests.",
69
  "city": "Jerusalem", "season": "Winter", "budget": 140_000,
70
- "type": "Corporate Gala", "guests": 200,
71
- "date": "December 5, 2026", "venue": "The King David Hotel Ballroom",
72
  "notes": "Black-tie dress code. Award presentation segment.",
73
  },
74
  {
@@ -76,8 +92,7 @@ QUICK_STARTERS = [
76
  "brief": "Outdoor team building day β€” interactive entertainment, DJ, "
77
  "logistics, casual catering for 150 employees.",
78
  "city": "Haifa", "season": "Spring", "budget": 65_000,
79
- "type": "Team Building", "guests": 150,
80
- "date": "April 22, 2026", "venue": "Carmel Forest Resort",
81
  "notes": "Outdoor venue preferred. Vegetarian options required.",
82
  },
83
  {
@@ -85,17 +100,17 @@ QUICK_STARTERS = [
85
  "brief": "Intimate outdoor wedding β€” elegant catering, DJ, floral design, "
86
  "photography, and logistics for 250 guests.",
87
  "city": "Netanya", "season": "Spring", "budget": 120_000,
88
- "type": "Wedding", "guests": 250,
89
- "date": "May 14, 2027", "venue": "Private Estate, Kfar Yona Area",
90
- "notes": "Chuppah at sunset. Vegan and gluten-free menu options required.",
91
  },
92
  ]
93
 
94
  # ================================================================
95
- # DATA β€” module level (no race condition)
96
  # ================================================================
97
 
98
- def _to_list(val):
 
99
  if isinstance(val, list): return val
100
  if isinstance(val, str):
101
  try:
@@ -104,84 +119,223 @@ def _to_list(val):
104
  except Exception: return []
105
  return []
106
 
107
- print("⏳ Loading vendor data …")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  try:
109
- _df = pd.read_csv("dataset_b_vendors.csv")
110
- for c in ["coverage_cities", "seasonal_availability",
111
- "specializations", "certifications"]:
112
- _df[c] = _df[c].apply(_to_list)
113
- _df["day_rate_mid"] = (_df["day_rate_min_usd"] + _df["day_rate_max_usd"]) / 2
114
- rmin, rmax = _df["avg_rating"].min(), _df["avg_rating"].max()
115
- _df["rating_norm"] = (_df["avg_rating"] - rmin) / (rmax - rmin + 1e-9)
116
- _df["value_score"] = 1 - (_df["price_tier"] - 1) / 4
117
- _df["composite_score"] = (0.4 * _df["rating_norm"]
118
- + 0.4 * _df["sla_compliance_rate"]
119
- + 0.2 * _df["value_score"])
120
- _VCITIES = _df["coverage_cities"].tolist()
121
- _VSEASONS = _df["seasonal_availability"].tolist()
122
  _VCATS = _df["category"].values
123
  _VRATES = _df["day_rate_mid"].values
124
  _VCOMP = _df["composite_score"].values
125
  _VIDX = np.arange(len(_df))
126
- print(f"βœ… {len(_df):,} vendors loaded.")
127
- except FileNotFoundError:
128
- print("❌ dataset_b_vendors.csv not found.")
 
129
  _df = None
130
 
131
  # ================================================================
132
- # EMBEDDING MODEL β€” CPU
133
  # ================================================================
134
 
135
- print("⏳ Loading embedding model …")
136
  _embed = SentenceTransformer(EMBED_MODEL_ID, device="cpu")
137
 
138
  if _df is not None:
139
- print("⏳ Computing vendor embeddings …")
140
  _vemb = _embed.encode(
141
  _df["vendor_profile_text"].tolist(),
142
  batch_size=128, show_progress_bar=True,
143
  normalize_embeddings=True, convert_to_tensor=True,
144
  device="cpu",
145
  )
146
- print(f"βœ… Embeddings: {_vemb.shape}")
147
  else:
148
  _vemb = None
149
 
150
-
151
-
152
-
153
  # ================================================================
154
- # CORE ENGINE
 
 
155
  # ================================================================
156
 
157
- def recommend_vendors(brief, city, season, budget, top_n=3):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  if _df is None or _vemb is None:
159
- return {"error": "Vendor data not loaded. Check dataset_b_vendors.csv."}
160
- if not brief.strip():
161
  return {"error": "Please enter an event description."}
162
 
163
- city_ok = np.array([city in c for c in _VCITIES], dtype=bool)
164
- season_ok = np.array([season in s for s in _VSEASONS], dtype=bool)
165
- alloc = np.array([budget * ALLOC_RATIOS.get(cat, 0.10) for cat in _VCATS])
166
- budget_ok = _VRATES <= alloc
 
 
 
 
167
  combined = city_ok & season_ok & budget_ok
168
  pool_idx = _VIDX[combined].tolist()
169
 
170
  if not pool_idx:
 
171
  return {"error": (
172
- f"No vendors matched all filters "
173
- f"(city: {int(city_ok.sum())}, season: {int(season_ok.sum())}, "
174
- f"budget: {int(budget_ok.sum())}, combined: 0). "
175
- f"Try a larger budget or a different city."
 
 
176
  )}
177
 
178
- q = _embed.encode(brief, convert_to_tensor=True,
179
- normalize_embeddings=True, device="cpu")
180
- sims = st_util.cos_sim(q, _vemb[pool_idx])[0].cpu().numpy()
 
 
 
 
181
 
182
  pool = _df.iloc[pool_idx].copy().reset_index(drop=True)
183
  pool["similarity"] = sims
184
- pool["final_score"] = 0.8 * sims + 0.2 * _VCOMP[pool_idx]
185
 
186
  results = {}
187
  for cat in VENDOR_CATEGORIES:
@@ -194,36 +348,40 @@ def recommend_vendors(brief, city, season, budget, top_n=3):
194
  ]].to_dict("records")
195
  return results
196
 
 
 
 
197
 
198
-
199
-
200
- def _stars(r):
201
  n = min(5, max(0, int(round(float(r)))))
202
  return "β˜…" * n + "β˜†" * (5 - n)
203
 
204
 
205
- def _fmt_vendors(recs, budget):
206
  if "error" in recs:
207
  return f"### ⚠️ No Results\n\n```\n{recs['error']}\n```"
 
208
  lines = []
209
  for cat in VENDOR_CATEGORIES:
210
  if cat not in recs: continue
211
  alloc = budget * ALLOC_RATIOS[cat]
212
  cat_name = cat.replace("_", " ")
213
  lines.append(
214
- f"### {CATEGORY_EMOJI[cat]} {cat_name} Β· Budget: ${alloc:,.0f}\n"
 
215
  )
216
  for i, v in enumerate(recs[cat], 1):
217
  sp = v.get("specializations", [])
218
  if isinstance(sp, str):
219
  try: sp = json.loads(sp)
220
  except: sp = []
221
- sc = v.get("final_score", v.get("similarity", 0))
222
  lines.append(
223
- f"**#{i} {v['vendor_name']}**\n"
224
  f"{_stars(v.get('avg_rating', 0))} Β· "
 
225
  f"${v.get('day_rate_mid', 0):,.0f}/day Β· "
226
- f"Match `{sc:.3f}`\n\n"
227
  f"*{', '.join(sp[:2]) if sp else 'β€”'}*\n"
228
  )
229
  lines.append("---\n")
@@ -235,18 +393,53 @@ def _fmt_vendors(recs, budget):
235
 
236
  def handle_submit(brief, city, season, budget, ev_type,
237
  date_from, date_to, guests, notes):
238
- date_range = date_from if date_from == date_to else f"{date_from} β†’ {date_to}"
239
- recs = recommend_vendors(brief, city, season, float(budget))
240
  return _fmt_vendors(recs, float(budget))
241
 
242
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  def _qs(idx):
244
  q = QUICK_STARTERS[idx]
245
  b, c, s, bu = q["brief"], q["city"], q["season"], q["budget"]
246
  et, dt = q["type"], q["date"]
247
  gs, nt = q["guests"], q["notes"]
248
  vm = handle_submit(b, c, s, bu, et, dt, dt, gs, nt)
249
- # Also update the visual date picker HTML so it reflects the quick starter date
250
  return b, c, s, bu, et, dt, dt, gs, nt, _date_html(dt, dt), vm
251
 
252
  def _qs0(): return _qs(0)
@@ -255,11 +448,12 @@ def _qs2(): return _qs(2)
255
  def _qs3(): return _qs(3)
256
 
257
  # ================================================================
258
- # CSS
259
  # ================================================================
260
 
261
  CSS = """
262
  @import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700&family=Inter:wght@300;400;500;600&display=swap');
 
263
  body, .gradio-container {
264
  background-color: #FAF7F2 !important;
265
  font-family: 'Inter', sans-serif !important;
@@ -267,14 +461,15 @@ body, .gradio-container {
267
  }
268
  .ps-header {
269
  background: linear-gradient(135deg, #3D2314 0%, #7A4E2D 60%, #B8895A 100%);
270
- border-radius: 16px; padding: 36px 40px; margin-bottom: 20px;
271
- box-shadow: 0 8px 32px rgba(61,35,20,.25); color: white;
272
  }
273
  .ps-header h1 {
274
- font-family: 'Playfair Display', serif;
275
- font-size: 2.4rem; margin: 0 0 6px; letter-spacing: .5px;
276
  }
277
  .ps-header p { color: #DDD0BE; font-size: 1.05rem; margin: 0; }
 
278
  label span, .label-wrap span {
279
  font-weight: 500 !important; font-size: .88rem !important;
280
  color: #5C3D1E !important; text-transform: uppercase !important;
@@ -283,16 +478,24 @@ label span, .label-wrap span {
283
  textarea, input[type="text"], input[type="number"] {
284
  background: #FFFFFF !important; border: 1.5px solid #DDD0BE !important;
285
  border-radius: 10px !important; color: #2C1810 !important;
 
286
  }
287
  textarea:focus, input:focus {
288
  border-color: #B8895A !important;
289
  box-shadow: 0 0 0 3px rgba(184,137,90,.12) !important;
290
  }
291
  input[type="range"] { accent-color: #B8895A !important; }
 
 
 
 
 
 
292
  .qs-btn {
293
  background: #F5EFE6 !important; border: 1.5px solid #D4B896 !important;
294
- color: #5C3D1E !important; border-radius: 10px !important;
295
- font-weight: 500 !important; transition: all .2s !important;
 
296
  }
297
  .qs-btn:hover {
298
  background: #EDE0CE !important; border-color: #B8895A !important;
@@ -300,74 +503,37 @@ input[type="range"] { accent-color: #B8895A !important; }
300
  }
301
  .submit-btn {
302
  background: linear-gradient(135deg, #5C3D1E 0%, #8B6239 100%) !important;
303
- color: #FAF7F2 !important; font-size: 1.05rem !important;
304
- font-weight: 600 !important; border: none !important;
305
- border-radius: 12px !important; padding: 14px 28px !important;
306
- width: 100% !important; margin-top: 8px !important;
 
307
  box-shadow: 0 4px 16px rgba(61,35,20,.25) !important;
308
  }
309
  .submit-btn:hover {
310
  background: linear-gradient(135deg, #3D2314 0%, #7A4E2D 100%) !important;
311
  transform: translateY(-1px) !important;
312
  }
313
- .status-bar {
314
- background: #F5EFE6; border: 1px solid #DDD0BE; border-radius: 8px;
315
- padding: 8px 14px; font-size: .82rem; color: #7A4E2D; margin-bottom: 14px;
 
 
 
 
 
 
 
 
 
 
316
  }
317
- .prose, .markdown-body { font-family: 'Inter', sans-serif !important;
318
- color: #2C1810 !important; line-height: 1.7 !important; }
319
- .prose h3 { color: #5C3D1E !important;
320
- border-bottom: 1px solid #DDD0BE; padding-bottom: 4px; }
321
- .prose code { background: #F5EFE6 !important; color: #7A4E2D !important;
322
- border-radius: 4px !important; padding: 1px 5px !important; }
323
  .ps-footer {
324
  text-align: center; color: #A68B6A; font-size: .78rem;
325
  margin-top: 28px; border-top: 1px solid #EDE0CE; padding-top: 14px;
326
  }
327
  """
328
 
329
-
330
-
331
- def _date_html(df="2026-10-15", dt="2026-10-15"):
332
- """Generate HTML for the calendar date range picker."""
333
- label_style = (
334
- "font-size:.88rem;font-weight:500;color:#5C3D1E;"
335
- "text-transform:uppercase;letter-spacing:.4px;margin-bottom:6px;display:block;"
336
- )
337
- input_style = (
338
- "width:100%;padding:9px 12px;border:1.5px solid #DDD0BE;"
339
- "border-radius:10px;background:#fff;color:#2C1810;"
340
- "font-family:Inter,sans-serif;font-size:.95rem;"
341
- "box-sizing:border-box;cursor:pointer;"
342
- "accent-color:#B8895A;"
343
- )
344
- return f"""
345
- <div style="display:flex;gap:16px;margin:4px 0 8px;">
346
- <div style="flex:1;">
347
- <span style="{label_style}">Event Start Date</span>
348
- <input type="date" id="ps_date_from" value="{df}"
349
- style="{input_style}"
350
- oninput="(function(v){{
351
- var el=document.querySelector('#ps_df_hidden');
352
- if(!el)return;
353
- var ta=el.querySelector('textarea')||el.querySelector('input');
354
- if(ta){{ta.value=v;ta.dispatchEvent(new Event('input',{{bubbles:true}}));}}
355
- }})(this.value)">
356
- </div>
357
- <div style="flex:1;">
358
- <span style="{label_style}">Event End Date</span>
359
- <input type="date" id="ps_date_to" value="{dt}"
360
- style="{input_style}"
361
- oninput="(function(v){{
362
- var el=document.querySelector('#ps_dt_hidden');
363
- if(!el)return;
364
- var ta=el.querySelector('textarea')||el.querySelector('input');
365
- if(ta){{ta.value=v;ta.dispatchEvent(new Event('input',{{bubbles:true}}));}}
366
- }})(this.value)">
367
- </div>
368
- </div>
369
- """
370
-
371
  # ================================================================
372
  # UI
373
  # ================================================================
@@ -376,12 +542,13 @@ with gr.Blocks(css=CSS, theme=gr.themes.Base(), title="ProSync AI") as demo:
376
 
377
  gr.HTML("""
378
  <div class="ps-header">
379
- <h1 style="text-align:center;">ProSync AI</h1>
380
- <p>The Event Producer's Command Center β€” intelligent vendor matching &amp; automated document generation</p>
381
  </div>
382
  """)
383
 
384
- gr.Markdown("#### ⚑ Quick Starters β€” click to auto-fill and run")
 
385
  with gr.Row():
386
  qs0 = gr.Button(QUICK_STARTERS[0]["label"], elem_classes=["qs-btn"])
387
  qs1 = gr.Button(QUICK_STARTERS[1]["label"], elem_classes=["qs-btn"])
@@ -391,56 +558,80 @@ with gr.Blocks(css=CSS, theme=gr.themes.Base(), title="ProSync AI") as demo:
391
 
392
  gr.Markdown("---")
393
 
 
394
  brief = gr.Textbox(
395
  label="Describe your event", lines=4,
396
- placeholder="e.g. Tech summit for 400 guests β€” advanced AV, live streaming, "
397
- "kosher catering, VIP security…",
 
 
398
  )
399
  with gr.Row():
400
- city = gr.Dropdown(label="City", choices=CITIES, value="Tel Aviv", allow_custom_value=False)
401
- season = gr.Dropdown(label="Season", choices=SEASONS, value="Summer", allow_custom_value=False)
 
 
 
 
 
 
 
402
  budget = gr.Number(
403
- label="Total Budget (USD)",
404
- value=200_000, minimum=5_000, maximum=2_000_000,
405
  )
406
 
407
  gr.Markdown("---")
408
 
 
409
  with gr.Row():
410
  ev_type = gr.Dropdown(
411
  label="Event Type", choices=EVENT_TYPES, value="Tech Summit",
412
  allow_custom_value=False,
413
  )
414
- guests = gr.Number(label="Guest Count", value=300, minimum=10, maximum=5000)
 
 
 
 
415
  date_picker = gr.HTML(value=_date_html())
416
- date_from = gr.Textbox(value="2026-10-15", visible=False, elem_id="ps_df_hidden")
417
- date_to = gr.Textbox(value="2026-10-15", visible=False, elem_id="ps_dt_hidden")
 
418
  notes = gr.Textbox(
419
  label="Special Requirements",
420
- placeholder="e.g. Kosher catering, black-tie dress code, vegan menu…",
421
  lines=2,
422
  )
 
423
  submit = gr.Button(
424
- "✨ Find Vendors & Generate Documents",
425
  elem_classes=["submit-btn"],
426
  )
427
 
428
  gr.Markdown("---")
 
 
429
  gr.Markdown("### πŸͺ Vendor Matches")
430
  vendor_out = gr.Markdown(
431
- value="_Complete the form above to see recommendations._",
432
  elem_classes=["prose"],
433
  )
434
 
435
- gr.HTML('<div class="ps-footer">ProSync AI Β· Gradio + HuggingFace Β· '
436
- 'MiniLM-L6-v2</div>')
 
 
 
 
 
 
437
 
438
- # ── Wiring ────────────────────────────────────────────────
439
  _in = [brief, city, season, budget, ev_type, date_from, date_to, guests, notes]
440
  _out = [vendor_out]
441
  _form = [brief, city, season, budget, ev_type, date_from, date_to, guests, notes]
442
- _qs_out = [brief, city, season, budget, ev_type, date_from, date_to, guests, notes,
443
- date_picker, vendor_out]
444
 
445
  submit.click(fn=handle_submit, inputs=_in, outputs=_out)
446
  qs0.click(fn=_qs0, outputs=_qs_out)
@@ -450,6 +641,4 @@ with gr.Blocks(css=CSS, theme=gr.themes.Base(), title="ProSync AI") as demo:
450
 
451
 
452
  if __name__ == "__main__":
453
- # server_name="0.0.0.0" is required on HF Spaces β€”
454
- # without it Gradio raises ValueError: When localhost is not accessible
455
  demo.launch(server_name="0.0.0.0", server_port=7860)
 
1
  # ================================================================
2
  # ProSync AI β€” The Event Producer's Command Center
3
+ # Gradio application for Hugging Face Spaces
4
+ #
5
+ # Data source : HF Dataset repo eliel2003/events (vendors file)
6
+ # Embed model : sentence-transformers/all-MiniLM-L6-v2
7
+ # Scoring : 60% semantic similarity + 40% composite quality
8
  # ================================================================
9
 
10
+ import spaces # required by HF GPU Space infrastructure β€” do not remove
11
+
12
  import os
13
+ import io
14
  import json
15
  import warnings
16
 
 
19
  import torch
20
  import gradio as gr
21
  from sentence_transformers import SentenceTransformer, util as st_util
 
22
 
23
  warnings.filterwarnings("ignore")
24
+ os.environ["CUDA_VISIBLE_DEVICES"] = ""
25
+ os.environ["TOKENIZERS_PARALLELISM"] = "false"
26
 
27
+ # Required by HF GPU Space infrastructure β€” satisfies the
28
+ # "@spaces.GPU function detected" startup check.
29
  @spaces.GPU
30
+ def _gpu_stub():
31
  pass
32
 
33
+ # ── Configuration ─────────────────────────────────────────────
34
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
35
+ HF_DATASET = "eliel2003/events"
36
  EMBED_MODEL_ID = "sentence-transformers/all-MiniLM-L6-v2"
37
 
38
+ # ── Domain constants (match notebook exactly) ─────────────────
39
+ ALLOC_RATIOS = {
40
+ "Catering": 0.304,
41
+ "Venue": 0.228,
42
+ "AV_Technology": 0.175,
43
+ "Entertainment": 0.104,
44
+ "Photography_Video": 0.076,
45
+ "Logistics": 0.057,
46
+ "Security": 0.057,
47
+ }
48
+ VENDOR_CATEGORIES = sorted(ALLOC_RATIOS.keys())
49
 
 
 
 
 
50
  CATEGORY_EMOJI = {
51
+ "Catering": "🍽️",
52
+ "AV_Technology": "🎬",
53
+ "Venue": "πŸ›οΈ",
54
+ "Security": "πŸ›‘οΈ",
55
+ "Photography_Video": "πŸ“·",
56
+ "Entertainment": "🎭",
57
+ "Logistics": "🚚",
 
58
  }
59
+
60
+ CITIES = [
61
+ "Beer Sheva", "Haifa", "Herzliya", "Jerusalem",
62
+ "Netanya", "Petah Tikva", "Ramat Gan", "Tel Aviv",
63
+ ]
64
  SEASONS = ["Winter", "Spring", "Summer", "Fall"]
65
  EVENT_TYPES = [
66
+ "Annual Conference", "Award Ceremony", "Bar/Bat Mitzvah",
67
+ "Brand Activation", "Corporate Gala", "Family Reunion",
68
+ "Investor Day", "Private Birthday", "Product Launch",
69
+ "Team Building", "Tech Summit", "Trade Show",
70
  "Wedding", "Workshop Series",
71
  ]
72
+
73
  QUICK_STARTERS = [
74
  {
75
  "label": "πŸ™οΈ Tech Summit Β· Tel Aviv",
76
  "brief": "Large-scale tech summit β€” advanced AV, LED walls, live streaming, "
77
  "kosher catering for 400 guests, VIP executive security.",
78
  "city": "Tel Aviv", "season": "Summer", "budget": 250_000,
79
+ "type": "Tech Summit", "guests": 400, "date": "2026-10-15",
 
80
  "notes": "Kosher catering required. VIP lounge for 30 executives.",
81
  },
82
  {
 
84
  "brief": "Elegant annual corporate gala β€” plated fine dining, live band, "
85
  "professional photography and videography for 200 guests.",
86
  "city": "Jerusalem", "season": "Winter", "budget": 140_000,
87
+ "type": "Corporate Gala", "guests": 200, "date": "2026-12-05",
 
88
  "notes": "Black-tie dress code. Award presentation segment.",
89
  },
90
  {
 
92
  "brief": "Outdoor team building day β€” interactive entertainment, DJ, "
93
  "logistics, casual catering for 150 employees.",
94
  "city": "Haifa", "season": "Spring", "budget": 65_000,
95
+ "type": "Team Building", "guests": 150, "date": "2026-04-22",
 
96
  "notes": "Outdoor venue preferred. Vegetarian options required.",
97
  },
98
  {
 
100
  "brief": "Intimate outdoor wedding β€” elegant catering, DJ, floral design, "
101
  "photography, and logistics for 250 guests.",
102
  "city": "Netanya", "season": "Spring", "budget": 120_000,
103
+ "type": "Wedding", "guests": 250, "date": "2027-05-14",
104
+ "notes": "Chuppah at sunset. Vegan and gluten-free menu options.",
 
105
  },
106
  ]
107
 
108
  # ================================================================
109
+ # DATA LOADING β€” from HF Dataset repo (not local file)
110
  # ================================================================
111
 
112
+ def _safe_to_list(val) -> list:
113
+ """Parse a column value to list regardless of storage type."""
114
  if isinstance(val, list): return val
115
  if isinstance(val, str):
116
  try:
 
119
  except Exception: return []
120
  return []
121
 
122
+
123
+ def _load_vendors() -> pd.DataFrame:
124
+ """
125
+ Load the vendor dataset from HF Dataset repo eliel2003/events.
126
+ Tries three approaches in order:
127
+ 1. datasets.load_dataset (handles private repos via HF_TOKEN)
128
+ 2. hf_hub_download (direct file download)
129
+ 3. pd.read_csv via URL (public repo fallback)
130
+ """
131
+ token = HF_TOKEN or None
132
+
133
+ # ── Approach 1: datasets library ─────────────────────────
134
+ try:
135
+ from datasets import load_dataset
136
+ print("⏳ Trying datasets.load_dataset …")
137
+ ds = load_dataset(HF_DATASET, token=token)
138
+
139
+ # Find the vendors split β€” try common names
140
+ vendor_split = None
141
+ for name in ["vendors", "dataset_b_vendors", "vendor", "train"]:
142
+ if name in ds:
143
+ vendor_split = name
144
+ break
145
+ if vendor_split is None:
146
+ vendor_split = list(ds.keys())[0]
147
+
148
+ df = ds[vendor_split].to_pandas()
149
+
150
+ # If the dataset has both events and vendors in one split,
151
+ # filter to vendor rows using the vendor_id column pattern
152
+ if "vendor_id" not in df.columns and "event_id" in df.columns:
153
+ raise ValueError("Split contains events, not vendors.")
154
+
155
+ print(f"βœ… Loaded {len(df):,} vendors from '{vendor_split}' split.")
156
+ return df
157
+
158
+ except Exception as e1:
159
+ print(f"⚠️ datasets.load_dataset failed: {e1}")
160
+
161
+ # ── Approach 2: hf_hub_download ───────────────────────────
162
+ try:
163
+ from huggingface_hub import hf_hub_download
164
+ print("⏳ Trying hf_hub_download …")
165
+ for fname in ["dataset_b_vendors.csv", "vendors.csv",
166
+ "data/dataset_b_vendors.csv"]:
167
+ try:
168
+ path = hf_hub_download(
169
+ repo_id=HF_DATASET, filename=fname,
170
+ repo_type="dataset", token=token,
171
+ )
172
+ df = pd.read_csv(path)
173
+ print(f"βœ… Loaded {len(df):,} vendors from '{fname}'.")
174
+ return df
175
+ except Exception:
176
+ continue
177
+ except Exception as e2:
178
+ print(f"⚠️ hf_hub_download failed: {e2}")
179
+
180
+ # ── Approach 3: direct URL ────────────────────────────────
181
+ print("⏳ Trying direct CSV URL …")
182
+ base = f"https://huggingface.co/datasets/{HF_DATASET}/resolve/main"
183
+ for fname in ["dataset_b_vendors.csv", "vendors.csv"]:
184
+ try:
185
+ headers = {}
186
+ if token:
187
+ headers["Authorization"] = f"Bearer {token}"
188
+ import urllib.request
189
+ req = urllib.request.Request(f"{base}/{fname}", headers=headers)
190
+ with urllib.request.urlopen(req, timeout=30) as r:
191
+ df = pd.read_csv(io.BytesIO(r.read()))
192
+ print(f"βœ… Loaded {len(df):,} vendors via URL '{fname}'.")
193
+ return df
194
+ except Exception:
195
+ continue
196
+
197
+ raise RuntimeError(
198
+ f"Could not load vendor data from '{HF_DATASET}'. "
199
+ "Make sure the repository is public or set HF_TOKEN as a Space Secret."
200
+ )
201
+
202
+
203
+ def _engineer_features(df: pd.DataFrame) -> pd.DataFrame:
204
+ """Apply the exact same feature engineering as EDA Cell 3."""
205
+ JSON_COLS = ["coverage_cities", "seasonal_availability",
206
+ "specializations", "certifications"]
207
+
208
+ # Parse JSON list columns β€” exclude them from the str.strip() loop
209
+ for col in JSON_COLS:
210
+ df[col] = df[col].apply(_safe_to_list)
211
+
212
+ # Strip whitespace from plain string columns only (not JSON lists)
213
+ for col in df.select_dtypes(include="object").columns:
214
+ if col not in JSON_COLS and col != "vendor_profile_text":
215
+ df[col] = df[col].str.strip()
216
+
217
+ # Strip LLM artifact prefix from profile text
218
+ artifact = "**Vendor Profile:**"
219
+ df["vendor_profile_text"] = (
220
+ df["vendor_profile_text"].astype(str).str.strip()
221
+ .str.removeprefix(artifact).str.strip()
222
+ )
223
+
224
+ # Numeric features
225
+ df["day_rate_mid"] = (df["day_rate_min_usd"] + df["day_rate_max_usd"]) / 2
226
+
227
+ # Composite vendor quality score (mirrors EDA Cell 3 exactly)
228
+ r_min, r_max = df["avg_rating"].min(), df["avg_rating"].max()
229
+ df["rating_norm"] = (df["avg_rating"] - r_min) / (r_max - r_min + 1e-9)
230
+ df["value_score"] = 1 - (df["price_tier"] - 1) / 4
231
+ df["composite_score"] = (
232
+ 0.4 * df["rating_norm"]
233
+ + 0.4 * df["sla_compliance_rate"]
234
+ + 0.2 * df["value_score"]
235
+ )
236
+ return df
237
+
238
+
239
+ # ── Load and prepare data ─────────────────────────────────────
240
+ print("⏳ Loading vendor data from HF Dataset repo …")
241
  try:
242
+ _df = _load_vendors()
243
+ _df = _engineer_features(_df)
244
+
245
+ # Pre-extract arrays for vectorized filtering (Section 13 pattern)
246
+ _VCITIES = [_safe_to_list(v) for v in _df["coverage_cities"]]
247
+ _VSEASONS = [_safe_to_list(v) for v in _df["seasonal_availability"]]
 
 
 
 
 
 
 
248
  _VCATS = _df["category"].values
249
  _VRATES = _df["day_rate_mid"].values
250
  _VCOMP = _df["composite_score"].values
251
  _VIDX = np.arange(len(_df))
252
+ print(f"βœ… {len(_df):,} vendors ready.")
253
+
254
+ except Exception as e:
255
+ print(f"❌ Vendor data load failed: {e}")
256
  _df = None
257
 
258
  # ================================================================
259
+ # EMBEDDING MODEL β€” loaded from HF model repo
260
  # ================================================================
261
 
262
+ print(f"⏳ Loading embedding model ({EMBED_MODEL_ID}) …")
263
  _embed = SentenceTransformer(EMBED_MODEL_ID, device="cpu")
264
 
265
  if _df is not None:
266
+ print("⏳ Encoding vendor profiles …")
267
  _vemb = _embed.encode(
268
  _df["vendor_profile_text"].tolist(),
269
  batch_size=128, show_progress_bar=True,
270
  normalize_embeddings=True, convert_to_tensor=True,
271
  device="cpu",
272
  )
273
+ print(f"βœ… Embeddings ready: {_vemb.shape}")
274
  else:
275
  _vemb = None
276
 
 
 
 
277
  # ================================================================
278
+ # RECOMMENDATION ENGINE
279
+ # Scoring: 60% semantic similarity + 40% composite quality score
280
+ # (mirrors the design choice documented in Section 13 notebook)
281
  # ================================================================
282
 
283
+ def recommend_vendors(
284
+ event_brief: str,
285
+ event_city: str,
286
+ event_season: str,
287
+ total_budget_usd: float,
288
+ top_n: int = 3,
289
+ ) -> dict:
290
+ """
291
+ Stage 1 β€” Vectorized hard filters:
292
+ β€’ City : vendor must cover event_city
293
+ β€’ Season : vendor must be available in event_season
294
+ β€’ Budget : vendor day_rate_mid ≀ category-specific allocation
295
+
296
+ Stage 2 β€” Semantic ranking (60/40 blend):
297
+ final_score = 0.6 Γ— cosine_similarity + 0.4 Γ— composite_score
298
+
299
+ Returns {category: [vendor_dicts]} or {"error": str}.
300
+ """
301
  if _df is None or _vemb is None:
302
+ return {"error": "Vendor data not loaded. Check Space logs."}
303
+ if not event_brief.strip():
304
  return {"error": "Please enter an event description."}
305
 
306
+ # Stage 1: hard filters (vectorized β€” no apply())
307
+ city_ok = np.array([event_city in c for c in _VCITIES], dtype=bool)
308
+ season_ok = np.array([event_season in s for s in _VSEASONS], dtype=bool)
309
+ alloc_vec = np.array(
310
+ [total_budget_usd * ALLOC_RATIOS.get(cat, 0.10) for cat in _VCATS],
311
+ dtype=float,
312
+ )
313
+ budget_ok = _VRATES <= alloc_vec
314
  combined = city_ok & season_ok & budget_ok
315
  pool_idx = _VIDX[combined].tolist()
316
 
317
  if not pool_idx:
318
+ n_c, n_s, n_b = int(city_ok.sum()), int(season_ok.sum()), int(budget_ok.sum())
319
  return {"error": (
320
+ f"No vendors matched all three filters.\n"
321
+ f" City '{event_city}': {n_c} vendors\n"
322
+ f" Season '{event_season}': {n_s} vendors\n"
323
+ f" Budget ${total_budget_usd:,.0f}: {n_b} vendors\n"
324
+ f" Combined: 0 vendors\n\n"
325
+ f"Try increasing the budget or selecting a different city."
326
  )}
327
 
328
+ # Stage 2: semantic similarity
329
+ q_vec = _embed.encode(
330
+ event_brief, convert_to_tensor=True,
331
+ normalize_embeddings=True, device="cpu",
332
+ )
333
+ pool_embeds = _vemb[pool_idx]
334
+ sims = st_util.cos_sim(q_vec, pool_embeds)[0].cpu().numpy()
335
 
336
  pool = _df.iloc[pool_idx].copy().reset_index(drop=True)
337
  pool["similarity"] = sims
338
+ pool["final_score"] = 0.6 * sims + 0.4 * _VCOMP[pool_idx]
339
 
340
  results = {}
341
  for cat in VENDOR_CATEGORIES:
 
348
  ]].to_dict("records")
349
  return results
350
 
351
+ # ================================================================
352
+ # OUTPUT FORMATTER
353
+ # ================================================================
354
 
355
+ def _stars(r: float) -> str:
 
 
356
  n = min(5, max(0, int(round(float(r)))))
357
  return "β˜…" * n + "β˜†" * (5 - n)
358
 
359
 
360
+ def _fmt_vendors(recs: dict, budget: float) -> str:
361
  if "error" in recs:
362
  return f"### ⚠️ No Results\n\n```\n{recs['error']}\n```"
363
+
364
  lines = []
365
  for cat in VENDOR_CATEGORIES:
366
  if cat not in recs: continue
367
  alloc = budget * ALLOC_RATIOS[cat]
368
  cat_name = cat.replace("_", " ")
369
  lines.append(
370
+ f"### {CATEGORY_EMOJI[cat]} {cat_name} "
371
+ f"Β· Budget ceiling: ${alloc:,.0f}\n"
372
  )
373
  for i, v in enumerate(recs[cat], 1):
374
  sp = v.get("specializations", [])
375
  if isinstance(sp, str):
376
  try: sp = json.loads(sp)
377
  except: sp = []
378
+ sc = v.get("final_score", 0)
379
  lines.append(
380
+ f"**#{i} {v['vendor_name']}** \n"
381
  f"{_stars(v.get('avg_rating', 0))} Β· "
382
+ f"{v.get('sla_compliance_rate', 0):.0%} SLA Β· "
383
  f"${v.get('day_rate_mid', 0):,.0f}/day Β· "
384
+ f"Score `{sc:.3f}`\n\n"
385
  f"*{', '.join(sp[:2]) if sp else 'β€”'}*\n"
386
  )
387
  lines.append("---\n")
 
393
 
394
  def handle_submit(brief, city, season, budget, ev_type,
395
  date_from, date_to, guests, notes):
396
+ recs = recommend_vendors(brief, city, season, float(budget))
 
397
  return _fmt_vendors(recs, float(budget))
398
 
399
 
400
+ def _date_html(df="2026-10-15", dt="2026-10-15"):
401
+ """Generate HTML calendar date range picker styled to match the palette."""
402
+ label_css = (
403
+ "font-size:.88rem;font-weight:500;color:#5C3D1E;"
404
+ "text-transform:uppercase;letter-spacing:.4px;"
405
+ "margin-bottom:6px;display:block;"
406
+ )
407
+ input_css = (
408
+ "width:100%;padding:9px 12px;border:1.5px solid #DDD0BE;"
409
+ "border-radius:10px;background:#fff;color:#2C1810;"
410
+ "font-family:Inter,sans-serif;font-size:.95rem;"
411
+ "box-sizing:border-box;cursor:pointer;"
412
+ )
413
+ sync_js = lambda eid: (
414
+ f"(function(v){{"
415
+ f"var el=document.querySelector('#{eid}');"
416
+ f"if(!el)return;"
417
+ f"var t=el.querySelector('textarea')||el.querySelector('input');"
418
+ f"if(t){{t.value=v;t.dispatchEvent(new Event('input',{{bubbles:true}}))}}"
419
+ f"}})(this.value)"
420
+ )
421
+ return f"""
422
+ <div style="display:flex;gap:16px;margin:4px 0 12px;">
423
+ <div style="flex:1;">
424
+ <span style="{label_css}">Event Start Date</span>
425
+ <input type="date" id="ps_df" value="{df}"
426
+ style="{input_css}" oninput="{sync_js('ps_df_hid')}">
427
+ </div>
428
+ <div style="flex:1;">
429
+ <span style="{label_css}">Event End Date</span>
430
+ <input type="date" id="ps_dt" value="{dt}"
431
+ style="{input_css}" oninput="{sync_js('ps_dt_hid')}">
432
+ </div>
433
+ </div>
434
+ """
435
+
436
+
437
  def _qs(idx):
438
  q = QUICK_STARTERS[idx]
439
  b, c, s, bu = q["brief"], q["city"], q["season"], q["budget"]
440
  et, dt = q["type"], q["date"]
441
  gs, nt = q["guests"], q["notes"]
442
  vm = handle_submit(b, c, s, bu, et, dt, dt, gs, nt)
 
443
  return b, c, s, bu, et, dt, dt, gs, nt, _date_html(dt, dt), vm
444
 
445
  def _qs0(): return _qs(0)
 
448
  def _qs3(): return _qs(3)
449
 
450
  # ================================================================
451
+ # CSS β€” WARM BROWN / CREAM / BEIGE PALETTE
452
  # ================================================================
453
 
454
  CSS = """
455
  @import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700&family=Inter:wght@300;400;500;600&display=swap');
456
+
457
  body, .gradio-container {
458
  background-color: #FAF7F2 !important;
459
  font-family: 'Inter', sans-serif !important;
 
461
  }
462
  .ps-header {
463
  background: linear-gradient(135deg, #3D2314 0%, #7A4E2D 60%, #B8895A 100%);
464
+ border-radius: 16px; padding: 36px 40px; margin-bottom: 24px;
465
+ box-shadow: 0 8px 32px rgba(61,35,20,.25); text-align: center;
466
  }
467
  .ps-header h1 {
468
+ font-family: 'Playfair Display', serif; font-size: 2.4rem;
469
+ font-weight: 700; color: #FAF7F2; margin: 0 0 6px; letter-spacing: .5px;
470
  }
471
  .ps-header p { color: #DDD0BE; font-size: 1.05rem; margin: 0; }
472
+
473
  label span, .label-wrap span {
474
  font-weight: 500 !important; font-size: .88rem !important;
475
  color: #5C3D1E !important; text-transform: uppercase !important;
 
478
  textarea, input[type="text"], input[type="number"] {
479
  background: #FFFFFF !important; border: 1.5px solid #DDD0BE !important;
480
  border-radius: 10px !important; color: #2C1810 !important;
481
+ font-family: 'Inter', sans-serif !important; font-size: .95rem !important;
482
  }
483
  textarea:focus, input:focus {
484
  border-color: #B8895A !important;
485
  box-shadow: 0 0 0 3px rgba(184,137,90,.12) !important;
486
  }
487
  input[type="range"] { accent-color: #B8895A !important; }
488
+
489
+ .wrap-inner, .svelte-select {
490
+ background: #FFFFFF !important; border: 1.5px solid #DDD0BE !important;
491
+ border-radius: 10px !important; color: #2C1810 !important;
492
+ }
493
+
494
  .qs-btn {
495
  background: #F5EFE6 !important; border: 1.5px solid #D4B896 !important;
496
+ color: #5C3D1E !important; font-family: 'Inter', sans-serif !important;
497
+ font-weight: 500 !important; border-radius: 10px !important;
498
+ padding: 10px 16px !important; transition: all .2s !important;
499
  }
500
  .qs-btn:hover {
501
  background: #EDE0CE !important; border-color: #B8895A !important;
 
503
  }
504
  .submit-btn {
505
  background: linear-gradient(135deg, #5C3D1E 0%, #8B6239 100%) !important;
506
+ color: #FAF7F2 !important; font-family: 'Inter', sans-serif !important;
507
+ font-size: 1.05rem !important; font-weight: 600 !important;
508
+ border: none !important; border-radius: 12px !important;
509
+ padding: 14px 28px !important; width: 100% !important;
510
+ margin-top: 8px !important;
511
  box-shadow: 0 4px 16px rgba(61,35,20,.25) !important;
512
  }
513
  .submit-btn:hover {
514
  background: linear-gradient(135deg, #3D2314 0%, #7A4E2D 100%) !important;
515
  transform: translateY(-1px) !important;
516
  }
517
+ .prose, .markdown-body {
518
+ font-family: 'Inter', sans-serif !important;
519
+ color: #2C1810 !important; line-height: 1.7 !important;
520
+ }
521
+ .prose h3 {
522
+ font-family: 'Playfair Display', serif !important;
523
+ color: #5C3D1E !important;
524
+ border-bottom: 1px solid #DDD0BE; padding-bottom: 4px;
525
+ }
526
+ .prose hr { border-color: #EDE0CE !important; }
527
+ .prose code {
528
+ background: #F5EFE6 !important; color: #7A4E2D !important;
529
+ border-radius: 4px !important; padding: 1px 5px !important;
530
  }
 
 
 
 
 
 
531
  .ps-footer {
532
  text-align: center; color: #A68B6A; font-size: .78rem;
533
  margin-top: 28px; border-top: 1px solid #EDE0CE; padding-top: 14px;
534
  }
535
  """
536
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
537
  # ================================================================
538
  # UI
539
  # ================================================================
 
542
 
543
  gr.HTML("""
544
  <div class="ps-header">
545
+ <h1>ProSync AI</h1>
546
+ <p>The Event Producer's Command Center β€” intelligent vendor matching</p>
547
  </div>
548
  """)
549
 
550
+ # ── Quick Starters ────────────────────────────────────────
551
+ gr.Markdown("#### ⚑ Quick Starters β€” click to auto-fill and search")
552
  with gr.Row():
553
  qs0 = gr.Button(QUICK_STARTERS[0]["label"], elem_classes=["qs-btn"])
554
  qs1 = gr.Button(QUICK_STARTERS[1]["label"], elem_classes=["qs-btn"])
 
558
 
559
  gr.Markdown("---")
560
 
561
+ # ── Event inputs ─────────────────────────────────────────
562
  brief = gr.Textbox(
563
  label="Describe your event", lines=4,
564
+ placeholder=(
565
+ "e.g. Tech summit for 400 guests β€” advanced AV, live streaming, "
566
+ "kosher catering, VIP security…"
567
+ ),
568
  )
569
  with gr.Row():
570
+ city = gr.Dropdown(
571
+ label="City", choices=CITIES, value="Tel Aviv",
572
+ allow_custom_value=False,
573
+ )
574
+ season = gr.Dropdown(
575
+ label="Season", choices=SEASONS, value="Summer",
576
+ allow_custom_value=False,
577
+ )
578
+
579
  budget = gr.Number(
580
+ label="Total Budget (USD)", value=200_000,
581
+ minimum=5_000, maximum=2_000_000,
582
  )
583
 
584
  gr.Markdown("---")
585
 
586
+ # ── Document settings ─────────────────────────────────────
587
  with gr.Row():
588
  ev_type = gr.Dropdown(
589
  label="Event Type", choices=EVENT_TYPES, value="Tech Summit",
590
  allow_custom_value=False,
591
  )
592
+ guests = gr.Number(
593
+ label="Guest Count", value=300, minimum=10, maximum=5000,
594
+ )
595
+
596
+ # Calendar date range picker (real <input type="date"> elements)
597
  date_picker = gr.HTML(value=_date_html())
598
+ date_from = gr.Textbox(value="2026-10-15", visible=False, elem_id="ps_df_hid")
599
+ date_to = gr.Textbox(value="2026-10-15", visible=False, elem_id="ps_dt_hid")
600
+
601
  notes = gr.Textbox(
602
  label="Special Requirements",
603
+ placeholder="e.g. Kosher catering, black-tie dress code, outdoor setting…",
604
  lines=2,
605
  )
606
+
607
  submit = gr.Button(
608
+ "πŸ” Find Matching Vendors",
609
  elem_classes=["submit-btn"],
610
  )
611
 
612
  gr.Markdown("---")
613
+
614
+ # ── Results ───────────────────────────────────────────────
615
  gr.Markdown("### πŸͺ Vendor Matches")
616
  vendor_out = gr.Markdown(
617
+ value="_Complete the form above and click **Find Matching Vendors**._",
618
  elem_classes=["prose"],
619
  )
620
 
621
+ gr.HTML(
622
+ '<div class="ps-footer">'
623
+ 'ProSync AI Β· Gradio + HuggingFace Β· '
624
+ 'Dataset: eliel2003/events Β· '
625
+ 'Embedding: all-MiniLM-L6-v2 Β· '
626
+ 'Scoring: 60% semantic + 40% quality'
627
+ '</div>'
628
+ )
629
 
630
+ # ── Wiring ───────────────────────────────────────────────
631
  _in = [brief, city, season, budget, ev_type, date_from, date_to, guests, notes]
632
  _out = [vendor_out]
633
  _form = [brief, city, season, budget, ev_type, date_from, date_to, guests, notes]
634
+ _qs_out = _form + [date_picker, vendor_out]
 
635
 
636
  submit.click(fn=handle_submit, inputs=_in, outputs=_out)
637
  qs0.click(fn=_qs0, outputs=_qs_out)
 
641
 
642
 
643
  if __name__ == "__main__":
 
 
644
  demo.launch(server_name="0.0.0.0", server_port=7860)