# ================================================================ # 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"""
The Event Producer's Command Center — intelligent vendor matching