Spaces:
Sleeping
Sleeping
| from langgraph.graph import StateGraph,START,END,add_messages | |
| from typing import TypedDict,Optional | |
| from langgraph.checkpoint.memory import MemorySaver | |
| from langchain_groq import ChatGroq | |
| from datetime import datetime,timedelta | |
| from hotel_api import fetch_inventory_availability,update_inventory,update_rates,fetch_room_availability,create_reservation | |
| import os | |
| import re | |
| import json | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| memory = MemorySaver() | |
| class SupportState(TypedDict): | |
| query: str | |
| next_agent: str | |
| response: str | |
| # awaiting_confirmation: Optional[bool] | |
| # pending_action: Optional[dict] | |
| pending_manager: Optional[str] | |
| pending_action: Optional[dict] | |
| phone_number_id: str | |
| booking_link: str | |
| booking_step: str | |
| llm = ChatGroq( | |
| model="llama-3.3-70b-versatile", | |
| api_key=os.getenv("GROQ_API_KEY"), | |
| temperature=0 | |
| ) | |
| async def choose_agent(state: SupportState): | |
| if state.get("pending_manager"): | |
| return {"next_agent":state["pending_manager"]} | |
| prompt = f"""Classify the hotel owner's request into one of these 3 managers: | |
| 'inventory' - Check availability, block, unblock, release, or unrelease rooms. | |
| 'rates' - Check room prices or change room rates. | |
| 'reservations' - Make a new booking, modify existing, or cancel. | |
| 'unknown' - Any request not related to hotel management (coding, general knowledge, etc.) | |
| Request: {state['query']} | |
| IMPORTANT RULES: | |
| - If the user is asking "how many" (कितने) or asking for counts, it is ALWAYS "inventory", even if they use the word "booked". | |
| - ONLY return "reservations" if there is a specific guest involved or an intent to create a new booking right now. | |
| Return ONLY one word: inventory, rates, reservations, or unknown.""" | |
| response = llm.invoke(prompt) | |
| manager = response.content.strip().lower().replace(".","").replace("'","") | |
| if manager not in ["inventory","rates","reservations"]: | |
| manager = "unknown" | |
| print(f"LLM returned: '{response.content.strip().lower()}'") | |
| return {"next_agent": manager} | |
| async def inventory_manager(state: SupportState): | |
| if state.get("pending_manager") == "inventory": | |
| user_reply = str(state.get("query","")).lower() | |
| prompt = f"""User said: "{user_reply}" | |
| If this is a positive confirmation for a booking, return "yes" | |
| otherwise return "no". Nothing extra.""" | |
| response = llm.invoke(prompt) | |
| confirmation = response.content.strip().lower() | |
| if confirmation == "yes": | |
| pa = state.get("pending_action") | |
| if not pa: | |
| return {"response": "I seem to have lost the room details. Could you please repeat your inventory request?", "pending_manager": None, "pending_action": None} | |
| from datetime import datetime, timedelta | |
| today = datetime.now() | |
| start_date = today.strftime("%Y-%m-%d") | |
| end_date = (today + timedelta(days=1)).strftime("%Y-%m-%d") | |
| success, data = await update_inventory( | |
| category=pa["category"], | |
| operation=pa["action"], | |
| count=pa["count"], | |
| start_date=start_date, | |
| end_date=end_date | |
| ) | |
| action_words = {"block": "blocked", "unblock": "unblocked", "release": "released", "unrelease": "unreleased"} | |
| past_action = action_words.get(pa["action"], pa["action"]) | |
| if success: | |
| status = f"Got it. I have successfully {past_action} {pa['count']} {pa['category']} room(s) for tonight." | |
| else: | |
| error_msg = data.get("error","an unknown API error") | |
| status = f"I tried to {pa['action']} those {pa['category']} rooms, but the system returned an error: {error_msg}." | |
| return {"response": status,"pending_action": None,"pending_manager": None} | |
| else: | |
| return {"response": "Okay, I have cancelled the inventory change.", "pending_manager": "", "pending_action": {}} | |
| from hotel_api import get_room_map | |
| room_map = await get_room_map() | |
| real_categories = list(room_map.keys()) | |
| prompt = f"""Extract inventory details from the request. | |
| Request: {state['query']} | |
| AVAILABLE_ROOM_CATEGORIES: {real_categories} | |
| Return ONLY a JSON object: {{"action": "check|block|unblock|release|unrelease", "category": "category_name_from_list|all", "count": number}} | |
| IMPORTANT HINGLISH NUMBER RULES: | |
| - "ek" = 1, "do" = 2, "teen" = 3, "char" = 4. | |
| - Example: "do double room block kar do" means count=2, category="double", action="block". | |
| - Example: "teen stanndard room book kar do" means count=2, category="standard", action="book". | |
| RULES: | |
| 1. For 'category', you MUST pick the exact closest match strictly from the AVAILABLE ROOM CATEGORIES list. Do not guess or use standard names if they aren't in the list. If they do not specify a category, use "all". | |
| 2. If count is missing, use 1. | |
| If category is missing, use "all". If count is missing, use 1. | |
| Return ONLY JSON, no extra text.""" | |
| response = llm.invoke(prompt) | |
| try: | |
| data = json.loads(response.content.strip()) | |
| print(f"EXTRACTED: {data}") | |
| except: | |
| return {"response": "I couldn't quite understand your inventory"} | |
| action = data.get("action","check") | |
| category = data.get("category","all") | |
| try: | |
| count = int(data.get("count",1)) | |
| except: | |
| count = 1 | |
| if action == "check": | |
| from datetime import datetime,timedelta | |
| today = datetime.now() | |
| start_date = today.strftime("%Y-%m-%d") | |
| end_date = (today + timedelta(days=1)).strftime("%Y-%m-%d") | |
| success, data = await fetch_inventory_availability(start_date, end_date) | |
| print(f"API SUCCESS: {success}, DATA KEYS: {data.keys()}") | |
| if success: | |
| categories = data.get("categories",[]) | |
| if category == "all": | |
| today_str = today.strftime("%d-%m-%Y") | |
| total_physical = 0 | |
| total_avail = 0 | |
| total_booked = 0 | |
| total_blocked = 0 | |
| total_ooo = 0 | |
| for cat in categories: | |
| total_physical += int(cat.get("totalRooms", 0)) | |
| exact_daily_data = {} | |
| for dd in cat.get("dateWiseData", []): | |
| if dd.get("date") == today_str: | |
| exact_daily_data = dd | |
| break | |
| summary = cat.get("summary") or {} | |
| # 2. Extract values. If daily data is missing/null, fallback to summary. | |
| avail = exact_daily_data.get("availableRooms") | |
| if avail is None: avail = summary.get("totalAvailableRooms", 0) | |
| booked = exact_daily_data.get("bookedRooms") | |
| if booked is None: booked = summary.get("totalBookedRooms", 0) | |
| blocked = exact_daily_data.get("blockedRooms") | |
| if blocked is None: blocked = summary.get("totalBlockedRooms", 0) | |
| ooo = exact_daily_data.get("outOfOrderRooms") | |
| if ooo is None: ooo = summary.get("totalOutOfOrderRooms", 0) | |
| # 3. Ensure they are integers to prevent math crashes | |
| total_avail += int(avail or 0) | |
| total_booked += int(booked or 0) | |
| total_blocked += int(blocked or 0) | |
| total_ooo += int(ooo or 0) | |
| accounted_rooms = total_avail + total_booked + total_blocked + total_ooo | |
| syncing_rooms = total_physical - accounted_rooms | |
| if syncing_rooms > 0: | |
| status = f"Out of {total_physical} total rooms, {total_avail} are available today. ध्यान दें: {syncing_rooms} room(s) are currently updating their blocked or booked status in the system." | |
| else: | |
| status = f"Out of {total_physical} total rooms today, {total_avail} are available, {total_booked} are booked, {total_blocked} are blocked, and {total_ooo} are out of order." | |
| else: | |
| status = f"I checked the system, but I couldn't find any inventory data for the {category} category." | |
| for cat in categories: | |
| if category in cat.get("categoryName", "").lower(): | |
| today_str = today.strftime("%d-%m-%Y") | |
| exact_daily_data = {} | |
| for dd in cat.get("dateWiseData",[]): | |
| if dd.get("date") == today_str: | |
| exact_daily_data = dd | |
| break | |
| summary = cat.get("summary") or {} | |
| avail = exact_daily_data.get("availableRooms") | |
| if avail is None: avail = summary.get("totalAvailableRooms", 0) | |
| booked = exact_daily_data.get("bookedRooms") | |
| if booked is None: booked = summary.get("totalBookedRooms", 0) | |
| blocked = exact_daily_data.get("blockedRooms") | |
| if blocked is None: blocked = summary.get("totalBlockedRooms", 0) | |
| ooo = exact_daily_data.get("outOfOrderRooms") | |
| if ooo is None: ooo = summary.get("totalOutOfOrderRooms", 0) | |
| avail = int(avail or 0) | |
| booked = int(booked or 0) | |
| blocked = int(blocked or 0) | |
| ooo = int(ooo or 0) | |
| # Catch the Ghost Rooms for this specific category! | |
| total_physical = int(cat.get("totalRooms", 0)) | |
| accounted_rooms = avail + booked + blocked + ooo | |
| syncing_rooms = total_physical - accounted_rooms | |
| if syncing_rooms > 0: | |
| status = f"For {cat['categoryName']}, {avail} are available right now. ध्यान दें: {syncing_rooms} room(s) are currently syncing their blocked or booked status." | |
| else: | |
| status = f"{cat['categoryName']}: {avail} available, {booked} booked, {blocked} blocked, and {ooo} out of order." | |
| break | |
| else: | |
| status = "I couldn't connect to the hotel system. Please try again." | |
| return {"response":status} | |
| else: | |
| pending_data = { | |
| "action": action, | |
| "category": category, | |
| "count": count | |
| } | |
| status = f"I am ready to {action} {count} {category} room(s). Should I proceed?" | |
| return {"response": status,"pending_manager":"inventory","pending_action":pending_data} | |
| async def rate_manager(state: SupportState): | |
| if state.get("pending_manager") == "rates": | |
| user_reply = str(state.get("query","")).lower() | |
| prompt = f"""User said: "{user_reply}" | |
| If this is a positive confirmation for a booking, return "yes" | |
| otherwise return "no". Nothing extra.""" | |
| response = llm.invoke(prompt) | |
| confirmation = response.content.strip().lower() | |
| if confirmation == "yes": | |
| pa = state["pending_action"] | |
| from datetime import datetime | |
| today = datetime.now() | |
| date_str = today.strftime("%Y-%m-%d") | |
| success, data = await update_rates( | |
| category=pa["category"], | |
| new_price=pa["new_price"], | |
| start_date=date_str, | |
| end_date=date_str | |
| ) | |
| if success: | |
| status = f"Done! The rate for {pa['category']} rooms has been successfully updated to {pa['new_price']} rupees." | |
| else: | |
| status = f"Sorry, I failed to update the rate for {pa['category']} rooms." | |
| return {"response": status, "pending_manager": None, "pending_action": None} | |
| else: | |
| return {"response": "No problem, I have cancelled the rate change.", "pending_manager": None, "pending_action": None} | |
| prompt = f"""Extract rate details from the request. | |
| Request: {state['query']} | |
| Return ONLY a JSON object: {{"action": "check|change", "category": "standard|deluxe|suite|presidential", "new_price": 0, "date":"today"}} | |
| - If it's just checking the price, leave new_price as 0. | |
| - If they don't mention a specific date, default to "today". | |
| Return ONLY JSON, no extra text.""" | |
| response = llm.invoke(prompt) | |
| try: | |
| data = json.loads(response.content.strip()) | |
| except: | |
| return {"response": "sorry i couldn't quite catch your rates"} | |
| action = data.get("action","check") | |
| category = data.get("category","single") | |
| if action == "check": | |
| # check_in = data.get("check_in") | |
| # check_out = data.get("check_out") | |
| from datetime import datetime,timedelta | |
| today = datetime.now() | |
| check_in = today.strftime("%Y-%m-%d") | |
| check_out = (today + timedelta(days=1)).strftime("%Y-%m-%d") | |
| adults = int(data.get("adults",1)) | |
| rooms_payload = [{"roomNumber": 1, "guests": {"adults":adults,"children":0}}] | |
| success, api_response = await fetch_room_availability( | |
| phone_number_id=os.getenv("PHONE_NUMBER_ID"), | |
| check_in=check_in, | |
| check_out=check_out, | |
| rooms=rooms_payload | |
| ) | |
| # print(f"Success: {success}") | |
| # print(f"API RESPONSE KEY: {api_response.keys()}") | |
| # print(f"RAW reservationPrice: {api_response.get('reservationPrice', 'KEY NOT FOUND')}") | |
| # print(f"RAW data key: {api_response.get('data', 'KEY NOT FOUND')}") | |
| if success: | |
| print("--- LIVE RATE DATA ---") | |
| print(json.dumps(api_response.get("data", {}).get("reservationPrice",[]), indent=2)) | |
| prices_list = api_response.get("data", {}).get("reservationPrice", []) | |
| found_price = None | |
| print(f"PRICES LIST LENGTH: {len(prices_list)}") | |
| if prices_list: | |
| print(f"FIRST ITEM KEY: {prices_list[0].keys()}") | |
| print(f"FIRST ITEM roomType: {prices_list[0].get("roomType")}") | |
| from hotel_api import get_room_map | |
| room_map = await get_room_map() | |
| # room_info = room_map.get(category.lower()) | |
| category_prompt = f""" | |
| The user wants to check rates for "{category}" room. | |
| Available room categories: {list(room_map.keys())} | |
| Match to the closest category. Return ONLY JSON: | |
| {{"matched_category": "exact_key_from_list_or_null"}} | |
| """ | |
| cat_response = await llm.ainvoke(category_prompt) | |
| cat_clean = cat_response.content.replace("```json","").replace("```","").strip() | |
| cat_data = json.loads(cat_clean) | |
| matched_category = cat_data.get("matched_category") | |
| if matched_category and matched_category in room_map: | |
| category = matched_category | |
| room_info = room_map.get(category) | |
| print(f"LOOKING FOR: {category.lower()}, FOUND: {room_info}") | |
| for room in prices_list: | |
| if category in room.get("roomType", "").lower(): | |
| # Attempt to grab the price. We might need to change 'price' to the exact key! | |
| cats = room.get("categories",[]) | |
| if cats: | |
| found_price = cats[0].get("totalPrice") | |
| break | |
| if found_price is not None: | |
| status = f"The current rate for a {category} room today is {found_price} rupees." | |
| else: | |
| status = f"I checked the system, but I couldn't find the rate for the {category} category." | |
| else: | |
| status = "I tried to check the live rates, but the system returned an error." | |
| return {"response": status} | |
| else: | |
| # User wants to CHANGE the rate | |
| price = data.get("new_price", 0) | |
| if price == 0: | |
| return {"response": "I didn't catch the new price. Please tell me the amount you want to change it to."} | |
| pending_data = { | |
| "category": category, | |
| "new_price": price | |
| } | |
| status = f"I am ready to change the rate of {category} rooms to {price} rupees. Should I confirm this change?" | |
| return {"response": status, "pending_manager": "rates", "pending_action": pending_data} | |
| async def reservation_manager(state: SupportState): | |
| pb = state.get("pending_action", {}) | |
| user_reply = str(state.get("query", "")).strip().lower() | |
| extracted = state.get("extracted_data", {}) | |
| booking_step = state.get("booking_step","") | |
| current_manager = state.get("pending_manager") | |
| if current_manager == "reservations" or not current_manager: | |
| # cancel_words = ["cancel","stop","no","don't","abort","quit"] | |
| cancel_pattern = r'\b(cancel|stop|no|don\'t|abort|quit)\b' | |
| # if any(word in user_reply for word in cancel_words): | |
| if re.search(cancel_pattern, user_reply): | |
| return { | |
| "response": "No problem, I have canceled the current process. How else can I help you?", | |
| "pending_manager": "", | |
| "pending_action": {}, | |
| "booking_step": "" | |
| } | |
| if booking_step == "": | |
| from hotel_api import get_room_map | |
| room_map = await get_room_map() | |
| real_categories = list(room_map.keys()) | |
| today_str = datetime.now().strftime("%Y-%m-%d") | |
| today_day = datetime.now().strftime("%A") | |
| prompt = f"""Extract booking details from the hotel owner's request. | |
| Request: {state['query']} | |
| CRITICAL CONTEXT: | |
| - Today is {today_day}, {today_str}. Use this to calculate "tomorrow", "next Friday", etc. | |
| - AVAILABLE ROOM CATEGORIES: {real_categories} | |
| Return ONLY a JSON object: | |
| {{ | |
| "action": "book|cancel|modify|check_availability", | |
| "category": "category_name_from_list_or_null", | |
| "guest_name": "name_or_null", | |
| "check_in": "YYYY-MM-DD_or_null", | |
| "check_out": "YYYY-MM-DD_or_null", | |
| "adults": 1, | |
| "meal_plan": "EP|CP|MAP|AP|null", | |
| "phone_number": "string_or_null", | |
| "custom_price": "integer_or_null", | |
| "advance_payment": "integer_or_null", | |
| "payment_method": "Online Payment|Credit Card|Bank Transfer|Pay at Hotel|null" | |
| }} | |
| Rules: | |
| - CRITICAL CALENDAR CONTEXT: Today is {today_day}, {today_str}. | |
| - Use today's day and date to perfectly calculate relative words like "tomorrow", "Wednesday", "this week", etc. | |
| - If check-out is missing, default to 1 day after check-in. | |
| - If adults are not mentioned, default to 1. | |
| - meal_plan: If they mention EP, CP, MAP, AP, "room only", "with breakfast", etc., extract the acronym. Else null. | |
| - phone_number: Extract the full phone number string if provided. Else null. | |
| - custom_price: If the owner states a specific total price for the room (e.g., "price is 5000"), extract as integer. Else null. | |
| - advance_payment: If they state an advance payment amount, extract as integer. Else null. | |
| Return ONLY valid JSON.""" | |
| response = llm.invoke(prompt) | |
| raw_content = response.content.strip() | |
| # print(f"--- RAW LLM RESPONSE ---\n{raw_content}\n------------------------") | |
| clean_content = raw_content.replace("```json","").replace("```","").strip() | |
| try: | |
| data = json.loads(clean_content) | |
| print(f"--- DEBUG JSON FROM LLM: {data} ---") | |
| except Exception as e: | |
| print(f"JSON Parsing error: {e}") | |
| return {"response": "sorry i couldn't understand your reservation details"} | |
| if data.get("guest_name"): pb["guest_name"] = data["guest_name"] | |
| if data.get("category"): pb["category"] = data["category"] | |
| if data.get("check_in"): pb["check_in"] = data["check_in"] | |
| if data.get("check_out"): pb["check_out"] = data["check_out"] | |
| if data.get("meal_plan"): pb["meal_plan"] = data["meal_plan"] | |
| if data.get("phone_number"): pb["phone_number"] = data["phone_number"] | |
| if data.get("custom_price"): pb["custom_price"] = data["custom_price"] | |
| if data.get("advance_payment") is not None: | |
| pb["advance_payment"] = data["advance_payment"] | |
| if data.get("payment_method"): pb["payment_method"] = data["payment_method"] | |
| category = pb.get("category") | |
| check_in = pb.get("check_in") | |
| check_out = pb.get("check_out") | |
| adults = int(extracted.get("adults", pb.get("adults", 1))) | |
| guest_name = pb.get("guest_name") | |
| if booking_step == "collect_name" and not pb.get("guest_name"): | |
| pb["guest_name"] = state.get("query", "").strip() | |
| elif booking_step == "collect_category" and not pb.get("category"): | |
| pb["category"] = state.get("query", "").strip() | |
| elif booking_step == "collect_checkin" and not pb.get("check_in"): | |
| pb["check_in"] = state.get("query", "").strip() | |
| check_in = pb["check_in"] | |
| elif booking_step == "collect_checkout" and not pb.get("check_out"): | |
| pb["check_out"] = state.get("query", "").strip() | |
| check_out = pb["check_out"] | |
| #collecting meal plan | |
| elif booking_step == "meal_plan": | |
| user_reply = state.get("query","") | |
| meal_prompt = f""" | |
| The user is choosing a hotel meal plan. | |
| User's reply: "{user_reply}" | |
| Available Plans: | |
| - "EP": European Plan (Room Only, no meals, just the room) | |
| - "CP": Continental Plan (Room + Breakfast) | |
| - "MAP": Modified American Plan (Half Board, two meals) | |
| - "AP": American Plan (Full Board, all meals) | |
| Analyze the user's reply. Account for STT phonetic typos (e.g., "ee pee" -> EP, "see pee" -> CP) and conversational phrases. | |
| Return ONLY a valid JSON object. If you cannot confidently map their reply to a plan, return null. | |
| OUTPUT FORMAT: | |
| {{"meal_plan": "EP" | "CP" | "MAP" | "AP" | null}} | |
| """ | |
| try: | |
| response = await llm.ainvoke(meal_prompt) | |
| cleaned_response = response.content.replace("```json", "").replace("```", "").strip() | |
| extracted = json.loads(cleaned_response) | |
| selected_meal = extracted.get("meal_plan") | |
| except Exception as e: | |
| print(f"Meal extraction error: {e}") | |
| selected_meal = None | |
| if not selected_meal: | |
| return { | |
| "response": "I didn't catch that. Please say EP (room only), CP (with breakfast), MAP (half board), or AP (all meals).", | |
| "pending_manager": "reservations", | |
| "pending_action": pb, | |
| "booking_step": "meal_plan" | |
| } | |
| pb["meal_plan"] = selected_meal | |
| meal_categories = pb.get("meal_categories", []) | |
| # Map the EP/CP/MAP/AP choice to the words the Xipper API actually uses | |
| search_key = { | |
| "EP": "only", | |
| "CP": "breakfast", | |
| "MAP": "lunch", # Matches 'Room With Breakfast + Lunch/Dinner' | |
| "AP": "all" # Matches 'Room With All Meals' | |
| }.get(selected_meal, "only") | |
| for plan in meal_categories: | |
| api_plan_name = plan.get("category", "").lower() | |
| # If we find the matching meal plan in the API list... | |
| if search_key in api_plan_name: | |
| new_base = plan.get("basePrice", 4000) | |
| new_tax = plan.get("totalTax", new_base * 0.12) | |
| if "pricing" not in pb: | |
| pb["pricing"] = {} | |
| # Overwrite the memory with the new, accurate prices! | |
| pb["pricing"]["basePrice"] = new_base | |
| pb["pricing"]["totalTax"] = new_tax | |
| pb["pricing"]["totalPrice"] = plan.get("totalPrice", new_base + new_tax) | |
| pb["pricing"]["pricePerNight"] = plan.get("pricePerNight", new_base + new_tax) | |
| if pb.get("custom_price"): | |
| custom_total = int(pb.get("custom_price")) | |
| pb["pricing"]["totalPrice"] = custom_total | |
| break | |
| # return {"response": "Got it. What is the guest's phone number?", "pending_manager": "reservations", "pending_action": pb, "booking_step": "phone"} | |
| #collecting phone number | |
| elif booking_step == "phone": | |
| phone = "".join(filter(str.isdigit, user_reply)) | |
| if len(phone) < 10: | |
| return {"response": "Please say a valid 10 digit phone number.", "pending_manager": "reservations", "pending_action": pb, "booking_step": "phone"} | |
| pb["phone_number"] = phone | |
| # return { | |
| # "response": "How much customer paid in advance?", | |
| # "pending_manager": "reservations", | |
| # "pending_action": pb, | |
| # "booking_step": "advance_payment" | |
| # } | |
| elif booking_step == "advance_payment": | |
| user_reply = state.get("query","") | |
| total_price = pb.get("pricing", {}).get("totalPrice", 0) | |
| payment_prompt = f""" | |
| The user is stating how much advance payment was collected for a hotel booking. | |
| The total price of the booking is {total_price}. | |
| User's reply: "{user_reply}" | |
| Extract the exact numerical amount paid in advance. Account for conversational phrases: | |
| - If they say "nothing", "zero", or "pay at hotel", return 0. | |
| - If they say "half", return exactly half of the total price. | |
| - If they say "full" or "all of it", return the total price. | |
| Return ONLY a valid JSON object. | |
| OUTPUT FORMAT: | |
| {{"advance_amount": integer_number_here}} | |
| """ | |
| try: | |
| response = await llm.ainvoke(payment_prompt) | |
| cleaned_response = response.content.replace("```json", "").replace("```", "").strip() | |
| extracted = json.loads(cleaned_response) | |
| advance_amount = extracted.get("advance_amount", 0) | |
| except Exception as e: | |
| print(f"Advance payment extraction error: {e}") | |
| advance_amount = 0 | |
| pb["advance_payment"] = advance_amount | |
| # guest_name = pb.get("guest_name", "Guest") | |
| # room_type = pb.get("category", "room") | |
| # meal_plan = pb.get("meal_plan", "EP") | |
| # return { | |
| # "response": "What is the payment method? Online payment, Credit card, Bank transfer, or Pay at hotel?", | |
| # "pending_manager": "reservations", | |
| # "booking_step": "payment", | |
| # "pending_action": pb | |
| # } | |
| #cpllecting payment method | |
| elif booking_step == "payment": | |
| try: | |
| payment_map = { | |
| "online": "Online Payment", | |
| "credit": "Credit Card", | |
| "card": "Credit Card", | |
| "bank": "Bank Transfer", | |
| "transfer": "Bank Transfer", | |
| "hotel": "Pay at Hotel", | |
| "cash": "Pay at Hotel", | |
| "upi": "Online Payment" | |
| } | |
| payment_method = None | |
| for key,val in payment_map.items(): | |
| if key in user_reply.lower(): | |
| payment_method = val | |
| break | |
| if not payment_method: | |
| return {"response": "Please say Online payment, Credit card, Bank transfer, or Pay at hotel.", "pending_manager": "reservations", "pending_action": pb, "booking_step": "payment"} | |
| pb["payment_method"] = payment_method | |
| pricing_data = pb.get("pricing") or {} | |
| total_price = pricing_data.get("totalPrice", 0) | |
| total_price = pb.get("pricing", {}).get("totalPrice", 0) | |
| # status = f"Confirming: {pb['category']} room for {pb['guest_name']}, phone {pb['phone_number']}, {pb['meal_plan']} meal plan, {payment_method} payment. Total {total_price} rupees. Shall I book?" | |
| # return { | |
| # "response": status, | |
| # "pending_manager": "reservations", | |
| # "pending_action": pb, | |
| # "booking_step": "confirm" | |
| # } | |
| except Exception as e: | |
| # IF IT CRASHES, CATCH IT AND PRINT THE EXACT CAUSE! | |
| print("\n" + "="*50) | |
| print(f"🔥 CRASH IN PAYMENT STEP: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| print("="*50 + "\n") | |
| # Return safely so the server doesn't throw a 503 | |
| safe_pb = state.get("pending_action") or {} | |
| return {"response": "Sorry, I encountered an internal glitch while processing the payment type. Could you repeat that?", "pending_manager": "reservations", "pending_action": safe_pb, "booking_step": "payment"} | |
| from functions import sanitize_date | |
| check_in = sanitize_date(check_in) | |
| check_out = sanitize_date(check_out) | |
| pb["check_in"] = check_in | |
| pb["check_out"] = check_out | |
| if not pb.get("availability_checked") and category and check_in and check_out: | |
| rooms_payload = [{"roomNumber": 1, "guests": {"adults":adults,"children":0}}] | |
| # safe_phone_id = state.get("phone_number_id") | |
| # if not safe_phone_id: | |
| safe_phone_id = "1220503234474080" | |
| success,avail_data = await fetch_room_availability( | |
| phone_number_id=safe_phone_id, | |
| check_in=check_in, | |
| check_out=check_out, | |
| rooms=rooms_payload | |
| ) | |
| # print(f"PRICES LIST: {avail_data.get('reservationPrice', [])}") | |
| if not success: | |
| return {"response": "I tried to check availability for those dates, but the system returned an error."} | |
| prices_list = avail_data.get("data", {}).get("reservationPrice",[]) | |
| found_room = None | |
| # from hotel_api import get_room_map | |
| # room_map = await get_room_map() | |
| # print(f"ROOM MAP KEYS: {list(room_map.keys())}") | |
| # room_info = None | |
| # for key,value in room_map.items(): | |
| # if key in category.lower(): | |
| # room_info = value | |
| # category = key | |
| # pb["category"] = key | |
| # break | |
| # # room_info = room_map.get(category.lower()) | |
| # # print(f"LOOKING FOR: {category.lower()}, FOUND: {room_info}") | |
| # if not room_info: | |
| # pb["category"] = None #-> wipe bad data | |
| # return { | |
| # "response": f"Sorry, I don't recognize {category} category", | |
| # "pending_manager":"reservations", | |
| # "booking_step":"collect_category", | |
| # "pending_action": pb | |
| # } | |
| from hotel_api import get_room_map | |
| room_map = await get_room_map() | |
| print(f"ROOM MAP KEYS: {list(room_map.keys())}") | |
| category_prompt = f""" | |
| The hotel owner said they want to book a "{category}" room. | |
| Available room categories: {list(room_map.keys())} | |
| Match the owner's input to the closest category from the list. | |
| If there is no reasonable match, return null. | |
| Return ONLY a JSON object: | |
| {{"matched_category": "exact_key_from_list_or_null"}} | |
| """ | |
| try: | |
| cat_response = await llm.ainvoke(category_prompt) | |
| cat_clean = cat_response.content.replace("```json","").replace("```","").strip() | |
| cat_data = json.loads(cat_clean) | |
| matched_category = cat_data.get("matched_category") | |
| except Exception as e: | |
| print(f"Category match error: {e}") | |
| matched_category = None | |
| room_info = None | |
| if matched_category and matched_category in room_map: | |
| room_info = room_map[matched_category] | |
| category = matched_category | |
| pb["category"] = matched_category | |
| if not room_info: | |
| pb["category"] = None | |
| return { | |
| "response": f"I couldn't find a matching room category for '{category}'. Available categories are: {', '.join(room_map.keys())}. Which one would you like?", | |
| "pending_manager": "reservations", | |
| "booking_step": "collect_category", | |
| "pending_action": pb | |
| } | |
| target_name = room_info["categoryName"].lower() | |
| for room in prices_list: | |
| if target_name == room.get("roomType", "").lower(): | |
| found_room = room | |
| break | |
| if not found_room: | |
| pb["category"] = None | |
| return { | |
| "response": f"I checked the inventory, but the {category} category is either sold out or has no pricing set for those dates.", | |
| "pending_manager":"reservations", | |
| "pending_action":pb, | |
| "booking_step": "collect_category" | |
| } | |
| print("\n" + "="*50) | |
| print(f"API DATA FOR {category.upper()}: {found_room}") | |
| print("="*50 + "\n") | |
| exact_room_name = found_room.get("roomType", category.title() + " room") | |
| meal_categories = found_room.get("categories", []) | |
| if meal_categories: | |
| # Default to the first one in the list (usually 'Room Only') for the initial quote | |
| default_plan = meal_categories[0] | |
| base_price = default_plan.get("basePrice", 4000) | |
| total_tax = default_plan.get("totalTax", base_price * 0.12) | |
| total_price = default_plan.get("totalPrice", base_price + total_tax) | |
| else: | |
| # Fallback if the categories array is empty | |
| base_price = 4000 | |
| total_tax = 480 | |
| total_price = 4480 | |
| if pb.get("custom_price"): | |
| total_price = int(pb["custom_price"]) | |
| base_price = total_price / 1.12 | |
| total_tax = total_price - base_price | |
| pb["category"] = category | |
| pb["roomType"] = exact_room_name | |
| pb["guest_name"] = guest_name | |
| pb["check_in"] = check_in | |
| pb["check_out"] = check_out | |
| pb["adults"] = adults | |
| pb["children"] = 0 | |
| pb["meal_categories"] = meal_categories | |
| pb["availability_checked"] = True # Flag to show we found the room | |
| pb["pricing"] = { | |
| "basePrice": base_price, | |
| "totalTax": total_tax, | |
| "totalPrice": total_price, | |
| "pricePerNight": meal_categories[0].get("pricePerNight", total_price) if meal_categories else total_price, | |
| "numberOfNights": meal_categories[0].get("numberOfNights", 1) if meal_categories else 1, | |
| "dateWisePricing": [], | |
| "meal_categories": meal_categories | |
| } | |
| #------------------------------------------------------------------------------------------------- | |
| current_price = pb.get("pricing", {}).get("totalPrice", 0) | |
| if not pb.get("guest_name"): | |
| return { | |
| "response": "I can definitely help you check availability. Could you please tell me the guest's name for the reservation?", | |
| "pending_manager":"reservations", | |
| "booking_step":"collect_name", | |
| "pending_action": pb | |
| } | |
| if not pb.get("category"): #or pb.get("category").lower() == None: | |
| return { | |
| "response": f"Got it, booking for {pb['guest_name']}. What category of room would you like?", | |
| "pending_manager":"reservations", | |
| "booking_step": "collect_category", | |
| "pending_action": pb | |
| } | |
| if not pb.get("check_in"): | |
| return { | |
| "response": f"Perfect. What is your check-in date for the {pb['category']} room?", | |
| "pending_manager":"reservations", | |
| "booking_step":"collect_checkin", | |
| "pending_action": pb | |
| } | |
| if not pb.get("check_out"): | |
| return { | |
| "response": "And what will be your check-out date?", | |
| "pending_manager": "reservations", | |
| "booking_step":"collect_checkout", | |
| "pending_action": pb | |
| } | |
| if not pb.get("meal_plan"): | |
| return { | |
| "response": "what meal plan would you like?", | |
| "pending_manager": "reservations", | |
| "booking_step": "meal_plan", | |
| "pending_action": pb | |
| } | |
| if not pb.get("phone_number"): | |
| return { | |
| "response": "what is guest phone number", | |
| "pending_manager": "reservations", | |
| "booking_step": "phone", #_______________________________________________ | |
| "pending_action": pb | |
| } | |
| if pb.get("advance_payment") is None: | |
| return { | |
| "response": "How much advance payment made?", | |
| "pending_manager": "reservations", | |
| "booking_step": "advance_payment", | |
| "pending_action": pb | |
| } | |
| if not pb.get("payment_method"): | |
| return { | |
| "response": "What is the payment method? Online, Credit card, Bank transfer, or Pay at hotel?", | |
| "pending_manager": "reservations", | |
| "booking_step": "payment", | |
| "pending_action": pb | |
| } | |
| print("🎉 All slots filled successfully:", pb) | |
| if booking_step != "confirm": | |
| guest = pb.get("guest_name", "Guest") | |
| method = pb.get("payment_method", "Pay at Hotel") | |
| return { | |
| "response": f"Got everything. Shall I confirm the {pb.get('meal_plan')} booking for {guest} using {method} at {current_price} rupees?", | |
| "pending_manager": "reservations", | |
| "booking_step": "confirm", | |
| "pending_action": pb | |
| } | |
| if booking_step == "confirm": | |
| try: | |
| prompt = f"""User said: "{user_reply}" | |
| If this is a positive confirmation for a booking, return "yes" | |
| otherwise return "no". Nothing extra.""" | |
| response = await llm.ainvoke(prompt) | |
| confirmation = response.content.strip().lower() | |
| if confirmation == "yes": | |
| #need review ------------ | |
| meal_abbreviation = pb.get("meal_plan", "EP") | |
| backend_meal_map = { | |
| "EP": "Room Only", | |
| "CP": "Room With Breakfast", | |
| "MAP": "Room With Breakfast + Lunch/Dinner", | |
| "AP": "Room With All Meals" | |
| } | |
| correct_meal_name = backend_meal_map.get(meal_abbreviation, "Room Only") | |
| cin_str = pb.get("check_in") | |
| nights = pb.get("pricing", {}).get("numberOfNights", 1) | |
| base_price = pb.get("pricing", {}).get("basePrice", 0) | |
| total_tax = pb.get("pricing", {}).get("totalTax", 0) | |
| nightly_base = round(base_price / nights, 2) if nights > 0 else base_price | |
| nightly_tax = round(total_tax / nights, 2) if nights > 0 else total_tax | |
| date_wise_pricing = [] | |
| try: | |
| cin_date = datetime.strptime(cin_str, "%Y-%m-%d") | |
| for i in range(nights): | |
| current_date = cin_date + timedelta(days=i) | |
| date_wise_pricing.append({ | |
| "date": current_date.strftime("%d-%m-%Y"), | |
| "price": nightly_base, | |
| "tax": nightly_tax | |
| }) | |
| except Exception as e: | |
| print(f"Date generation error: {e}") | |
| # Update the pricing dictionary with our new array | |
| pricing_dict = pb.get("pricing", {}) | |
| pricing_dict["dateWisePricing"] = date_wise_pricing | |
| #---------------------------------------------------------------------------- | |
| room_details = [{ | |
| "roomName": "Room 1", | |
| "roomType": pb["roomType"], | |
| "mealPlan": correct_meal_name, | |
| "adults": pb["adults"], | |
| "children": pb["children"], | |
| "pets": "No", | |
| "pricing": pricing_dict | |
| }] | |
| guest_details = [{ | |
| "title": "Mr", | |
| "firstName": pb["guest_name"].split()[0], | |
| "lastName": pb["guest_name"].split()[-1] if len(pb["guest_name"].split()) > 1 else "", | |
| "phoneNumber": pb["phone_number"], | |
| "email": "", | |
| "xipperId": "", | |
| "cXipperId": "" | |
| }] | |
| success,data = await create_reservation( | |
| hotel_id="XH33228365", | |
| check_in=pb["check_in"], | |
| check_out=pb["check_out"], | |
| # user_id=state.get("phone_number_id", "1098093760057955"), | |
| user_id="X645108004", | |
| room_details=room_details, | |
| guest_details=guest_details, | |
| payment_method=pb["payment_method"], | |
| total_amount=pb.get("pricing", {}).get("totalPrice", 0.0), | |
| advance_payment=pb.get("advance_payment", 0) | |
| ) | |
| if success: | |
| status = f"Done! Booking for {pb['guest_name']} successfully completed." | |
| return {"response": status, "pending_manager": "", "pending_action": {}, "booking_step": ""} | |
| else: | |
| error_msg = data.get("data", {}).get("message", data.get("message", "an unknown API error")) | |
| status = f"Sorry, the booking failed. The server said: {error_msg}." | |
| return {"response": status, "pending_manager": "", "pending_action": {}, "booking_step": ""} | |
| else: | |
| return {"response": "No problem, I have cancelled the reservation process.", "pending_manager": "", "pending_action": {},"booking_step": ""} | |
| except Exception as e: | |
| print("\n" + "="*50) | |
| print(f"🔥 CRASH IN CONFIRM STEP: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| print("="*50 + "\n") | |
| safe_pb = state.get("pending_action") or {} | |
| return {"response": "Sorry, an internal error occurred while finalizing the booking. Should I try to submit it again?", "pending_manager": "reservations", "pending_action": safe_pb, "booking_step": "confirm"} | |
| # from hotel_api import get_room_map | |
| # room_map = await get_room_map() | |
| # real_categories = list(room_map.keys()) | |
| # today_str = datetime.now().strftime("%Y-%m-%d") | |
| # today_day = datetime.now().strftime("%A") | |
| # prompt = f"""Extract booking details from the hotel owner's request. | |
| # Request: {state['query']} | |
| # CRITICAL CONTEXT: | |
| # - Today is {today_day}, {today_str}. Use this to calculate "tomorrow", "next Friday", etc. | |
| # - AVAILABLE ROOM CATEGORIES: {real_categories} | |
| # Return ONLY a JSON object: | |
| # {{ | |
| # "action": "book|cancel|modify|check_availability", | |
| # "category": "category_name_from_list_or_null", | |
| # "guest_name": "name_or_null", | |
| # "check_in": "YYYY-MM-DD_or_null", | |
| # "check_out": "YYYY-MM-DD_or_null", | |
| # "adults": 1, | |
| # "meal_plan": "EP|CP|MAP|AP|null", | |
| # "phone_number": "string_or_null", | |
| # "custom_price": "integer_or_null", | |
| # "advance_payment": "integer_or_null" | |
| # }} | |
| # Rules: | |
| # Rules: | |
| # - CRITICAL CALENDAR CONTEXT: Today is {today_day}, {today_str}. | |
| # - Use today's day and date to perfectly calculate relative words like "tomorrow", "Wednesday", "this week", etc. | |
| # - If check-out is missing, default to 1 day after check-in. | |
| # - If adults are not mentioned, default to 1. | |
| # - meal_plan: If they mention EP, CP, MAP, AP, "room only", "with breakfast", etc., extract the acronym. Else null. | |
| # - phone_number: Extract the full phone number string if provided. Else null. | |
| # - custom_price: If the owner states a specific total price for the room (e.g., "price is 5000"), extract as integer. Else null. | |
| # - advance_payment: If they state an advance payment amount, extract as integer. Else null. | |
| # Return ONLY valid JSON.""" | |
| # response = llm.invoke(prompt) | |
| # raw_content = response.content.strip() | |
| # # print(f"--- RAW LLM RESPONSE ---\n{raw_content}\n------------------------") | |
| # clean_content = raw_content.replace("```json","").replace("```","").strip() | |
| # try: | |
| # data = json.loads(clean_content) | |
| # print(f"--- DEBUG JSON FROM LLM: {data} ---") | |
| # except Exception as e: | |
| # print(f"JSON Parsing error: {e}") | |
| # return {"response": "sorry i couldn't understand your reservation details"} | |
| # category = data.get("category", "deluxe") | |
| # guest_name = data.get("guest_name", "Guest") | |
| # check_in = data.get("check_in", today_str) | |
| # check_out = data.get("check_out", (datetime.strptime(check_in, '%Y-%m-%d') + timedelta(days=1)).strftime('%Y-%m-%d')) | |
| # adults = int(data.get("adults", 1)) | |
| # status = f"I found a {category} room for {guest_name}. Total price with taxes will be {total_price} rupees. What meal plan would you like? EP (room only), CP (with breakfast), MAP (half board), or AP (all meals)?" | |
| # return {"response": status, "pending_manager": "reservations", "pending_action": pending_data,"booking_step":"meal_plan"} | |
| async def unknown_handler(state: SupportState): | |
| response = "Sorry but i can't help with you this query because i am here to help with your hotel management" | |
| return {"response": response} | |
| def agent_router(state: SupportState): | |
| return state["next_agent"] | |
| graph = StateGraph(SupportState) | |
| graph.add_node("choose_agent",choose_agent) | |
| graph.add_node("inventory_manager",inventory_manager) | |
| graph.add_node("rate_manager",rate_manager) | |
| graph.add_node("reservation_manager",reservation_manager) | |
| graph.add_node("unknown_handler",unknown_handler) | |
| graph.add_edge(START,"choose_agent") | |
| graph.add_conditional_edges( | |
| "choose_agent", | |
| agent_router, | |
| { | |
| "inventory": "inventory_manager", | |
| "rates": "rate_manager", | |
| "reservations": "reservation_manager", | |
| "unknown": "unknown_handler" | |
| } | |
| ) | |
| graph.add_edge("inventory_manager", END) | |
| graph.add_edge("rate_manager", END) | |
| graph.add_edge("reservation_manager", END) | |
| graph.add_edge("unknown_handler",END) | |
| graph = graph.compile(checkpointer=memory) | |
| # import asyncio | |
| # async def test(): | |
| # result = await app.ainvoke({"query": "book room 101 for Kanhaiya check-in 20th june check-out 22nd june"}) | |
| # print(result["booking_status"]) | |
| # asyncio.run(test()) |