Jadjoueidi commited on
Commit
ad03a6c
·
verified ·
1 Parent(s): 1b6d673

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +269 -211
app.py CHANGED
@@ -1,7 +1,5 @@
1
-
2
  import os
3
  import json
4
- import math
5
  import requests
6
  import pandas as pd
7
  import gradio as gr
@@ -9,17 +7,15 @@ import gradio as gr
9
  APP_NAME = "StayWise AI"
10
 
11
  DATA_FILE = "synthetic_airbnb_project_data.csv"
12
- OUTPUT_FILE = "airbnb_recommendation_output.csv"
13
-
14
- # Optional: set this in Hugging Face Space Secrets as N8N_WEBHOOK_URL
15
  N8N_WEBHOOK_URL = os.getenv("N8N_WEBHOOK_URL", "").strip()
16
 
 
17
  def load_data():
18
- data = pd.read_csv(DATA_FILE)
19
- output = pd.read_csv(OUTPUT_FILE) if os.path.exists(OUTPUT_FILE) else data.copy()
20
- return data, output
 
21
 
22
- df, output_df = load_data()
23
 
24
  def safe_mean(series, default=0):
25
  series = pd.to_numeric(series, errors="coerce").dropna()
@@ -27,79 +23,130 @@ def safe_mean(series, default=0):
27
  return default
28
  return float(series.mean())
29
 
 
30
  def get_choices(column):
31
- if column in df.columns:
32
- return sorted([str(x) for x in df[column].dropna().unique().tolist()])
33
- return []
34
 
35
  neighbourhood_groups = get_choices("neighbourhood_group")
36
- room_types = get_choices("room_type")
37
  neighbourhoods = get_choices("neighbourhood")
38
- seasons = get_choices("season") if "season" in df.columns else ["Low Season", "Shoulder Season", "High Season"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
- def estimate_pipeline(neighbourhood_group, neighbourhood, room_type, price, availability_365, season, local_event_score, synthetic_rating, customer_sentiment_score, send_to_n8n):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  price = float(price)
42
  availability_365 = float(availability_365)
43
  local_event_score = float(local_event_score)
44
- synthetic_rating = float(synthetic_rating)
45
- customer_sentiment_score = float(customer_sentiment_score)
46
 
47
  comparable = df.copy()
48
 
49
- if "neighbourhood_group" in comparable.columns:
50
- comparable = comparable[comparable["neighbourhood_group"].astype(str) == str(neighbourhood_group)]
 
 
51
 
52
- if "room_type" in comparable.columns:
53
- comparable = comparable[comparable["room_type"].astype(str) == str(room_type)]
54
 
55
- if "neighbourhood" in comparable.columns and neighbourhood:
56
- local_comp = comparable[comparable["neighbourhood"].astype(str) == str(neighbourhood)]
57
- if len(local_comp) >= 5:
58
- comparable = local_comp
59
 
60
  if len(comparable) < 5:
61
- comparable = df[df["room_type"].astype(str) == str(room_type)] if "room_type" in df.columns else df.copy()
62
 
63
- competitor_avg_price = safe_mean(comparable.get("price", pd.Series(dtype=float)), default=price)
64
- avg_occupancy = safe_mean(comparable.get("occupancy_rate", pd.Series(dtype=float)), default=0.50)
65
- avg_demand = safe_mean(comparable.get("demand_score", pd.Series(dtype=float)), default=50)
66
- avg_revenue = safe_mean(comparable.get("monthly_revenue", pd.Series(dtype=float)), default=price * 15)
67
 
68
- if competitor_avg_price == 0:
69
- price_vs_competitor_pct = 0
70
- else:
71
- price_vs_competitor_pct = ((price - competitor_avg_price) / competitor_avg_price) * 100
72
 
73
- # Occupancy estimate: base from comparable listings, adjusted for price gap, events, season, rating and sentiment
74
  season_boost = {
75
- "Low": -0.08,
76
  "Low Season": -0.08,
 
 
 
77
  "Medium": 0.00,
78
- "Shoulder Season": 0.00,
79
  "High": 0.08,
80
- "High Season": 0.08,
81
- "Peak": 0.12,
82
- "Peak Season": 0.12
83
  }.get(str(season), 0.00)
84
 
85
- price_penalty = max(min(price_vs_competitor_pct / 100, 0.35), -0.35) * 0.30
86
  event_boost = (local_event_score / 100) * 0.12
87
- rating_boost = (synthetic_rating - 4.0) * 0.06
88
- sentiment_boost = customer_sentiment_score * 0.08
89
 
90
- occupancy_estimate = avg_occupancy + season_boost + event_boost + rating_boost + sentiment_boost - price_penalty
91
- occupancy_estimate = max(0.05, min(0.95, occupancy_estimate))
92
 
93
- booked_nights_month = round(occupancy_estimate * 30)
94
- monthly_revenue = round(price * booked_nights_month, 2)
95
 
96
  demand_score = (
97
- 0.45 * avg_demand
98
- + 0.25 * (occupancy_estimate * 100)
99
- + 0.15 * local_event_score
100
- + 0.10 * ((synthetic_rating / 5) * 100)
101
- + 0.05 * ((customer_sentiment_score + 1) / 2 * 100)
102
  )
 
103
  demand_score = round(max(0, min(100, demand_score)), 1)
104
 
105
  if demand_score >= 70:
@@ -109,37 +156,31 @@ def estimate_pipeline(neighbourhood_group, neighbourhood, room_type, price, avai
109
  else:
110
  demand_level = "Low"
111
 
112
- if price_vs_competitor_pct > 15 and demand_level != "High":
113
  pricing_recommendation = "Consider lowering price"
114
  suggested_price = round(competitor_avg_price * 1.05, 2)
115
- elif price_vs_competitor_pct < -10 and demand_level in ["Medium", "High"]:
 
 
116
  pricing_recommendation = "Consider raising price"
117
  suggested_price = round(min(competitor_avg_price * 0.98, price * 1.12), 2)
 
 
118
  else:
119
  pricing_recommendation = "Keep price stable"
120
  suggested_price = round(price, 2)
 
 
121
 
122
  opportunity_score = round(
123
- (demand_score * 0.45)
124
- + (occupancy_estimate * 100 * 0.25)
125
- + (synthetic_rating / 5 * 100 * 0.15)
126
- + ((customer_sentiment_score + 1) / 2 * 100 * 0.15),
127
  2
128
  )
129
 
130
- if pricing_recommendation == "Consider lowering price":
131
- insight = "The listing appears overpriced compared with similar properties, which may limit occupancy."
132
- next_step = f"Test a lower price around ${suggested_price} and monitor occupancy changes."
133
- elif pricing_recommendation == "Consider raising price":
134
- insight = "The listing appears underpriced relative to demand and comparable properties."
135
- next_step = f"Consider increasing the price toward ${suggested_price} while maintaining review quality."
136
- else:
137
- insight = "The current price is broadly aligned with the comparable market."
138
- next_step = "Keep the price stable and focus on improving visibility, reviews, and conversion."
139
-
140
- final_recommendation = f"{pricing_recommendation}. {next_step}"
141
-
142
- result = {
143
  "app_name": APP_NAME,
144
  "neighbourhood_group": neighbourhood_group,
145
  "neighbourhood": neighbourhood,
@@ -147,168 +188,183 @@ def estimate_pipeline(neighbourhood_group, neighbourhood, room_type, price, avai
147
  "current_price": price,
148
  "suggested_price": suggested_price,
149
  "competitor_avg_price": round(competitor_avg_price, 2),
150
- "price_vs_competitor_pct": round(price_vs_competitor_pct, 2),
151
- "occupancy_estimate": round(occupancy_estimate, 3),
152
- "booked_nights_month": booked_nights_month,
153
  "monthly_revenue": monthly_revenue,
154
  "demand_score": demand_score,
155
  "demand_level": demand_level,
156
  "opportunity_score": opportunity_score,
157
  "pricing_recommendation": pricing_recommendation,
158
  "insight": insight,
159
- "next_step": next_step,
160
- "final_recommendation": final_recommendation
161
  }
162
 
163
- n8n_result = call_n8n(result) if send_to_n8n else {
164
- "status": "not_sent",
165
- "insight": "n8n automation was not triggered.",
166
- "next_step": "Turn on the checkbox and add your N8N_WEBHOOK_URL secret to activate automation.",
167
- "log": "Local pipeline only."
168
- }
 
 
 
169
 
170
- summary_md = f"""
171
- # {APP_NAME} Pipeline Result
172
 
173
- ## Pricing recommendation
174
  **{pricing_recommendation}**
175
 
176
- | Metric | Result |
177
- |---|---:|
178
- | Current price | ${price:,.2f} |
179
- | Suggested price | ${suggested_price:,.2f} |
180
- | Comparable average price | ${competitor_avg_price:,.2f} |
181
- | Price vs competitors | {price_vs_competitor_pct:.2f}% |
182
- | Estimated occupancy | {occupancy_estimate * 100:.1f}% |
183
- | Estimated booked nights/month | {booked_nights_month} |
184
- | Estimated monthly revenue | ${monthly_revenue:,.2f} |
185
- | Demand score | {demand_score}/100 |
186
- | Demand level | {demand_level} |
187
- | Opportunity score | {opportunity_score}/100 |
188
-
189
- ## Business insight
190
  {insight}
191
 
192
- ## Next step
193
  {next_step}
194
  """
195
 
196
- n8n_md = f"""
197
- # Automation Output
198
-
199
- **Status:** {n8n_result.get("status", "unknown")}
200
 
201
- **Insight:** {n8n_result.get("insight", "No insight returned.")}
202
-
203
- **Next step:** {n8n_result.get("next_step", "No next step returned.")}
204
-
205
- **Log:** {n8n_result.get("log", "No log returned.")}
206
  """
207
 
208
- comp_cols = [c for c in ["id", "name", "neighbourhood_group", "neighbourhood", "room_type", "price", "occupancy_rate", "monthly_revenue", "demand_score", "demand_level", "pricing_recommendation"] if c in comparable.columns]
209
- comparable_preview = comparable[comp_cols].head(10).copy() if comp_cols else comparable.head(10).copy()
 
 
 
 
 
 
 
 
 
 
 
210
 
211
- return summary_md, n8n_md, comparable_preview, json.dumps(result, indent=2)
 
212
 
213
- def call_n8n(payload):
214
- if not N8N_WEBHOOK_URL:
215
- return {
216
- "status": "not_configured",
217
- "insight": "No n8n webhook URL has been configured in the Space secrets.",
218
- "next_step": "Add N8N_WEBHOOK_URL in Hugging Face Space Settings → Secrets.",
219
- "log": "Pipeline calculated locally, but automation was not sent."
220
- }
221
 
222
- try:
223
- response = requests.post(N8N_WEBHOOK_URL, json=payload, timeout=15)
224
- if response.status_code >= 200 and response.status_code < 300:
225
- try:
226
- data = response.json()
227
- return {
228
- "status": data.get("status", "success"),
229
- "insight": data.get("insight", "n8n received and processed the recommendation."),
230
- "next_step": data.get("next_step", "Review the stored record or report generated by n8n."),
231
- "log": data.get("log", "Automation completed.")
232
- }
233
- except Exception:
234
- return {
235
- "status": "success",
236
- "insight": "n8n received the recommendation.",
237
- "next_step": "Check the connected output in n8n.",
238
- "log": response.text[:500]
239
- }
240
 
241
- return {
242
- "status": "error",
243
- "insight": f"n8n returned HTTP {response.status_code}.",
244
- "next_step": "Check the webhook URL and the Respond to Webhook node.",
245
- "log": response.text[:500]
246
- }
247
 
248
- except Exception as e:
249
- return {
250
- "status": "error",
251
- "insight": "The app could not reach the n8n workflow.",
252
- "next_step": "Verify that the production webhook URL is active and public.",
253
- "log": str(e)
254
- }
255
 
256
- custom_css = """
257
- .gradio-container {
258
- max-width: 1200px !important;
259
- margin: auto !important;
260
- }
261
- #hero {
262
- background: linear-gradient(135deg, #0f172a 0%, #1e3a8a 55%, #0284c7 100%);
263
- padding: 28px;
264
- border-radius: 22px;
265
- color: white;
266
- margin-bottom: 20px;
267
- }
268
- #hero h1 {
269
- font-size: 42px;
270
- margin-bottom: 6px;
271
- }
272
- #hero p {
273
- font-size: 17px;
274
- opacity: 0.92;
275
- }
276
- .card {
277
- border-radius: 18px;
278
- }
279
- """
280
 
281
- with gr.Blocks(css=custom_css, title=APP_NAME, theme=gr.themes.Soft()) as demo:
282
- gr.HTML(f"""
283
- <div id="hero">
284
- <h1>{APP_NAME}</h1>
285
- <p>AI-powered pricing and performance optimization for short-term rentals.</p>
286
- </div>
287
- """)
288
 
289
  with gr.Row():
290
- with gr.Column(scale=1):
291
  gr.Markdown("## Property Inputs")
292
- neighbourhood_group = gr.Dropdown(neighbourhood_groups, label="Neighbourhood Group", value=neighbourhood_groups[0] if neighbourhood_groups else None)
293
- neighbourhood = gr.Dropdown(neighbourhoods, label="Neighbourhood", value=neighbourhoods[0] if neighbourhoods else None)
294
- room_type = gr.Dropdown(room_types, label="Room Type", value=room_types[0] if room_types else None)
295
- price = gr.Slider(20, 1000, value=150, step=5, label="Current Nightly Price ($)")
296
- availability_365 = gr.Slider(0, 365, value=180, step=1, label="Availability per Year")
297
- season = gr.Dropdown(seasons, label="Season", value=seasons[0] if seasons else None)
298
- local_event_score = gr.Slider(0, 100, value=50, step=1, label="Local Event Demand Score")
299
- synthetic_rating = gr.Slider(1, 5, value=4.4, step=0.1, label="Guest Rating")
300
- customer_sentiment_score = gr.Slider(-1, 1, value=0.2, step=0.05, label="Customer Sentiment Score")
301
- send_to_n8n = gr.Checkbox(label="Send pipeline output to n8n", value=False)
302
- run_btn = gr.Button("Run Full Pipeline", variant="primary")
303
-
304
- with gr.Column(scale=2):
305
- pipeline_output = gr.Markdown(label="Pipeline Result")
306
- n8n_output = gr.Markdown(label="Automation Output")
307
- comparable_table = gr.Dataframe(label="Comparable Listings", interactive=False)
308
- json_output = gr.Code(label="Pipeline JSON Output", language="json")
309
-
310
- run_btn.click(
311
- estimate_pipeline,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
  inputs=[
313
  neighbourhood_group,
314
  neighbourhood,
@@ -317,15 +373,17 @@ with gr.Blocks(css=custom_css, title=APP_NAME, theme=gr.themes.Soft()) as demo:
317
  availability_365,
318
  season,
319
  local_event_score,
320
- synthetic_rating,
321
- customer_sentiment_score,
322
  send_to_n8n
323
  ],
324
- outputs=[pipeline_output, n8n_output, comparable_table, json_output]
 
 
 
 
 
325
  )
326
 
327
- if __name__ == "__main__":
328
- demo.launch(
329
- server_name="0.0.0.0",
330
- server_port=7860
331
- )
 
 
1
  import os
2
  import json
 
3
  import requests
4
  import pandas as pd
5
  import gradio as gr
 
7
  APP_NAME = "StayWise AI"
8
 
9
  DATA_FILE = "synthetic_airbnb_project_data.csv"
 
 
 
10
  N8N_WEBHOOK_URL = os.getenv("N8N_WEBHOOK_URL", "").strip()
11
 
12
+
13
  def load_data():
14
+ return pd.read_csv(DATA_FILE)
15
+
16
+
17
+ df = load_data()
18
 
 
19
 
20
  def safe_mean(series, default=0):
21
  series = pd.to_numeric(series, errors="coerce").dropna()
 
23
  return default
24
  return float(series.mean())
25
 
26
+
27
  def get_choices(column):
28
+ return sorted(df[column].dropna().astype(str).unique().tolist())
29
+
 
30
 
31
  neighbourhood_groups = get_choices("neighbourhood_group")
 
32
  neighbourhoods = get_choices("neighbourhood")
33
+ room_types = get_choices("room_type")
34
+ seasons = get_choices("season") if "season" in df.columns else ["Low Season", "Medium Season", "High Season"]
35
+
36
+
37
+ def call_n8n(payload):
38
+ if not N8N_WEBHOOK_URL:
39
+ return {
40
+ "status": "not_configured",
41
+ "insight": "n8n webhook is not configured yet.",
42
+ "next_step": "Add N8N_WEBHOOK_URL in Hugging Face Space secrets.",
43
+ "log": "Pipeline ran locally only."
44
+ }
45
+
46
+ try:
47
+ response = requests.post(N8N_WEBHOOK_URL, json=payload, timeout=15)
48
 
49
+ if response.status_code >= 200 and response.status_code < 300:
50
+ try:
51
+ data = response.json()
52
+ return {
53
+ "status": data.get("status", "success"),
54
+ "insight": data.get("insight", "n8n processed the recommendation."),
55
+ "next_step": data.get("next_step", "Review the generated automation output."),
56
+ "log": data.get("log", "Automation completed.")
57
+ }
58
+ except Exception:
59
+ return {
60
+ "status": "success",
61
+ "insight": "n8n received the data.",
62
+ "next_step": "Check your n8n workflow output.",
63
+ "log": response.text[:500]
64
+ }
65
+
66
+ return {
67
+ "status": "error",
68
+ "insight": f"n8n returned status code {response.status_code}.",
69
+ "next_step": "Check your webhook and Respond to Webhook node.",
70
+ "log": response.text[:500]
71
+ }
72
+
73
+ except Exception as e:
74
+ return {
75
+ "status": "error",
76
+ "insight": "The app could not reach n8n.",
77
+ "next_step": "Check that the n8n production webhook is active.",
78
+ "log": str(e)
79
+ }
80
+
81
+
82
+ def run_pipeline(
83
+ neighbourhood_group,
84
+ neighbourhood,
85
+ room_type,
86
+ price,
87
+ availability_365,
88
+ season,
89
+ local_event_score,
90
+ rating,
91
+ sentiment_score,
92
+ send_to_n8n
93
+ ):
94
  price = float(price)
95
  availability_365 = float(availability_365)
96
  local_event_score = float(local_event_score)
97
+ rating = float(rating)
98
+ sentiment_score = float(sentiment_score)
99
 
100
  comparable = df.copy()
101
 
102
+ comparable = comparable[
103
+ (comparable["neighbourhood_group"].astype(str) == str(neighbourhood_group)) &
104
+ (comparable["room_type"].astype(str) == str(room_type))
105
+ ]
106
 
107
+ local_comparable = comparable[comparable["neighbourhood"].astype(str) == str(neighbourhood)]
 
108
 
109
+ if len(local_comparable) >= 5:
110
+ comparable = local_comparable
 
 
111
 
112
  if len(comparable) < 5:
113
+ comparable = df[df["room_type"].astype(str) == str(room_type)]
114
 
115
+ competitor_avg_price = safe_mean(comparable["price"], price)
116
+ avg_occupancy = safe_mean(comparable["occupancy_rate"], 0.5)
117
+ avg_demand = safe_mean(comparable["demand_score"], 50)
 
118
 
119
+ price_gap_pct = ((price - competitor_avg_price) / competitor_avg_price) * 100 if competitor_avg_price else 0
 
 
 
120
 
 
121
  season_boost = {
 
122
  "Low Season": -0.08,
123
+ "Medium Season": 0.00,
124
+ "High Season": 0.08,
125
+ "Low": -0.08,
126
  "Medium": 0.00,
 
127
  "High": 0.08,
128
+ "Peak": 0.12
 
 
129
  }.get(str(season), 0.00)
130
 
131
+ price_penalty = max(min(price_gap_pct / 100, 0.35), -0.35) * 0.30
132
  event_boost = (local_event_score / 100) * 0.12
133
+ rating_boost = (rating - 4.0) * 0.06
134
+ sentiment_boost = sentiment_score * 0.08
135
 
136
+ occupancy = avg_occupancy + season_boost + event_boost + rating_boost + sentiment_boost - price_penalty
137
+ occupancy = max(0.05, min(0.95, occupancy))
138
 
139
+ booked_nights = round(occupancy * 30)
140
+ monthly_revenue = round(price * booked_nights, 2)
141
 
142
  demand_score = (
143
+ 0.45 * avg_demand +
144
+ 0.25 * (occupancy * 100) +
145
+ 0.15 * local_event_score +
146
+ 0.10 * ((rating / 5) * 100) +
147
+ 0.05 * ((sentiment_score + 1) / 2 * 100)
148
  )
149
+
150
  demand_score = round(max(0, min(100, demand_score)), 1)
151
 
152
  if demand_score >= 70:
 
156
  else:
157
  demand_level = "Low"
158
 
159
+ if price_gap_pct > 15 and demand_level != "High":
160
  pricing_recommendation = "Consider lowering price"
161
  suggested_price = round(competitor_avg_price * 1.05, 2)
162
+ insight = "The listing appears overpriced compared with similar properties."
163
+ next_step = f"Test a lower price around ${suggested_price} to improve occupancy."
164
+ elif price_gap_pct < -10 and demand_level in ["Medium", "High"]:
165
  pricing_recommendation = "Consider raising price"
166
  suggested_price = round(min(competitor_avg_price * 0.98, price * 1.12), 2)
167
+ insight = "The listing appears underpriced relative to comparable demand."
168
+ next_step = f"Consider increasing the price toward ${suggested_price}."
169
  else:
170
  pricing_recommendation = "Keep price stable"
171
  suggested_price = round(price, 2)
172
+ insight = "The current price is aligned with comparable listings."
173
+ next_step = "Keep price stable and focus on visibility, reviews, and conversion."
174
 
175
  opportunity_score = round(
176
+ demand_score * 0.45 +
177
+ occupancy * 100 * 0.25 +
178
+ rating / 5 * 100 * 0.15 +
179
+ ((sentiment_score + 1) / 2 * 100) * 0.15,
180
  2
181
  )
182
 
183
+ payload = {
 
 
 
 
 
 
 
 
 
 
 
 
184
  "app_name": APP_NAME,
185
  "neighbourhood_group": neighbourhood_group,
186
  "neighbourhood": neighbourhood,
 
188
  "current_price": price,
189
  "suggested_price": suggested_price,
190
  "competitor_avg_price": round(competitor_avg_price, 2),
191
+ "price_vs_competitor_pct": round(price_gap_pct, 2),
192
+ "occupancy_estimate": round(occupancy, 3),
193
+ "booked_nights_month": booked_nights,
194
  "monthly_revenue": monthly_revenue,
195
  "demand_score": demand_score,
196
  "demand_level": demand_level,
197
  "opportunity_score": opportunity_score,
198
  "pricing_recommendation": pricing_recommendation,
199
  "insight": insight,
200
+ "next_step": next_step
 
201
  }
202
 
203
+ if send_to_n8n:
204
+ n8n_response = call_n8n(payload)
205
+ else:
206
+ n8n_response = {
207
+ "status": "not_sent",
208
+ "insight": "n8n automation was not triggered.",
209
+ "next_step": "Tick the n8n checkbox to send this result to the workflow.",
210
+ "log": "Local pipeline only."
211
+ }
212
 
213
+ result_text = f"""
214
+ # StayWise AI Pipeline Result
215
 
216
+ ## Recommendation
217
  **{pricing_recommendation}**
218
 
219
+ ## Key Metrics
220
+
221
+ - Current price: **${price:,.2f}**
222
+ - Suggested price: **${suggested_price:,.2f}**
223
+ - Competitor average price: **${competitor_avg_price:,.2f}**
224
+ - Price vs competitors: **{price_gap_pct:.2f}%**
225
+ - Estimated occupancy: **{occupancy * 100:.1f}%**
226
+ - Estimated booked nights per month: **{booked_nights}**
227
+ - Estimated monthly revenue: **${monthly_revenue:,.2f}**
228
+ - Demand score: **{demand_score}/100**
229
+ - Demand level: **{demand_level}**
230
+ - Opportunity score: **{opportunity_score}/100**
231
+
232
+ ## Business Insight
233
  {insight}
234
 
235
+ ## Next Step
236
  {next_step}
237
  """
238
 
239
+ automation_text = f"""
240
+ # n8n Automation Output
 
 
241
 
242
+ - Status: **{n8n_response.get("status", "unknown")}**
243
+ - Insight: {n8n_response.get("insight", "No insight returned.")}
244
+ - Next step: {n8n_response.get("next_step", "No next step returned.")}
245
+ - Log: {n8n_response.get("log", "No log returned.")}
 
246
  """
247
 
248
+ cols = [
249
+ "id",
250
+ "name",
251
+ "neighbourhood_group",
252
+ "neighbourhood",
253
+ "room_type",
254
+ "price",
255
+ "occupancy_rate",
256
+ "monthly_revenue",
257
+ "demand_score",
258
+ "demand_level",
259
+ "pricing_recommendation"
260
+ ]
261
 
262
+ available_cols = [c for c in cols if c in comparable.columns]
263
+ comparable_table = comparable[available_cols].head(10)
264
 
265
+ json_output = json.dumps(payload, indent=2)
 
 
 
 
 
 
 
266
 
267
+ return result_text, automation_text, comparable_table, json_output
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
 
 
 
 
 
 
 
269
 
270
+ with gr.Blocks() as demo:
271
+ gr.Markdown(
272
+ """
273
+ # 🏠 StayWise AI
 
 
 
274
 
275
+ **AI-powered pricing and performance optimization for short-term rentals.**
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
 
277
+ Enter listing details, run the full pipeline, compare with similar listings, and optionally send the result to n8n.
278
+ """
279
+ )
 
 
 
 
280
 
281
  with gr.Row():
282
+ with gr.Column():
283
  gr.Markdown("## Property Inputs")
284
+
285
+ neighbourhood_group = gr.Dropdown(
286
+ choices=neighbourhood_groups,
287
+ label="Neighbourhood Group",
288
+ value=neighbourhood_groups[0]
289
+ )
290
+
291
+ neighbourhood = gr.Dropdown(
292
+ choices=neighbourhoods,
293
+ label="Neighbourhood",
294
+ value=neighbourhoods[0]
295
+ )
296
+
297
+ room_type = gr.Dropdown(
298
+ choices=room_types,
299
+ label="Room Type",
300
+ value=room_types[0]
301
+ )
302
+
303
+ price = gr.Slider(
304
+ minimum=20,
305
+ maximum=1000,
306
+ value=150,
307
+ step=5,
308
+ label="Current Nightly Price ($)"
309
+ )
310
+
311
+ availability_365 = gr.Slider(
312
+ minimum=0,
313
+ maximum=365,
314
+ value=180,
315
+ step=1,
316
+ label="Availability per Year"
317
+ )
318
+
319
+ season = gr.Dropdown(
320
+ choices=seasons,
321
+ label="Season",
322
+ value=seasons[0]
323
+ )
324
+
325
+ local_event_score = gr.Slider(
326
+ minimum=0,
327
+ maximum=100,
328
+ value=50,
329
+ step=1,
330
+ label="Local Event Demand Score"
331
+ )
332
+
333
+ rating = gr.Slider(
334
+ minimum=1,
335
+ maximum=5,
336
+ value=4.4,
337
+ step=0.1,
338
+ label="Guest Rating"
339
+ )
340
+
341
+ sentiment_score = gr.Slider(
342
+ minimum=-1,
343
+ maximum=1,
344
+ value=0.2,
345
+ step=0.05,
346
+ label="Customer Sentiment Score"
347
+ )
348
+
349
+ send_to_n8n = gr.Checkbox(
350
+ label="Send output to n8n",
351
+ value=False
352
+ )
353
+
354
+ run_button = gr.Button("Run Full Pipeline")
355
+
356
+ with gr.Column():
357
+ result_output = gr.Markdown()
358
+ automation_output = gr.Markdown()
359
+
360
+ gr.Markdown("## Comparable Listings")
361
+ comparable_output = gr.Dataframe()
362
+
363
+ gr.Markdown("## JSON Pipeline Output")
364
+ json_output = gr.Code(language="json")
365
+
366
+ run_button.click(
367
+ fn=run_pipeline,
368
  inputs=[
369
  neighbourhood_group,
370
  neighbourhood,
 
373
  availability_365,
374
  season,
375
  local_event_score,
376
+ rating,
377
+ sentiment_score,
378
  send_to_n8n
379
  ],
380
+ outputs=[
381
+ result_output,
382
+ automation_output,
383
+ comparable_output,
384
+ json_output
385
+ ]
386
  )
387
 
388
+
389
+ demo.launch()