Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -116,7 +116,7 @@ KEYWORD_OVERRIDE = {
|
|
| 116 |
"goodbye": ["ู
ุน ุงูุณูุงู
ุฉ","ู
ุน ุงูุณูุงู
ู","ุจุงู","ูุฏุงุนุง","bye","goodbye","ุชุตุจุญ ุนูู ุฎูุฑ",
|
| 117 |
"ูู ุงู
ุงู ุงููู","ุงููู ูุณูู
ู","ุณูุงู
ุชู"],
|
| 118 |
"greeting":["ุงูุณูุงู
ุนูููู
","ูุนูููู
ุงูุณูุงู
","ุงููุง","ุฃููุง","ููุง","ููู","ู
ุฑุญุจุง","ู
ุฑุญุจุงู",
|
| 119 |
-
"ุตุจุงุญ ุงูุฎูุฑ","ู
ุณุงุก ุงูุฎูุฑ","ูุงู","hi","hello","
|
| 120 |
"thanks": ["ุดูุฑุง","ุดูุฑุงู","ุชุณูู
","ูุณูู
ู","ู
ู
ููู","ู
ุดููุฑ","thanks","thank","ุงูู ุดูุฑ"],
|
| 121 |
}
|
| 122 |
CATEGORY_KEYWORDS = {
|
|
@@ -1942,22 +1942,43 @@ _ITEM_TRIGGERS = {
|
|
| 1942 |
"ูุฑุงุฎ","ูุฑุฎู","ูุฑุฎุฉ","ูุฑุฎ","ุฏุฌุงุฌ","ูุญู
ู","ูุญู
ุฉ","ูุญู
","ูุจุงุจ","ููุชู",
|
| 1943 |
"ุณู
ู","ุฌู
ุจุฑู","ุจูุทู","ู
ุดููุงุช","ู
ุดูู","ู
ุดููู",
|
| 1944 |
# ู
ุดุฑูุจุงุช ูุญูููุงุช
|
| 1945 |
-
"ุนุตูุฑ","ูููู","ูููุฉ","ูููู","ุชุดูุฒ ููู","ูุงูู","ููู","ุชูุฑุชู",
|
| 1946 |
# ุทูุจ ุจุงูุงุณู
|
| 1947 |
"ุนุงูุฒ ุงููุฉ","ุนุงูุฒุฉ ุงููุฉ","ููู ุงูู",
|
| 1948 |
}
|
| 1949 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1950 |
def _detect_new_intent(text: str) -> str | None:
|
| 1951 |
"""
|
| 1952 |
Detects search_by_item or show_menu from text.
|
| 1953 |
Returns intent string or None.
|
| 1954 |
"""
|
| 1955 |
t = norm(text)
|
|
|
|
|
|
|
|
|
|
| 1956 |
if any(kw in t for kw in _MENU_TRIGGERS):
|
| 1957 |
return "show_menu"
|
| 1958 |
-
# item search: any food keyword present
|
| 1959 |
-
if any(kw in t for kw in _ITEM_TRIGGERS):
|
| 1960 |
-
return "search_by_item"
|
| 1961 |
return None
|
| 1962 |
|
| 1963 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
@@ -1976,6 +1997,7 @@ _FOOD_STOP_WORDS = {
|
|
| 1976 |
"ุนุงูุฒ ุงูู","ุนุงูุฒุฉ ุงูู","ุนุงูุฒ ุงููุฉ","ุงุจู ุงูู",
|
| 1977 |
# categories
|
| 1978 |
"ู
ููู","menu","ูุงุฆู
ู","ูุงูู
ุฉ","ู
ุทุนู
","ู
ุทุงุนู
","ูุงููู","restaurant","cafe",
|
|
|
|
| 1979 |
# ุญุฑูู
|
| 1980 |
"ูู","ู
ู","ุนูู","ุนู","ููุง","ูุฑูุจ","ุงูุฑุจ","ุญูู","ูููุณ","ุฑุฎูุต",
|
| 1981 |
}
|
|
@@ -1984,8 +2006,40 @@ _FOOD_STOP_WORDS = {
|
|
| 1984 |
_ITEM_PHRASE_STRIP = [
|
| 1985 |
"ุนุงูุฒ ุงููุฉ","ุนุงูุฒุฉ ุงููุฉ","ุนุงูุฒ ุงูู","ุนุงูุฒุฉ ุงูู",
|
| 1986 |
"ููู ุงูู","ุงุจุญุซ ุนู","ุจุฏูุฑ ุนูู","ุจุฏูุฑ ุนู",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1987 |
]
|
| 1988 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1989 |
_MENU_PHRASE_STRIP = [
|
| 1990 |
"ุนุงูุฒ ุงุดูู ู
ููู","ุนุงูุฒุฉ ุงุดูู ู
ููู","ุนุงูุฒ ุงุดูู ุงูู
ููู","ุนุงูุฒุฉ ุงุดูู ุงูู
ููู",
|
| 1991 |
"ูุงุช ู
ููู","ูุงุชูู ู
ููู","ูุงุชููุง ู
ููู",
|
|
@@ -2141,12 +2195,40 @@ def search_places_by_item(item_query: str) -> list[dict]:
|
|
| 2141 |
return places
|
| 2142 |
|
| 2143 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2144 |
def filter_item_results_strict(places: list, item_query: str) -> list:
|
| 2145 |
"""
|
| 2146 |
-
|
| 2147 |
-
-
|
| 2148 |
-
-
|
| 2149 |
-
-
|
| 2150 |
"""
|
| 2151 |
if not places:
|
| 2152 |
return []
|
|
@@ -2154,34 +2236,36 @@ def filter_item_results_strict(places: list, item_query: str) -> list:
|
|
| 2154 |
q = clean_text(item_query)
|
| 2155 |
|
| 2156 |
food_keywords = {
|
| 2157 |
-
"ุดุงูุฑู
ุง", "ุจุฑุฌุฑ", "ุจูุชุฒุง", "ูุฑูุจ", "ู
ุดููุงุช", "ูุฑุงุฎ",
|
| 2158 |
-
"
|
| 2159 |
-
"
|
| 2160 |
-
"
|
| 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','')} "
|
|
@@ -2190,26 +2274,34 @@ def filter_item_results_strict(places: list, item_query: str) -> list:
|
|
| 2190 |
f"{p.get('tags','')}"
|
| 2191 |
)
|
| 2192 |
|
| 2193 |
-
if
|
| 2194 |
continue
|
| 2195 |
|
| 2196 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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("
|
| 2212 |
-
if
|
|
|
|
|
|
|
| 2213 |
seen.add(pid)
|
| 2214 |
unique.append(p)
|
| 2215 |
return unique
|
|
@@ -2307,7 +2399,8 @@ def _sort_places_by_distance(places: list[dict], user_lat=None, user_lon=None) -
|
|
| 2307 |
def format_item_search_response(places: list[dict], item_query: str, user_lat=None, user_lon=None) -> dict:
|
| 2308 |
"""
|
| 2309 |
Builds the structured response for search_by_item intent.
|
| 2310 |
-
|
|
|
|
| 2311 |
"""
|
| 2312 |
if not places:
|
| 2313 |
return {
|
|
@@ -2315,23 +2408,39 @@ def format_item_search_response(places: list[dict], item_query: str, user_lat=No
|
|
| 2315 |
"intent": "search_by_item",
|
| 2316 |
"ui_type": CFG.UI_TEXT_ONLY,
|
| 2317 |
"place_cards": [],
|
|
|
|
| 2318 |
"best_place": None,
|
| 2319 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2320 |
places = _sort_places_by_distance(places, user_lat, user_lon)
|
| 2321 |
cards = [clean_place(p) for p in places[:CFG.MAX_CARDS]]
|
| 2322 |
# dedup
|
| 2323 |
seen, unique = set(), []
|
| 2324 |
for c in cards:
|
| 2325 |
-
pid = c.get("place_id") or c.get("id")
|
| 2326 |
if pid not in seen:
|
| 2327 |
seen.add(pid)
|
| 2328 |
unique.append(c)
|
| 2329 |
-
reply = f"๐ฝ๏ธ ูุงููุชูู {len(unique)} ู
|
| 2330 |
return {
|
| 2331 |
"reply": reply,
|
| 2332 |
"intent": "search_by_item",
|
| 2333 |
"ui_type": CFG.UI_PLACES_CARDS,
|
| 2334 |
"place_cards": unique,
|
|
|
|
| 2335 |
"best_place": unique[0] if unique else None,
|
| 2336 |
}
|
| 2337 |
|
|
@@ -2647,7 +2756,7 @@ def chat_endpoint(req: ChatRequest):
|
|
| 2647 |
ui_type=res.get("ui_type", CFG.UI_TEXT_ONLY),
|
| 2648 |
best_place=res.get("best_place"),
|
| 2649 |
place_cards=res.get("place_cards", []),
|
| 2650 |
-
menu_items=[],
|
| 2651 |
)
|
| 2652 |
|
| 2653 |
if new_intent == "show_menu":
|
|
|
|
| 116 |
"goodbye": ["ู
ุน ุงูุณูุงู
ุฉ","ู
ุน ุงูุณูุงู
ู","ุจุงู","ูุฏุงุนุง","bye","goodbye","ุชุตุจุญ ุนูู ุฎูุฑ",
|
| 117 |
"ูู ุงู
ุงู ุงููู","ุงููู ูุณูู
ู","ุณูุงู
ุชู"],
|
| 118 |
"greeting":["ุงูุณูุงู
ุนูููู
","ูุนูููู
ุงูุณูุงู
","ุงููุง","ุฃููุง","ููุง","ููู","ู
ุฑุญุจุง","ู
ุฑุญุจุงู",
|
| 119 |
+
"ุตุจุงุญ ุงูุฎูุฑ","ู
ุณุงุก ุงูุฎูุฑ","ูุงู","hi","hello","ุตุจุงุญ","ู
ุณุงุก"],
|
| 120 |
"thanks": ["ุดูุฑุง","ุดูุฑุงู","ุชุณูู
","ูุณูู
ู","ู
ู
ููู","ู
ุดููุฑ","thanks","thank","ุงูู ุดูุฑ"],
|
| 121 |
}
|
| 122 |
CATEGORY_KEYWORDS = {
|
|
|
|
| 1942 |
"ูุฑุงุฎ","ูุฑุฎู","ูุฑุฎุฉ","ูุฑุฎ","ุฏุฌุงุฌ","ูุญู
ู","ูุญู
ุฉ","ูุญู
","ูุจุงุจ","ููุชู",
|
| 1943 |
"ุณู
ู","ุฌู
ุจุฑู","ุจูุทู","ู
ุดููุงุช","ู
ุดูู","ู
ุดููู",
|
| 1944 |
# ู
ุดุฑูุจุงุช ูุญูููุงุช
|
| 1945 |
+
"ุนุตูุฑ","ูููู","ูููุฉ","ูููู","ุงุณู
ูุฒู","ุฅุณู
ูุฒู","ุณู
ูุฒู","ุณู
ูุฐู","smoothie","ุชุดูุฒ ููู","ูุงูู","ููู","ุชูุฑุชู",
|
| 1946 |
# ุทูุจ ุจุงูุงุณู
|
| 1947 |
"ุนุงูุฒ ุงููุฉ","ุนุงูุฒุฉ ุงููุฉ","ููู ุงูู",
|
| 1948 |
}
|
| 1949 |
|
| 1950 |
+
_ITEM_SEARCH_PATTERNS = [
|
| 1951 |
+
"ู
ูุงู ุนูุฏู", "ู
ูุงู ุนูุฏูุง", "ุงู
ุงูู ุนูุฏูุง", "ุฃู
ุงูู ุนูุฏูุง",
|
| 1952 |
+
"ู
ุทุนู
ุนูุฏู", "ูุงููู ุนูุฏู", "ุนูุฏู", "ุนูุฏูุง",
|
| 1953 |
+
"ุจูุนู
ู", "ุจุชุนู
ู", "ุจููุฏู
", "ุจุชูุฏู
", "ููู", "ูููุง",
|
| 1954 |
+
"ุนุงูุฒ", "ุนุงูุฒุฉ", "ุนุงูุฒู", "ุนุงูุฒ", "ููุณู ูู", "ุจุฏูุฑ ุนูู", "ููู",
|
| 1955 |
+
"place has", "places have", "has", "serves", "serve", "where can i find"
|
| 1956 |
+
]
|
| 1957 |
+
|
| 1958 |
+
def _looks_like_item_search(text: str) -> bool:
|
| 1959 |
+
"""Detect item/menu-item search even if the item word is not in our keywords."""
|
| 1960 |
+
t = norm(text)
|
| 1961 |
+
# menu display is different from item search
|
| 1962 |
+
if any(kw in t for kw in _MENU_TRIGGERS) and not any(p in t for p in ["ุนูุฏู", "ุนูุฏูุง", "ุจูุนู
ู", "ุจุชุนู
ู", "ุจููุฏู
", "ุจุชูุฏู
", "ููู", "ูููุง"]):
|
| 1963 |
+
return False
|
| 1964 |
+
has_item_pattern = any(norm(p) in t for p in _ITEM_SEARCH_PATTERNS)
|
| 1965 |
+
category_context = infer_category(text) in ("restaurant", "cafe")
|
| 1966 |
+
has_item_keyword = any(norm(kw) in t for kw in _ITEM_TRIGGERS)
|
| 1967 |
+
candidate = extract_item_query(text)
|
| 1968 |
+
# ูู ููู ุตูุบุฉ ุทูุจ + ุจุงูู ูุต ุจุนุฏ ุงูุดูู ูุจูู ุบุงูุจูุง ุตููุ ุญุชู ูู ู
ุด ูู keywords
|
| 1969 |
+
return bool(has_item_keyword or (has_item_pattern and candidate) or (category_context and has_item_pattern and candidate))
|
| 1970 |
+
|
| 1971 |
def _detect_new_intent(text: str) -> str | None:
|
| 1972 |
"""
|
| 1973 |
Detects search_by_item or show_menu from text.
|
| 1974 |
Returns intent string or None.
|
| 1975 |
"""
|
| 1976 |
t = norm(text)
|
| 1977 |
+
# item-search patterns take priority over menu, e.g. "ู
ูุงู ุนูุฏู ุงุณู
ูุฒู ุฎูุฎ"
|
| 1978 |
+
if _looks_like_item_search(text):
|
| 1979 |
+
return "search_by_item"
|
| 1980 |
if any(kw in t for kw in _MENU_TRIGGERS):
|
| 1981 |
return "show_menu"
|
|
|
|
|
|
|
|
|
|
| 1982 |
return None
|
| 1983 |
|
| 1984 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
|
| 1997 |
"ุนุงูุฒ ุงูู","ุนุงูุฒุฉ ุงูู","ุนุงูุฒ ุงููุฉ","ุงุจู ุงูู",
|
| 1998 |
# categories
|
| 1999 |
"ู
ููู","menu","ูุงุฆู
ู","ูุงูู
ุฉ","ู
ุทุนู
","ู
ุทุงุนู
","ูุงููู","restaurant","cafe",
|
| 2000 |
+
"ู
ูุงู","ุงู
ุงูู","ุฃู
ุงูู","ู
ุญู","ุนูุฏู","ุนูุฏูุง","ุจูุนู
ู","ุจุชุนู
ู","ุจููุฏู
","ุจุชูุฏู
","ููู","ูููุง",
|
| 2001 |
# ุญุฑูู
|
| 2002 |
"ูู","ู
ู","ุนูู","ุนู","ููุง","ูุฑูุจ","ุงูุฑุจ","ุญูู","ูููุณ","ุฑุฎูุต",
|
| 2003 |
}
|
|
|
|
| 2006 |
_ITEM_PHRASE_STRIP = [
|
| 2007 |
"ุนุงูุฒ ุงููุฉ","ุนุงูุฒุฉ ุงููุฉ","ุนุงูุฒ ุงูู","ุนุงูุฒุฉ ุงูู",
|
| 2008 |
"ููู ุงูู","ุงุจุญุซ ุนู","ุจุฏูุฑ ุนูู","ุจุฏูุฑ ุนู",
|
| 2009 |
+
"ุนุงูุฒู ู
ูุงู ุนูุฏู","ุนุงูุฒุฉ ู
ูุงู ุนูุฏู","ุนุงูุฒ ู
ูุงู ุนูุฏู","ุนุงูุฒ ู
ูุงู ุนูุฏู",
|
| 2010 |
+
"ุนุงูุฒู ู
ูุงู ุนูุฏูุง","ุนุงูุฒุฉ ู
ูุงู ุนูุฏูุง","ุนุงูุฒ ู
ูุงู ุนูุฏูุง",
|
| 2011 |
+
"ู
ูุงู ุนูุฏู","ู
ูุงู ุนูุฏูุง","ู
ุทุนู
ุนูุฏู","ูุงููู ุนูุฏู",
|
| 2012 |
+
"ุนุงูุฒู","ุนุงูุฒุฉ","ุนุงูุฒ","ุนุงูุฒ","ููุณู ูู",
|
| 2013 |
]
|
| 2014 |
|
| 2015 |
+
_ITEM_SYNONYMS = {
|
| 2016 |
+
"ุงุณู
ูุฒู": ["ุงุณู
ูุฒู", "ุฅุณู
ูุฒู", "ุณู
ูุฒู", "ุณู
ูุฐู", "smoothie"],
|
| 2017 |
+
"ุณู
ูุฒู": ["ุงุณู
ูุฒู", "ุฅุณู
ูุฒู", "ุณู
ูุฒู", "ุณู
ูุฐู", "smoothie"],
|
| 2018 |
+
"ุณู
ูุฐู": ["ุงุณู
ูุฒู", "ุฅุณู
ูุฒู", "ุณู
ูุฒู", "ุณู
ูุฐู", "smoothie"],
|
| 2019 |
+
"ูุฑุงุฎ": ["ูุฑุงุฎ", "ูุฑุฎุฉ", "ูุฑุฎู", "ุฏุฌุงุฌ", "chicken"],
|
| 2020 |
+
"ูุฑุฎู": ["ูุฑุงุฎ", "ูุฑุฎุฉ", "ูุฑุฎู", "ุฏุฌุงุฌ", "chicken"],
|
| 2021 |
+
"ุฏุฌุงุฌ": ["ูุฑุงุฎ", "ูุฑุฎุฉ", "ูุฑุฎู", "ุฏุฌุงุฌ", "chicken"],
|
| 2022 |
+
"ุณุงูุฏูุชุด": ["ุณุงูุฏูุชุด", "ุณูุฏูุชุด", "sandwich"],
|
| 2023 |
+
"ุณูุฏูุชุด": ["ุณุงูุฏูุชุด", "ุณูุฏูุชุด", "sandwich"],
|
| 2024 |
+
}
|
| 2025 |
+
|
| 2026 |
+
def _stem_ar_word(w: str) -> str:
|
| 2027 |
+
w = clean_text(w)
|
| 2028 |
+
for sfx in ("ุงุช", "ูู", "ูู", "ูุฉ", "ูู", "ู", "ุฉ", "ู", "ุง"):
|
| 2029 |
+
if w.endswith(sfx) and len(w) - len(sfx) >= 3:
|
| 2030 |
+
return w[:-len(sfx)]
|
| 2031 |
+
return w
|
| 2032 |
+
|
| 2033 |
+
def _expand_item_query_words(item_query: str) -> list[str]:
|
| 2034 |
+
words = [clean_text(w) for w in str(item_query).split() if len(clean_text(w)) > 1]
|
| 2035 |
+
expanded = set(words)
|
| 2036 |
+
for w in words:
|
| 2037 |
+
expanded.add(_stem_ar_word(w))
|
| 2038 |
+
for alt in _ITEM_SYNONYMS.get(w, []):
|
| 2039 |
+
expanded.add(clean_text(alt))
|
| 2040 |
+
expanded.add(_stem_ar_word(alt))
|
| 2041 |
+
return [w for w in expanded if w]
|
| 2042 |
+
|
| 2043 |
_MENU_PHRASE_STRIP = [
|
| 2044 |
"ุนุงูุฒ ุงุดูู ู
ููู","ุนุงูุฒุฉ ุงุดูู ู
ููู","ุนุงูุฒ ุงุดูู ุงูู
ููู","ุนุงูุฒุฉ ุงุดูู ุงูู
ููู",
|
| 2045 |
"ูุงุช ู
ููู","ูุงุชูู ู
ููู","ูุงุชููุง ู
ููู",
|
|
|
|
| 2195 |
return places
|
| 2196 |
|
| 2197 |
|
| 2198 |
+
def _is_menu_item_object(p: dict) -> bool:
|
| 2199 |
+
"""True when backend returned menu-item rows, not place rows."""
|
| 2200 |
+
if not isinstance(p, dict):
|
| 2201 |
+
return False
|
| 2202 |
+
has_item_keys = any(k in p for k in ("item_name", "subcategory_name", "sub_category", "price"))
|
| 2203 |
+
has_place_keys = any(k in p for k in ("place_id", "lat", "lon", "location", "address", "rating", "opening_hours"))
|
| 2204 |
+
return bool(has_item_keys and not has_place_keys)
|
| 2205 |
+
|
| 2206 |
+
def _text_matches_item_query(text_blob: str, item_query: str) -> bool:
|
| 2207 |
+
"""Flexible token/stem/synonym matching for menu items."""
|
| 2208 |
+
blob = clean_text(text_blob)
|
| 2209 |
+
q_words = _expand_item_query_words(item_query)
|
| 2210 |
+
# Ignore weak generic terms when deciding if the item itself matched.
|
| 2211 |
+
weak = {"ุงูู", "ุงููู", "ุงููุฉ", "ูุฌุจู", "ูุฌุจุฉ", "ู
ุดุฑูุจ", "ู
ุดุฑูุจุงุช", "ู
ูุงู"}
|
| 2212 |
+
q_words = [w for w in q_words if w not in weak and len(w) > 1]
|
| 2213 |
+
if not q_words:
|
| 2214 |
+
return True
|
| 2215 |
+
blob_tokens = blob.split()
|
| 2216 |
+
blob_stems = {_stem_ar_word(t) for t in blob_tokens}
|
| 2217 |
+
hits = 0
|
| 2218 |
+
for w in q_words:
|
| 2219 |
+
sw = _stem_ar_word(w)
|
| 2220 |
+
if w in blob or sw in blob_stems or any(sw and (sw in bt or bt in sw) for bt in blob_stems):
|
| 2221 |
+
hits += 1
|
| 2222 |
+
# For multi-word items like "ุงุณู
ูุฒู ุฎูุฎ", one strong word is enough for broad recall,
|
| 2223 |
+
# but two-word match is preferred and kept by backend ordering.
|
| 2224 |
+
return hits >= 1
|
| 2225 |
+
|
| 2226 |
def filter_item_results_strict(places: list, item_query: str) -> list:
|
| 2227 |
"""
|
| 2228 |
+
Flexible but safe filter for search_by_item:
|
| 2229 |
+
- understands unseen item names by matching returned menu text, not only fixed keywords.
|
| 2230 |
+
- if backend returns menu-item rows with no parent place, keep matching items instead of false no_result.
|
| 2231 |
+
- if backend/local returns place rows, apply category guard only when category exists.
|
| 2232 |
"""
|
| 2233 |
if not places:
|
| 2234 |
return []
|
|
|
|
| 2236 |
q = clean_text(item_query)
|
| 2237 |
|
| 2238 |
food_keywords = {
|
| 2239 |
+
"ุดุงูุฑู
ุง", "ุจุฑุฌุฑ", "ุจูุชุฒุง", "ูุฑูุจ", "ู
ุดููุงุช", "ูุฑุงุฎ", "ุฏุฌุงุฌ",
|
| 2240 |
+
"ูุจุงุจ", "ููุชู", "ููุชุฉ", "ุณู
ู", "ุณุงูุฏูุชุด", "ุณูุฏูุชุด", "ุญูุงูุดู",
|
| 2241 |
+
"ูุดุฑู", "ู
ูุฑููู", "ู
ูุฑููุฉ", "ูุฌุจู", "ูุฌุจุฉ", "ุงูู", "ุฃูู",
|
| 2242 |
+
"burger", "pizza", "shawarma", "crepe", "chicken", "grill", "grills"
|
|
|
|
| 2243 |
}
|
| 2244 |
|
| 2245 |
drink_keywords = {
|
| 2246 |
+
"ูููุฉ", "ูููู", "ูููู", "ุนุตูุฑ", "ู
ุดุฑูุจ", "ู
ุดุฑูุจุงุช", "ูุงุชูู",
|
| 2247 |
+
"ุงุณุจุฑูุณู", "ูุณูุงููู", "ุณู
ูุฐู", "ุณู
ูุฒู", "ุงุณู
ูุฒู", "ุฅุณู
ูุฒู",
|
| 2248 |
"coffee", "juice", "latte", "espresso", "smoothie"
|
| 2249 |
}
|
| 2250 |
|
| 2251 |
is_food = any(clean_text(w) in q for w in food_keywords)
|
| 2252 |
is_drink = any(clean_text(w) in q for w in drink_keywords)
|
| 2253 |
|
|
|
|
| 2254 |
filtered = []
|
|
|
|
| 2255 |
for p in places:
|
| 2256 |
+
if not isinstance(p, dict):
|
| 2257 |
+
continue
|
| 2258 |
+
|
| 2259 |
raw_category = str(p.get("category", ""))
|
| 2260 |
category = normalize_category(raw_category)
|
| 2261 |
|
| 2262 |
text_blob = clean_text(
|
| 2263 |
+
f"{p.get('item_name','')} "
|
| 2264 |
f"{p.get('name','')} "
|
| 2265 |
f"{p.get('name_ar','')} "
|
| 2266 |
f"{p.get('name_en','')} "
|
| 2267 |
f"{p.get('category','')} "
|
| 2268 |
+
f"{p.get('subcategory_name','')} "
|
| 2269 |
f"{p.get('sub_category','')} "
|
| 2270 |
f"{p.get('description','')} "
|
| 2271 |
f"{p.get('matched_item','')} "
|
|
|
|
| 2274 |
f"{p.get('tags','')}"
|
| 2275 |
)
|
| 2276 |
|
| 2277 |
+
if not _text_matches_item_query(text_blob, item_query):
|
| 2278 |
continue
|
| 2279 |
|
| 2280 |
+
# If this is a pure menu item row, don't reject it because it has no place category.
|
| 2281 |
+
if _is_menu_item_object(p):
|
| 2282 |
+
filtered.append(p)
|
| 2283 |
+
continue
|
| 2284 |
+
|
| 2285 |
+
# Place rows: keep category safe when the item type is clear.
|
| 2286 |
+
if is_food and raw_category and category != "restaurant" and not _category_match_value(raw_category, "restaurant"):
|
| 2287 |
+
continue
|
| 2288 |
+
|
| 2289 |
+
if is_drink and raw_category and not (
|
| 2290 |
category in ("cafe", "restaurant") or
|
| 2291 |
_category_match_value(raw_category, "cafe") or
|
| 2292 |
_category_match_value(raw_category, "restaurant")
|
| 2293 |
):
|
| 2294 |
continue
|
| 2295 |
|
|
|
|
|
|
|
|
|
|
| 2296 |
filtered.append(p)
|
| 2297 |
|
| 2298 |
+
# dedup; for menu items use id+name+price, for places use place_id.
|
| 2299 |
seen, unique = set(), []
|
| 2300 |
for p in filtered:
|
| 2301 |
+
pid = str(p.get("place_id") or p.get("placeId") or p.get("place_id_id") or "")
|
| 2302 |
+
if not pid:
|
| 2303 |
+
pid = f"item:{p.get('id','')}:{p.get('item_name') or p.get('name','')}:{p.get('price','')}"
|
| 2304 |
+
if pid not in seen:
|
| 2305 |
seen.add(pid)
|
| 2306 |
unique.append(p)
|
| 2307 |
return unique
|
|
|
|
| 2399 |
def format_item_search_response(places: list[dict], item_query: str, user_lat=None, user_lon=None) -> dict:
|
| 2400 |
"""
|
| 2401 |
Builds the structured response for search_by_item intent.
|
| 2402 |
+
Preferred output is places_cards when parent places are available.
|
| 2403 |
+
If backend returns menu-item rows only, return menu_list so the matched item is not lost.
|
| 2404 |
"""
|
| 2405 |
if not places:
|
| 2406 |
return {
|
|
|
|
| 2408 |
"intent": "search_by_item",
|
| 2409 |
"ui_type": CFG.UI_TEXT_ONLY,
|
| 2410 |
"place_cards": [],
|
| 2411 |
+
"menu_items": [],
|
| 2412 |
"best_place": None,
|
| 2413 |
}
|
| 2414 |
+
|
| 2415 |
+
# Backend sometimes returns menu-item rows without parent place data.
|
| 2416 |
+
# In this case, showing the matched menu items is better than saying no_result.
|
| 2417 |
+
if all(_is_menu_item_object(p) for p in places):
|
| 2418 |
+
items = [_fmt_menu_item(i) for i in places[:20]]
|
| 2419 |
+
return {
|
| 2420 |
+
"reply": f"ูููุช ุฃุตูุงู ู
ุทุงุจูุฉ ูู *{item_query}* ๐",
|
| 2421 |
+
"intent": "search_by_item",
|
| 2422 |
+
"ui_type": CFG.UI_MENU_LIST,
|
| 2423 |
+
"place_cards": [],
|
| 2424 |
+
"menu_items": items,
|
| 2425 |
+
"best_place": None,
|
| 2426 |
+
}
|
| 2427 |
+
|
| 2428 |
places = _sort_places_by_distance(places, user_lat, user_lon)
|
| 2429 |
cards = [clean_place(p) for p in places[:CFG.MAX_CARDS]]
|
| 2430 |
# dedup
|
| 2431 |
seen, unique = set(), []
|
| 2432 |
for c in cards:
|
| 2433 |
+
pid = c.get("place_id") or c.get("id") or c.get("name")
|
| 2434 |
if pid not in seen:
|
| 2435 |
seen.add(pid)
|
| 2436 |
unique.append(c)
|
| 2437 |
+
reply = f"๐ฝ๏ธ ูุงููุชูู {len(unique)} ู
ูุงู ููู *{item_query}* โ ุงุฎุชุงุฑ:"
|
| 2438 |
return {
|
| 2439 |
"reply": reply,
|
| 2440 |
"intent": "search_by_item",
|
| 2441 |
"ui_type": CFG.UI_PLACES_CARDS,
|
| 2442 |
"place_cards": unique,
|
| 2443 |
+
"menu_items": [],
|
| 2444 |
"best_place": unique[0] if unique else None,
|
| 2445 |
}
|
| 2446 |
|
|
|
|
| 2756 |
ui_type=res.get("ui_type", CFG.UI_TEXT_ONLY),
|
| 2757 |
best_place=res.get("best_place"),
|
| 2758 |
place_cards=res.get("place_cards", []),
|
| 2759 |
+
menu_items=res.get("menu_items", []),
|
| 2760 |
)
|
| 2761 |
|
| 2762 |
if new_intent == "show_menu":
|