Danielos100 commited on
Commit
d30a052
Β·
verified Β·
1 Parent(s): 6966c21

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +56 -56
app.py CHANGED
@@ -13,7 +13,7 @@ from sklearn.neighbors import NearestNeighbors
13
  import gradio as gr
14
 
15
  # ============= Config =============
16
- USE_EMBEDDINGS = False # keep False for now (baseline). Can upgrade later.
17
  MAX_ROWS = int(os.getenv("MAX_ROWS", "5000"))
18
  DEFAULT_OCCASIONS = "birthday, thank_you, housewarming"
19
 
@@ -43,42 +43,31 @@ def _to_price_usd(x):
43
  except: return np.nan
44
 
45
  def _infer_age_from_category(cat: str) -> str:
46
- """Very simple heuristic: map Amazon categories to an age_range tag."""
47
  s = (cat or "").lower()
48
- if any(k in s for k in ["baby", "toddler", "infant"]):
49
- return "kids"
50
- if "toys & games" in s or "board games" in s or "toy" in s:
51
- return "kids"
52
- if any(k in s for k in ["teen", "ya", "young adult"]):
53
- return "teens"
54
- # otherwise
55
  return "any"
56
 
57
  def map_amazon_to_schema(df_raw: pd.DataFrame) -> pd.DataFrame:
58
  cols = {c.lower().strip(): c for c in df_raw.columns}
59
  get = lambda key: df_raw.get(cols.get(key, ""), "")
60
-
61
  out = pd.DataFrame({
62
  "name": get("product name"),
63
  "short_desc": get("description"),
64
  "tags": get("category"),
65
  "price_usd": get("selling price").map(_to_price_usd) if "selling price" in cols else np.nan,
66
- "age_range": "", # fill by inference below
67
  "gender_tags": "any",
68
  "occasion_tags": DEFAULT_OCCASIONS,
69
  "persona_fit": get("category"),
70
  "image_url": get("image") if "image" in cols else "",
71
  })
72
-
73
- # clean
74
- out["name"] = out["name"].astype(str).str.strip().str.slice(0, 120)
75
- out["short_desc"] = out["short_desc"].astype(str).str.strip().str.slice(0, 400)
76
  out["tags"] = out["tags"].astype(str).str.replace("|", ", ").str.lower()
77
  out["persona_fit"] = out["persona_fit"].astype(str).str.lower()
78
-
79
- # age inference
80
  out["age_range"] = out["tags"].map(_infer_age_from_category).fillna("any")
81
-
82
  return out
83
 
84
  def build_doc(row: pd.Series) -> str:
@@ -97,7 +86,6 @@ def load_catalog() -> pd.DataFrame:
97
  ds = load_dataset("ckandemir/amazon-products", split="train")
98
  raw = ds.to_pandas()
99
  except Exception:
100
- # tiny fallback so the app doesn't crash offline
101
  raw = pd.DataFrame({
102
  "Product Name": ["Wireless Earbuds", "Coffee Sampler", "Strategy Board Game"],
103
  "Description": [
@@ -144,11 +132,11 @@ def filter_business(df: pd.DataFrame, budget_min=None, budget_max=None,
144
  if occasion:
145
  m &= _contains_ci(df["occasion_tags"], occasion)
146
  if age_range and age_range != "any":
147
- # keep items explicitly tagged as this age OR 'any'
148
  m &= (df["age_range"].fillna("any").isin([age_range, "any"]))
149
  return df[m]
150
 
151
  def recommend_topk(profile: Dict, k: int=3) -> pd.DataFrame:
 
152
  q = profile_to_query(profile)
153
  q_vec = _vectorizer.transform([q])
154
 
@@ -162,24 +150,37 @@ def recommend_topk(profile: Dict, k: int=3) -> pd.DataFrame:
162
  if df_f.empty:
163
  df_f = CATALOG
164
 
165
- idx = df_f.index.values
166
- dists, inds = _nn.kneighbors(q_vec, n_neighbors=min(max(k*4,k), len(df_f)))
167
- cand_idx = idx[inds[0]]
 
168
  d = dists[0]
169
- order = np.argsort(d)
170
- cand_idx = cand_idx[order]
171
- d = d[order]
172
-
173
  seen, picks = set(), []
174
- for ci, dist in zip(cand_idx, d):
175
- nm = CATALOG.loc[ci, "name"]
176
- if nm in seen: continue
 
 
 
177
  seen.add(nm)
178
- picks.append((ci, 1 - float(dist)))
179
- if len(picks) >= k: break
180
-
181
- res = CATALOG.loc[[ci for ci,_ in picks]].copy()
182
- res["similarity"] = [sim for _,sim in picks]
 
 
 
 
 
 
 
 
 
 
 
 
183
  return res[["name","short_desc","price_usd","occasion_tags","persona_fit","age_range","image_url","similarity"]]
184
 
185
  # ============= Synthetic item + message =============
@@ -200,15 +201,12 @@ def generate_item(profile: Dict) -> Dict:
200
  else:
201
  base_name = f"{core.title()} starter bundle"
202
  base_desc = f"A curated set to kickstart their {core} passion."
203
-
204
- # small age tweak
205
  if age == "kids":
206
  base_desc += " Suitable for kids with safe, age-appropriate materials."
207
  elif age == "teens":
208
  base_desc += " Trendy pick that suits young enthusiasts."
209
  elif age == "senior":
210
  base_desc += " Comfortable and easy to use."
211
-
212
  price = float(np.clip(float(budget), 10, 250))
213
  return {
214
  "name": f"{base_name} ({occasion})",
@@ -230,26 +228,29 @@ def generate_message(profile: Dict) -> str:
230
 
231
  # ============= Gradio UI (GIfty) =============
232
  EXAMPLES = [
233
- [["reading","travel","aesthetic"], "birthday", 20, 60, "Noa", "adult", "warm and friendly"],
234
- [["coffee","home","practical"], "housewarming", 25, 45, "Daniel", "adult", "warm"],
235
- [["tech","photography" if "photography" in INTEREST_OPTIONS else "digital"], "birthday", 30, 120, "Omer", "teen (13–17)", "fun"],
236
  ]
237
 
238
- def ui_predict(interests_list: List[str], occasion: str, budget_min: float, budget_max: float,
239
- recipient_name: str, age_label: str, tone: str):
240
  try:
241
- age_range = AGE_OPTIONS.get(age_label, "any")
242
- # Ensure min <= max
243
- if budget_min is not None and budget_max is not None and budget_min > budget_max:
 
 
 
244
  budget_min, budget_max = budget_max, budget_min
245
 
 
246
  profile = {
247
  "recipient_name": recipient_name or "Friend",
248
  "interests": interests_list or [],
249
  "occasion": occasion or "birthday",
250
- "budget_min": float(budget_min) if budget_min is not None else None,
251
- "budget_max": float(budget_max) if budget_max is not None else None,
252
- "budget_usd": float(budget_max) if budget_max is not None else 50.0,
253
  "age_range": age_range,
254
  "tone": tone or "warm and friendly",
255
  }
@@ -262,7 +263,6 @@ def ui_predict(interests_list: List[str], occasion: str, budget_min: float, budg
262
  gen_md = f"**{gen['name']}**\n\n{gen['short_desc']}\n\n~${gen['price_usd']:.0f}"
263
  return top3_md, gen_md, msg
264
  except Exception as e:
265
- # Return error message in the UI instead of "nothing happens"
266
  return f":warning: Error: {e}", "", ""
267
 
268
  with gr.Blocks() as demo:
@@ -276,9 +276,10 @@ with gr.Blocks() as demo:
276
  with gr.Row():
277
  occasion = gr.Dropdown(label="Occasion", choices=OCCASION_OPTIONS, value="birthday")
278
  age = gr.Dropdown(label="Age group", choices=list(AGE_OPTIONS.keys()), value="adult (18–64)")
279
- with gr.Row():
280
- budget_min = gr.Slider(label="Budget min (USD)", minimum=5, maximum=300, step=1, value=20)
281
- budget_max = gr.Slider(label="Budget max (USD)", minimum=10, maximum=500, step=1, value=60)
 
282
  with gr.Row():
283
  recipient_name = gr.Textbox(label="Recipient name", value="Noa")
284
  tone = gr.Textbox(label="Message tone", value="warm and friendly")
@@ -290,16 +291,15 @@ with gr.Blocks() as demo:
290
 
291
  gr.Examples(
292
  EXAMPLES,
293
- [interests, occasion, budget_min, budget_max, recipient_name, age, tone],
294
  label="Quick examples",
295
  )
296
 
297
  go.click(
298
  ui_predict,
299
- [interests, occasion, budget_min, budget_max, recipient_name, age, tone],
300
  [out_top3, out_gen, out_msg]
301
  )
302
 
303
- # For Spaces
304
  if __name__ == "__main__":
305
  demo.launch()
 
13
  import gradio as gr
14
 
15
  # ============= Config =============
16
+ USE_EMBEDDINGS = False
17
  MAX_ROWS = int(os.getenv("MAX_ROWS", "5000"))
18
  DEFAULT_OCCASIONS = "birthday, thank_you, housewarming"
19
 
 
43
  except: return np.nan
44
 
45
  def _infer_age_from_category(cat: str) -> str:
 
46
  s = (cat or "").lower()
47
+ if any(k in s for k in ["baby", "toddler", "infant"]): return "kids"
48
+ if "toys & games" in s or "board games" in s or "toy" in s: return "kids"
49
+ if any(k in s for k in ["teen", "ya", "young adult"]): return "teens"
 
 
 
 
50
  return "any"
51
 
52
  def map_amazon_to_schema(df_raw: pd.DataFrame) -> pd.DataFrame:
53
  cols = {c.lower().strip(): c for c in df_raw.columns}
54
  get = lambda key: df_raw.get(cols.get(key, ""), "")
 
55
  out = pd.DataFrame({
56
  "name": get("product name"),
57
  "short_desc": get("description"),
58
  "tags": get("category"),
59
  "price_usd": get("selling price").map(_to_price_usd) if "selling price" in cols else np.nan,
60
+ "age_range": "",
61
  "gender_tags": "any",
62
  "occasion_tags": DEFAULT_OCCASIONS,
63
  "persona_fit": get("category"),
64
  "image_url": get("image") if "image" in cols else "",
65
  })
66
+ out["name"] = out["name"].astype(str).str.strip().str.slice(0,120)
67
+ out["short_desc"] = out["short_desc"].astype(str).str.strip().str.slice(0,400)
 
 
68
  out["tags"] = out["tags"].astype(str).str.replace("|", ", ").str.lower()
69
  out["persona_fit"] = out["persona_fit"].astype(str).str.lower()
 
 
70
  out["age_range"] = out["tags"].map(_infer_age_from_category).fillna("any")
 
71
  return out
72
 
73
  def build_doc(row: pd.Series) -> str:
 
86
  ds = load_dataset("ckandemir/amazon-products", split="train")
87
  raw = ds.to_pandas()
88
  except Exception:
 
89
  raw = pd.DataFrame({
90
  "Product Name": ["Wireless Earbuds", "Coffee Sampler", "Strategy Board Game"],
91
  "Description": [
 
132
  if occasion:
133
  m &= _contains_ci(df["occasion_tags"], occasion)
134
  if age_range and age_range != "any":
 
135
  m &= (df["age_range"].fillna("any").isin([age_range, "any"]))
136
  return df[m]
137
 
138
  def recommend_topk(profile: Dict, k: int=3) -> pd.DataFrame:
139
+ """Global kNN β†’ filter to the business subset (fixes index mismatch)."""
140
  q = profile_to_query(profile)
141
  q_vec = _vectorizer.transform([q])
142
 
 
150
  if df_f.empty:
151
  df_f = CATALOG
152
 
153
+ # Search on the global index, then keep only rows inside df_f
154
+ n_cand = min(max(k*50, k), len(CATALOG))
155
+ dists, inds = _nn.kneighbors(q_vec, n_neighbors=n_cand)
156
+ cand_global = inds[0] # indices in CATALOG
157
  d = dists[0]
158
+ order = np.argsort(d) # ascending distance
 
 
 
159
  seen, picks = set(), []
160
+ for gi in cand_global[order]:
161
+ if gi not in df_f.index: # keep only filtered subset
162
+ continue
163
+ nm = CATALOG.loc[gi, "name"]
164
+ if nm in seen:
165
+ continue
166
  seen.add(nm)
167
+ # similarity = 1 - distance
168
+ sim = 1 - float(_nn.kneighbors_graph(q_vec, n_neighbors=1, mode="distance")[0, gi]) if False else 1.0
169
+ # we already have distances in d; recompute sim from them using same order index:
170
+ # get distance for this gi:
171
+ # (for simplicity we just set sim to 1 - current min distance; not critical for UI ranking)
172
+ picks.append((gi, None))
173
+ if len(picks) >= k:
174
+ break
175
+
176
+ if not picks:
177
+ return df_f.head(k).assign(similarity=np.nan)[["name","short_desc","price_usd","occasion_tags","persona_fit","age_range","image_url","similarity"]]
178
+
179
+ sel = [gi for gi,_ in picks]
180
+ res = CATALOG.loc[sel].copy()
181
+ # compute similarity from the original distances vector for display
182
+ gi_to_dist = {int(gi): float(dist) for gi, dist in zip(cand_global, d)}
183
+ res["similarity"] = [1.0 - gi_to_dist.get(int(gi), 0.0) for gi in sel]
184
  return res[["name","short_desc","price_usd","occasion_tags","persona_fit","age_range","image_url","similarity"]]
185
 
186
  # ============= Synthetic item + message =============
 
201
  else:
202
  base_name = f"{core.title()} starter bundle"
203
  base_desc = f"A curated set to kickstart their {core} passion."
 
 
204
  if age == "kids":
205
  base_desc += " Suitable for kids with safe, age-appropriate materials."
206
  elif age == "teens":
207
  base_desc += " Trendy pick that suits young enthusiasts."
208
  elif age == "senior":
209
  base_desc += " Comfortable and easy to use."
 
210
  price = float(np.clip(float(budget), 10, 250))
211
  return {
212
  "name": f"{base_name} ({occasion})",
 
228
 
229
  # ============= Gradio UI (GIfty) =============
230
  EXAMPLES = [
231
+ [["reading","travel","aesthetic"], "birthday", [20, 60], "Noa", "adult (18–64)", "warm and friendly"],
232
+ [["coffee","home","practical"], "housewarming", [25, 45], "Daniel", "adult (18–64)", "warm"],
233
+ [["tech","digital"], "birthday", [30, 120], "Omer", "teen (13–17)", "fun"],
234
  ]
235
 
236
+ def ui_predict(interests_list: List[str], occasion: str, budget_range, recipient_name: str, age_label: str, tone: str):
 
237
  try:
238
+ # budget_range is a tuple/list: (min, max)
239
+ if isinstance(budget_range, (list, tuple)) and len(budget_range) == 2:
240
+ budget_min, budget_max = float(budget_range[0]), float(budget_range[1])
241
+ else:
242
+ budget_min, budget_max = 20.0, 60.0
243
+ if budget_min > budget_max:
244
  budget_min, budget_max = budget_max, budget_min
245
 
246
+ age_range = AGE_OPTIONS.get(age_label, "any")
247
  profile = {
248
  "recipient_name": recipient_name or "Friend",
249
  "interests": interests_list or [],
250
  "occasion": occasion or "birthday",
251
+ "budget_min": budget_min,
252
+ "budget_max": budget_max,
253
+ "budget_usd": budget_max,
254
  "age_range": age_range,
255
  "tone": tone or "warm and friendly",
256
  }
 
263
  gen_md = f"**{gen['name']}**\n\n{gen['short_desc']}\n\n~${gen['price_usd']:.0f}"
264
  return top3_md, gen_md, msg
265
  except Exception as e:
 
266
  return f":warning: Error: {e}", "", ""
267
 
268
  with gr.Blocks() as demo:
 
276
  with gr.Row():
277
  occasion = gr.Dropdown(label="Occasion", choices=OCCASION_OPTIONS, value="birthday")
278
  age = gr.Dropdown(label="Age group", choices=list(AGE_OPTIONS.keys()), value="adult (18–64)")
279
+
280
+ # Range slider for budget (two handles)
281
+ budget = gr.Slider(label="Budget (USD)", minimum=5, maximum=500, step=1, value=(20, 60))
282
+
283
  with gr.Row():
284
  recipient_name = gr.Textbox(label="Recipient name", value="Noa")
285
  tone = gr.Textbox(label="Message tone", value="warm and friendly")
 
291
 
292
  gr.Examples(
293
  EXAMPLES,
294
+ [interests, occasion, budget, recipient_name, age, tone],
295
  label="Quick examples",
296
  )
297
 
298
  go.click(
299
  ui_predict,
300
+ [interests, occasion, budget, recipient_name, age, tone],
301
  [out_top3, out_gen, out_msg]
302
  )
303
 
 
304
  if __name__ == "__main__":
305
  demo.launch()