Spaces:
Sleeping
Sleeping
Panagiotis Spanakis commited on
Commit ·
6b287bc
1
Parent(s): c820115
Fix hallucinations
Browse files
app.py
CHANGED
|
@@ -127,7 +127,7 @@ HOTEL = HotelSim()
|
|
| 127 |
|
| 128 |
HOTEL_INFO = {
|
| 129 |
"name": "Bluebird Hotel",
|
| 130 |
-
"location": "
|
| 131 |
"star_rating": 5,
|
| 132 |
"amenities": [
|
| 133 |
"Rooftop infinity pool",
|
|
@@ -154,6 +154,46 @@ HOTEL_INFO = {
|
|
| 154 |
"check_in_time": "15:00",
|
| 155 |
"check_out_time": "11:00",
|
| 156 |
"late_checkout_policy": "Subject to availability; fees may apply after 13:00.",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
}
|
| 158 |
|
| 159 |
OFFERS_SEED = [
|
|
@@ -283,6 +323,50 @@ def init_offers_state(session):
|
|
| 283 |
return format_offer_card(first), accepted_summary_md(session["accepted_offers"]), 0, session
|
| 284 |
|
| 285 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
def next_offer_action(index, session):
|
| 287 |
offers = get_active_offers()
|
| 288 |
if not offers:
|
|
@@ -348,8 +432,10 @@ def load_llm():
|
|
| 348 |
model=model,
|
| 349 |
tokenizer=tok,
|
| 350 |
max_new_tokens=int(os.getenv("LLM_MAX_NEW_TOKENS", 256)),
|
| 351 |
-
temperature=float(os.getenv("LLM_TEMP", 0.
|
| 352 |
top_p=float(os.getenv("LLM_TOP_P", 0.9)),
|
|
|
|
|
|
|
| 353 |
do_sample=True,
|
| 354 |
)
|
| 355 |
if _LANGCHAIN_AVAILABLE:
|
|
@@ -363,7 +449,7 @@ def load_llm():
|
|
| 363 |
return None
|
| 364 |
|
| 365 |
|
| 366 |
-
def compose_system_prompt():
|
| 367 |
amenities = "\n".join(f"- {a}" for a in HOTEL_INFO["amenities"])
|
| 368 |
treatments = "\n".join(f"- {t}" for t in HOTEL_INFO["spa"]["signature_treatments"])
|
| 369 |
try:
|
|
@@ -371,12 +457,49 @@ def compose_system_prompt():
|
|
| 371 |
except Exception:
|
| 372 |
offers_list = []
|
| 373 |
offers = "\n".join(f"* {o['title']}: {o['desc']} ({o['price_text']})" for o in offers_list if o.get("active"))
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
|
| 381 |
|
| 382 |
def _sanitize_llm_output(text: str) -> str:
|
|
@@ -388,12 +511,21 @@ def _sanitize_llm_output(text: str) -> str:
|
|
| 388 |
for p in ("Assistant:", "ASSISTANT:", "assistant:", "AI:", "Bot:"):
|
| 389 |
if t.startswith(p):
|
| 390 |
t = t[len(p):].lstrip()
|
|
|
|
|
|
|
|
|
|
| 391 |
# Cut at first sign of a new role line
|
| 392 |
for marker in ("\nUser:", "\nUSER:", "\nHuman:", "\nHUMAN:", "\nAssistant:", "\nASSISTANT:", "\nSystem:"):
|
| 393 |
idx = t.find(marker)
|
| 394 |
if idx != -1:
|
| 395 |
t = t[:idx].rstrip()
|
| 396 |
break
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
# Normalize spaces
|
| 398 |
t = " ".join(t.split())
|
| 399 |
# Soft cap ~120 words
|
|
@@ -403,22 +535,56 @@ def _sanitize_llm_output(text: str) -> str:
|
|
| 403 |
return t
|
| 404 |
|
| 405 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
def ai_general_response(user_message: str, session: dict) -> Optional[str]:
|
| 407 |
llm = load_llm()
|
| 408 |
if not llm:
|
| 409 |
return "(AI assistant unavailable right now — please try again later or ask at the front desk.)"
|
| 410 |
-
|
|
|
|
| 411 |
# Use an instruction-style prompt to avoid multi-turn role simulation
|
| 412 |
final_prompt = system_prompt + "\n\nQuestion: " + user_message.strip() + "\nAnswer:"
|
| 413 |
try:
|
| 414 |
if hasattr(llm, "__call__") and not hasattr(llm, "_pipeline"):
|
| 415 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 416 |
if isinstance(out, list):
|
| 417 |
raw = out[0].get("generated_text", "")
|
| 418 |
text = raw[len(final_prompt):] if raw.startswith(final_prompt) else raw
|
| 419 |
else:
|
| 420 |
text = str(out)
|
| 421 |
else:
|
|
|
|
| 422 |
text = llm(final_prompt) # type: ignore
|
| 423 |
return _sanitize_llm_output(text)
|
| 424 |
except Exception as e:
|
|
@@ -559,8 +725,49 @@ def handle_chat(message, chat_history, session, room_type_dd, ci, co):
|
|
| 559 |
return chat
|
| 560 |
chat.append(user_reply(msg))
|
| 561 |
lower = msg.lower()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 562 |
if any(k in lower for k in ["clean", "housekeeping", "maid"]):
|
| 563 |
return call_cleaning_action(chat, session)
|
|
|
|
| 564 |
if "book" in lower:
|
| 565 |
guessed_type = None
|
| 566 |
for t in HOTEL.room_types.keys():
|
|
@@ -632,6 +839,15 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue")) as demo:
|
|
| 632 |
with gr.Row():
|
| 633 |
send = gr.Button("Send", variant="primary")
|
| 634 |
clean_btn = gr.Button("Call Cleaning")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 635 |
|
| 636 |
# Right: Booking + Offers
|
| 637 |
with gr.Column(scale=1):
|
|
@@ -679,6 +895,16 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue")) as demo:
|
|
| 679 |
inputs=[chat, session],
|
| 680 |
outputs=[chat],
|
| 681 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 682 |
book_btn.click(
|
| 683 |
book_action,
|
| 684 |
inputs=[guest_name, room_type_dd, check_in, check_out, chat, session],
|
|
|
|
| 127 |
|
| 128 |
HOTEL_INFO = {
|
| 129 |
"name": "Bluebird Hotel",
|
| 130 |
+
"location": "Volos, Greece — Pagasetic Gulf waterfront, gateway to Mount Pelion",
|
| 131 |
"star_rating": 5,
|
| 132 |
"amenities": [
|
| 133 |
"Rooftop infinity pool",
|
|
|
|
| 154 |
"check_in_time": "15:00",
|
| 155 |
"check_out_time": "11:00",
|
| 156 |
"late_checkout_policy": "Subject to availability; fees may apply after 13:00.",
|
| 157 |
+
# Curated local guide for Volos & nearby Pelion
|
| 158 |
+
"local_guide": {
|
| 159 |
+
"dining_tsipouradika": [
|
| 160 |
+
"MeZen (modern tsipouro meze, city center)",
|
| 161 |
+
"Ouzeri Ta Kymata (classic seafood meze on the promenade)",
|
| 162 |
+
"Iolkos Tsipouradiko (casual, fresh small plates, near port)",
|
| 163 |
+
],
|
| 164 |
+
"beaches": [
|
| 165 |
+
"Anavros Beach (city beach, easy walk from center)",
|
| 166 |
+
"Kala Nera (Pelion west coast ~25–35 min drive)",
|
| 167 |
+
"Mylopotamos (Pelion east coast ~1h 15m; iconic rock arch)",
|
| 168 |
+
"Papa Nero & Agios Ioannis (east Pelion ~1h 10m; organized)"
|
| 169 |
+
],
|
| 170 |
+
"pelion_villages": [
|
| 171 |
+
"Makrinitsa (balconies of Pelion, views over Volos ~25 min)",
|
| 172 |
+
"Portaria (traditional square & tavernas ~20 min)",
|
| 173 |
+
"Tsagarada (stone bridges & chestnut forests ~1h 10m)",
|
| 174 |
+
"Milies (terminus of Pelion steam train ~50 min)"
|
| 175 |
+
],
|
| 176 |
+
"activities": [
|
| 177 |
+
"Pelion Train (Ano Lechonia → Milies; weekends seasonal)",
|
| 178 |
+
"Waterfront promenade sunset stroll (Argonauts Avenue)",
|
| 179 |
+
"Sea kayaking / sailing in Pagasetic Gulf (calm waters)",
|
| 180 |
+
"Hiking Centaurs' Path (Portaria ↔ Makrinitsa)"
|
| 181 |
+
],
|
| 182 |
+
"museums_culture": [
|
| 183 |
+
"Athanasakeion Archaeological Museum of Volos",
|
| 184 |
+
"Rooftile & Brickworks Museum N. & S. Tsalapatas",
|
| 185 |
+
"Volos Municipal Gallery — Giorgio de Chirico Art Center",
|
| 186 |
+
],
|
| 187 |
+
"day_trips": [
|
| 188 |
+
"Ferry to Skiathos (summer schedule from Volos port)",
|
| 189 |
+
"Meteora monasteries (long day ~2h each way by car)",
|
| 190 |
+
],
|
| 191 |
+
"cafes_bars": [
|
| 192 |
+
"Isalos (seafront coffee & cocktails)",
|
| 193 |
+
"Canteen on the waterfront (casual bites)",
|
| 194 |
+
"Grooove (drinks, music near center)",
|
| 195 |
+
],
|
| 196 |
+
},
|
| 197 |
}
|
| 198 |
|
| 199 |
OFFERS_SEED = [
|
|
|
|
| 323 |
return format_offer_card(first), accepted_summary_md(session["accepted_offers"]), 0, session
|
| 324 |
|
| 325 |
|
| 326 |
+
def context_summary_md(session: dict) -> str:
|
| 327 |
+
s = session or {}
|
| 328 |
+
parts = []
|
| 329 |
+
if s.get("time_of_day"):
|
| 330 |
+
parts.append(f"Time: {s['time_of_day']}")
|
| 331 |
+
if s.get("weather"):
|
| 332 |
+
parts.append(f"Weather: {s['weather']}")
|
| 333 |
+
nk = s.get("near_km")
|
| 334 |
+
if isinstance(nk, (int, float)) and nk > 0:
|
| 335 |
+
parts.append(f"Near me: ~{int(nk)} km")
|
| 336 |
+
if not parts:
|
| 337 |
+
return "_No extra context set. Seasonal notes apply automatically._"
|
| 338 |
+
return "**Context:** " + ", ".join(parts)
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
def set_context_action(time_of_day, weather, near_km, session):
|
| 342 |
+
s = session or {}
|
| 343 |
+
if time_of_day:
|
| 344 |
+
s["time_of_day"] = str(time_of_day)
|
| 345 |
+
else:
|
| 346 |
+
s.pop("time_of_day", None)
|
| 347 |
+
if weather:
|
| 348 |
+
s["weather"] = str(weather)
|
| 349 |
+
else:
|
| 350 |
+
s.pop("weather", None)
|
| 351 |
+
try:
|
| 352 |
+
nk = float(near_km) if near_km is not None else None
|
| 353 |
+
except Exception:
|
| 354 |
+
nk = None
|
| 355 |
+
if nk is not None and nk >= 0:
|
| 356 |
+
s["near_km"] = nk
|
| 357 |
+
else:
|
| 358 |
+
s.pop("near_km", None)
|
| 359 |
+
return s, context_summary_md(s)
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
def clear_context_action(session):
|
| 363 |
+
s = session or {}
|
| 364 |
+
s.pop("time_of_day", None)
|
| 365 |
+
s.pop("weather", None)
|
| 366 |
+
s.pop("near_km", None)
|
| 367 |
+
return s, context_summary_md(s), None, None, 0
|
| 368 |
+
|
| 369 |
+
|
| 370 |
def next_offer_action(index, session):
|
| 371 |
offers = get_active_offers()
|
| 372 |
if not offers:
|
|
|
|
| 432 |
model=model,
|
| 433 |
tokenizer=tok,
|
| 434 |
max_new_tokens=int(os.getenv("LLM_MAX_NEW_TOKENS", 256)),
|
| 435 |
+
temperature=float(os.getenv("LLM_TEMP", 0.3)),
|
| 436 |
top_p=float(os.getenv("LLM_TOP_P", 0.9)),
|
| 437 |
+
repetition_penalty=float(os.getenv("LLM_REP_PENALTY", 1.1)),
|
| 438 |
+
no_repeat_ngram_size=int(os.getenv("LLM_NO_REPEAT_NGRAM", 3)),
|
| 439 |
do_sample=True,
|
| 440 |
)
|
| 441 |
if _LANGCHAIN_AVAILABLE:
|
|
|
|
| 449 |
return None
|
| 450 |
|
| 451 |
|
| 452 |
+
def compose_system_prompt(dynamic_context: str = ""):
|
| 453 |
amenities = "\n".join(f"- {a}" for a in HOTEL_INFO["amenities"])
|
| 454 |
treatments = "\n".join(f"- {t}" for t in HOTEL_INFO["spa"]["signature_treatments"])
|
| 455 |
try:
|
|
|
|
| 457 |
except Exception:
|
| 458 |
offers_list = []
|
| 459 |
offers = "\n".join(f"* {o['title']}: {o['desc']} ({o['price_text']})" for o in offers_list if o.get("active"))
|
| 460 |
+
# Local guide sections (Volos & Pelion)
|
| 461 |
+
lg = HOTEL_INFO.get("local_guide", {})
|
| 462 |
+
def _fmt(cat: str) -> str:
|
| 463 |
+
items = lg.get(cat, [])
|
| 464 |
+
return "\n".join(f"- {x}" for x in items)
|
| 465 |
+
sections: List[str] = []
|
| 466 |
+
if lg.get("dining_tsipouradika"):
|
| 467 |
+
sections.append("Dining (tsipouradika):\n" + _fmt("dining_tsipouradika"))
|
| 468 |
+
if lg.get("beaches"):
|
| 469 |
+
sections.append("Beaches:\n" + _fmt("beaches"))
|
| 470 |
+
if lg.get("pelion_villages"):
|
| 471 |
+
sections.append("Pelion villages:\n" + _fmt("pelion_villages"))
|
| 472 |
+
if lg.get("activities"):
|
| 473 |
+
sections.append("Activities:\n" + _fmt("activities"))
|
| 474 |
+
if lg.get("museums_culture"):
|
| 475 |
+
sections.append("Museums & culture:\n" + _fmt("museums_culture"))
|
| 476 |
+
if lg.get("day_trips"):
|
| 477 |
+
sections.append("Day trips & ferries:\n" + _fmt("day_trips"))
|
| 478 |
+
if lg.get("cafes_bars"):
|
| 479 |
+
sections.append("Cafés & bars:\n" + _fmt("cafes_bars"))
|
| 480 |
+
local_guide = "\n\n".join(sections)
|
| 481 |
+
|
| 482 |
+
base = f"""You are the concierge AI for {HOTEL_INFO['name']}. Provide concise, friendly answers.
|
| 483 |
+
Hotel Facts:
|
| 484 |
+
Location: {HOTEL_INFO['location']}
|
| 485 |
+
Star Rating: {HOTEL_INFO['star_rating']}⭐
|
| 486 |
+
Check-in: {HOTEL_INFO['check_in_time']} | Check-out: {HOTEL_INFO['check_out_time']}
|
| 487 |
+
Amenities:\n{amenities}\nSpa: {HOTEL_INFO['spa']['name']} (Open {HOTEL_INFO['spa']['open']})\nSignature Treatments:\n{treatments}\nDining Venues: {', '.join(HOTEL_INFO['dining'])}\nLate Checkout: {HOTEL_INFO['late_checkout_policy']}\nCurrent Upsell Offers:\n{offers}
|
| 488 |
+
Local Guide — Volos & Pelion:\n{local_guide}
|
| 489 |
+
"""
|
| 490 |
+
if dynamic_context:
|
| 491 |
+
base += f"Dynamic Context:\n{dynamic_context}\n"
|
| 492 |
+
base += (
|
| 493 |
+
"Instructions:\n"
|
| 494 |
+
"- Answer the user's question directly using the facts above.\n"
|
| 495 |
+
"- Do NOT include any role prefixes like 'User:' or 'Assistant:'.\n"
|
| 496 |
+
"- Do NOT create additional 'User:' turns in your output.\n"
|
| 497 |
+
"- Do NOT echo 'Question:' or 'Answer:' in the output.\n"
|
| 498 |
+
"- Prefer nearby Volos & Pelion suggestions; for food, prioritize tsipouradika seafood meze. Include short distance/time if helpful.\n"
|
| 499 |
+
"- Keep responses under 120 words.\n"
|
| 500 |
+
"Reply with only the answer text."
|
| 501 |
+
)
|
| 502 |
+
return base
|
| 503 |
|
| 504 |
|
| 505 |
def _sanitize_llm_output(text: str) -> str:
|
|
|
|
| 511 |
for p in ("Assistant:", "ASSISTANT:", "assistant:", "AI:", "Bot:"):
|
| 512 |
if t.startswith(p):
|
| 513 |
t = t[len(p):].lstrip()
|
| 514 |
+
# Remove leading Answer: prefix if present
|
| 515 |
+
if t.startswith("Answer:"):
|
| 516 |
+
t = t[len("Answer:"):].lstrip()
|
| 517 |
# Cut at first sign of a new role line
|
| 518 |
for marker in ("\nUser:", "\nUSER:", "\nHuman:", "\nHUMAN:", "\nAssistant:", "\nASSISTANT:", "\nSystem:"):
|
| 519 |
idx = t.find(marker)
|
| 520 |
if idx != -1:
|
| 521 |
t = t[:idx].rstrip()
|
| 522 |
break
|
| 523 |
+
# If the model starts another pseudo Q/A, cut at repeated Question/Answer markers
|
| 524 |
+
for marker in ("\nQuestion:", "\nQUESTION:", "\nAnswer:", "\nANSWER:"):
|
| 525 |
+
idx = t.find(marker)
|
| 526 |
+
if idx != -1:
|
| 527 |
+
t = t[:idx].rstrip()
|
| 528 |
+
break
|
| 529 |
# Normalize spaces
|
| 530 |
t = " ".join(t.split())
|
| 531 |
# Soft cap ~120 words
|
|
|
|
| 535 |
return t
|
| 536 |
|
| 537 |
|
| 538 |
+
def _build_dynamic_context(session: dict) -> str:
|
| 539 |
+
"""Lightweight contextual notes; placeholder for time/weather/nearby in future UI.
|
| 540 |
+
Uses session hints if present; otherwise empty string.
|
| 541 |
+
"""
|
| 542 |
+
parts: List[str] = []
|
| 543 |
+
# Time of day
|
| 544 |
+
tod = (session or {}).get("time_of_day")
|
| 545 |
+
if tod:
|
| 546 |
+
parts.append(f"Time of day: {tod}")
|
| 547 |
+
# Weather
|
| 548 |
+
wx = (session or {}).get("weather")
|
| 549 |
+
if wx:
|
| 550 |
+
parts.append(f"Weather: {wx}")
|
| 551 |
+
# Near-me radius (km)
|
| 552 |
+
near = (session or {}).get("near_km")
|
| 553 |
+
if isinstance(near, (int, float)) and near > 0:
|
| 554 |
+
parts.append(f"Focus on places within ~{near} km when relevant.")
|
| 555 |
+
# Seasonal hint
|
| 556 |
+
month = dt.datetime.now().month
|
| 557 |
+
season = (
|
| 558 |
+
"summer (beaches, ferries to Sporades)" if month in (6, 7, 8) else
|
| 559 |
+
"shoulder season (milder weather; Pelion villages, hikes)" if month in (5, 9, 10) else
|
| 560 |
+
"cool season (mountain villages, museums, hearty food)"
|
| 561 |
+
)
|
| 562 |
+
parts.append(f"Seasonal note: {season}.")
|
| 563 |
+
return "\n".join(parts)
|
| 564 |
+
|
| 565 |
+
|
| 566 |
def ai_general_response(user_message: str, session: dict) -> Optional[str]:
|
| 567 |
llm = load_llm()
|
| 568 |
if not llm:
|
| 569 |
return "(AI assistant unavailable right now — please try again later or ask at the front desk.)"
|
| 570 |
+
dynamic_ctx = _build_dynamic_context(session)
|
| 571 |
+
system_prompt = compose_system_prompt(dynamic_ctx)
|
| 572 |
# Use an instruction-style prompt to avoid multi-turn role simulation
|
| 573 |
final_prompt = system_prompt + "\n\nQuestion: " + user_message.strip() + "\nAnswer:"
|
| 574 |
try:
|
| 575 |
if hasattr(llm, "__call__") and not hasattr(llm, "_pipeline"):
|
| 576 |
+
# Likely a raw HF pipeline; try passing conservative decoding args to reduce hallucinations
|
| 577 |
+
try:
|
| 578 |
+
out = llm(final_prompt, max_new_tokens=200, temperature=0.2, top_p=0.9)
|
| 579 |
+
except Exception:
|
| 580 |
+
out = llm(final_prompt)
|
| 581 |
if isinstance(out, list):
|
| 582 |
raw = out[0].get("generated_text", "")
|
| 583 |
text = raw[len(final_prompt):] if raw.startswith(final_prompt) else raw
|
| 584 |
else:
|
| 585 |
text = str(out)
|
| 586 |
else:
|
| 587 |
+
# LangChain or other wrapper; fall back to simple call
|
| 588 |
text = llm(final_prompt) # type: ignore
|
| 589 |
return _sanitize_llm_output(text)
|
| 590 |
except Exception as e:
|
|
|
|
| 725 |
return chat
|
| 726 |
chat.append(user_reply(msg))
|
| 727 |
lower = msg.lower()
|
| 728 |
+
# Offer navigation & acceptance: deterministic actions
|
| 729 |
+
if any(k in lower for k in ["next offer", "next >>", "next ▶", "show next offer"]):
|
| 730 |
+
card, idx = next_offer_action(session.get("offers_index", 0), session)
|
| 731 |
+
session["offers_index"] = idx
|
| 732 |
+
chat.append(system_reply("Showing next offer."))
|
| 733 |
+
chat.append(system_reply(card))
|
| 734 |
+
return chat
|
| 735 |
+
if any(k in lower for k in ["prev offer", "previous offer", "◀ prev", "show previous offer"]):
|
| 736 |
+
card, idx = prev_offer_action(session.get("offers_index", 0), session)
|
| 737 |
+
session["offers_index"] = idx
|
| 738 |
+
chat.append(system_reply("Showing previous offer."))
|
| 739 |
+
chat.append(system_reply(card))
|
| 740 |
+
return chat
|
| 741 |
+
if any(k in lower for k in ["accept offer", "i'll take it", "i will take it", "buy offer", "add offer"]):
|
| 742 |
+
# accept current index
|
| 743 |
+
updated_chat, card, accepted_md_text, new_idx = accept_offer_action(session.get("offers_index", 0), chat, session)
|
| 744 |
+
session["offers_index"] = new_idx
|
| 745 |
+
chat = updated_chat
|
| 746 |
+
chat.append(system_reply(card))
|
| 747 |
+
chat.append(system_reply(accepted_md_text))
|
| 748 |
+
return chat
|
| 749 |
+
# Accept a specific offer by title keywords, e.g., "I want the Spa Thermal Circuit Upgrade"
|
| 750 |
+
if any(k in lower for k in ["i want", "i'd like", "i will take", "i will get", "i want the", "i want to buy"]):
|
| 751 |
+
offers = get_active_offers()
|
| 752 |
+
if offers:
|
| 753 |
+
# find best title match
|
| 754 |
+
best_idx, best_score = None, 0
|
| 755 |
+
for i, o in enumerate(offers):
|
| 756 |
+
title = o.get("title", "").lower()
|
| 757 |
+
score = sum(1 for w in title.split() if w in lower)
|
| 758 |
+
if score > best_score:
|
| 759 |
+
best_score, best_idx = score, i
|
| 760 |
+
if best_idx is not None and best_score > 0:
|
| 761 |
+
updated_chat, card, accepted_md_text, new_idx = accept_offer_action(best_idx, chat, session)
|
| 762 |
+
session["offers_index"] = new_idx
|
| 763 |
+
chat = updated_chat
|
| 764 |
+
chat.append(system_reply(card))
|
| 765 |
+
chat.append(system_reply(accepted_md_text))
|
| 766 |
+
return chat
|
| 767 |
+
# Cleaning intents
|
| 768 |
if any(k in lower for k in ["clean", "housekeeping", "maid"]):
|
| 769 |
return call_cleaning_action(chat, session)
|
| 770 |
+
# Booking intents
|
| 771 |
if "book" in lower:
|
| 772 |
guessed_type = None
|
| 773 |
for t in HOTEL.room_types.keys():
|
|
|
|
| 839 |
with gr.Row():
|
| 840 |
send = gr.Button("Send", variant="primary")
|
| 841 |
clean_btn = gr.Button("Call Cleaning")
|
| 842 |
+
with gr.Accordion("Context (time, weather, near me)", open=False):
|
| 843 |
+
with gr.Row():
|
| 844 |
+
time_of_day_dd = gr.Dropdown(["morning", "afternoon", "evening", "night"], label="Time of day", value=None)
|
| 845 |
+
weather_dd = gr.Dropdown(["clear", "cloudy", "rain", "windy", "hot"], label="Weather", value=None)
|
| 846 |
+
near_slider = gr.Slider(0, 50, value=0, step=1, label="Near me radius (km)")
|
| 847 |
+
with gr.Row():
|
| 848 |
+
apply_ctx_btn = gr.Button("Apply Context")
|
| 849 |
+
clear_ctx_btn = gr.Button("Clear Context")
|
| 850 |
+
ctx_status = gr.Markdown("_No extra context set. Seasonal notes apply automatically._")
|
| 851 |
|
| 852 |
# Right: Booking + Offers
|
| 853 |
with gr.Column(scale=1):
|
|
|
|
| 895 |
inputs=[chat, session],
|
| 896 |
outputs=[chat],
|
| 897 |
)
|
| 898 |
+
apply_ctx_btn.click(
|
| 899 |
+
set_context_action,
|
| 900 |
+
inputs=[time_of_day_dd, weather_dd, near_slider, session],
|
| 901 |
+
outputs=[session, ctx_status],
|
| 902 |
+
)
|
| 903 |
+
clear_ctx_btn.click(
|
| 904 |
+
clear_context_action,
|
| 905 |
+
inputs=[session],
|
| 906 |
+
outputs=[session, ctx_status, time_of_day_dd, weather_dd, near_slider],
|
| 907 |
+
)
|
| 908 |
book_btn.click(
|
| 909 |
book_action,
|
| 910 |
inputs=[guest_name, room_type_dd, check_in, check_out, chat, session],
|