Panagiotis Spanakis commited on
Commit
6b287bc
·
1 Parent(s): c820115

Fix hallucinations

Browse files
Files changed (1) hide show
  1. app.py +237 -11
app.py CHANGED
@@ -127,7 +127,7 @@ HOTEL = HotelSim()
127
 
128
  HOTEL_INFO = {
129
  "name": "Bluebird Hotel",
130
- "location": "Seaside promenade, 5 minutes from the marina",
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.7)),
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
- return f"""You are the concierge AI for {HOTEL_INFO['name']}. Provide concise, friendly answers.
375
- Hotel Facts:
376
- Location: {HOTEL_INFO['location']}
377
- Star Rating: {HOTEL_INFO['star_rating']}⭐
378
- Check-in: {HOTEL_INFO['check_in_time']} | Check-out: {HOTEL_INFO['check_out_time']}
379
- 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}\nInstructions:\n- Answer the user's question directly using the facts above.\n- Do NOT include any role prefixes like 'User:' or 'Assistant:'.\n- Do NOT create additional 'User:' turns in your output.\n- Keep responses under 120 words.\nReply with only the answer text."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- system_prompt = compose_system_prompt()
 
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
- out = llm(final_prompt)
 
 
 
 
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],