Youmnaaaa commited on
Commit
53562d8
ยท
verified ยท
1 Parent(s): e9ce216

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +171 -34
app.py CHANGED
@@ -116,14 +116,14 @@ KEYWORD_OVERRIDE = {
116
  "goodbye": ["ู…ุน ุงู„ุณู„ุงู…ุฉ","ู…ุน ุงู„ุณู„ุงู…ู‡","ุจุงูŠ","ูˆุฏุงุนุง","bye","goodbye","ุชุตุจุญ ุนู„ู‰ ุฎูŠุฑ",
117
  "ููŠ ุงู…ุงู† ุงู„ู„ู‡","ุงู„ู„ู‡ ูŠุณู„ู…ูƒ","ุณู„ุงู…ุชูƒ"],
118
  "greeting":["ุงู„ุณู„ุงู… ุนู„ูŠูƒู…","ูˆุนู„ูŠูƒู… ุงู„ุณู„ุงู…","ุงู‡ู„ุง","ุฃู‡ู„ุง","ู‡ู„ุง","ู‡ู„ูˆ","ู…ุฑุญุจุง","ู…ุฑุญุจุงู‹",
119
- "ุตุจุงุญ ุงู„ุฎูŠุฑ","ู…ุณุงุก ุงู„ุฎูŠุฑ","ู‡ุงูŠ","hi","hello","ุตุจุงุญ","ู…ุณุงุก"],
120
  "thanks": ["ุดูƒุฑุง","ุดูƒุฑุงู‹","ุชุณู„ู…","ูŠุณู„ู…ูˆ","ู…ู…ู†ูˆู†","ู…ุดูƒูˆุฑ","thanks","thank","ุงู„ู ุดูƒุฑ"],
121
  }
122
  CATEGORY_KEYWORDS = {
123
- "restaurant":["ู…ุทุนู…","ุงูƒู„","ูˆุฌุจุงุช","ู…ุดูˆูŠุงุช","ูƒุจุงุจ","ุดุงูˆุฑู…ุง","ูƒุฑูŠุจ","ุณู†ุฏูˆุชุด","ุณู†ุฏูˆุชุดุงุช","ุจุฑุฌุฑ","ุณู…ูƒ","ูุฑุงูŠุฏ","ู…ุทุงุนู…","ููˆู„","ุทุนู…ูŠุฉ"],
124
- "pharmacy": ["ุตูŠุฏู„ูŠู‡","ุตูŠุฏู„ูŠุฉ","ุฏูˆุง","ุงุฏูˆูŠู‡","ุฏูˆุงุก","ุตูŠุฏู„ู‡","ุฏูƒุชูˆุฑ","ุนู„ุงุฌ"],
125
- "cafe": ["ูƒุงููŠู‡","ูƒูˆููŠ","ู‚ู‡ูˆู‡","ู‚ู‡ูˆุฉ","ูƒุงููŠุชูŠุฑูŠุง","ูƒุงููŠุฉ","ู‚ู‡ูˆูŠ"],
126
- "supermarket":["ุณูˆุจุฑู…ุงุฑูƒุช","ู…ุงุฑูƒุช","ุจู‚ุงู„ู‡","ู‡ุงูŠุจุฑ","ุจู‚ุงู„ุฉ","ุณูˆุจุฑ ู…ุงุฑูƒุช","ุณูˆุจุฑู…ุงุฑูƒุงุช","ุฌุฑูˆุณุฑูŠ"],
127
  "housing": ["ุดู‚ู‡","ุดู‚ุฉ","ุงูŠุฌุงุฑ","ุฅูŠุฌุงุฑ","ูู†ุฏู‚","ู‡ูˆุณุชู„","ุณูƒู†","ุณูƒู†"],
128
  }
129
  _CAT_MAP = {
@@ -420,15 +420,63 @@ def rebuild_embeddings(df: pd.DataFrame, force: bool = False):
420
  """HELPER FUNCTIONS"""
421
 
422
  def normalize_category(cat):
423
- if not cat: return cat
 
 
424
  cat_s = str(cat).strip()
425
- if cat_s in ("restaurant","pharmacy","cafe","supermarket","housing"):
426
- return cat_s
427
  c = clean_text(cat_s)
428
- if c in _CAT_MAP: return _CAT_MAP[c]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
429
  for ar, en in _CAT_MAP.items():
430
- if ar in c or c in ar: return en
431
- return cat_s
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
432
 
433
  def apply_keyword_override(text):
434
  t = norm(text); tw = set(t.split())
@@ -621,11 +669,12 @@ def apply_filters(df, category=None, sub_category=None, location=None,
621
 
622
  # โ”€โ”€ category โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
623
  if category:
624
- cat_c = clean_text(category)
625
- f_cat = f[f["category_clean"].astype(str).str.contains(re.escape(cat_c), na=False)]
626
- if not f_cat.empty:
627
- f = f_cat
628
- # ู„ูˆ ู…ููŠุด ูˆู†ุนู… semantic ู…ู…ูƒู† ุชุฏูŠู‡ โ€” ุงู„ู€ caller ูŠู‚ุฑุฑ
 
629
 
630
  # โ”€โ”€ sub_category โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
631
  if sub_category:
@@ -771,6 +820,15 @@ def rank(df, query, user_lat=None, user_lon=None, intent=None):
771
  axis=1
772
  )
773
 
 
 
 
 
 
 
 
 
 
774
  return df.sort_values("final_score", ascending=False).reset_index(drop=True)
775
 
776
  def search_places(query, top_k_final=5, category=None, sub_category=None,
@@ -834,17 +892,8 @@ def search_places(query, top_k_final=5, category=None, sub_category=None,
834
  strict_location=bool(location),
835
  )
836
 
837
- # ู„ูˆ ู…ููŠุด location ูˆู…ููŠุด ู†ุชุงุฆุฌ ุจุนุฏ categoryุŒ ุฌุฑุจ ู…ู† ุบูŠุฑ category ูู‚ุท ู„ู„ุฃุณุฆู„ุฉ ุงู„ุนุงู…ุฉ
838
- if filtered.empty and category and not location:
839
- filtered = apply_filters(
840
- all_cands,
841
- sub_category=sub_category,
842
- price_range=price_range,
843
- open_now_only=open_now_only,
844
- min_rating=min_rating,
845
- strict_location=False,
846
- )
847
-
848
  if filtered.empty:
849
  return pd.DataFrame()
850
 
@@ -1710,10 +1759,9 @@ def chat(text: str, session, user_lat=None, user_lon=None):
1710
 
1711
  # โ”€โ”€ proximity query: ู„ูˆ ู…ููŠุด lat/lon โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1712
  t_low = norm(text)
1713
- PROX_REQ = ["ู‚ุฑูŠุจ ู…ู†ูŠ","ุฌู†ุจูŠ","ุญูˆุงู„ูŠุง","ุฃู‚ุฑุจ ู„ูŠุง","ุงู„ุฃู‚ุฑุจ ู„ูŠุง"]
1714
- if any(p in t_low for p in PROX_REQ) and not (user_lat and user_lon):
1715
- reply = ("๐Ÿ“ ุนุดุงู† ุฃุฌูŠุจ ุงู„ุฃู‚ุฑุจ ู„ูŠูƒ ู…ุญุชุงุฌ ู…ูˆู‚ุนูƒ.\n"
1716
- "ูุนู‘ู„ ุงู„ู€ location ู…ู† ุงู„ุชุทุจูŠู‚ ุฃูˆ ู‚ูˆู„ูŠ ุฃู†ุช ููŠ ุฃู†ู‡ูŠ ู…ู†ุทู‚ุฉุŸ")
1717
  result.update(reply=reply, intent="missing_info",
1718
  best_place=None, all_results=[])
1719
  session.add(text, reply, "missing_info", ents, None, [])
@@ -1887,7 +1935,7 @@ SEARCH_INTENTS.add("show_menu")
1887
  _MENU_TRIGGERS = {"ู…ู†ูŠูˆ","menu","ุงู„ู‚ุงุฆู…ู‡","ู‚ุงูŠู…ุฉ","ู‚ุงุฆู…ุฉ","ุงู„ุงูƒู„ุงุช","ุงูƒู„ุงุช","ุนุฑูˆุถู‡","ุนุฑุถ ุงู„ู…ุทุนู…"}
1888
  _ITEM_TRIGGERS = {
1889
  # ุฃูƒู„ุงุช ุฑุฆูŠุณูŠุฉ
1890
- "ุจูŠุชุฒุง","ุจุฑุฌุฑ","ูƒุฑูŠุจ","ุดุงูˆุฑู…ุง","ุจุฑูˆุณุช","ุณูˆุดูŠ","ุฑุงู†ุด",
1891
  "ุณุงู†ุฏูˆุชุด","ุณู†ุฏูˆุชุด","ุจุณุทุฑู…ู‡","ูƒุจุฏู‡","ูƒุจุฏุฉ","ุญูˆุงูˆุดูŠ",
1892
  "ุทุนู…ูŠู‡","ุทุนู…ูŠุฉ","ููˆู„","ูƒุดุฑูŠ","ู…ูƒุฑูˆู†ู‡","ู„ุงุฒุงู†ูŠุง",
1893
  # ู„ุญูˆู… ูˆุฏุฌุงุฌ ูˆุณู…ูƒ
@@ -2092,6 +2140,80 @@ def search_places_by_item(item_query: str) -> list[dict]:
2092
  log.info(f"๐Ÿ” search_by_item fuzzy โ†’ {len(places)} results")
2093
  return places
2094
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2095
  def get_place_menu(place_id: str) -> list[dict]:
2096
  """
2097
  Calls GET /api/mobile/places/<place_id>/menu
@@ -2222,7 +2344,8 @@ def handle_search_by_item(text: str, session, user_lat=None, user_lon=None) -> d
2222
  Handles search_by_item intent end-to-end.
2223
  1. Extract item_query
2224
  2. Call backend API
2225
- 3. Return formatted response
 
2226
  """
2227
  item_query = extract_item_query(text)
2228
  item_query = clean_item_query(item_query)
@@ -2234,7 +2357,21 @@ def handle_search_by_item(text: str, session, user_lat=None, user_lon=None) -> d
2234
  "place_cards": [],
2235
  "best_place": None,
2236
  }
 
2237
  places = search_places_by_item(item_query)
 
 
 
 
 
 
 
 
 
 
 
 
 
2238
  result = format_item_search_response(places, item_query, user_lat, user_lon)
2239
  session.add(text, result["reply"], "search_by_item", {"item": item_query},
2240
  result.get("best_place"), places)
@@ -2598,4 +2735,4 @@ def reload_data(x_secret: Optional[str] = Header(default=None)):
2598
  def reset_session(session_id: str):
2599
  SESSIONS.pop(session_id, None)
2600
  log.info(f"๐Ÿ—‘๏ธ Session {session_id} reset")
2601
- return {"status": "reset", "session_id": session_id}
 
116
  "goodbye": ["ู…ุน ุงู„ุณู„ุงู…ุฉ","ู…ุน ุงู„ุณู„ุงู…ู‡","ุจุงูŠ","ูˆุฏุงุนุง","bye","goodbye","ุชุตุจุญ ุนู„ู‰ ุฎูŠุฑ",
117
  "ููŠ ุงู…ุงู† ุงู„ู„ู‡","ุงู„ู„ู‡ ูŠุณู„ู…ูƒ","ุณู„ุงู…ุชูƒ"],
118
  "greeting":["ุงู„ุณู„ุงู… ุนู„ูŠูƒู…","ูˆุนู„ูŠูƒู… ุงู„ุณู„ุงู…","ุงู‡ู„ุง","ุฃู‡ู„ุง","ู‡ู„ุง","ู‡ู„ูˆ","ู…ุฑุญุจุง","ู…ุฑุญุจุงู‹",
119
+ "ุตุจุงุญ ุงู„ุฎูŠุฑ","ู…ุณุงุก ุงู„ุฎูŠุฑ","ู‡ุงูŠ","hi","hello","ุงุฒูŠูƒ","ุงุฎุจุงุฑูƒ","ุนุงู…ู„ ุงูŠู‡","ุตุจุงุญ","ู…ุณุงุก"],
120
  "thanks": ["ุดูƒุฑุง","ุดูƒุฑุงู‹","ุชุณู„ู…","ูŠุณู„ู…ูˆ","ู…ู…ู†ูˆู†","ู…ุดูƒูˆุฑ","thanks","thank","ุงู„ู ุดูƒุฑ"],
121
  }
122
  CATEGORY_KEYWORDS = {
123
+ "restaurant":["ู…ุทุนู…","ุงูƒู„","ุฃูƒู„","ูˆุฌุจุงุช","ู…ุดูˆูŠุงุช","ูƒุจุงุจ","ุดุงูˆุฑู…ุง","ูƒุฑูŠุจ","ุณู†ุฏูˆุชุด","ุณู†ุฏูˆุชุดุงุช","ุจุฑุฌุฑ","ุณู…ูƒ","ูุฑุงูŠุฏ","ู…ุทุงุนู…","ููˆู„","ุทุนู…ูŠุฉ","restaurant","restaurants","food","meal","burger","pizza","shawarma","crepe","grill","grills"],
124
+ "pharmacy": ["ุตูŠุฏู„ูŠู‡","ุตูŠุฏู„ูŠุฉ","ุฏูˆุง","ุงุฏูˆูŠู‡","ุฏูˆุงุก","ุตูŠุฏู„ู‡","ุฏูƒุชูˆุฑ","ุนู„ุงุฌ","pharmacy","drugstore","medicine"],
125
+ "cafe": ["ูƒุงููŠู‡","ูƒูˆููŠ","ู‚ู‡ูˆู‡","ู‚ู‡ูˆุฉ","ูƒุงููŠุชูŠุฑูŠุง","ูƒุงููŠุฉ","ู‚ู‡ูˆูŠ","cafe","coffee","coffee shop"],
126
+ "supermarket":["ุณูˆุจุฑู…ุงุฑูƒุช","ู…ุงุฑูƒุช","ุจู‚ุงู„ู‡","ู‡ุงูŠุจุฑ","ุจู‚ุงู„ุฉ","ุณูˆุจุฑ ู…ุงุฑูƒุช","ุณูˆุจุฑู…ุงุฑูƒุงุช","ุฌุฑูˆุณุฑูŠ","supermarket","market","grocery","hypermarket"],
127
  "housing": ["ุดู‚ู‡","ุดู‚ุฉ","ุงูŠุฌุงุฑ","ุฅูŠุฌุงุฑ","ูู†ุฏู‚","ู‡ูˆุณุชู„","ุณูƒู†","ุณูƒู†"],
128
  }
129
  _CAT_MAP = {
 
420
  """HELPER FUNCTIONS"""
421
 
422
  def normalize_category(cat):
423
+ """Normalize DB/API categories to the app canonical categories."""
424
+ if not cat:
425
+ return cat
426
  cat_s = str(cat).strip()
 
 
427
  c = clean_text(cat_s)
428
+
429
+ # exact canonical values
430
+ if c in ("restaurant", "pharmacy", "cafe", "supermarket", "housing"):
431
+ return c
432
+
433
+ # English / mixed API categories
434
+ if "pharmacy" in c or "ุตูŠุฏู„ูŠ" in c or "ุตูŠุฏู„ู‡" in c:
435
+ return "pharmacy"
436
+ if "supermarket" in c or "market" in c or "ู…ุงุฑูƒุช" in c or "ุจู‚ุงู„" in c or "ู‡ุงูŠุจุฑ" in c:
437
+ return "supermarket"
438
+ if "housing" in c or "hotel" in c or "hostel" in c or "ุดู‚ู‡" in c or "ุณูƒู†" in c or "ูู†ุฏู‚" in c:
439
+ return "housing"
440
+ # Restaurant & Cafรฉ: ู„ูˆ ู…ุทู„ูˆุจ restaurant ู‡ูŠุนุฏูŠ ุจุงู„ู€ contains ููŠ ุงู„ูู„ุชุฑุŒ ู„ูƒู† normalized ุงู„ุฃุณุงุณูŠ restaurant
441
+ if "restaurant" in c or "ู…ุทุนู…" in c or "ู…ุทุงุนู…" in c:
442
+ return "restaurant"
443
+ if "cafe" in c or "coffee" in c or "ูƒุงููŠู‡" in c or "ูƒูˆููŠ" in c or "ู‚ู‡ูˆู‡" in c or "ู‚ู‡ูˆุฉ" in c:
444
+ return "cafe"
445
+
446
+ if c in _CAT_MAP:
447
+ return _CAT_MAP[c]
448
  for ar, en in _CAT_MAP.items():
449
+ if ar in c or c in ar:
450
+ return en
451
+ return c
452
+
453
+
454
+ def _category_match_value(raw_category: str, target_category: str) -> bool:
455
+ """Strict category match without allowing unrelated fallback."""
456
+ if not target_category:
457
+ return True
458
+ target = normalize_category(target_category)
459
+ raw = clean_text(raw_category)
460
+
461
+ if target == "restaurant":
462
+ return ("restaurant" in raw or "ู…ุทุนู…" in raw or "ู…ุทุงุนู…" in raw) and "pharmacy" not in raw and "ุตูŠุฏู„ูŠ" not in raw
463
+ if target == "cafe":
464
+ return "cafe" in raw or "coffee" in raw or "ูƒุงููŠู‡" in raw or "ูƒูˆููŠ" in raw or "ู‚ู‡ูˆู‡" in raw or "ู‚ู‡ูˆุฉ" in raw
465
+ if target == "pharmacy":
466
+ return "pharmacy" in raw or "ุตูŠุฏู„ูŠ" in raw or "ุตูŠุฏู„ู‡" in raw
467
+ if target == "supermarket":
468
+ return "supermarket" in raw or "market" in raw or "ู…ุงุฑูƒุช" in raw or "ุจู‚ุงู„" in raw or "ู‡ุงูŠุจุฑ" in raw
469
+ if target == "housing":
470
+ return "housing" in raw or "hotel" in raw or "hostel" in raw or "ุดู‚ู‡" in raw or "ุณูƒู†" in raw or "ูู†ุฏู‚" in raw
471
+ return target in raw
472
+
473
+
474
+ def _is_proximity_query(text: str) -> bool:
475
+ t = norm(text)
476
+ return any(w in t for w in [
477
+ "ุงู‚ุฑุจ", "ุฃู‚ุฑุจ", "ุงู„ุงู‚ุฑุจ", "ุงู„ุฃู‚ุฑุจ", "ู‚ุฑูŠุจ", "ู‚ุฑูŠุจู‡", "ู‚ุฑูŠุจุฉ",
478
+ "ุฌู†ุจูŠ", "ุญูˆุงู„ูŠุง", "ุญูˆู„ูŠ", "near", "nearest", "nearby", "close"
479
+ ])
480
 
481
  def apply_keyword_override(text):
482
  t = norm(text); tw = set(t.split())
 
669
 
670
  # โ”€โ”€ category โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
671
  if category:
672
+ target_cat = normalize_category(category)
673
+ f_cat = f[f["category"].astype(str).apply(lambda x: _category_match_value(x, target_cat))]
674
+ if f_cat.empty and "category_clean" in f.columns:
675
+ f_cat = f[f["category_clean"].astype(str).apply(lambda x: _category_match_value(x, target_cat))]
676
+ # strict: ู„ูˆ ุงู„ู…ุณุชุฎุฏู… ุทู„ุจ category ู…ุนูŠู†ุฉ ู…ู…ู†ูˆุน ู†ุจุฏู‘ู„ู‡ุง ุจู€ category ุชุงู†ูŠุฉ
677
+ f = f_cat
678
 
679
  # โ”€โ”€ sub_category โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
680
  if sub_category:
 
820
  axis=1
821
  )
822
 
823
+ if is_proximity and has_loc and "distance_km" in df.columns:
824
+ # ููŠ ุทู„ุจ "ุฃู‚ุฑุจ" ูˆู…ุนุงู†ุง location: ุงู„ู…ุณุงูุฉ ู‡ูŠ ุงู„ุญูŽูƒูŽู… ุงู„ุฃูˆู„
825
+ df = df.sort_values(
826
+ by=["distance_km", "rating_norm"],
827
+ ascending=[True, False],
828
+ na_position="last",
829
+ ).reset_index(drop=True)
830
+ return df
831
+
832
  return df.sort_values("final_score", ascending=False).reset_index(drop=True)
833
 
834
  def search_places(query, top_k_final=5, category=None, sub_category=None,
 
892
  strict_location=bool(location),
893
  )
894
 
895
+ # ู…ู‡ู…: ู…ู…ู†ูˆุน ู†ู„ุบูŠ category ู„ูˆ ุงู„ู…ุณุชุฎุฏู… ุทู„ุจ nearest_* ุฃูˆ category ูˆุงุถุญุฉ
896
+ # ู„ุฃู† ุฏู‡ ูƒุงู† ุจูŠุฑุฌุน restaurant ููŠ ุณุคุงู„ nearest_pharmacy ู…ุซู„ุงู‹.
 
 
 
 
 
 
 
 
 
897
  if filtered.empty:
898
  return pd.DataFrame()
899
 
 
1759
 
1760
  # โ”€โ”€ proximity query: ู„ูˆ ู…ููŠุด lat/lon โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1761
  t_low = norm(text)
1762
+ if _is_proximity_query(text) and not (user_lat and user_lon):
1763
+ reply = ("๐Ÿ“ ุนุดุงู† ุฃุญุฏุฏ ุงู„ุฃู‚ุฑุจ ู…ุญุชุงุฌ ุชุณู…ุญู„ูŠ ุจุงู„ู„ูˆูƒูŠุดู†.\n"
1764
+ "ุฃูˆ ู‚ูˆู„ูŠ ุงู„ู…ู†ุทู‚ุฉ ุงู„ู„ูŠ ุฃู†ุช ููŠู‡ุง.")
 
1765
  result.update(reply=reply, intent="missing_info",
1766
  best_place=None, all_results=[])
1767
  session.add(text, reply, "missing_info", ents, None, [])
 
1935
  _MENU_TRIGGERS = {"ู…ู†ูŠูˆ","menu","ุงู„ู‚ุงุฆู…ู‡","ู‚ุงูŠู…ุฉ","ู‚ุงุฆู…ุฉ","ุงู„ุงูƒู„ุงุช","ุงูƒู„ุงุช","ุนุฑูˆุถู‡","ุนุฑุถ ุงู„ู…ุทุนู…"}
1936
  _ITEM_TRIGGERS = {
1937
  # ุฃูƒู„ุงุช ุฑุฆูŠุณูŠุฉ
1938
+ "ุจูŠุชุฒุง","ุจุฑุฌุฑ","ูƒุฑูŠุจ","ุดุงูˆุฑู…ุง","ุจุฑูˆุณุช","ุณูˆุดูŠ","ุฑุงู†ุด","pizza","burger","crepe","shawarma","broast","sushi","ranch",
1939
  "ุณุงู†ุฏูˆุชุด","ุณู†ุฏูˆุชุด","ุจุณุทุฑู…ู‡","ูƒุจุฏู‡","ูƒุจุฏุฉ","ุญูˆุงูˆุดูŠ",
1940
  "ุทุนู…ูŠู‡","ุทุนู…ูŠุฉ","ููˆู„","ูƒุดุฑูŠ","ู…ูƒุฑูˆู†ู‡","ู„ุงุฒุงู†ูŠุง",
1941
  # ู„ุญูˆู… ูˆุฏุฌุงุฌ ูˆุณู…ูƒ
 
2140
  log.info(f"๐Ÿ” search_by_item fuzzy โ†’ {len(places)} results")
2141
  return places
2142
 
2143
+
2144
+ def filter_item_results_strict(places: list, item_query: str) -> list:
2145
+ """
2146
+ ูู„ุชุฑ ู‚ูˆูŠ ู„ู†ุชุงุฆุฌ search_by_item:
2147
+ - ู„ูˆ ุงู„ุณุคุงู„ ุนู† ุฃูƒู„: restaurants ูู‚ุท.
2148
+ - ู„ูˆ ุงู„ุณุคุงู„ ุนู† ู…ุดุฑูˆุจ: cafe ุฃูˆ restaurant ูู‚ุท.
2149
+ - ู„ุงุฒู… ุงู„ุตู†ู ู†ูุณู‡ ูŠูƒูˆู† ุธุงู‡ุฑ ููŠ matched_items / sub_category / description / name.
2150
+ """
2151
+ if not places:
2152
+ return []
2153
+
2154
+ q = clean_text(item_query)
2155
+
2156
+ food_keywords = {
2157
+ "ุดุงูˆุฑู…ุง", "ุจุฑุฌุฑ", "ุจูŠุชุฒุง", "ูƒุฑูŠุจ", "ู…ุดูˆูŠุงุช", "ูุฑุงุฎ",
2158
+ "ุฏุฌุงุฌ", "ูƒุจุงุจ", "ูƒูุชู‡", "ูƒูุชุฉ", "ุณู…ูƒ", "ุณุงู†ุฏูˆุชุด",
2159
+ "ุณู†ุฏูˆุชุด", "ุญูˆุงูˆุดูŠ", "ูƒุดุฑูŠ", "ู…ูƒุฑูˆู†ู‡", "ู…ูƒุฑูˆู†ุฉ",
2160
+ "ูˆุฌุจู‡", "ูˆุฌุจุฉ", "ุงูƒู„", "ุฃูƒู„", "burger", "pizza",
2161
+ "shawarma", "crepe", "chicken", "grill", "grills"
2162
+ }
2163
+
2164
+ drink_keywords = {
2165
+ "ู‚ู‡ูˆุฉ", "ู‚ู‡ูˆู‡", "ูƒูˆููŠ", "ุนุตูŠุฑ", "ู…ุดุฑูˆุจ", "ู…ุดุฑูˆุจุงุช",
2166
+ "ู„ุงุชูŠู‡", "ุงุณุจุฑูŠุณูˆ", "ู†ุณูƒุงููŠู‡", "ุณู…ูˆุฐูŠ",
2167
+ "coffee", "juice", "latte", "espresso", "smoothie"
2168
+ }
2169
+
2170
+ is_food = any(clean_text(w) in q for w in food_keywords)
2171
+ is_drink = any(clean_text(w) in q for w in drink_keywords)
2172
+
2173
+ query_words = [w for w in q.split() if len(w) > 2]
2174
+ filtered = []
2175
+
2176
+ for p in places:
2177
+ raw_category = str(p.get("category", ""))
2178
+ category = normalize_category(raw_category)
2179
+
2180
+ text_blob = clean_text(
2181
+ f"{p.get('name','')} "
2182
+ f"{p.get('name_ar','')} "
2183
+ f"{p.get('name_en','')} "
2184
+ f"{p.get('category','')} "
2185
+ f"{p.get('sub_category','')} "
2186
+ f"{p.get('description','')} "
2187
+ f"{p.get('matched_item','')} "
2188
+ f"{p.get('matched_items','')} "
2189
+ f"{p.get('menu_items','')} "
2190
+ f"{p.get('tags','')}"
2191
+ )
2192
+
2193
+ if is_food and category != "restaurant" and not _category_match_value(raw_category, "restaurant"):
2194
+ continue
2195
+
2196
+ if is_drink and not (
2197
+ category in ("cafe", "restaurant") or
2198
+ _category_match_value(raw_category, "cafe") or
2199
+ _category_match_value(raw_category, "restaurant")
2200
+ ):
2201
+ continue
2202
+
2203
+ if query_words and not any(w in text_blob for w in query_words):
2204
+ continue
2205
+
2206
+ filtered.append(p)
2207
+
2208
+ # dedup
2209
+ seen, unique = set(), []
2210
+ for p in filtered:
2211
+ pid = str(p.get("place_id") or p.get("id") or p.get("name") or "")
2212
+ if pid and pid not in seen:
2213
+ seen.add(pid)
2214
+ unique.append(p)
2215
+ return unique
2216
+
2217
  def get_place_menu(place_id: str) -> list[dict]:
2218
  """
2219
  Calls GET /api/mobile/places/<place_id>/menu
 
2344
  Handles search_by_item intent end-to-end.
2345
  1. Extract item_query
2346
  2. Call backend API
2347
+ 3. Strictly filter wrong categories/items
2348
+ 4. Return formatted response
2349
  """
2350
  item_query = extract_item_query(text)
2351
  item_query = clean_item_query(item_query)
 
2357
  "place_cards": [],
2358
  "best_place": None,
2359
  }
2360
+
2361
  places = search_places_by_item(item_query)
2362
+ places = filter_item_results_strict(places, item_query)
2363
+
2364
+ if not places:
2365
+ result = {
2366
+ "reply": f"ู…ุด ู„ุงู‚ูŠ ุฃู…ุงูƒู† ุจุชู‚ุฏู… {item_query} ุญุงู„ูŠู‹ุง ๐Ÿ˜”",
2367
+ "intent": "search_by_item",
2368
+ "ui_type": CFG.UI_TEXT_ONLY,
2369
+ "place_cards": [],
2370
+ "best_place": None,
2371
+ }
2372
+ session.add(text, result["reply"], "search_by_item", {"item": item_query}, None, [])
2373
+ return result
2374
+
2375
  result = format_item_search_response(places, item_query, user_lat, user_lon)
2376
  session.add(text, result["reply"], "search_by_item", {"item": item_query},
2377
  result.get("best_place"), places)
 
2735
  def reset_session(session_id: str):
2736
  SESSIONS.pop(session_id, None)
2737
  log.info(f"๐Ÿ—‘๏ธ Session {session_id} reset")
2738
+ return {"status": "reset", "session_id": session_id}