eliel2003's picture
Update app.py
6750912 verified
Raw
History Blame Contribute Delete
24.8 kB
# ================================================================
# 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"""
<div style="display:flex;gap:16px;margin:4px 0 12px;">
<div style="flex:1;">
<span style="{label_css}">Event Start Date</span>
<input type="date" id="ps_df" value="{df}"
style="{input_css}" oninput="{sync_js('ps_df_hid')}">
</div>
<div style="flex:1;">
<span style="{label_css}">Event End Date</span>
<input type="date" id="ps_dt" value="{dt}"
style="{input_css}" oninput="{sync_js('ps_dt_hid')}">
</div>
</div>
"""
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("""
<div class="ps-header">
<h1>ProSync AI</h1>
<p>The Event Producer's Command Center β€” intelligent vendor matching</p>
</div>
""")
# ── 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 <input type="date"> 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(
'<div class="ps-footer">'
'ProSync AI Β· Gradio + HuggingFace Β· '
'Dataset: eliel2003/events Β· '
'Embedding: all-MiniLM-L6-v2 Β· '
'Scoring: 60% semantic + 40% quality'
'</div>'
)
# ── 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)