# ================================================================ # ProSync AI — The Event Producer's Command Center # Gradio application for Hugging Face Spaces # # Data source : HF Dataset repo eliel2003/events (vendors file) # Embed model : sentence-transformers/all-MiniLM-L6-v2 # Scoring : 60% semantic similarity + 40% composite quality # ================================================================ import spaces # required by HF GPU Space infrastructure — do not remove import os import io import json import warnings import numpy as np import pandas as pd import torch import gradio as gr from sentence_transformers import SentenceTransformer, util as st_util warnings.filterwarnings("ignore") os.environ["CUDA_VISIBLE_DEVICES"] = "" os.environ["TOKENIZERS_PARALLELISM"] = "false" # Required by HF GPU Space infrastructure — satisfies the # "@spaces.GPU function detected" startup check. @spaces.GPU def _gpu_stub(): pass # ── Configuration ───────────────────────────────────────────── HF_TOKEN = os.environ.get("HF_TOKEN", "") HF_DATASET = "eliel2003/events" EMBED_MODEL_ID = "sentence-transformers/all-MiniLM-L6-v2" # ── Domain constants (match notebook exactly) ───────────────── ALLOC_RATIOS = { "Catering": 0.304, "Venue": 0.228, "AV_Technology": 0.175, "Entertainment": 0.104, "Photography_Video": 0.076, "Logistics": 0.057, "Security": 0.057, } VENDOR_CATEGORIES = sorted(ALLOC_RATIOS.keys()) CATEGORY_EMOJI = { "Catering": "🍽️", "AV_Technology": "🎬", "Venue": "🏛️", "Security": "🛡️", "Photography_Video": "📷", "Entertainment": "🎭", "Logistics": "🚚", } CITIES = [ "Beer Sheva", "Haifa", "Herzliya", "Jerusalem", "Netanya", "Petah Tikva", "Ramat Gan", "Tel Aviv", ] SEASONS = ["Winter", "Spring", "Summer", "Fall"] EVENT_TYPES = [ "Annual Conference", "Award Ceremony", "Bar/Bat Mitzvah", "Brand Activation", "Corporate Gala", "Family Reunion", "Investor Day", "Private Birthday", "Product Launch", "Team Building", "Tech Summit", "Trade Show", "Wedding", "Workshop Series", ] QUICK_STARTERS = [ { "label": "🏙️ Tech Summit · Tel Aviv", "brief": "Large-scale tech summit — advanced AV, LED walls, live streaming, " "kosher catering for 400 guests, VIP executive security.", "city": "Tel Aviv", "season": "Summer", "budget": 250_000, "type": "Tech Summit", "guests": 400, "date": "2026-10-15", "notes": "Kosher catering required. VIP lounge for 30 executives.", }, { "label": "🍽️ Corporate Gala · Jerusalem", "brief": "Elegant annual corporate gala — plated fine dining, live band, " "professional photography and videography for 200 guests.", "city": "Jerusalem", "season": "Winter", "budget": 140_000, "type": "Corporate Gala", "guests": 200, "date": "2026-12-05", "notes": "Black-tie dress code. Award presentation segment.", }, { "label": "🌿 Team Building · Haifa", "brief": "Outdoor team building day — interactive entertainment, DJ, " "logistics, casual catering for 150 employees.", "city": "Haifa", "season": "Spring", "budget": 65_000, "type": "Team Building", "guests": 150, "date": "2026-04-22", "notes": "Outdoor venue preferred. Vegetarian options required.", }, { "label": "💍 Boutique Wedding · Netanya", "brief": "Intimate outdoor wedding — elegant catering, DJ, floral design, " "photography, and logistics for 250 guests.", "city": "Netanya", "season": "Spring", "budget": 120_000, "type": "Wedding", "guests": 250, "date": "2027-05-14", "notes": "Chuppah at sunset. Vegan and gluten-free menu options.", }, ] # ================================================================ # DATA LOADING — from HF Dataset repo (not local file) # ================================================================ def _safe_to_list(val) -> list: """Parse a column value to list regardless of storage type.""" if isinstance(val, list): return val if isinstance(val, str): try: r = json.loads(val) return r if isinstance(r, list) else [] except Exception: return [] return [] def _load_vendors() -> pd.DataFrame: """ Load the vendor dataset from HF Dataset repo eliel2003/events. Tries three approaches in order: 1. datasets.load_dataset (handles private repos via HF_TOKEN) 2. hf_hub_download (direct file download) 3. pd.read_csv via URL (public repo fallback) """ token = HF_TOKEN or None # ── Approach 1: datasets library ───────────────────────── try: from datasets import load_dataset print("⏳ Trying datasets.load_dataset …") ds = load_dataset(HF_DATASET, token=token) # Find the vendors split — try common names vendor_split = None for name in ["vendors", "dataset_b_vendors", "vendor", "train"]: if name in ds: vendor_split = name break if vendor_split is None: vendor_split = list(ds.keys())[0] df = ds[vendor_split].to_pandas() # If the dataset has both events and vendors in one split, # filter to vendor rows using the vendor_id column pattern if "vendor_id" not in df.columns and "event_id" in df.columns: raise ValueError("Split contains events, not vendors.") print(f"✅ Loaded {len(df):,} vendors from '{vendor_split}' split.") return df except Exception as e1: print(f"⚠️ datasets.load_dataset failed: {e1}") # ── Approach 2: hf_hub_download ─────────────────────────── try: from huggingface_hub import hf_hub_download print("⏳ Trying hf_hub_download …") for fname in ["dataset_b_vendors.csv", "vendors.csv", "data/dataset_b_vendors.csv"]: try: path = hf_hub_download( repo_id=HF_DATASET, filename=fname, repo_type="dataset", token=token, ) df = pd.read_csv(path) print(f"✅ Loaded {len(df):,} vendors from '{fname}'.") return df except Exception: continue except Exception as e2: print(f"⚠️ hf_hub_download failed: {e2}") # ── Approach 3: direct URL ──────────────────────────────── print("⏳ Trying direct CSV URL …") base = f"https://huggingface.co/datasets/{HF_DATASET}/resolve/main" for fname in ["dataset_b_vendors.csv", "vendors.csv"]: try: headers = {} if token: headers["Authorization"] = f"Bearer {token}" import urllib.request req = urllib.request.Request(f"{base}/{fname}", headers=headers) with urllib.request.urlopen(req, timeout=30) as r: df = pd.read_csv(io.BytesIO(r.read())) print(f"✅ Loaded {len(df):,} vendors via URL '{fname}'.") return df except Exception: continue raise RuntimeError( f"Could not load vendor data from '{HF_DATASET}'. " "Make sure the repository is public or set HF_TOKEN as a Space Secret." ) def _engineer_features(df: pd.DataFrame) -> pd.DataFrame: """Apply the exact same feature engineering as EDA Cell 3.""" JSON_COLS = ["coverage_cities", "seasonal_availability", "specializations", "certifications"] # Parse JSON list columns — exclude them from the str.strip() loop for col in JSON_COLS: df[col] = df[col].apply(_safe_to_list) # Strip whitespace from plain string columns only (not JSON lists) for col in df.select_dtypes(include="object").columns: if col not in JSON_COLS and col != "vendor_profile_text": df[col] = df[col].str.strip() # Strip LLM artifact prefix from profile text artifact = "**Vendor Profile:**" df["vendor_profile_text"] = ( df["vendor_profile_text"].astype(str).str.strip() .str.removeprefix(artifact).str.strip() ) # Numeric features df["day_rate_mid"] = (df["day_rate_min_usd"] + df["day_rate_max_usd"]) / 2 # Composite vendor quality score (mirrors EDA Cell 3 exactly) r_min, r_max = df["avg_rating"].min(), df["avg_rating"].max() df["rating_norm"] = (df["avg_rating"] - r_min) / (r_max - r_min + 1e-9) df["value_score"] = 1 - (df["price_tier"] - 1) / 4 df["composite_score"] = ( 0.4 * df["rating_norm"] + 0.4 * df["sla_compliance_rate"] + 0.2 * df["value_score"] ) return df # ── Load and prepare data ───────────────────────────────────── print("⏳ Loading vendor data from HF Dataset repo …") try: _df = _load_vendors() _df = _engineer_features(_df) # Pre-extract arrays for vectorized filtering (Section 13 pattern) _VCITIES = [_safe_to_list(v) for v in _df["coverage_cities"]] _VSEASONS = [_safe_to_list(v) for v in _df["seasonal_availability"]] _VCATS = _df["category"].values _VRATES = _df["day_rate_mid"].values _VCOMP = _df["composite_score"].values _VIDX = np.arange(len(_df)) print(f"✅ {len(_df):,} vendors ready.") except Exception as e: print(f"❌ Vendor data load failed: {e}") _df = None # ================================================================ # EMBEDDING MODEL — loaded from HF model repo # ================================================================ print(f"⏳ Loading embedding model ({EMBED_MODEL_ID}) …") _embed = SentenceTransformer(EMBED_MODEL_ID, device="cpu") if _df is not None: print("⏳ Encoding vendor profiles …") _vemb = _embed.encode( _df["vendor_profile_text"].tolist(), batch_size=128, show_progress_bar=True, normalize_embeddings=True, convert_to_tensor=True, device="cpu", ) print(f"✅ Embeddings ready: {_vemb.shape}") else: _vemb = None # ================================================================ # RECOMMENDATION ENGINE # Scoring: 60% semantic similarity + 40% composite quality score # (mirrors the design choice documented in Section 13 notebook) # ================================================================ def recommend_vendors( event_brief: str, event_city: str, event_season: str, total_budget_usd: float, top_n: int = 3, ) -> dict: """ Stage 1 — Vectorized hard filters: • City : vendor must cover event_city • Season : vendor must be available in event_season • Budget : vendor day_rate_mid ≤ category-specific allocation Stage 2 — Semantic ranking (60/40 blend): final_score = 0.6 × cosine_similarity + 0.4 × composite_score Returns {category: [vendor_dicts]} or {"error": str}. """ if _df is None or _vemb is None: return {"error": "Vendor data not loaded. Check Space logs."} if not event_brief.strip(): return {"error": "Please enter an event description."} # Stage 1: hard filters (vectorized — no apply()) city_ok = np.array([event_city in c for c in _VCITIES], dtype=bool) season_ok = np.array([event_season in s for s in _VSEASONS], dtype=bool) alloc_vec = np.array( [total_budget_usd * ALLOC_RATIOS.get(cat, 0.10) for cat in _VCATS], dtype=float, ) budget_ok = _VRATES <= alloc_vec combined = city_ok & season_ok & budget_ok pool_idx = _VIDX[combined].tolist() if not pool_idx: n_c, n_s, n_b = int(city_ok.sum()), int(season_ok.sum()), int(budget_ok.sum()) return {"error": ( f"No vendors matched all three filters.\n" f" City '{event_city}': {n_c} vendors\n" f" Season '{event_season}': {n_s} vendors\n" f" Budget ${total_budget_usd:,.0f}: {n_b} vendors\n" f" Combined: 0 vendors\n\n" f"Try increasing the budget or selecting a different city." )} # Stage 2: semantic similarity q_vec = _embed.encode( event_brief, convert_to_tensor=True, normalize_embeddings=True, device="cpu", ) pool_embeds = _vemb[pool_idx] sims = st_util.cos_sim(q_vec, pool_embeds)[0].cpu().numpy() pool = _df.iloc[pool_idx].copy().reset_index(drop=True) pool["similarity"] = sims pool["final_score"] = 0.6 * sims + 0.4 * _VCOMP[pool_idx] results = {} for cat in VENDOR_CATEGORIES: sub = pool[pool["category"] == cat].nlargest(top_n, "final_score") if len(sub): results[cat] = sub[[ "vendor_name", "category", "price_tier", "avg_rating", "sla_compliance_rate", "day_rate_mid", "specializations", "similarity", "composite_score", "final_score", ]].to_dict("records") return results # ================================================================ # OUTPUT FORMATTER # ================================================================ def _stars(r: float) -> str: n = min(5, max(0, int(round(float(r))))) return "★" * n + "☆" * (5 - n) def _fmt_vendors(recs: dict, budget: float) -> str: if "error" in recs: return f"### ⚠️ No Results\n\n```\n{recs['error']}\n```" lines = [] for cat in VENDOR_CATEGORIES: if cat not in recs: continue alloc = budget * ALLOC_RATIOS[cat] cat_name = cat.replace("_", " ") lines.append( f"### {CATEGORY_EMOJI[cat]} {cat_name} " f"· Budget ceiling: ${alloc:,.0f}\n" ) for i, v in enumerate(recs[cat], 1): sp = v.get("specializations", []) if isinstance(sp, str): try: sp = json.loads(sp) except: sp = [] sc = v.get("final_score", 0) lines.append( f"**#{i} {v['vendor_name']}** \n" f"{_stars(v.get('avg_rating', 0))} · " f"{v.get('sla_compliance_rate', 0):.0%} SLA · " f"${v.get('day_rate_mid', 0):,.0f}/day · " f"Score `{sc:.3f}`\n\n" f"*{', '.join(sp[:2]) if sp else '—'}*\n" ) lines.append("---\n") return "\n".join(lines) or "_No results._" # ================================================================ # GRADIO HANDLER # ================================================================ def handle_submit(brief, city, season, budget, ev_type, date_from, date_to, guests, notes): recs = recommend_vendors(brief, city, season, float(budget)) return _fmt_vendors(recs, float(budget)) def _date_html(df="2026-10-15", dt="2026-10-15"): """Generate HTML calendar date range picker styled to match the palette.""" label_css = ( "font-size:.88rem;font-weight:500;color:#5C3D1E;" "text-transform:uppercase;letter-spacing:.4px;" "margin-bottom:6px;display:block;" ) input_css = ( "width:100%;padding:9px 12px;border:1.5px solid #DDD0BE;" "border-radius:10px;background:#fff;color:#2C1810;" "font-family:Inter,sans-serif;font-size:.95rem;" "box-sizing:border-box;cursor:pointer;" ) sync_js = lambda eid: ( f"(function(v){{" f"var el=document.querySelector('#{eid}');" f"if(!el)return;" f"var t=el.querySelector('textarea')||el.querySelector('input');" f"if(t){{t.value=v;t.dispatchEvent(new Event('input',{{bubbles:true}}))}}" f"}})(this.value)" ) return f"""
Event Start Date
Event End Date
""" def _qs(idx): q = QUICK_STARTERS[idx] b, c, s, bu = q["brief"], q["city"], q["season"], q["budget"] et, dt = q["type"], q["date"] gs, nt = q["guests"], q["notes"] vm = handle_submit(b, c, s, bu, et, dt, dt, gs, nt) return b, c, s, bu, et, dt, dt, gs, nt, _date_html(dt, dt), vm def _qs0(): return _qs(0) def _qs1(): return _qs(1) def _qs2(): return _qs(2) def _qs3(): return _qs(3) # ================================================================ # CSS — WARM BROWN / CREAM / BEIGE PALETTE # ================================================================ CSS = """ @import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700&family=Inter:wght@300;400;500;600&display=swap'); body, .gradio-container { background-color: #FAF7F2 !important; font-family: 'Inter', sans-serif !important; color: #2C1810 !important; } .ps-header { background: linear-gradient(135deg, #3D2314 0%, #7A4E2D 60%, #B8895A 100%); border-radius: 16px; padding: 36px 40px; margin-bottom: 24px; box-shadow: 0 8px 32px rgba(61,35,20,.25); text-align: center; } .ps-header h1 { font-family: 'Playfair Display', serif; font-size: 2.4rem; font-weight: 700; color: #FAF7F2; margin: 0 0 6px; letter-spacing: .5px; } .ps-header p { color: #DDD0BE; font-size: 1.05rem; margin: 0; } label span, .label-wrap span { font-weight: 500 !important; font-size: .88rem !important; color: #5C3D1E !important; text-transform: uppercase !important; letter-spacing: .4px !important; } textarea, input[type="text"], input[type="number"] { background: #FFFFFF !important; border: 1.5px solid #DDD0BE !important; border-radius: 10px !important; color: #2C1810 !important; font-family: 'Inter', sans-serif !important; font-size: .95rem !important; } textarea:focus, input:focus { border-color: #B8895A !important; box-shadow: 0 0 0 3px rgba(184,137,90,.12) !important; } input[type="range"] { accent-color: #B8895A !important; } .wrap-inner, .svelte-select { background: #FFFFFF !important; border: 1.5px solid #DDD0BE !important; border-radius: 10px !important; color: #2C1810 !important; } .qs-btn { background: #F5EFE6 !important; border: 1.5px solid #D4B896 !important; color: #5C3D1E !important; font-family: 'Inter', sans-serif !important; font-weight: 500 !important; border-radius: 10px !important; padding: 10px 16px !important; transition: all .2s !important; } .qs-btn:hover { background: #EDE0CE !important; border-color: #B8895A !important; transform: translateY(-1px) !important; } .submit-btn { background: linear-gradient(135deg, #5C3D1E 0%, #8B6239 100%) !important; color: #FAF7F2 !important; font-family: 'Inter', sans-serif !important; font-size: 1.05rem !important; font-weight: 600 !important; border: none !important; border-radius: 12px !important; padding: 14px 28px !important; width: 100% !important; margin-top: 8px !important; box-shadow: 0 4px 16px rgba(61,35,20,.25) !important; } .submit-btn:hover { background: linear-gradient(135deg, #3D2314 0%, #7A4E2D 100%) !important; transform: translateY(-1px) !important; } .prose, .markdown-body { font-family: 'Inter', sans-serif !important; color: #2C1810 !important; line-height: 1.7 !important; } .prose h3 { font-family: 'Playfair Display', serif !important; color: #5C3D1E !important; border-bottom: 1px solid #DDD0BE; padding-bottom: 4px; } .prose hr { border-color: #EDE0CE !important; } .prose code { background: #F5EFE6 !important; color: #7A4E2D !important; border-radius: 4px !important; padding: 1px 5px !important; } .ps-footer { text-align: center; color: #A68B6A; font-size: .78rem; margin-top: 28px; border-top: 1px solid #EDE0CE; padding-top: 14px; } """ # ================================================================ # UI # ================================================================ with gr.Blocks(css=CSS, theme=gr.themes.Base(), title="ProSync AI") as demo: gr.HTML("""

ProSync AI

The Event Producer's Command Center — intelligent vendor matching

""") # ── Quick Starters ──────────────────────────────────────── gr.Markdown("#### ⚡ Quick Starters — click to auto-fill and search") with gr.Row(): qs0 = gr.Button(QUICK_STARTERS[0]["label"], elem_classes=["qs-btn"]) qs1 = gr.Button(QUICK_STARTERS[1]["label"], elem_classes=["qs-btn"]) with gr.Row(): qs2 = gr.Button(QUICK_STARTERS[2]["label"], elem_classes=["qs-btn"]) qs3 = gr.Button(QUICK_STARTERS[3]["label"], elem_classes=["qs-btn"]) gr.Markdown("---") # ── Event inputs ───────────────────────────────────────── brief = gr.Textbox( label="Describe your event", lines=4, placeholder=( "e.g. Tech summit for 400 guests — advanced AV, live streaming, " "kosher catering, VIP security…" ), ) with gr.Row(): city = gr.Dropdown( label="City", choices=CITIES, value="Tel Aviv", allow_custom_value=False, ) season = gr.Dropdown( label="Season", choices=SEASONS, value="Summer", allow_custom_value=False, ) budget = gr.Number( label="Total Budget (USD)", value=200_000, minimum=5_000, maximum=2_000_000, ) gr.Markdown("---") # ── Document settings ───────────────────────────────────── with gr.Row(): ev_type = gr.Dropdown( label="Event Type", choices=EVENT_TYPES, value="Tech Summit", allow_custom_value=False, ) guests = gr.Number( label="Guest Count", value=300, minimum=10, maximum=5000, ) # Calendar date range picker (real elements) date_picker = gr.HTML(value=_date_html()) date_from = gr.Textbox(value="2026-10-15", visible=False, elem_id="ps_df_hid") date_to = gr.Textbox(value="2026-10-15", visible=False, elem_id="ps_dt_hid") notes = gr.Textbox( label="Special Requirements", placeholder="e.g. Kosher catering, black-tie dress code, outdoor setting…", lines=2, ) submit = gr.Button( "🔍 Find Matching Vendors", elem_classes=["submit-btn"], ) gr.Markdown("---") # ── Results ─────────────────────────────────────────────── gr.Markdown("### 🏪 Vendor Matches") vendor_out = gr.Markdown( value="_Complete the form above and click **Find Matching Vendors**._", elem_classes=["prose"], ) gr.HTML( '' ) # ── Wiring ─────────────────────────────────────────────── _in = [brief, city, season, budget, ev_type, date_from, date_to, guests, notes] _out = [vendor_out] _form = [brief, city, season, budget, ev_type, date_from, date_to, guests, notes] _qs_out = _form + [date_picker, vendor_out] submit.click(fn=handle_submit, inputs=_in, outputs=_out) qs0.click(fn=_qs0, outputs=_qs_out) qs1.click(fn=_qs1, outputs=_qs_out) qs2.click(fn=_qs2, outputs=_qs_out) qs3.click(fn=_qs3, outputs=_qs_out) if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=7860)