import os import json import requests import pandas as pd import gradio as gr APP_NAME = "StayWise AI" DATA_FILE = "synthetic_airbnb_project_data.csv" N8N_WEBHOOK_URL = os.getenv("N8N_WEBHOOK_URL", "").strip() def load_data(): return pd.read_csv(DATA_FILE) df = load_data() def safe_mean(series, default=0): series = pd.to_numeric(series, errors="coerce").dropna() if len(series) == 0: return default return float(series.mean()) def get_choices(column): return sorted(df[column].dropna().astype(str).unique().tolist()) neighbourhood_groups = get_choices("neighbourhood_group") neighbourhoods = get_choices("neighbourhood") room_types = get_choices("room_type") seasons = get_choices("season") if "season" in df.columns else ["Low Season", "Medium Season", "High Season"] def call_n8n(payload): if not N8N_WEBHOOK_URL: return { "status": "not_configured", "insight": "n8n webhook is not configured yet.", "next_step": "Add N8N_WEBHOOK_URL in Hugging Face Space secrets.", "log": "Pipeline ran locally only." } try: response = requests.post(N8N_WEBHOOK_URL, json=payload, timeout=15) if response.status_code >= 200 and response.status_code < 300: try: data = response.json() return { "status": data.get("status", "success"), "insight": data.get("insight", "n8n processed the recommendation."), "next_step": data.get("next_step", "Review the generated automation output."), "log": data.get("log", "Automation completed.") } except Exception: return { "status": "success", "insight": "n8n received the data.", "next_step": "Check your n8n workflow output.", "log": response.text[:500] } return { "status": "error", "insight": f"n8n returned status code {response.status_code}.", "next_step": "Check your webhook and Respond to Webhook node.", "log": response.text[:500] } except Exception as e: return { "status": "error", "insight": "The app could not reach n8n.", "next_step": "Check that the n8n production webhook is active.", "log": str(e) } def run_pipeline( neighbourhood_group, neighbourhood, room_type, price, availability_365, season, local_event_score, rating, sentiment_score, send_to_n8n ): price = float(price) availability_365 = float(availability_365) local_event_score = float(local_event_score) rating = float(rating) sentiment_score = float(sentiment_score) comparable = df.copy() comparable = comparable[ (comparable["neighbourhood_group"].astype(str) == str(neighbourhood_group)) & (comparable["room_type"].astype(str) == str(room_type)) ] local_comparable = comparable[comparable["neighbourhood"].astype(str) == str(neighbourhood)] if len(local_comparable) >= 5: comparable = local_comparable if len(comparable) < 5: comparable = df[df["room_type"].astype(str) == str(room_type)] competitor_avg_price = safe_mean(comparable["price"], price) avg_occupancy = safe_mean(comparable["occupancy_rate"], 0.5) avg_demand = safe_mean(comparable["demand_score"], 50) price_gap_pct = ((price - competitor_avg_price) / competitor_avg_price) * 100 if competitor_avg_price else 0 season_boost = { "Low Season": -0.08, "Medium Season": 0.00, "High Season": 0.08, "Low": -0.08, "Medium": 0.00, "High": 0.08, "Peak": 0.12, "Spring": 0.03, "Summer": 0.08, "Autumn": 0.00, "Winter": -0.05 }.get(str(season), 0.00) price_penalty = max(min(price_gap_pct / 100, 0.35), -0.35) * 0.30 event_boost = (local_event_score / 100) * 0.12 rating_boost = (rating - 4.0) * 0.06 sentiment_boost = sentiment_score * 0.08 occupancy = avg_occupancy + season_boost + event_boost + rating_boost + sentiment_boost - price_penalty occupancy = max(0.05, min(0.95, occupancy)) booked_nights = round(occupancy * 30) monthly_revenue = round(price * booked_nights, 2) demand_score = ( 0.45 * avg_demand + 0.25 * (occupancy * 100) + 0.15 * local_event_score + 0.10 * ((rating / 5) * 100) + 0.05 * ((sentiment_score + 1) / 2 * 100) ) demand_score = round(max(0, min(100, demand_score)), 1) if demand_score >= 70: demand_level = "High" demand_badge = "๐ŸŸข High" elif demand_score >= 45: demand_level = "Medium" demand_badge = "๐ŸŸก Medium" else: demand_level = "Low" demand_badge = "๐Ÿ”ด Low" if price_gap_pct > 15 and demand_level != "High": pricing_recommendation = "Consider lowering price" suggested_price = round(competitor_avg_price * 1.05, 2) insight = "The listing appears overpriced compared with similar properties." next_step = f"Test a lower price around ${suggested_price} to improve occupancy." recommendation_badge = "๐Ÿ”ป Price Reduction Suggested" elif price_gap_pct < -10 and demand_level in ["Medium", "High"]: pricing_recommendation = "Consider raising price" suggested_price = round(min(competitor_avg_price * 0.98, price * 1.12), 2) insight = "The listing appears underpriced relative to comparable demand." next_step = f"Consider increasing the price toward ${suggested_price}." recommendation_badge = "๐Ÿš€ Revenue Opportunity" else: pricing_recommendation = "Keep price stable" suggested_price = round(price, 2) insight = "The current price is aligned with comparable listings." next_step = "Keep price stable and focus on visibility, reviews, and conversion." recommendation_badge = "โœ… Stable Positioning" opportunity_score = round( demand_score * 0.45 + occupancy * 100 * 0.25 + rating / 5 * 100 * 0.15 + ((sentiment_score + 1) / 2 * 100) * 0.15, 2 ) payload = { "app_name": APP_NAME, "neighbourhood_group": neighbourhood_group, "neighbourhood": neighbourhood, "room_type": room_type, "current_price": price, "suggested_price": suggested_price, "competitor_avg_price": round(competitor_avg_price, 2), "price_vs_competitor_pct": round(price_gap_pct, 2), "occupancy_estimate": round(occupancy, 3), "booked_nights_month": booked_nights, "monthly_revenue": monthly_revenue, "demand_score": demand_score, "demand_level": demand_level, "opportunity_score": opportunity_score, "pricing_recommendation": pricing_recommendation, "insight": insight, "next_step": next_step } if send_to_n8n: n8n_response = call_n8n(payload) else: n8n_response = { "status": "not_sent", "insight": "n8n automation was not triggered.", "next_step": "Tick the n8n checkbox to send this result to the workflow.", "log": "Local pipeline only." } result_text = f"""
Pipeline Result

{recommendation_badge}

Current Price
${price:,.0f}
Suggested Price
${suggested_price:,.0f}
Monthly Revenue
${monthly_revenue:,.0f}
Demand
{demand_badge}

Key Metrics

Competitor average price${competitor_avg_price:,.2f}
Price vs competitors{price_gap_pct:.2f}%
Estimated occupancy{occupancy * 100:.1f}%
Estimated booked nights / month{booked_nights}
Demand score{demand_score}/100
Opportunity score{opportunity_score}/100

Business Insight

{insight}

Next Step

{next_step}

""" automation_text = f"""
n8n Automation Output

Automation Status: {n8n_response.get("status", "unknown")}

Insight{n8n_response.get("insight", "No insight returned.")}
Next step{n8n_response.get("next_step", "No next step returned.")}
Log{n8n_response.get("log", "No log returned.")}
""" cols = [ "id", "name", "neighbourhood_group", "neighbourhood", "room_type", "price", "occupancy_rate", "monthly_revenue", "demand_score", "demand_level", "pricing_recommendation" ] available_cols = [c for c in cols if c in comparable.columns] comparable_table = comparable[available_cols].head(10) json_output = json.dumps(payload, indent=2) return result_text, automation_text, comparable_table, json_output custom_css = """ /* Main background */ body { background: radial-gradient(circle at top left, #1e3a8a 0%, #0f172a 35%, #020617 100%) !important; color: #f8fafc !important; } /* Main container */ .gradio-container { max-width: 1250px !important; margin: auto !important; font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, sans-serif !important; } /* Header */ #hero { background: linear-gradient(135deg, rgba(37, 99, 235, 0.95), rgba(14, 165, 233, 0.85)); border-radius: 28px; padding: 38px 42px; margin-bottom: 26px; box-shadow: 0 25px 60px rgba(0, 0, 0, 0.35); border: 1px solid rgba(255,255,255,0.16); } #hero h1 { font-size: 48px; margin: 0 0 8px 0; color: white; letter-spacing: -1px; } #hero p { font-size: 18px; margin: 6px 0 0 0; color: #e0f2fe; } .hero-badge { display: inline-block; padding: 6px 12px; background: rgba(255,255,255,0.16); border: 1px solid rgba(255,255,255,0.22); border-radius: 999px; font-size: 13px; margin-bottom: 14px; color: white; } /* Panels */ .gr-block, .gr-form, .gr-box, .gr-panel { border-radius: 20px !important; } .input-panel { background: rgba(15, 23, 42, 0.82); border: 1px solid rgba(148, 163, 184, 0.25); border-radius: 24px; padding: 20px; box-shadow: 0 15px 45px rgba(0,0,0,0.25); } /* Buttons */ button.primary, .gr-button { border-radius: 14px !important; font-weight: 700 !important; background: linear-gradient(90deg, #2563eb, #06b6d4) !important; border: none !important; color: white !important; box-shadow: 0 10px 25px rgba(37, 99, 235, 0.32) !important; } button.primary:hover, .gr-button:hover { transform: translateY(-1px); filter: brightness(1.08); } /* Labels */ label, .wrap label { color: #dbeafe !important; font-weight: 600 !important; } /* Inputs */ input, textarea, select { border-radius: 12px !important; } /* Slider */ input[type="range"] { accent-color: #38bdf8 !important; } /* Output cards */ .result-card, .automation-card { background: rgba(15, 23, 42, 0.88); border: 1px solid rgba(148, 163, 184, 0.25); border-radius: 26px; padding: 26px; margin-bottom: 22px; box-shadow: 0 18px 50px rgba(0,0,0,0.28); } .result-card h2, .automation-card h2 { margin-top: 8px; color: #f8fafc; font-size: 26px; } .result-card h3, .automation-card h3 { margin-top: 24px; color: #bae6fd; font-size: 20px; } .result-card p, .automation-card p { color: #e5e7eb; line-height: 1.6; } .section-label { display: inline-block; background: rgba(56, 189, 248, 0.13); border: 1px solid rgba(56, 189, 248, 0.35); color: #7dd3fc; padding: 7px 13px; border-radius: 999px; font-size: 13px; font-weight: 700; letter-spacing: 0.4px; text-transform: uppercase; } /* KPI Cards */ .kpi-grid { display: grid; grid-template-columns: repeat(4, minmax(120px, 1fr)); gap: 14px; margin: 22px 0; } .kpi-card { background: linear-gradient(180deg, rgba(30, 41, 59, 0.95), rgba(15, 23, 42, 0.95)); border: 1px solid rgba(148, 163, 184, 0.25); border-radius: 18px; padding: 18px; } .kpi-title { color: #94a3b8; font-size: 13px; margin-bottom: 8px; } .kpi-value { color: #f8fafc; font-size: 22px; font-weight: 800; } /* Tables */ .metric-table { width: 100%; border-collapse: collapse; overflow: hidden; border-radius: 14px; margin-top: 12px; } .metric-table td { padding: 12px 14px; border-bottom: 1px solid rgba(148, 163, 184, 0.18); color: #e5e7eb; } .metric-table td:first-child { color: #93c5fd; font-weight: 650; width: 45%; } .metric-table tr:last-child td { border-bottom: none; } /* Dataframe and code areas */ .dataframe, .wrap, .contain { border-radius: 18px !important; } /* Footer text */ .small-note { color: #94a3b8; font-size: 13px; margin-top: -8px; margin-bottom: 18px; } @media (max-width: 900px) { .kpi-grid { grid-template-columns: repeat(2, minmax(120px, 1fr)); } #hero h1 { font-size: 36px; } } """ with gr.Blocks(css=custom_css) as demo: gr.HTML( """
Short-Term Rental Pricing Assistant

๐Ÿ  StayWise AI

AI-powered pricing and performance optimization for short-term rentals.

Run the full pipeline, benchmark a listing against comparable properties, and return automation insights from n8n.

""" ) with gr.Row(): with gr.Column(scale=1): gr.Markdown("## ๐Ÿ“Š Property Inputs") neighbourhood_group = gr.Dropdown( choices=neighbourhood_groups, label="Neighbourhood Group", value=neighbourhood_groups[0] ) neighbourhood = gr.Dropdown( choices=neighbourhoods, label="Neighbourhood", value=neighbourhoods[0] ) room_type = gr.Dropdown( choices=room_types, label="Room Type", value=room_types[0] ) price = gr.Slider( minimum=20, maximum=1000, value=150, step=5, label="Current Nightly Price ($)" ) availability_365 = gr.Slider( minimum=0, maximum=365, value=180, step=1, label="Availability per Year" ) season = gr.Dropdown( choices=seasons, label="Season", value=seasons[0] ) local_event_score = gr.Slider( minimum=0, maximum=100, value=50, step=1, label="Local Event Demand Score" ) rating = gr.Slider( minimum=1, maximum=5, value=4.4, step=0.1, label="Guest Rating" ) sentiment_score = gr.Slider( minimum=-1, maximum=1, value=0.2, step=0.05, label="Customer Sentiment Score" ) send_to_n8n = gr.Checkbox( label="Send output to n8n", value=False ) run_button = gr.Button("๐Ÿš€ Run Full Pipeline", variant="primary") with gr.Column(scale=2): result_output = gr.HTML() automation_output = gr.HTML() gr.Markdown("## ๐Ÿ“ˆ Comparable Listings") comparable_output = gr.Dataframe() gr.Markdown("## ๐Ÿงพ Pipeline Data Output") json_output = gr.Code(language="json") run_button.click( fn=run_pipeline, inputs=[ neighbourhood_group, neighbourhood, room_type, price, availability_365, season, local_event_score, rating, sentiment_score, send_to_n8n ], outputs=[ result_output, automation_output, comparable_output, json_output ] ) demo.launch()