Spaces:
Running
Running
Upload 2 files
#1
by kanhaiyaML - opened
- .gitattributes +35 -0
- .gitignore +0 -21
- README.md +7 -5
- agents/__init__.py +0 -0
- agents/inventory_agent.py +0 -194
- agents/llm.py +0 -17
- agents/rate_agent.py +0 -225
- agents/reservation_agent.py +0 -651
- agents/unknown_agent.py +0 -4
- frontend/index2.html +0 -477
- functions.py +0 -17
- graph_main.py +0 -88
- graph_test.py +17 -81
- hotel_api.py +8 -15
- index.html +654 -0
- main.py +125 -259
- packages.txt +0 -1
- requirements.txt +0 -0
- tools/__init__.py +0 -0
- tools/category_matcher.py +0 -23
- tools/date_utiles.py +0 -39
- tools/pricing_utils.py +0 -39
- translate.py +2 -22
.gitattributes
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
.gitignore
DELETED
|
@@ -1,21 +0,0 @@
|
|
| 1 |
-
__pycache_/
|
| 2 |
-
backup/
|
| 3 |
-
database/
|
| 4 |
-
.env
|
| 5 |
-
backup_graph.py
|
| 6 |
-
graph.py
|
| 7 |
-
langgraph_test.ipynb
|
| 8 |
-
received.webm
|
| 9 |
-
response_audio.mp3
|
| 10 |
-
test_output.wav
|
| 11 |
-
test.ipynb
|
| 12 |
-
test.py
|
| 13 |
-
tools.py
|
| 14 |
-
.venv/
|
| 15 |
-
__pycache__/
|
| 16 |
-
*.py[cod]
|
| 17 |
-
testing_audio_files/
|
| 18 |
-
*.wav
|
| 19 |
-
*.mp3
|
| 20 |
-
.venv/
|
| 21 |
-
whatsapp_api.py
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
| 1 |
---
|
| 2 |
title: Xipper
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
-
|
| 8 |
-
---
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: Xipper
|
| 3 |
+
emoji: 🤖
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
agents/__init__.py
DELETED
|
File without changes
|
agents/inventory_agent.py
DELETED
|
@@ -1,194 +0,0 @@
|
|
| 1 |
-
from hotel_api import update_inventory,fetch_inventory_availability
|
| 2 |
-
from agents.llm import llm
|
| 3 |
-
import json
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
async def inventory_manager(state):
|
| 7 |
-
if state.get("pending_manager") == "inventory":
|
| 8 |
-
user_reply = str(state.get("query","")).lower()
|
| 9 |
-
prompt = f"""User said: "{user_reply}"
|
| 10 |
-
If this is a positive confirmation for a booking, return "yes"
|
| 11 |
-
otherwise return "no". Nothing extra."""
|
| 12 |
-
|
| 13 |
-
response = llm.invoke(prompt)
|
| 14 |
-
confirmation = response.content.strip().lower()
|
| 15 |
-
if hasattr(response, 'usage_metadata') and response.usage_metadata:
|
| 16 |
-
print(f"Tokens used: {response.usage_metadata}")
|
| 17 |
-
|
| 18 |
-
if confirmation == "yes":
|
| 19 |
-
pa = state.get("pending_action")
|
| 20 |
-
if not pa:
|
| 21 |
-
return {"response": "I seem to have lost the room details. Could you please repeat your inventory request?", "pending_manager": None, "pending_action": None}
|
| 22 |
-
|
| 23 |
-
from datetime import datetime, timedelta
|
| 24 |
-
today = datetime.now()
|
| 25 |
-
start_date = today.strftime("%Y-%m-%d")
|
| 26 |
-
end_date = (today + timedelta(days=1)).strftime("%Y-%m-%d")
|
| 27 |
-
|
| 28 |
-
success, data = await update_inventory(
|
| 29 |
-
category=pa["category"],
|
| 30 |
-
operation=pa["action"],
|
| 31 |
-
count=pa["count"],
|
| 32 |
-
start_date=start_date,
|
| 33 |
-
end_date=end_date
|
| 34 |
-
)
|
| 35 |
-
|
| 36 |
-
action_words = {"block": "blocked", "unblock": "unblocked", "release": "released", "unrelease": "unreleased"}
|
| 37 |
-
past_action = action_words.get(pa["action"], pa["action"])
|
| 38 |
-
|
| 39 |
-
if success:
|
| 40 |
-
status = f"Got it. I have successfully {past_action} {pa['count']} {pa['category']} room(s) for tonight."
|
| 41 |
-
else:
|
| 42 |
-
error_msg = data.get("error","an unknown API error")
|
| 43 |
-
status = f"I tried to {pa['action']} those {pa['category']} rooms, but the system returned an error: {error_msg}."
|
| 44 |
-
return {"response": status,"pending_action": None,"pending_manager": None}
|
| 45 |
-
else:
|
| 46 |
-
return {"response": "Okay, I have cancelled the inventory change.", "pending_manager": "", "pending_action": {}}
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
from hotel_api import get_room_map
|
| 50 |
-
room_map = await get_room_map()
|
| 51 |
-
real_categories = list(room_map.keys())
|
| 52 |
-
|
| 53 |
-
prompt = f"""Extract inventory details from the request.
|
| 54 |
-
Request: {state['query']}
|
| 55 |
-
AVAILABLE_ROOM_CATEGORIES: {real_categories}
|
| 56 |
-
Return ONLY a JSON object: {{"action": "check|block|unblock|release|unrelease", "category": "category_name_from_list|all", "count": number}}
|
| 57 |
-
|
| 58 |
-
IMPORTANT HINGLISH NUMBER RULES:
|
| 59 |
-
- "ek" = 1, "do" = 2, "teen" = 3, "char" = 4.
|
| 60 |
-
- Example: "do double room block kar do" means count=2, category="double", action="block".
|
| 61 |
-
- Example: "teen stanndard room book kar do" means count=2, category="standard", action="book".
|
| 62 |
-
RULES:
|
| 63 |
-
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".
|
| 64 |
-
2. If count is missing, use 1.
|
| 65 |
-
If category is missing, use "all". If count is missing, use 1.
|
| 66 |
-
Return ONLY JSON, no extra text."""
|
| 67 |
-
|
| 68 |
-
response = llm.invoke(prompt)
|
| 69 |
-
if hasattr(response, 'usage_metadata') and response.usage_metadata:
|
| 70 |
-
print(f"Tokens used: {response.usage_metadata}")
|
| 71 |
-
try:
|
| 72 |
-
data = json.loads(response.content.strip())
|
| 73 |
-
print(f"EXTRACTED: {data}")
|
| 74 |
-
except:
|
| 75 |
-
return {"response": "I couldn't quite understand your inventory"}
|
| 76 |
-
|
| 77 |
-
action = data.get("action","check")
|
| 78 |
-
category = data.get("category","all")
|
| 79 |
-
try:
|
| 80 |
-
count = int(data.get("count",1))
|
| 81 |
-
except:
|
| 82 |
-
count = 1
|
| 83 |
-
|
| 84 |
-
if action == "check":
|
| 85 |
-
from datetime import datetime,timedelta
|
| 86 |
-
today = datetime.now()
|
| 87 |
-
start_date = today.strftime("%Y-%m-%d")
|
| 88 |
-
end_date = (today + timedelta(days=1)).strftime("%Y-%m-%d")
|
| 89 |
-
success, data = await fetch_inventory_availability(start_date, end_date)
|
| 90 |
-
print(f"API SUCCESS: {success}, DATA KEYS: {data.keys()}")
|
| 91 |
-
|
| 92 |
-
if success:
|
| 93 |
-
categories = data.get("categories",[])
|
| 94 |
-
|
| 95 |
-
if category == "all":
|
| 96 |
-
today_str = today.strftime("%d-%m-%Y")
|
| 97 |
-
total_physical = 0
|
| 98 |
-
total_avail = 0
|
| 99 |
-
total_booked = 0
|
| 100 |
-
total_blocked = 0
|
| 101 |
-
total_ooo = 0
|
| 102 |
-
|
| 103 |
-
for cat in categories:
|
| 104 |
-
total_physical += int(cat.get("totalRooms", 0))
|
| 105 |
-
|
| 106 |
-
exact_daily_data = {}
|
| 107 |
-
for dd in cat.get("dateWiseData", []):
|
| 108 |
-
if dd.get("date") == today_str:
|
| 109 |
-
exact_daily_data = dd
|
| 110 |
-
break
|
| 111 |
-
|
| 112 |
-
summary = cat.get("summary") or {}
|
| 113 |
-
|
| 114 |
-
# 2. Extract values. If daily data is missing/null, fallback to summary.
|
| 115 |
-
avail = exact_daily_data.get("availableRooms")
|
| 116 |
-
if avail is None: avail = summary.get("totalAvailableRooms", 0)
|
| 117 |
-
|
| 118 |
-
booked = exact_daily_data.get("bookedRooms")
|
| 119 |
-
if booked is None: booked = summary.get("totalBookedRooms", 0)
|
| 120 |
-
|
| 121 |
-
blocked = exact_daily_data.get("blockedRooms")
|
| 122 |
-
if blocked is None: blocked = summary.get("totalBlockedRooms", 0)
|
| 123 |
-
|
| 124 |
-
ooo = exact_daily_data.get("outOfOrderRooms")
|
| 125 |
-
if ooo is None: ooo = summary.get("totalOutOfOrderRooms", 0)
|
| 126 |
-
|
| 127 |
-
# 3. Ensure they are integers to prevent math crashes
|
| 128 |
-
total_avail += int(avail or 0)
|
| 129 |
-
total_booked += int(booked or 0)
|
| 130 |
-
total_blocked += int(blocked or 0)
|
| 131 |
-
total_ooo += int(ooo or 0)
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
accounted_rooms = total_avail + total_booked + total_blocked + total_ooo
|
| 135 |
-
syncing_rooms = total_physical - accounted_rooms
|
| 136 |
-
|
| 137 |
-
if syncing_rooms > 0:
|
| 138 |
-
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."
|
| 139 |
-
else:
|
| 140 |
-
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."
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
else:
|
| 144 |
-
status = f"I checked the system, but I couldn't find any inventory data for the {category} category."
|
| 145 |
-
for cat in categories:
|
| 146 |
-
if category in cat.get("categoryName", "").lower():
|
| 147 |
-
today_str = today.strftime("%d-%m-%Y")
|
| 148 |
-
exact_daily_data = {}
|
| 149 |
-
|
| 150 |
-
for dd in cat.get("dateWiseData",[]):
|
| 151 |
-
if dd.get("date") == today_str:
|
| 152 |
-
exact_daily_data = dd
|
| 153 |
-
break
|
| 154 |
-
summary = cat.get("summary") or {}
|
| 155 |
-
|
| 156 |
-
avail = exact_daily_data.get("availableRooms")
|
| 157 |
-
if avail is None: avail = summary.get("totalAvailableRooms", 0)
|
| 158 |
-
|
| 159 |
-
booked = exact_daily_data.get("bookedRooms")
|
| 160 |
-
if booked is None: booked = summary.get("totalBookedRooms", 0)
|
| 161 |
-
|
| 162 |
-
blocked = exact_daily_data.get("blockedRooms")
|
| 163 |
-
if blocked is None: blocked = summary.get("totalBlockedRooms", 0)
|
| 164 |
-
|
| 165 |
-
ooo = exact_daily_data.get("outOfOrderRooms")
|
| 166 |
-
if ooo is None: ooo = summary.get("totalOutOfOrderRooms", 0)
|
| 167 |
-
|
| 168 |
-
avail = int(avail or 0)
|
| 169 |
-
booked = int(booked or 0)
|
| 170 |
-
blocked = int(blocked or 0)
|
| 171 |
-
ooo = int(ooo or 0)
|
| 172 |
-
|
| 173 |
-
# Catch the Ghost Rooms for this specific category!
|
| 174 |
-
total_physical = int(cat.get("totalRooms", 0))
|
| 175 |
-
accounted_rooms = avail + booked + blocked + ooo
|
| 176 |
-
syncing_rooms = total_physical - accounted_rooms
|
| 177 |
-
|
| 178 |
-
if syncing_rooms > 0:
|
| 179 |
-
status = f"For {cat['categoryName']}, {avail} are available right now. ध्यान दें: {syncing_rooms} room(s) are currently syncing their blocked or booked status."
|
| 180 |
-
else:
|
| 181 |
-
status = f"{cat['categoryName']}: {avail} available, {booked} booked, {blocked} blocked, and {ooo} out of order."
|
| 182 |
-
break
|
| 183 |
-
else:
|
| 184 |
-
status = "I couldn't connect to the hotel system. Please try again."
|
| 185 |
-
return {"response":status}
|
| 186 |
-
|
| 187 |
-
else:
|
| 188 |
-
pending_data = {
|
| 189 |
-
"action": action,
|
| 190 |
-
"category": category,
|
| 191 |
-
"count": count
|
| 192 |
-
}
|
| 193 |
-
status = f"I am ready to {action} {count} {category} room(s). Should I proceed?"
|
| 194 |
-
return {"response": status,"pending_manager":"inventory","pending_action":pending_data}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
agents/llm.py
DELETED
|
@@ -1,17 +0,0 @@
|
|
| 1 |
-
from langchain_groq import ChatGroq
|
| 2 |
-
# from langchain_openai import ChatOpenAI
|
| 3 |
-
import os
|
| 4 |
-
from dotenv import load_dotenv
|
| 5 |
-
load_dotenv()
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
llm = ChatGroq(
|
| 9 |
-
model="llama-3.3-70b-versatile",
|
| 10 |
-
api_key=os.getenv("GROQ_API_KEY"),
|
| 11 |
-
temperature=0.0
|
| 12 |
-
)
|
| 13 |
-
|
| 14 |
-
# llm = ChatOpenAI(
|
| 15 |
-
# model="gpt-4.1-nano",
|
| 16 |
-
# api_key=os.getenv("OPENAI_API_KEY")
|
| 17 |
-
# )
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
agents/rate_agent.py
DELETED
|
@@ -1,225 +0,0 @@
|
|
| 1 |
-
from agents.llm import llm
|
| 2 |
-
from hotel_api import update_rates,fetch_room_availability
|
| 3 |
-
import json
|
| 4 |
-
import os
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
async def rate_manager(state):
|
| 9 |
-
if state.get("pending_manager") == "rates":
|
| 10 |
-
user_reply = str(state.get("query","")).lower()
|
| 11 |
-
|
| 12 |
-
prompt = f"""User said: "{user_reply}"
|
| 13 |
-
If this is a positive confirmation for a booking, return "yes"
|
| 14 |
-
otherwise return "no". Nothing extra."""
|
| 15 |
-
response = llm.invoke(prompt)
|
| 16 |
-
confirmation = response.content.strip().lower()
|
| 17 |
-
if hasattr(response, 'usage_metadata') and response.usage_metadata:
|
| 18 |
-
print(f"Tokens used: {response.usage_metadata}")
|
| 19 |
-
|
| 20 |
-
if confirmation == "yes":
|
| 21 |
-
pa = state["pending_action"]
|
| 22 |
-
|
| 23 |
-
room_info = pa.get("room_info", {})
|
| 24 |
-
|
| 25 |
-
from datetime import datetime
|
| 26 |
-
today = datetime.now()
|
| 27 |
-
date_str = today.strftime("%Y-%m-%d")
|
| 28 |
-
|
| 29 |
-
success, data = await update_rates(
|
| 30 |
-
category=pa["category"],
|
| 31 |
-
new_price=pa["new_price"],
|
| 32 |
-
start_date=date_str,
|
| 33 |
-
end_date=date_str,
|
| 34 |
-
room_code=room_info.get("roomCode")
|
| 35 |
-
)
|
| 36 |
-
|
| 37 |
-
if success:
|
| 38 |
-
status = f"Done! The rate for {pa['category']} rooms has been successfully updated to {pa['new_price']} rupees."
|
| 39 |
-
else:
|
| 40 |
-
status = f"Sorry, I failed to update the rate for {pa['category']} rooms."
|
| 41 |
-
return {"response": status, "pending_manager": None, "pending_action": None}
|
| 42 |
-
else:
|
| 43 |
-
return {"response": "No problem, I have cancelled the rate change.", "pending_manager": None, "pending_action": None}
|
| 44 |
-
|
| 45 |
-
prompt = f"""Extract rate details from the request.
|
| 46 |
-
Request: {state['query']}
|
| 47 |
-
Return ONLY a JSON object: {{"action": "check|change", "category": "standard|deluxe|suite|presidential", "new_price": 0, "date":"today"}}
|
| 48 |
-
- If it's just checking the price, leave new_price as 0.
|
| 49 |
-
- If they don't mention a specific date, default to "today".
|
| 50 |
-
Return ONLY JSON, no extra text."""
|
| 51 |
-
|
| 52 |
-
response = llm.invoke(prompt)
|
| 53 |
-
if hasattr(response, 'usage_metadata') and response.usage_metadata:
|
| 54 |
-
print(f"Tokens used: {response.usage_metadata}")
|
| 55 |
-
|
| 56 |
-
try:
|
| 57 |
-
data = json.loads(response.content.strip())
|
| 58 |
-
except:
|
| 59 |
-
return {"response": "sorry i couldn't quite catch your rates"}
|
| 60 |
-
|
| 61 |
-
# action = data.get("action","check")
|
| 62 |
-
# category = data.get("category","single")
|
| 63 |
-
|
| 64 |
-
# if action == "check":
|
| 65 |
-
# # check_in = data.get("check_in")
|
| 66 |
-
# # check_out = data.get("check_out")
|
| 67 |
-
# from datetime import datetime,timedelta
|
| 68 |
-
|
| 69 |
-
# today = datetime.now()
|
| 70 |
-
# check_in = today.strftime("%Y-%m-%d")
|
| 71 |
-
# check_out = (today + timedelta(days=1)).strftime("%Y-%m-%d")
|
| 72 |
-
# adults = int(data.get("adults",1))
|
| 73 |
-
|
| 74 |
-
# rooms_payload = [{"roomNumber": 1, "guests": {"adults":adults,"children":0}}]
|
| 75 |
-
|
| 76 |
-
# success, api_response = await fetch_room_availability(
|
| 77 |
-
# phone_number_id=os.getenv("PHONE_NUMBER_ID"),
|
| 78 |
-
# check_in=check_in,
|
| 79 |
-
# check_out=check_out,
|
| 80 |
-
# rooms=rooms_payload
|
| 81 |
-
# )
|
| 82 |
-
# # print(f"Success: {success}")
|
| 83 |
-
# # print(f"API RESPONSE KEY: {api_response.keys()}")
|
| 84 |
-
# # print(f"RAW reservationPrice: {api_response.get('reservationPrice', 'KEY NOT FOUND')}")
|
| 85 |
-
# # print(f"RAW data key: {api_response.get('data', 'KEY NOT FOUND')}")
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
# if success:
|
| 89 |
-
# print("--- LIVE RATE DATA ---")
|
| 90 |
-
# print(json.dumps(api_response.get("data", {}).get("reservationPrice",[]), indent=2))
|
| 91 |
-
|
| 92 |
-
# prices_list = api_response.get("data", {}).get("reservationPrice", [])
|
| 93 |
-
# found_price = None
|
| 94 |
-
# print(f"PRICES LIST LENGTH: {len(prices_list)}")
|
| 95 |
-
# if prices_list:
|
| 96 |
-
# print(f"FIRST ITEM KEY: {prices_list[0].keys()}")
|
| 97 |
-
# print(f"FIRST ITEM roomType: {prices_list[0].get("roomType")}")
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
# from hotel_api import get_room_map
|
| 102 |
-
# from tools.category_matcher import match_category
|
| 103 |
-
|
| 104 |
-
# room_map = await get_room_map()
|
| 105 |
-
# matched_category = await match_category(category, room_map)
|
| 106 |
-
|
| 107 |
-
# if matched_category and matched_category in room_map:
|
| 108 |
-
# category = matched_category
|
| 109 |
-
|
| 110 |
-
# room_info = room_map.get(category)
|
| 111 |
-
# print(f"LOOKING FOR: {category.lower()}, FOUND: {room_info}")
|
| 112 |
-
|
| 113 |
-
# print(f"ALL ROOM TYPES: {[r.get('roomType') for r in prices_list]}")
|
| 114 |
-
|
| 115 |
-
# for room in prices_list:
|
| 116 |
-
# if category in room.get("roomType", "").lower():
|
| 117 |
-
# # Attempt to grab the price. We might need to change 'price' to the exact key!
|
| 118 |
-
# cats = room.get("categories",[])
|
| 119 |
-
# if cats:
|
| 120 |
-
# found_price = cats[0].get("totalPrice")
|
| 121 |
-
# break
|
| 122 |
-
|
| 123 |
-
# if found_price is not None:
|
| 124 |
-
# status = f"The current rate for a {category} room today is {found_price} rupees."
|
| 125 |
-
# else:
|
| 126 |
-
# status = f"I checked the system, but I couldn't find the rate for the {category} category."
|
| 127 |
-
# else:
|
| 128 |
-
# status = "I tried to check the live rates, but the system returned an error."
|
| 129 |
-
|
| 130 |
-
# return {"response": status}
|
| 131 |
-
# else:
|
| 132 |
-
# # User wants to CHANGE the rate
|
| 133 |
-
# price = data.get("new_price", 0)
|
| 134 |
-
# if price == 0:
|
| 135 |
-
# return {"response": "I didn't catch the new price. Please tell me the amount you want to change it to."}
|
| 136 |
-
|
| 137 |
-
# pending_data = {
|
| 138 |
-
# "category": category,
|
| 139 |
-
# "new_price": price
|
| 140 |
-
# }
|
| 141 |
-
# status = f"I am ready to change the rate of {category} rooms to {price} rupees. Should I confirm this change?"
|
| 142 |
-
# return {"response": status, "pending_manager": "rates", "pending_action": pending_data}
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
action = data.get("action", "check")
|
| 146 |
-
raw_category = data.get("category", "single")
|
| 147 |
-
|
| 148 |
-
# ==========================================
|
| 149 |
-
# 1. MAP CATEGORY FOR BOTH CHECK AND CHANGE
|
| 150 |
-
# ==========================================
|
| 151 |
-
from hotel_api import get_room_map
|
| 152 |
-
from tools.category_matcher import match_category
|
| 153 |
-
|
| 154 |
-
room_map = await get_room_map()
|
| 155 |
-
matched_category = await match_category(raw_category, room_map)
|
| 156 |
-
|
| 157 |
-
# Resolve to the exact key in your room map
|
| 158 |
-
if matched_category and matched_category in room_map:
|
| 159 |
-
category = matched_category
|
| 160 |
-
else:
|
| 161 |
-
category = raw_category
|
| 162 |
-
|
| 163 |
-
# Grab the exact official name for the Channex API
|
| 164 |
-
room_info = room_map.get(category, {})
|
| 165 |
-
exact_api_name = room_info.get("categoryName", category)
|
| 166 |
-
# ==========================================
|
| 167 |
-
|
| 168 |
-
print("\n" + "🔥"*20)
|
| 169 |
-
print(f"DEBUGGING CHANNEX FOR: {category}")
|
| 170 |
-
print(json.dumps(room_info, indent=2))
|
| 171 |
-
print("🔥"*20 + "\n")
|
| 172 |
-
|
| 173 |
-
if action == "check":
|
| 174 |
-
from datetime import datetime, timedelta
|
| 175 |
-
|
| 176 |
-
today = datetime.now()
|
| 177 |
-
check_in = today.strftime("%Y-%m-%d")
|
| 178 |
-
check_out = (today + timedelta(days=1)).strftime("%Y-%m-%d")
|
| 179 |
-
adults = int(data.get("adults", 1))
|
| 180 |
-
|
| 181 |
-
rooms_payload = [{"roomNumber": 1, "guests": {"adults": adults, "children": 0}}]
|
| 182 |
-
|
| 183 |
-
success, api_response = await fetch_room_availability(
|
| 184 |
-
phone_number_id=os.getenv("PHONE_NUMBER_ID"),
|
| 185 |
-
check_in=check_in,
|
| 186 |
-
check_out=check_out,
|
| 187 |
-
rooms=rooms_payload
|
| 188 |
-
)
|
| 189 |
-
|
| 190 |
-
if success:
|
| 191 |
-
prices_list = api_response.get("data", {}).get("reservationPrice", [])
|
| 192 |
-
found_price = None
|
| 193 |
-
|
| 194 |
-
for room in prices_list:
|
| 195 |
-
# Use exact_api_name to match against the API's response
|
| 196 |
-
if exact_api_name.lower() in room.get("roomType", "").lower():
|
| 197 |
-
cats = room.get("categories", [])
|
| 198 |
-
if cats:
|
| 199 |
-
found_price = cats[0].get("totalPrice")
|
| 200 |
-
break
|
| 201 |
-
|
| 202 |
-
if found_price is not None:
|
| 203 |
-
status = f"The current rate for a {exact_api_name} room today is {found_price} rupees."
|
| 204 |
-
else:
|
| 205 |
-
status = f"I checked the system, but I couldn't find the rate for the {exact_api_name} category."
|
| 206 |
-
else:
|
| 207 |
-
status = "I tried to check the live rates, but the system returned an error."
|
| 208 |
-
|
| 209 |
-
return {"response": status}
|
| 210 |
-
|
| 211 |
-
else:
|
| 212 |
-
# User wants to CHANGE the rate
|
| 213 |
-
price = data.get("new_price", 0)
|
| 214 |
-
if price == 0:
|
| 215 |
-
return {"response": "I didn't catch the new price. Please tell me the amount you want to change it to."}
|
| 216 |
-
|
| 217 |
-
# 2. SAVE THE EXACT API NAME TO PENDING ACTION
|
| 218 |
-
pending_data = {
|
| 219 |
-
"category": exact_api_name,
|
| 220 |
-
"new_price": price,
|
| 221 |
-
"room_info": room_info
|
| 222 |
-
}
|
| 223 |
-
status = f"I am ready to change the rate of {exact_api_name} rooms to {price} rupees. Should I confirm this change?"
|
| 224 |
-
return {"response": status, "pending_manager": "rates", "pending_action": pending_data}
|
| 225 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
agents/reservation_agent.py
DELETED
|
@@ -1,651 +0,0 @@
|
|
| 1 |
-
from hotel_api import fetch_room_availability,create_reservation
|
| 2 |
-
from datetime import datetime,timedelta
|
| 3 |
-
from agents.llm import llm
|
| 4 |
-
import re
|
| 5 |
-
import json
|
| 6 |
-
import os
|
| 7 |
-
from dotenv import load_dotenv
|
| 8 |
-
load_dotenv()
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
async def reservation_manager(state):
|
| 12 |
-
pb = state.get("pending_action") or {}
|
| 13 |
-
user_reply = str(state.get("query", "")).strip().lower()
|
| 14 |
-
extracted = state.get("extracted_data", {})
|
| 15 |
-
booking_step = state.get("booking_step","")
|
| 16 |
-
current_manager = state.get("pending_manager")
|
| 17 |
-
|
| 18 |
-
if current_manager == "reservations" or not current_manager:
|
| 19 |
-
|
| 20 |
-
# cancel_words = ["cancel","stop","no","don't","abort","quit"]
|
| 21 |
-
cancel_pattern = r'\b(cancel|stop|don\'t|abort|quit)\b'
|
| 22 |
-
# if any(word in user_reply for word in cancel_words):
|
| 23 |
-
if re.search(cancel_pattern, user_reply):
|
| 24 |
-
return {
|
| 25 |
-
"response": "No problem, I have canceled the current process. How else can I help you?",
|
| 26 |
-
"pending_manager": "",
|
| 27 |
-
"pending_action": {},
|
| 28 |
-
"booking_step": ""
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
if booking_step == "":
|
| 32 |
-
from hotel_api import get_room_map
|
| 33 |
-
room_map = await get_room_map()
|
| 34 |
-
real_categories = list(room_map.keys())
|
| 35 |
-
today_str = datetime.now().strftime("%Y-%m-%d")
|
| 36 |
-
today_day = datetime.now().strftime("%A")
|
| 37 |
-
prompt = f"""Extract booking details from the hotel owner's request.
|
| 38 |
-
Request: {state['query']}
|
| 39 |
-
CRITICAL CONTEXT:
|
| 40 |
-
- Today is {today_day}, {today_str}. Use this to calculate "tomorrow", "next Friday", etc.
|
| 41 |
-
- AVAILABLE ROOM CATEGORIES: {real_categories}
|
| 42 |
-
Return ONLY a JSON object:
|
| 43 |
-
{{
|
| 44 |
-
"action": "book|cancel|modify|check_availability",
|
| 45 |
-
"category": "category_name_from_list_or_null",
|
| 46 |
-
"guest_name": "The person's name. Could be the first word, or after 'name:', 'guest:', 'for'. Example: 'Kabilan', 'Mr. Sharma', 'John'. Look carefully — it's often the very first word in the message.",
|
| 47 |
-
"check_in": "YYYY-MM-DD_or_null",
|
| 48 |
-
"check_out": "YYYY-MM-DD_or_null",
|
| 49 |
-
"adults": 1,
|
| 50 |
-
"room_count": 1,
|
| 51 |
-
"meal_plan": "EP|CP|MAP|AP|null",
|
| 52 |
-
"phone_number": "string_or_null",
|
| 53 |
-
"custom_price": "integer_or_null",
|
| 54 |
-
"advance_payment": "integer_or_null",
|
| 55 |
-
"payment_method": "Online Payment or Credit Card or Bank Transfer or Pay at Hotel. Return null if not mentioned." }}
|
| 56 |
-
Rules:
|
| 57 |
-
- CRITICAL CALENDAR CONTEXT: Today is {today_day}, {today_str}.
|
| 58 |
-
- Use today's day and date to perfectly calculate relative words like "tomorrow", "Wednesday", "this week", etc.
|
| 59 |
-
- If check-out is missing, default to 1 day after check-in.
|
| 60 |
-
- If adults are not mentioned, default to 1.
|
| 61 |
-
- meal_plan: If they mention EP, CP, MAP, AP, "room only", "with breakfast", etc., extract the acronym. Else null.
|
| 62 |
-
- phone_number: Extract the full phone number string if provided. Else null.
|
| 63 |
-
- custom_price: If the owner states a specific total price for the room (e.g., "price is 5000"), extract as integer. Else null.
|
| 64 |
-
- advance_payment: If they state an advance payment amount, extract as integer. Else null.
|
| 65 |
-
Return ONLY valid JSON."""
|
| 66 |
-
|
| 67 |
-
response = llm.invoke(prompt)
|
| 68 |
-
raw_content = response.content.strip()
|
| 69 |
-
if hasattr(response, 'usage_metadata') and response.usage_metadata:
|
| 70 |
-
print(f"Tokens used: {response.usage_metadata}")
|
| 71 |
-
|
| 72 |
-
# print(f"--- RAW LLM RESPONSE ---\n{raw_content}\n------------------------")
|
| 73 |
-
clean_content = raw_content.replace("```json","").replace("```","").strip()
|
| 74 |
-
try:
|
| 75 |
-
data = json.loads(clean_content)
|
| 76 |
-
print(f"--- DEBUG JSON FROM LLM: {data} ---")
|
| 77 |
-
except Exception as e:
|
| 78 |
-
print(f"JSON Parsing error: {e}")
|
| 79 |
-
return {"response": "sorry i couldn't understand your reservation details"}
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
if data.get("guest_name"): pb["guest_name"] = data["guest_name"]
|
| 83 |
-
if data.get("category"): pb["category"] = data["category"]
|
| 84 |
-
if data.get("check_in"): pb["check_in"] = data["check_in"]
|
| 85 |
-
if data.get("check_out"): pb["check_out"] = data["check_out"]
|
| 86 |
-
if data.get("meal_plan"): pb["meal_plan"] = data["meal_plan"]
|
| 87 |
-
if data.get("phone_number"): pb["phone_number"] = data["phone_number"]
|
| 88 |
-
if data.get("custom_price"): pb["custom_price"] = data["custom_price"]
|
| 89 |
-
if data.get("advance_payment") is not None:
|
| 90 |
-
pb["advance_payment"] = data["advance_payment"]
|
| 91 |
-
if data.get("payment_method"): pb["payment_method"] = data["payment_method"]
|
| 92 |
-
if data.get("room_count"): pb["room_count"] = data["room_count"]
|
| 93 |
-
|
| 94 |
-
category = pb.get("category")
|
| 95 |
-
check_in = pb.get("check_in")
|
| 96 |
-
check_out = pb.get("check_out")
|
| 97 |
-
adults = int(extracted.get("adults", pb.get("adults", 1)))
|
| 98 |
-
guest_name = pb.get("guest_name")
|
| 99 |
-
|
| 100 |
-
if booking_step == "collect_name" and not pb.get("guest_name"):
|
| 101 |
-
pb["guest_name"] = state.get("query", "").strip()
|
| 102 |
-
print(f"Collected name: {pb['guest_name']}")
|
| 103 |
-
|
| 104 |
-
elif booking_step == "collect_category" and not pb.get("category"):
|
| 105 |
-
pb["category"] = state.get("query", "").strip()
|
| 106 |
-
|
| 107 |
-
elif booking_step == "collect_checkin" and not pb.get("check_in"):
|
| 108 |
-
pb["check_in"] = state.get("query", "").strip()
|
| 109 |
-
check_in = pb["check_in"]
|
| 110 |
-
|
| 111 |
-
elif booking_step == "collect_checkout" and not pb.get("check_out"):
|
| 112 |
-
pb["check_out"] = state.get("query", "").strip()
|
| 113 |
-
check_out = pb["check_out"]
|
| 114 |
-
|
| 115 |
-
#collecting meal plan
|
| 116 |
-
elif booking_step == "meal_plan":
|
| 117 |
-
user_reply = state.get("query","")
|
| 118 |
-
|
| 119 |
-
meal_prompt = f"""
|
| 120 |
-
The user is choosing a hotel meal plan.
|
| 121 |
-
User's reply: "{user_reply}"
|
| 122 |
-
|
| 123 |
-
Available Plans:
|
| 124 |
-
- "EP": European Plan (Room Only, no meals, just the room)
|
| 125 |
-
- "CP": Continental Plan (Room + Breakfast)
|
| 126 |
-
- "MAP": Modified American Plan (Half Board, two meals)
|
| 127 |
-
- "AP": American Plan (Full Board, all meals)
|
| 128 |
-
|
| 129 |
-
Analyze the user's reply. Account for STT phonetic typos (e.g., "ee pee" -> EP, "see pee" -> CP) and conversational phrases.
|
| 130 |
-
Return ONLY a valid JSON object. If you cannot confidently map their reply to a plan, return null.
|
| 131 |
-
|
| 132 |
-
OUTPUT FORMAT:
|
| 133 |
-
{{"meal_plan": "EP" | "CP" | "MAP" | "AP" | null}}
|
| 134 |
-
"""
|
| 135 |
-
try:
|
| 136 |
-
response = await llm.ainvoke(meal_prompt)
|
| 137 |
-
cleaned_response = response.content.replace("```json", "").replace("```", "").strip()
|
| 138 |
-
if hasattr(response, 'usage_metadata') and response.usage_metadata:
|
| 139 |
-
print(f"Tokens used: {response.usage_metadata}")
|
| 140 |
-
extracted = json.loads(cleaned_response)
|
| 141 |
-
selected_meal = extracted.get("meal_plan")
|
| 142 |
-
|
| 143 |
-
except Exception as e:
|
| 144 |
-
print(f"Meal extraction error: {e}")
|
| 145 |
-
selected_meal = None
|
| 146 |
-
|
| 147 |
-
if not selected_meal:
|
| 148 |
-
return {
|
| 149 |
-
"response": "I didn't catch that. Please say EP (room only), CP (with breakfast), MAP (half board), or AP (all meals).",
|
| 150 |
-
"pending_manager": "reservations",
|
| 151 |
-
"pending_action": pb,
|
| 152 |
-
"booking_step": "meal_plan"
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
-
pb["meal_plan"] = selected_meal
|
| 156 |
-
|
| 157 |
-
meal_categories = pb.get("meal_categories", [])
|
| 158 |
-
|
| 159 |
-
# Map the EP/CP/MAP/AP choice to the words the Xipper API actually uses
|
| 160 |
-
search_key = {
|
| 161 |
-
"EP": "only",
|
| 162 |
-
"CP": "breakfast",
|
| 163 |
-
"MAP": "lunch", # Matches 'Room With Breakfast + Lunch/Dinner'
|
| 164 |
-
"AP": "all" # Matches 'Room With All Meals'
|
| 165 |
-
}.get(selected_meal, "only")
|
| 166 |
-
|
| 167 |
-
room_count = int(pb.get("room_count", 1))
|
| 168 |
-
|
| 169 |
-
for plan in meal_categories:
|
| 170 |
-
api_plan_name = plan.get("category", "").lower()
|
| 171 |
-
|
| 172 |
-
# If we find the matching meal plan in the API list...
|
| 173 |
-
if search_key in api_plan_name:
|
| 174 |
-
single_new_base = plan.get("basePrice", 4000)
|
| 175 |
-
single_new_tax = plan.get("totalTax", single_new_base * 0.12)
|
| 176 |
-
single_new_total = plan.get("totalPrice", single_new_base + single_new_tax)
|
| 177 |
-
|
| 178 |
-
if "pricing" not in pb:
|
| 179 |
-
pb["pricing"] = {}
|
| 180 |
-
|
| 181 |
-
# Overwrite the memory with the new, accurate prices!
|
| 182 |
-
pb["pricing"]["basePrice"] = single_new_base #* room_count
|
| 183 |
-
pb["pricing"]["totalTax"] = single_new_tax #* room_count
|
| 184 |
-
pb["pricing"]["totalPrice"] = single_new_total #* room_count
|
| 185 |
-
pb["pricing"]["pricePerNight"] = plan.get("pricePerNight", single_new_total)
|
| 186 |
-
|
| 187 |
-
if pb.get("custom_price"):
|
| 188 |
-
custom_total = int(pb.get("custom_price"))
|
| 189 |
-
pb["pricing"]["totalPrice"] = custom_total
|
| 190 |
-
break
|
| 191 |
-
|
| 192 |
-
# return {"response": "Got it. What is the guest's phone number?", "pending_manager": "reservations", "pending_action": pb, "booking_step": "phone"}
|
| 193 |
-
|
| 194 |
-
#collecting phone number
|
| 195 |
-
elif booking_step == "phone":
|
| 196 |
-
phone = "".join(filter(str.isdigit, user_reply))
|
| 197 |
-
if len(phone) < 10:
|
| 198 |
-
return {"response": "Please say a valid 10 digit phone number.", "pending_manager": "reservations", "pending_action": pb, "booking_step": "phone"}
|
| 199 |
-
pb["phone_number"] = phone
|
| 200 |
-
# return {
|
| 201 |
-
# "response": "How much customer paid in advance?",
|
| 202 |
-
# "pending_manager": "reservations",
|
| 203 |
-
# "pending_action": pb,
|
| 204 |
-
# "booking_step": "advance_payment"
|
| 205 |
-
# }
|
| 206 |
-
|
| 207 |
-
elif booking_step == "advance_payment":
|
| 208 |
-
user_reply = state.get("query","")
|
| 209 |
-
total_price = pb.get("pricing", {}).get("totalPrice", 0)
|
| 210 |
-
|
| 211 |
-
payment_prompt = f"""
|
| 212 |
-
The user is stating how much advance payment was collected for a hotel booking.
|
| 213 |
-
The total price of the booking is {total_price}.
|
| 214 |
-
User's reply: "{user_reply}"
|
| 215 |
-
|
| 216 |
-
Extract the exact numerical amount paid in advance. Account for conversational phrases:
|
| 217 |
-
- If they say "nothing", "zero", or "pay at hotel", return 0.
|
| 218 |
-
- If they say "half", return exactly half of the total price.
|
| 219 |
-
- If they say "full" or "all of it", return the total price.
|
| 220 |
-
|
| 221 |
-
Return ONLY a valid JSON object.
|
| 222 |
-
|
| 223 |
-
OUTPUT FORMAT:
|
| 224 |
-
{{"advance_amount": integer_number_here}}
|
| 225 |
-
"""
|
| 226 |
-
try:
|
| 227 |
-
response = await llm.ainvoke(payment_prompt)
|
| 228 |
-
cleaned_response = response.content.replace("```json", "").replace("```", "").strip()
|
| 229 |
-
if hasattr(response, 'usage_metadata') and response.usage_metadata:
|
| 230 |
-
print(f"Tokens used: {response.usage_metadata}")
|
| 231 |
-
extracted = json.loads(cleaned_response)
|
| 232 |
-
advance_amount = extracted.get("advance_amount", 0)
|
| 233 |
-
except Exception as e:
|
| 234 |
-
print(f"Advance payment extraction error: {e}")
|
| 235 |
-
advance_amount = 0
|
| 236 |
-
|
| 237 |
-
pb["advance_payment"] = advance_amount
|
| 238 |
-
|
| 239 |
-
# guest_name = pb.get("guest_name", "Guest")
|
| 240 |
-
# room_type = pb.get("category", "room")
|
| 241 |
-
# meal_plan = pb.get("meal_plan", "EP")
|
| 242 |
-
|
| 243 |
-
# return {
|
| 244 |
-
# "response": "What is the payment method? Online payment, Credit card, Bank transfer, or Pay at hotel?",
|
| 245 |
-
# "pending_manager": "reservations",
|
| 246 |
-
# "booking_step": "payment",
|
| 247 |
-
# "pending_action": pb
|
| 248 |
-
# }
|
| 249 |
-
|
| 250 |
-
#cpllecting payment method
|
| 251 |
-
elif booking_step == "payment":
|
| 252 |
-
try:
|
| 253 |
-
payment_map = {
|
| 254 |
-
"online": "Online Payment",
|
| 255 |
-
"credit": "Credit Card",
|
| 256 |
-
"card": "Credit Card",
|
| 257 |
-
"bank": "Bank Transfer",
|
| 258 |
-
"transfer": "Bank Transfer",
|
| 259 |
-
"hotel": "Pay at Hotel",
|
| 260 |
-
"cash": "Pay at Hotel",
|
| 261 |
-
"upi": "Online Payment"
|
| 262 |
-
}
|
| 263 |
-
payment_method = None
|
| 264 |
-
for key,val in payment_map.items():
|
| 265 |
-
if key in user_reply.lower():
|
| 266 |
-
payment_method = val
|
| 267 |
-
break
|
| 268 |
-
if not payment_method:
|
| 269 |
-
return {"response": "Please say Online payment, Credit card, Bank transfer, or Pay at hotel.", "pending_manager": "reservations", "pending_action": pb, "booking_step": "payment"}
|
| 270 |
-
|
| 271 |
-
pb["payment_method"] = payment_method
|
| 272 |
-
pricing_data = pb.get("pricing") or {}
|
| 273 |
-
total_price = pricing_data.get("totalPrice", 0)
|
| 274 |
-
total_price = pb.get("pricing", {}).get("totalPrice", 0)
|
| 275 |
-
|
| 276 |
-
# 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?"
|
| 277 |
-
# return {
|
| 278 |
-
# "response": status,
|
| 279 |
-
# "pending_manager": "reservations",
|
| 280 |
-
# "pending_action": pb,
|
| 281 |
-
# "booking_step": "confirm"
|
| 282 |
-
# }
|
| 283 |
-
|
| 284 |
-
except Exception as e:
|
| 285 |
-
# IF IT CRASHES, CATCH IT AND PRINT THE EXACT CAUSE!
|
| 286 |
-
print("\n" + "="*50)
|
| 287 |
-
print(f"🔥 CRASH IN PAYMENT STEP: {e}")
|
| 288 |
-
import traceback
|
| 289 |
-
traceback.print_exc()
|
| 290 |
-
print("="*50 + "\n")
|
| 291 |
-
|
| 292 |
-
# Return safely so the server doesn't throw a 503
|
| 293 |
-
safe_pb = state.get("pending_action") or {}
|
| 294 |
-
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"}
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
from tools.date_utiles import sanitize_date
|
| 298 |
-
# from agents.llm import llm
|
| 299 |
-
|
| 300 |
-
check_in = await sanitize_date(check_in,llm)
|
| 301 |
-
check_out = await sanitize_date(check_out,llm)
|
| 302 |
-
|
| 303 |
-
pb["check_in"] = check_in
|
| 304 |
-
pb["check_out"] = check_out
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
if not pb.get("availability_checked") and category and check_in and check_out:
|
| 308 |
-
room_count = int(pb.get("room_count",1))
|
| 309 |
-
|
| 310 |
-
adults_per_room = max(1, adults//room_count)
|
| 311 |
-
rooms_payload = []
|
| 312 |
-
for i in range(room_count):
|
| 313 |
-
rooms_payload.append({
|
| 314 |
-
"roomNumber": i + 1,
|
| 315 |
-
"guests": {"adults": adults_per_room, "children": 0}
|
| 316 |
-
})
|
| 317 |
-
# rooms_payload = [{"roomNumber": 1, "guests": {"adults":adults,"children":0}}]
|
| 318 |
-
# safe_phone_id = state.get("phone_number_id")
|
| 319 |
-
# if not safe_phone_id:
|
| 320 |
-
safe_phone_id = os.getenv("PHONE_NUMBER_ID")
|
| 321 |
-
|
| 322 |
-
success,avail_data = await fetch_room_availability(
|
| 323 |
-
phone_number_id=safe_phone_id,
|
| 324 |
-
check_in=check_in,
|
| 325 |
-
check_out=check_out,
|
| 326 |
-
rooms=rooms_payload
|
| 327 |
-
)
|
| 328 |
-
|
| 329 |
-
# print(f"PRICES LIST: {avail_data.get('reservationPrice', [])}")
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
if not success:
|
| 333 |
-
return {"response": "I tried to check availability for those dates, but the system returned an error."}
|
| 334 |
-
|
| 335 |
-
prices_list = avail_data.get("data", {}).get("reservationPrice",[])
|
| 336 |
-
found_room = None
|
| 337 |
-
|
| 338 |
-
# from hotel_api import get_room_map
|
| 339 |
-
# room_map = await get_room_map()
|
| 340 |
-
# print(f"ROOM MAP KEYS: {list(room_map.keys())}")
|
| 341 |
-
# room_info = None
|
| 342 |
-
# for key,value in room_map.items():
|
| 343 |
-
# if key in category.lower():
|
| 344 |
-
# room_info = value
|
| 345 |
-
# category = key
|
| 346 |
-
# pb["category"] = key
|
| 347 |
-
# break
|
| 348 |
-
# # room_info = room_map.get(category.lower())
|
| 349 |
-
# # print(f"LOOKING FOR: {category.lower()}, FOUND: {room_info}")
|
| 350 |
-
|
| 351 |
-
# if not room_info:
|
| 352 |
-
# pb["category"] = None #-> wipe bad data
|
| 353 |
-
# return {
|
| 354 |
-
# "response": f"Sorry, I don't recognize {category} category",
|
| 355 |
-
# "pending_manager":"reservations",
|
| 356 |
-
# "booking_step":"collect_category",
|
| 357 |
-
# "pending_action": pb
|
| 358 |
-
# }
|
| 359 |
-
|
| 360 |
-
from hotel_api import get_room_map
|
| 361 |
-
from tools.category_matcher import match_category
|
| 362 |
-
|
| 363 |
-
room_map = await get_room_map()
|
| 364 |
-
print(f"ROOM MAP KEYS: {list(room_map.keys())}")
|
| 365 |
-
|
| 366 |
-
matched_category = await match_category(category, room_map)
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
room_info = None
|
| 370 |
-
if matched_category and matched_category in room_map:
|
| 371 |
-
room_info = room_map[matched_category]
|
| 372 |
-
category = matched_category
|
| 373 |
-
pb["category"] = matched_category
|
| 374 |
-
|
| 375 |
-
if not room_info:
|
| 376 |
-
pb["category"] = None
|
| 377 |
-
return {
|
| 378 |
-
"response": f"I couldn't find a matching room category for '{category}'. Available categories are: {', '.join(room_map.keys())}. Which one would you like?",
|
| 379 |
-
"pending_manager": "reservations",
|
| 380 |
-
"booking_step": "collect_category",
|
| 381 |
-
"pending_action": pb
|
| 382 |
-
}
|
| 383 |
-
|
| 384 |
-
target_name = room_info["categoryName"].lower()
|
| 385 |
-
|
| 386 |
-
for room in prices_list:
|
| 387 |
-
if target_name == room.get("roomType", "").lower():
|
| 388 |
-
found_room = room
|
| 389 |
-
break
|
| 390 |
-
|
| 391 |
-
if not found_room:
|
| 392 |
-
pb["category"] = None
|
| 393 |
-
return {
|
| 394 |
-
"response": f"I checked the inventory, but the {category} category is either sold out or has no pricing set for those dates.",
|
| 395 |
-
"pending_manager":"reservations",
|
| 396 |
-
"pending_action":pb,
|
| 397 |
-
"booking_step": "collect_category"
|
| 398 |
-
}
|
| 399 |
-
|
| 400 |
-
print("\n" + "="*50)
|
| 401 |
-
print(f"API DATA FOR {category.upper()}: {found_room}")
|
| 402 |
-
print("="*50 + "\n")
|
| 403 |
-
|
| 404 |
-
exact_room_name = found_room.get("roomType", category.title() + " room")
|
| 405 |
-
|
| 406 |
-
meal_categories = found_room.get("categories", [])
|
| 407 |
-
|
| 408 |
-
room_count = int(pb.get("room_count",1))
|
| 409 |
-
|
| 410 |
-
if meal_categories:
|
| 411 |
-
# Default to the first one in the list (usually 'Room Only') for the initial quote
|
| 412 |
-
default_plan = meal_categories[0]
|
| 413 |
-
single_base = default_plan.get("basePrice", 4000)
|
| 414 |
-
single_tax = default_plan.get("totalTax", single_base * 0.12)
|
| 415 |
-
single_total = default_plan.get("totalPrice", single_base + single_tax)
|
| 416 |
-
|
| 417 |
-
#multiply by number of rooms
|
| 418 |
-
base_price = single_base * room_count
|
| 419 |
-
total_tax = single_tax * room_count
|
| 420 |
-
total_price = single_total * room_count
|
| 421 |
-
else:
|
| 422 |
-
# Fallback if the categories array is empty
|
| 423 |
-
base_price = 4000 * room_count
|
| 424 |
-
total_tax = 480 * room_count
|
| 425 |
-
total_price = 4480 * room_count
|
| 426 |
-
|
| 427 |
-
if pb.get("custom_price"):
|
| 428 |
-
total_price = int(pb["custom_price"])
|
| 429 |
-
base_price = total_price / 1.12
|
| 430 |
-
total_tax = total_price - base_price
|
| 431 |
-
|
| 432 |
-
pb["category"] = category
|
| 433 |
-
pb["roomType"] = exact_room_name
|
| 434 |
-
pb["guest_name"] = guest_name
|
| 435 |
-
pb["check_in"] = check_in
|
| 436 |
-
pb["check_out"] = check_out
|
| 437 |
-
pb["adults"] = adults
|
| 438 |
-
pb["children"] = 0
|
| 439 |
-
pb["meal_categories"] = meal_categories
|
| 440 |
-
pb["availability_checked"] = True # Flag to show we found the room
|
| 441 |
-
|
| 442 |
-
pb["pricing"] = {
|
| 443 |
-
"basePrice": base_price,
|
| 444 |
-
"totalTax": total_tax,
|
| 445 |
-
"totalPrice": total_price,
|
| 446 |
-
"pricePerNight": meal_categories[0].get("pricePerNight", total_price) if meal_categories else total_price,
|
| 447 |
-
"numberOfNights": meal_categories[0].get("numberOfNights", 1) if meal_categories else 1,
|
| 448 |
-
"dateWisePricing": [],
|
| 449 |
-
"meal_categories": meal_categories
|
| 450 |
-
}
|
| 451 |
-
#-------------------------------------------------------------------------------------------------
|
| 452 |
-
current_price = pb.get("pricing", {}).get("totalPrice", 0)
|
| 453 |
-
|
| 454 |
-
if not pb.get("guest_name"):
|
| 455 |
-
return {
|
| 456 |
-
"response": "I can definitely help you check availability. Could you please tell me the guest's name for the reservation?",
|
| 457 |
-
"pending_manager":"reservations",
|
| 458 |
-
"booking_step":"collect_name",
|
| 459 |
-
"pending_action": pb
|
| 460 |
-
}
|
| 461 |
-
|
| 462 |
-
if not pb.get("category"): #or pb.get("category").lower() == None:
|
| 463 |
-
return {
|
| 464 |
-
"response": f"Got it, booking for {pb['guest_name']}. What category of room would you like?",
|
| 465 |
-
"pending_manager":"reservations",
|
| 466 |
-
"booking_step": "collect_category",
|
| 467 |
-
"pending_action": pb
|
| 468 |
-
}
|
| 469 |
-
|
| 470 |
-
if not pb.get("check_in"):
|
| 471 |
-
return {
|
| 472 |
-
"response": f"Perfect. What is your check-in date for the {pb['category']} room?",
|
| 473 |
-
"pending_manager":"reservations",
|
| 474 |
-
"booking_step":"collect_checkin",
|
| 475 |
-
"pending_action": pb
|
| 476 |
-
}
|
| 477 |
-
|
| 478 |
-
if not pb.get("check_out"):
|
| 479 |
-
return {
|
| 480 |
-
"response": "And what will be your check-out date?",
|
| 481 |
-
"pending_manager": "reservations",
|
| 482 |
-
"booking_step":"collect_checkout",
|
| 483 |
-
"pending_action": pb
|
| 484 |
-
}
|
| 485 |
-
|
| 486 |
-
if not pb.get("meal_plan"):
|
| 487 |
-
return {
|
| 488 |
-
"response": "what meal plan would you like?",
|
| 489 |
-
"pending_manager": "reservations",
|
| 490 |
-
"booking_step": "meal_plan",
|
| 491 |
-
"pending_action": pb
|
| 492 |
-
}
|
| 493 |
-
|
| 494 |
-
if not pb.get("phone_number"):
|
| 495 |
-
return {
|
| 496 |
-
"response": "what is guest phone number",
|
| 497 |
-
"pending_manager": "reservations",
|
| 498 |
-
"booking_step": "phone", #_______________________________________________
|
| 499 |
-
"pending_action": pb
|
| 500 |
-
}
|
| 501 |
-
|
| 502 |
-
if pb.get("advance_payment") is None:
|
| 503 |
-
return {
|
| 504 |
-
"response": "How much advance payment made?",
|
| 505 |
-
"pending_manager": "reservations",
|
| 506 |
-
"booking_step": "advance_payment",
|
| 507 |
-
"pending_action": pb
|
| 508 |
-
}
|
| 509 |
-
|
| 510 |
-
if not pb.get("payment_method"):
|
| 511 |
-
if pb.get("advance_payment") is not None and pb.get("advance_payment") > 0:
|
| 512 |
-
pb["payment_method"] = "Pay at Hotel"
|
| 513 |
-
else:
|
| 514 |
-
return {
|
| 515 |
-
"response": "What is the payment method? Online, Credit card, Bank transfer, or Pay at hotel?",
|
| 516 |
-
"pending_manager": "reservations",
|
| 517 |
-
"booking_step": "payment",
|
| 518 |
-
"pending_action": pb
|
| 519 |
-
}
|
| 520 |
-
|
| 521 |
-
print("🎉 All slots filled successfully:", pb)
|
| 522 |
-
|
| 523 |
-
if booking_step != "confirm":
|
| 524 |
-
guest = pb.get("guest_name", "Guest")
|
| 525 |
-
method = pb.get("payment_method", "Pay at Hotel")
|
| 526 |
-
return {
|
| 527 |
-
"response": f"Got everything. Shall I confirm the {pb.get('meal_plan')} booking for {guest} using {method} at {current_price} rupees?",
|
| 528 |
-
"pending_manager": "reservations",
|
| 529 |
-
"booking_step": "confirm",
|
| 530 |
-
"pending_action": pb
|
| 531 |
-
}
|
| 532 |
-
|
| 533 |
-
if booking_step == "confirm":
|
| 534 |
-
try:
|
| 535 |
-
prompt = f"""User said: "{user_reply}"
|
| 536 |
-
If this is a positive confirmation for a booking, return "yes"
|
| 537 |
-
otherwise return "no". Nothing extra."""
|
| 538 |
-
response = await llm.ainvoke(prompt)
|
| 539 |
-
if hasattr(response, 'usage_metadata') and response.usage_metadata:
|
| 540 |
-
print(f"Tokens used: {response.usage_metadata}")
|
| 541 |
-
confirmation = response.content.strip().lower()
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
if confirmation == "yes":
|
| 545 |
-
|
| 546 |
-
cin_str = pb.get("check_in")
|
| 547 |
-
nights = pb.get("pricing", {}).get("numberOfNights", 1)
|
| 548 |
-
from tools.pricing_utils import build_date_wise_pricing,get_meal_name
|
| 549 |
-
correct_meal_name = get_meal_name(pb.get("meal_plan", "EP"))
|
| 550 |
-
|
| 551 |
-
pricing_dict = pb.get("pricing", {})
|
| 552 |
-
|
| 553 |
-
# base_price = pb.get("pricing", {}).get("basePrice", 0)
|
| 554 |
-
# total_tax = pb.get("pricing", {}).get("totalTax", 0)
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
# Update the pricing dictionary with our new array
|
| 559 |
-
# pricing_dict["dateWisePricing"] = date_wise_pricing
|
| 560 |
-
|
| 561 |
-
#----------------------------------------------------------------------------
|
| 562 |
-
room_count = int(pb.get("room_count",1))
|
| 563 |
-
adults_per_room = max(1,pb["adults"]//room_count)
|
| 564 |
-
|
| 565 |
-
per_room_base = pricing_dict.get("basePrice", 0) / room_count
|
| 566 |
-
per_room_tax = pricing_dict.get("totalTax", 0) / room_count
|
| 567 |
-
per_room_total = pricing_dict.get("totalPrice", 0) / room_count
|
| 568 |
-
|
| 569 |
-
date_wise_pricing = build_date_wise_pricing(cin_str, nights, per_room_base, per_room_tax)
|
| 570 |
-
|
| 571 |
-
room_details = []
|
| 572 |
-
for i in range(room_count):
|
| 573 |
-
room_pricing = {
|
| 574 |
-
"basePrice": per_room_base,
|
| 575 |
-
"totalTax": per_room_tax,
|
| 576 |
-
"totalPrice": per_room_total,
|
| 577 |
-
"pricePerNight": per_room_total,
|
| 578 |
-
"numberOfNights": pricing_dict.get("numberOfNights", 1),
|
| 579 |
-
"dateWisePricing": date_wise_pricing
|
| 580 |
-
}
|
| 581 |
-
room_details.append({
|
| 582 |
-
"roomName": f"Room {i+1}",
|
| 583 |
-
"roomType": pb["roomType"],
|
| 584 |
-
"mealPlan": correct_meal_name,
|
| 585 |
-
"adults": adults_per_room,
|
| 586 |
-
"children": pb["children"],
|
| 587 |
-
"pets": "No",
|
| 588 |
-
"pricing": room_pricing
|
| 589 |
-
})
|
| 590 |
-
|
| 591 |
-
guest_details = [{
|
| 592 |
-
"title": "Mr",
|
| 593 |
-
"firstName": pb["guest_name"].split()[0],
|
| 594 |
-
"lastName": pb["guest_name"].split()[-1] if len(pb["guest_name"].split()) > 1 else "",
|
| 595 |
-
"phoneNumber": pb["phone_number"],
|
| 596 |
-
"email": "",
|
| 597 |
-
"xipperId": "",
|
| 598 |
-
"cXipperId": ""
|
| 599 |
-
}]
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
success,data = await create_reservation(
|
| 603 |
-
hotel_id=os.getenv("HOTEL_ID"),
|
| 604 |
-
check_in=pb["check_in"],
|
| 605 |
-
check_out=pb["check_out"],
|
| 606 |
-
user_id=os.getenv("USER_ID"),
|
| 607 |
-
room_details=room_details,
|
| 608 |
-
guest_details=guest_details,
|
| 609 |
-
payment_method=pb["payment_method"],
|
| 610 |
-
total_amount=pb.get("pricing", {}).get("totalPrice", 0.0),
|
| 611 |
-
advance_payment=pb.get("advance_payment", 0)
|
| 612 |
-
)
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
if success:
|
| 616 |
-
status = f"Done! Booking for {pb['guest_name']} successfully completed."
|
| 617 |
-
|
| 618 |
-
# from whatsapp_api import send_booking_confirmation
|
| 619 |
-
# await send_booking_confirmation(
|
| 620 |
-
# guest_phone="9911943475",
|
| 621 |
-
# guest_name=pb["guest_name"],
|
| 622 |
-
# room_type=pb["category"],
|
| 623 |
-
# check_in=pb["check_in"],
|
| 624 |
-
# check_out=pb["check_out"],
|
| 625 |
-
# total_price=pricing_dict["totalPrice"]
|
| 626 |
-
# )
|
| 627 |
-
|
| 628 |
-
return {"response": status, "pending_manager": "", "pending_action": {}, "booking_step": ""}
|
| 629 |
-
else:
|
| 630 |
-
error_msg = data.get("data", {}).get("message", data.get("message", "an unknown API error"))
|
| 631 |
-
status = f"Sorry, the booking failed. The server said: {error_msg}."
|
| 632 |
-
return {"response": status, "pending_manager": "", "pending_action": {}, "booking_step": ""}
|
| 633 |
-
|
| 634 |
-
else:
|
| 635 |
-
return {
|
| 636 |
-
"response": "No problem, I have cancelled the reservation process.",
|
| 637 |
-
"pending_manager": "",
|
| 638 |
-
"pending_action": {},
|
| 639 |
-
"booking_step": "",
|
| 640 |
-
"conversation_language": None
|
| 641 |
-
}
|
| 642 |
-
|
| 643 |
-
except Exception as e:
|
| 644 |
-
print("\n" + "="*50)
|
| 645 |
-
print(f"🔥 CRASH IN CONFIRM STEP: {e}")
|
| 646 |
-
import traceback
|
| 647 |
-
traceback.print_exc()
|
| 648 |
-
print("="*50 + "\n")
|
| 649 |
-
|
| 650 |
-
safe_pb = state.get("pending_action") or {}
|
| 651 |
-
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"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
agents/unknown_agent.py
DELETED
|
@@ -1,4 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
async def unknown_handler(state):
|
| 3 |
-
response = "Sorry but i can't help with you this query because i am here to help with your hotel management"
|
| 4 |
-
return {"response": response}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/index2.html
DELETED
|
@@ -1,477 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="UTF-8">
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>Voice Agent · Xipper</title>
|
| 7 |
-
<style>
|
| 8 |
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 9 |
-
|
| 10 |
-
body {
|
| 11 |
-
background: linear-gradient(145deg, #1b2a3a 0%, #0f1a26 100%);
|
| 12 |
-
min-height: 100vh;
|
| 13 |
-
display: flex;
|
| 14 |
-
justify-content: center;
|
| 15 |
-
align-items: center;
|
| 16 |
-
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
| 17 |
-
padding: 1.5rem;
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
.card {
|
| 21 |
-
background: rgba(30, 44, 58, 0.7);
|
| 22 |
-
backdrop-filter: blur(8px);
|
| 23 |
-
border-radius: 4rem 4rem 3rem 3rem;
|
| 24 |
-
box-shadow: 0 25px 50px -8px rgba(0,0,0,0.8), inset 0 2px 4px rgba(255,255,255,0.06);
|
| 25 |
-
padding: 3rem 2.5rem 3.5rem;
|
| 26 |
-
max-width: 600px;
|
| 27 |
-
width: 100%;
|
| 28 |
-
text-align: center;
|
| 29 |
-
border: 1px solid rgba(255,255,255,0.04);
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
h1 {
|
| 33 |
-
font-weight: 300;
|
| 34 |
-
font-size: 1.8rem;
|
| 35 |
-
letter-spacing: 2px;
|
| 36 |
-
color: #d6e6f5;
|
| 37 |
-
text-shadow: 0 2px 3px rgba(0,0,0,0.4);
|
| 38 |
-
margin-bottom: 0.25rem;
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
.sub {
|
| 42 |
-
color: #8aa9c9;
|
| 43 |
-
font-weight: 300;
|
| 44 |
-
font-size: 0.95rem;
|
| 45 |
-
border-bottom: 1px dashed #3a556b;
|
| 46 |
-
padding-bottom: 1.2rem;
|
| 47 |
-
margin-bottom: 2.2rem;
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
.mic-wrapper {
|
| 51 |
-
position: relative;
|
| 52 |
-
display: inline-block;
|
| 53 |
-
margin: 0.5rem 0 1.8rem;
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
.mic-btn {
|
| 57 |
-
background: #2b445a;
|
| 58 |
-
border: none;
|
| 59 |
-
border-radius: 50%;
|
| 60 |
-
width: 150px;
|
| 61 |
-
height: 150px;
|
| 62 |
-
box-shadow: 0 12px 28px rgba(0,0,0,0.7), 0 0 0 2px #4b6d89 inset, 0 0 0 4px #1a2e3f inset;
|
| 63 |
-
cursor: pointer;
|
| 64 |
-
transition: all 0.15s ease;
|
| 65 |
-
display: flex;
|
| 66 |
-
align-items: center;
|
| 67 |
-
justify-content: center;
|
| 68 |
-
color: #d9edff;
|
| 69 |
-
fill: currentColor;
|
| 70 |
-
}
|
| 71 |
-
|
| 72 |
-
.mic-btn:hover { background: #365a77; transform: scale(1.02); }
|
| 73 |
-
.mic-btn svg {
|
| 74 |
-
width: 70px; height: 70px;
|
| 75 |
-
filter: drop-shadow(0 4px 6px rgba(0,0,0,0.3));
|
| 76 |
-
transition: transform 0.1s;
|
| 77 |
-
}
|
| 78 |
-
|
| 79 |
-
.mic-btn.idle { background: #2b445a; }
|
| 80 |
-
.mic-btn.listening {
|
| 81 |
-
background: #6f3a4a;
|
| 82 |
-
box-shadow: 0 0 0 4px #b34a5a inset, 0 0 0 6px #3f1f2b inset, 0 12px 28px #250f16;
|
| 83 |
-
}
|
| 84 |
-
.mic-btn.listening svg {
|
| 85 |
-
animation: pulse 1.2s infinite ease-in-out;
|
| 86 |
-
filter: drop-shadow(0 0 12px #ff7b7b);
|
| 87 |
-
}
|
| 88 |
-
.mic-btn.processing {
|
| 89 |
-
background: #1e3a6f;
|
| 90 |
-
box-shadow: 0 0 0 4px #2a5ab3 inset, 0 0 0 6px #0f1f3f inset, 0 12px 28px #0a1428;
|
| 91 |
-
}
|
| 92 |
-
.mic-btn.processing svg {
|
| 93 |
-
animation: pulse 0.8s infinite ease-in-out;
|
| 94 |
-
filter: drop-shadow(0 0 12px #4a9eff);
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
-
@keyframes pulse {
|
| 98 |
-
0% { transform: scale(1); }
|
| 99 |
-
50% { transform: scale(1.08); }
|
| 100 |
-
100% { transform: scale(1); }
|
| 101 |
-
}
|
| 102 |
-
|
| 103 |
-
.status-box {
|
| 104 |
-
background: #0e1a24cc;
|
| 105 |
-
border-radius: 60px;
|
| 106 |
-
padding: 0.9rem 1.8rem;
|
| 107 |
-
margin: 1.2rem 0 0.6rem;
|
| 108 |
-
display: inline-block;
|
| 109 |
-
border: 1px solid #3e5c74;
|
| 110 |
-
color: #c2d9ef;
|
| 111 |
-
font-size: 1rem;
|
| 112 |
-
min-width: 180px;
|
| 113 |
-
box-shadow: inset 0 2px 6px rgba(0,0,0,0.4);
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
/* Live transcript display */
|
| 117 |
-
.live-transcript {
|
| 118 |
-
background: #0e1a2488;
|
| 119 |
-
border-radius: 12px;
|
| 120 |
-
padding: 0.6rem 1.2rem;
|
| 121 |
-
margin: 0.4rem 0 1rem;
|
| 122 |
-
color: #7ab8e8;
|
| 123 |
-
font-size: 0.88rem;
|
| 124 |
-
font-style: italic;
|
| 125 |
-
min-height: 2rem;
|
| 126 |
-
border: 1px dashed #2a4a62;
|
| 127 |
-
}
|
| 128 |
-
|
| 129 |
-
.log-area {
|
| 130 |
-
background: #0f1923cc;
|
| 131 |
-
border-radius: 28px;
|
| 132 |
-
padding: 1rem 1.6rem;
|
| 133 |
-
margin: 1rem 0 0.8rem;
|
| 134 |
-
font-size: 0.9rem;
|
| 135 |
-
color: #b3ceea;
|
| 136 |
-
border-left: 3px solid #3d779b;
|
| 137 |
-
text-align: left;
|
| 138 |
-
min-height: 100px;
|
| 139 |
-
max-height: 200px;
|
| 140 |
-
overflow-y: auto;
|
| 141 |
-
word-break: break-word;
|
| 142 |
-
line-height: 1.6;
|
| 143 |
-
box-shadow: inset 0 4px 8px rgba(0,0,0,0.3);
|
| 144 |
-
}
|
| 145 |
-
|
| 146 |
-
.log-entry { margin-bottom: 0.4rem; }
|
| 147 |
-
.log-user { color: #aac2dd; }
|
| 148 |
-
.log-agent { color: #a6e0b0; }
|
| 149 |
-
.log-info { color: #d6ecff; opacity: 0.6; font-style: italic; }
|
| 150 |
-
.log-error { color: #ffa79e; }
|
| 151 |
-
|
| 152 |
-
.footnote {
|
| 153 |
-
margin-top: 1.5rem;
|
| 154 |
-
color: #5a7d99;
|
| 155 |
-
font-size: 0.75rem;
|
| 156 |
-
opacity: 0.7;
|
| 157 |
-
border-top: 1px solid #2f4b61;
|
| 158 |
-
padding-top: 1.2rem;
|
| 159 |
-
}
|
| 160 |
-
|
| 161 |
-
.btn-reset {
|
| 162 |
-
background: transparent;
|
| 163 |
-
border: 1px solid #3d6079;
|
| 164 |
-
color: #b3ceea;
|
| 165 |
-
border-radius: 40px;
|
| 166 |
-
padding: 0.4rem 1.2rem;
|
| 167 |
-
font-size: 0.8rem;
|
| 168 |
-
margin-top: 0.4rem;
|
| 169 |
-
cursor: pointer;
|
| 170 |
-
transition: 0.2s;
|
| 171 |
-
opacity: 0.6;
|
| 172 |
-
}
|
| 173 |
-
.btn-reset:hover { background: #2f4b61; opacity: 1; }
|
| 174 |
-
|
| 175 |
-
#chat-container { margin-top: 20px; }
|
| 176 |
-
#chatInput {
|
| 177 |
-
width: 300px; padding: 10px;
|
| 178 |
-
background: #1a2e3f;
|
| 179 |
-
border: 1px solid #3a5a75;
|
| 180 |
-
border-radius: 8px;
|
| 181 |
-
color: #d6ecff;
|
| 182 |
-
font-size: 0.9rem;
|
| 183 |
-
}
|
| 184 |
-
#btn-send-chat {
|
| 185 |
-
padding: 10px 16px;
|
| 186 |
-
background: #2b445a;
|
| 187 |
-
border: 1px solid #4b6d89;
|
| 188 |
-
border-radius: 8px;
|
| 189 |
-
color: #d9edff;
|
| 190 |
-
cursor: pointer;
|
| 191 |
-
margin-left: 6px;
|
| 192 |
-
}
|
| 193 |
-
#btn-send-chat:hover { background: #365a77; }
|
| 194 |
-
</style>
|
| 195 |
-
</head>
|
| 196 |
-
<body>
|
| 197 |
-
<div class="card">
|
| 198 |
-
<h1>🎙️ VOICE AGENT</h1>
|
| 199 |
-
<div class="sub">click mic · speak · pause naturally · agent responds</div>
|
| 200 |
-
|
| 201 |
-
<div class="mic-wrapper">
|
| 202 |
-
<button class="mic-btn idle" id="micButton" aria-label="Start voice conversation">
|
| 203 |
-
<svg viewBox="0 0 24 24" width="70" height="70">
|
| 204 |
-
<path d="M12 16c-2.206 0-4-1.794-4-4V6c0-2.206 1.794-4 4-4s4 1.794 4 4v6c0 2.206-1.794 4-4 4zm8-4h-2c0 2.607-2.126 4.5-4 4.5S10 14.607 10 12H8c0 3.157 2.416 5.5 5 5.944V20h-2v2h6v-2h-2v-2.056c2.584-.444 5-2.787 5-5.944z"/>
|
| 205 |
-
</svg>
|
| 206 |
-
</button>
|
| 207 |
-
</div>
|
| 208 |
-
|
| 209 |
-
<div class="status-box">
|
| 210 |
-
<span id="statusText">🔴 idle · click mic to start</span>
|
| 211 |
-
</div>
|
| 212 |
-
|
| 213 |
-
<!-- Live transcript shown while user is speaking -->
|
| 214 |
-
<div class="live-transcript" id="liveTranscript"></div>
|
| 215 |
-
|
| 216 |
-
<div id="chat-container">
|
| 217 |
-
<input type="text" id="chatInput" placeholder="Or type your message...">
|
| 218 |
-
<button id="btn-send-chat">Send</button>
|
| 219 |
-
</div>
|
| 220 |
-
|
| 221 |
-
<div class="log-area" id="logArea">
|
| 222 |
-
<div class="log-entry log-info">📋 ready · click mic to start conversation</div>
|
| 223 |
-
</div>
|
| 224 |
-
|
| 225 |
-
<button class="btn-reset" id="resetLogBtn">↻ clear log</button>
|
| 226 |
-
<div class="footnote">Server-side VAD · WebSocket streaming · Deepgram nova-2 · Sarvam TTS</div>
|
| 227 |
-
</div>
|
| 228 |
-
|
| 229 |
-
<script>
|
| 230 |
-
(function () {
|
| 231 |
-
"use strict";
|
| 232 |
-
|
| 233 |
-
const API_BASE = 'https://kanhaiyaml-xipper.hf.space';
|
| 234 |
-
const CHAT_URL = `${API_BASE}/chat`;
|
| 235 |
-
const WS_URL = `https://kanhaiyaml-xipper.hf.space/ws/voice`;
|
| 236 |
-
const SAMPLE_RATE = 16000;
|
| 237 |
-
|
| 238 |
-
const micBtn = document.getElementById('micButton');
|
| 239 |
-
const statusText = document.getElementById('statusText');
|
| 240 |
-
const logArea = document.getElementById('logArea');
|
| 241 |
-
const liveTranscript = document.getElementById('liveTranscript');
|
| 242 |
-
const chatInput = document.getElementById('chatInput');
|
| 243 |
-
const sendChatBtn = document.getElementById('btn-send-chat');
|
| 244 |
-
|
| 245 |
-
let isActive = false;
|
| 246 |
-
let audioContext = null;
|
| 247 |
-
let micSource = null;
|
| 248 |
-
let scriptProcessor = null;
|
| 249 |
-
let stream = null;
|
| 250 |
-
let socket = null;
|
| 251 |
-
|
| 252 |
-
function setStatus(text) { statusText.textContent = text; }
|
| 253 |
-
function setMicState(state) { micBtn.className = `mic-btn ${state}`; }
|
| 254 |
-
function addLog(msg, type = 'info') {
|
| 255 |
-
const div = document.createElement('div');
|
| 256 |
-
div.className = `log-entry log-${type}`;
|
| 257 |
-
div.textContent = msg;
|
| 258 |
-
logArea.appendChild(div);
|
| 259 |
-
logArea.scrollTop = logArea.scrollHeight;
|
| 260 |
-
}
|
| 261 |
-
|
| 262 |
-
function float32ToInt16Buffer(float32Array) {
|
| 263 |
-
const int16 = new Int16Array(float32Array.length);
|
| 264 |
-
for (let i = 0; i < float32Array.length; i++) {
|
| 265 |
-
const c = Math.max(-1, Math.min(1, float32Array[i]));
|
| 266 |
-
int16[i] = c < 0 ? c * 32768 : c * 32767;
|
| 267 |
-
}
|
| 268 |
-
return int16.buffer;
|
| 269 |
-
}
|
| 270 |
-
|
| 271 |
-
function playAudioAndContinue(audioBase64) {
|
| 272 |
-
let b64 = audioBase64;
|
| 273 |
-
if (b64.startsWith('data:audio/')) b64 = b64.substring(b64.indexOf(',') + 1);
|
| 274 |
-
const binary = window.atob(b64);
|
| 275 |
-
const bytes = new Uint8Array(binary.length);
|
| 276 |
-
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
| 277 |
-
|
| 278 |
-
const blob = new Blob([bytes], { type: 'audio/wav' });
|
| 279 |
-
const url = URL.createObjectURL(blob);
|
| 280 |
-
const audio = new Audio(url);
|
| 281 |
-
|
| 282 |
-
setMicState('processing');
|
| 283 |
-
setStatus('🔊 speaking...');
|
| 284 |
-
|
| 285 |
-
audio.onended = () => {
|
| 286 |
-
// Agent done speaking — go back to listening (socket still open)
|
| 287 |
-
if (isActive) {
|
| 288 |
-
setMicState('listening');
|
| 289 |
-
setStatus('🎤 listening...');
|
| 290 |
-
}
|
| 291 |
-
};
|
| 292 |
-
|
| 293 |
-
audio.play().catch(err => {
|
| 294 |
-
console.warn('Autoplay blocked:', err);
|
| 295 |
-
addLog('🔊 Autoplay blocked — click mic to continue', 'error');
|
| 296 |
-
setMicState('idle');
|
| 297 |
-
setStatus('🔴 autoplay blocked · click mic');
|
| 298 |
-
});
|
| 299 |
-
}
|
| 300 |
-
|
| 301 |
-
function openWebSocket() {
|
| 302 |
-
socket = new WebSocket(WS_URL);
|
| 303 |
-
socket.binaryType = 'arraybuffer';
|
| 304 |
-
|
| 305 |
-
socket.onopen = () => {
|
| 306 |
-
setMicState('listening');
|
| 307 |
-
setStatus('🎤 listening...');
|
| 308 |
-
addLog('🟢 Connected · speak now', 'info');
|
| 309 |
-
startStreamingAudio();
|
| 310 |
-
};
|
| 311 |
-
|
| 312 |
-
socket.onmessage = (event) => {
|
| 313 |
-
try {
|
| 314 |
-
const data = JSON.parse(event.data);
|
| 315 |
-
|
| 316 |
-
// Live transcript while user is speaking
|
| 317 |
-
if (data.interim_transcript !== undefined) {
|
| 318 |
-
liveTranscript.textContent = '👂 ' + data.interim_transcript;
|
| 319 |
-
return;
|
| 320 |
-
}
|
| 321 |
-
if (data.live_transcript !== undefined) {
|
| 322 |
-
liveTranscript.textContent = '✅ ' + data.live_transcript;
|
| 323 |
-
return;
|
| 324 |
-
}
|
| 325 |
-
|
| 326 |
-
// Server detected end of utterance, now processing
|
| 327 |
-
if (data.status === 'processing') {
|
| 328 |
-
setMicState('processing');
|
| 329 |
-
setStatus('⏳ thinking...');
|
| 330 |
-
if (data.transcript) addLog(`👤 You: ${data.transcript}`, 'user');
|
| 331 |
-
liveTranscript.textContent = '';
|
| 332 |
-
return;
|
| 333 |
-
}
|
| 334 |
-
|
| 335 |
-
if (data.error) {
|
| 336 |
-
addLog(`❌ ${data.error}`, 'error');
|
| 337 |
-
return;
|
| 338 |
-
}
|
| 339 |
-
|
| 340 |
-
// Final response with audio
|
| 341 |
-
const { transcript, response: agentReply, audio } = data;
|
| 342 |
-
if (transcript) addLog(`👤 You: ${transcript}`, 'user');
|
| 343 |
-
if (agentReply) addLog(`🤖 Agent: ${agentReply}`, 'agent');
|
| 344 |
-
liveTranscript.textContent = '';
|
| 345 |
-
|
| 346 |
-
if (audio) {
|
| 347 |
-
playAudioAndContinue(audio);
|
| 348 |
-
} else {
|
| 349 |
-
setMicState('listening');
|
| 350 |
-
setStatus('🎤 listening...');
|
| 351 |
-
}
|
| 352 |
-
|
| 353 |
-
} catch (e) {
|
| 354 |
-
console.error('Parse error:', e);
|
| 355 |
-
}
|
| 356 |
-
};
|
| 357 |
-
|
| 358 |
-
socket.onerror = (e) => {
|
| 359 |
-
console.error('WebSocket error:', e);
|
| 360 |
-
addLog('❌ Connection error', 'error');
|
| 361 |
-
setMicState('idle');
|
| 362 |
-
setStatus('⚠️ error');
|
| 363 |
-
};
|
| 364 |
-
|
| 365 |
-
socket.onclose = () => {
|
| 366 |
-
console.log('WebSocket closed');
|
| 367 |
-
if (isActive) {
|
| 368 |
-
// Unexpected close — try to reconnect after a short delay
|
| 369 |
-
setStatus('🔄 reconnecting...');
|
| 370 |
-
setTimeout(() => { if (isActive) openWebSocket(); }, 1000);
|
| 371 |
-
}
|
| 372 |
-
};
|
| 373 |
-
}
|
| 374 |
-
|
| 375 |
-
function startStreamingAudio() {
|
| 376 |
-
// Just pipe raw PCM to the WebSocket continuously.
|
| 377 |
-
// No VAD here — server decides when speech ends.
|
| 378 |
-
scriptProcessor.onaudioprocess = (event) => {
|
| 379 |
-
if (!socket || socket.readyState !== WebSocket.OPEN) return;
|
| 380 |
-
const float32 = event.inputBuffer.getChannelData(0);
|
| 381 |
-
socket.send(float32ToInt16Buffer(float32));
|
| 382 |
-
};
|
| 383 |
-
}
|
| 384 |
-
|
| 385 |
-
async function startConversation() {
|
| 386 |
-
if (isActive) {
|
| 387 |
-
stopConversation();
|
| 388 |
-
return;
|
| 389 |
-
}
|
| 390 |
-
|
| 391 |
-
try {
|
| 392 |
-
stream = await navigator.mediaDevices.getUserMedia({
|
| 393 |
-
audio: {
|
| 394 |
-
echoCancellation: true,
|
| 395 |
-
noiseSuppression: true,
|
| 396 |
-
autoGainControl: true,
|
| 397 |
-
sampleRate: SAMPLE_RATE,
|
| 398 |
-
channelCount: 1,
|
| 399 |
-
}
|
| 400 |
-
});
|
| 401 |
-
|
| 402 |
-
audioContext = new AudioContext({ sampleRate: SAMPLE_RATE });
|
| 403 |
-
micSource = audioContext.createMediaStreamSource(stream);
|
| 404 |
-
scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1);
|
| 405 |
-
micSource.connect(scriptProcessor);
|
| 406 |
-
scriptProcessor.connect(audioContext.destination);
|
| 407 |
-
|
| 408 |
-
isActive = true;
|
| 409 |
-
openWebSocket();
|
| 410 |
-
|
| 411 |
-
} catch (err) {
|
| 412 |
-
console.error('Mic error:', err);
|
| 413 |
-
let msg = '❌ Mic error: ';
|
| 414 |
-
if (err.name === 'NotAllowedError') msg += 'Permission denied.';
|
| 415 |
-
else msg += err.message;
|
| 416 |
-
addLog(msg, 'error');
|
| 417 |
-
setMicState('idle');
|
| 418 |
-
setStatus('⛔ mic error');
|
| 419 |
-
}
|
| 420 |
-
}
|
| 421 |
-
|
| 422 |
-
function stopConversation() {
|
| 423 |
-
isActive = false;
|
| 424 |
-
|
| 425 |
-
if (scriptProcessor) { scriptProcessor.disconnect(); scriptProcessor = null; }
|
| 426 |
-
if (micSource) { micSource.disconnect(); micSource = null; }
|
| 427 |
-
if (audioContext) { audioContext.close(); audioContext = null; }
|
| 428 |
-
if (stream) { stream.getTracks().forEach(t => t.stop()); stream = null; }
|
| 429 |
-
|
| 430 |
-
if (socket && socket.readyState === WebSocket.OPEN) {
|
| 431 |
-
socket.send("STOP"); // tell server we're done
|
| 432 |
-
socket.close();
|
| 433 |
-
}
|
| 434 |
-
|
| 435 |
-
liveTranscript.textContent = '';
|
| 436 |
-
setMicState('idle');
|
| 437 |
-
setStatus('🔴 idle · click mic to start');
|
| 438 |
-
addLog('🔴 Conversation ended', 'info');
|
| 439 |
-
}
|
| 440 |
-
|
| 441 |
-
micBtn.addEventListener('click', startConversation);
|
| 442 |
-
|
| 443 |
-
// Text chat
|
| 444 |
-
sendChatBtn.onclick = async function () {
|
| 445 |
-
const message = chatInput.value.trim();
|
| 446 |
-
if (!message) return;
|
| 447 |
-
chatInput.value = '';
|
| 448 |
-
addLog(`👤 You: ${message}`, 'user');
|
| 449 |
-
setStatus('⏳ thinking...');
|
| 450 |
-
try {
|
| 451 |
-
const res = await fetch(CHAT_URL, {
|
| 452 |
-
method: 'POST',
|
| 453 |
-
headers: { 'Content-Type': 'application/json' },
|
| 454 |
-
body: JSON.stringify({ message })
|
| 455 |
-
});
|
| 456 |
-
const data = await res.json();
|
| 457 |
-
addLog(`🤖 Agent: ${data.response}`, 'agent');
|
| 458 |
-
setStatus('🟢 ready');
|
| 459 |
-
if (data.audio) playAudioAndContinue(data.audio);
|
| 460 |
-
} catch (err) {
|
| 461 |
-
addLog('❌ Failed to send message.', 'error');
|
| 462 |
-
setStatus('🔴 error');
|
| 463 |
-
}
|
| 464 |
-
};
|
| 465 |
-
|
| 466 |
-
chatInput.addEventListener('keypress', e => { if (e.key === 'Enter') sendChatBtn.click(); });
|
| 467 |
-
document.getElementById('resetLogBtn').addEventListener('click', () => {
|
| 468 |
-
logArea.innerHTML = '<div class="log-entry log-info">📋 log cleared</div>';
|
| 469 |
-
});
|
| 470 |
-
window.addEventListener('beforeunload', () => { if (isActive) stopConversation(); });
|
| 471 |
-
|
| 472 |
-
setMicState('idle');
|
| 473 |
-
setStatus('🔴 idle · click mic to start');
|
| 474 |
-
})();
|
| 475 |
-
</script>
|
| 476 |
-
</body>
|
| 477 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
functions.py
DELETED
|
@@ -1,17 +0,0 @@
|
|
| 1 |
-
from datetime import datetime, timedelta
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
def sanitize_date(date_str):
|
| 5 |
-
if not date_str:
|
| 6 |
-
return None
|
| 7 |
-
d = str(date_str).strip().lower()
|
| 8 |
-
today_dt = datetime.now()
|
| 9 |
-
|
| 10 |
-
if d in ["today", "आज"]:
|
| 11 |
-
return today_dt.strftime("%Y-%m-%d")
|
| 12 |
-
if d in ["tomorrow", "कल"]:
|
| 13 |
-
return (today_dt + timedelta(days=1)).strftime("%Y-%m-%d")
|
| 14 |
-
if d in ["day after tomorrow", "परसों"]:
|
| 15 |
-
return (today_dt + timedelta(days=2)).strftime("%Y-%m-%d")
|
| 16 |
-
|
| 17 |
-
return date_str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
graph_main.py
DELETED
|
@@ -1,88 +0,0 @@
|
|
| 1 |
-
from agents.rate_agent import rate_manager
|
| 2 |
-
from agents.inventory_agent import inventory_manager
|
| 3 |
-
from agents.reservation_agent import reservation_manager
|
| 4 |
-
from agents.unknown_agent import unknown_handler
|
| 5 |
-
from agents.llm import llm
|
| 6 |
-
from langgraph.graph import StateGraph,START,END
|
| 7 |
-
from typing import TypedDict,Optional
|
| 8 |
-
from langgraph.checkpoint.memory import MemorySaver
|
| 9 |
-
from dotenv import load_dotenv
|
| 10 |
-
load_dotenv()
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
class SupportState(TypedDict):
|
| 15 |
-
query: str
|
| 16 |
-
next_agent: str
|
| 17 |
-
response: str
|
| 18 |
-
pending_manager: Optional[str]
|
| 19 |
-
pending_action: Optional[dict]
|
| 20 |
-
phone_number_id: str
|
| 21 |
-
booking_link: str
|
| 22 |
-
booking_step: str
|
| 23 |
-
conversation_language: Optional[str]
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
memory = MemorySaver()
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
async def choose_agent(state: SupportState):
|
| 30 |
-
if state.get("pending_manager"):
|
| 31 |
-
return {"next_agent":state["pending_manager"]}
|
| 32 |
-
|
| 33 |
-
prompt = f"""Classify the hotel owner's request into one of these 3 managers:
|
| 34 |
-
'inventory' - Check availability, block, unblock, release, or unrelease rooms.
|
| 35 |
-
'rates' - Check room prices or change room rates.
|
| 36 |
-
'reservations' - Make a new booking, modify existing, or cancel.
|
| 37 |
-
'unknown' - Any request not related to hotel management (coding, general knowledge, etc.)
|
| 38 |
-
Request: {state['query']}
|
| 39 |
-
|
| 40 |
-
IMPORTANT RULES:
|
| 41 |
-
- If the user is asking "how many" (कितने) or asking for counts, it is ALWAYS "inventory", even if they use the word "booked".
|
| 42 |
-
- ONLY return "reservations" if there is a specific guest involved or an intent to create a new booking right now.
|
| 43 |
-
Return ONLY one word: inventory, rates, reservations, or unknown."""
|
| 44 |
-
|
| 45 |
-
response = llm.invoke(prompt)
|
| 46 |
-
manager = response.content.strip().lower().replace(".","").replace("'","")
|
| 47 |
-
|
| 48 |
-
if hasattr(response, 'usage_metadata') and response.usage_metadata:
|
| 49 |
-
print(f"Tokens used: {response.usage_metadata}")
|
| 50 |
-
|
| 51 |
-
if manager not in ["inventory","rates","reservations"]:
|
| 52 |
-
manager = "unknown"
|
| 53 |
-
print(f"LLM returned: '{response.content.strip().lower()}'")
|
| 54 |
-
|
| 55 |
-
return {"next_agent": manager}
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
def agent_router(state: SupportState):
|
| 59 |
-
return state["next_agent"]
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
graph = StateGraph(SupportState)
|
| 64 |
-
graph.add_node("choose_agent",choose_agent)
|
| 65 |
-
graph.add_node("inventory_manager",inventory_manager)
|
| 66 |
-
graph.add_node("rate_manager",rate_manager)
|
| 67 |
-
graph.add_node("reservation_manager",reservation_manager)
|
| 68 |
-
graph.add_node("unknown_handler",unknown_handler)
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
graph.add_edge(START,"choose_agent")
|
| 72 |
-
graph.add_conditional_edges(
|
| 73 |
-
"choose_agent",
|
| 74 |
-
agent_router,
|
| 75 |
-
{
|
| 76 |
-
"inventory": "inventory_manager",
|
| 77 |
-
"rates": "rate_manager",
|
| 78 |
-
"reservations": "reservation_manager",
|
| 79 |
-
"unknown": "unknown_handler"
|
| 80 |
-
}
|
| 81 |
-
)
|
| 82 |
-
|
| 83 |
-
graph.add_edge("inventory_manager", END)
|
| 84 |
-
graph.add_edge("rate_manager", END)
|
| 85 |
-
graph.add_edge("reservation_manager", END)
|
| 86 |
-
graph.add_edge("unknown_handler",END)
|
| 87 |
-
|
| 88 |
-
graph = graph.compile(checkpointer=memory)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
graph_test.py
CHANGED
|
@@ -310,7 +310,7 @@ async def rate_manager(state: SupportState):
|
|
| 310 |
rooms_payload = [{"roomNumber": 1, "guests": {"adults":adults,"children":0}}]
|
| 311 |
|
| 312 |
success, api_response = await fetch_room_availability(
|
| 313 |
-
phone_number_id=
|
| 314 |
check_in=check_in,
|
| 315 |
check_out=check_out,
|
| 316 |
rooms=rooms_payload
|
|
@@ -336,23 +336,7 @@ async def rate_manager(state: SupportState):
|
|
| 336 |
|
| 337 |
from hotel_api import get_room_map
|
| 338 |
room_map = await get_room_map()
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
category_prompt = f"""
|
| 342 |
-
The user wants to check rates for "{category}" room.
|
| 343 |
-
Available room categories: {list(room_map.keys())}
|
| 344 |
-
Match to the closest category. Return ONLY JSON:
|
| 345 |
-
{{"matched_category": "exact_key_from_list_or_null"}}
|
| 346 |
-
"""
|
| 347 |
-
cat_response = await llm.ainvoke(category_prompt)
|
| 348 |
-
cat_clean = cat_response.content.replace("```json","").replace("```","").strip()
|
| 349 |
-
cat_data = json.loads(cat_clean)
|
| 350 |
-
matched_category = cat_data.get("matched_category")
|
| 351 |
-
|
| 352 |
-
if matched_category and matched_category in room_map:
|
| 353 |
-
category = matched_category
|
| 354 |
-
|
| 355 |
-
room_info = room_map.get(category)
|
| 356 |
print(f"LOOKING FOR: {category.lower()}, FOUND: {room_info}")
|
| 357 |
|
| 358 |
|
|
@@ -481,11 +465,9 @@ async def reservation_manager(state: SupportState):
|
|
| 481 |
|
| 482 |
elif booking_step == "collect_checkin" and not pb.get("check_in"):
|
| 483 |
pb["check_in"] = state.get("query", "").strip()
|
| 484 |
-
check_in = pb["check_in"]
|
| 485 |
|
| 486 |
elif booking_step == "collect_checkout" and not pb.get("check_out"):
|
| 487 |
pb["check_out"] = state.get("query", "").strip()
|
| 488 |
-
check_out = pb["check_out"]
|
| 489 |
|
| 490 |
#collecting meal plan
|
| 491 |
elif booking_step == "meal_plan":
|
|
@@ -661,19 +643,11 @@ async def reservation_manager(state: SupportState):
|
|
| 661 |
safe_pb = state.get("pending_action") or {}
|
| 662 |
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"}
|
| 663 |
|
| 664 |
-
|
| 665 |
-
from functions import sanitize_date
|
| 666 |
-
check_in = sanitize_date(check_in)
|
| 667 |
-
check_out = sanitize_date(check_out)
|
| 668 |
-
|
| 669 |
-
pb["check_in"] = check_in
|
| 670 |
-
pb["check_out"] = check_out
|
| 671 |
-
|
| 672 |
-
|
| 673 |
if not pb.get("availability_checked") and category and check_in and check_out:
|
| 674 |
rooms_payload = [{"roomNumber": 1, "guests": {"adults":adults,"children":0}}]
|
| 675 |
# safe_phone_id = state.get("phone_number_id")
|
| 676 |
# if not safe_phone_id:
|
|
|
|
| 677 |
safe_phone_id = "1220503234474080"
|
| 678 |
|
| 679 |
success,avail_data = await fetch_room_availability(
|
|
@@ -692,63 +666,25 @@ async def reservation_manager(state: SupportState):
|
|
| 692 |
prices_list = avail_data.get("data", {}).get("reservationPrice",[])
|
| 693 |
found_room = None
|
| 694 |
|
| 695 |
-
# from hotel_api import get_room_map
|
| 696 |
-
# room_map = await get_room_map()
|
| 697 |
-
# print(f"ROOM MAP KEYS: {list(room_map.keys())}")
|
| 698 |
-
# room_info = None
|
| 699 |
-
# for key,value in room_map.items():
|
| 700 |
-
# if key in category.lower():
|
| 701 |
-
# room_info = value
|
| 702 |
-
# category = key
|
| 703 |
-
# pb["category"] = key
|
| 704 |
-
# break
|
| 705 |
-
# # room_info = room_map.get(category.lower())
|
| 706 |
-
# # print(f"LOOKING FOR: {category.lower()}, FOUND: {room_info}")
|
| 707 |
-
|
| 708 |
-
# if not room_info:
|
| 709 |
-
# pb["category"] = None #-> wipe bad data
|
| 710 |
-
# return {
|
| 711 |
-
# "response": f"Sorry, I don't recognize {category} category",
|
| 712 |
-
# "pending_manager":"reservations",
|
| 713 |
-
# "booking_step":"collect_category",
|
| 714 |
-
# "pending_action": pb
|
| 715 |
-
# }
|
| 716 |
-
|
| 717 |
from hotel_api import get_room_map
|
| 718 |
room_map = await get_room_map()
|
| 719 |
print(f"ROOM MAP KEYS: {list(room_map.keys())}")
|
| 720 |
-
|
| 721 |
-
category_prompt = f"""
|
| 722 |
-
The hotel owner said they want to book a "{category}" room.
|
| 723 |
-
Available room categories: {list(room_map.keys())}
|
| 724 |
-
|
| 725 |
-
Match the owner's input to the closest category from the list.
|
| 726 |
-
If there is no reasonable match, return null.
|
| 727 |
-
|
| 728 |
-
Return ONLY a JSON object:
|
| 729 |
-
{{"matched_category": "exact_key_from_list_or_null"}}
|
| 730 |
-
"""
|
| 731 |
-
try:
|
| 732 |
-
cat_response = await llm.ainvoke(category_prompt)
|
| 733 |
-
cat_clean = cat_response.content.replace("```json","").replace("```","").strip()
|
| 734 |
-
cat_data = json.loads(cat_clean)
|
| 735 |
-
matched_category = cat_data.get("matched_category")
|
| 736 |
-
except Exception as e:
|
| 737 |
-
print(f"Category match error: {e}")
|
| 738 |
-
matched_category = None
|
| 739 |
-
|
| 740 |
room_info = None
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 745 |
|
| 746 |
-
if not room_info:
|
| 747 |
-
pb["category"] = None
|
| 748 |
return {
|
| 749 |
-
"response": f"I
|
| 750 |
-
"pending_manager":
|
| 751 |
-
"booking_step":
|
| 752 |
"pending_action": pb
|
| 753 |
}
|
| 754 |
|
|
@@ -976,7 +912,7 @@ async def reservation_manager(state: SupportState):
|
|
| 976 |
|
| 977 |
|
| 978 |
if success:
|
| 979 |
-
status = f"Done!
|
| 980 |
return {"response": status, "pending_manager": "", "pending_action": {}, "booking_step": ""}
|
| 981 |
else:
|
| 982 |
error_msg = data.get("data", {}).get("message", data.get("message", "an unknown API error"))
|
|
|
|
| 310 |
rooms_payload = [{"roomNumber": 1, "guests": {"adults":adults,"children":0}}]
|
| 311 |
|
| 312 |
success, api_response = await fetch_room_availability(
|
| 313 |
+
phone_number_id="1220503234474080",
|
| 314 |
check_in=check_in,
|
| 315 |
check_out=check_out,
|
| 316 |
rooms=rooms_payload
|
|
|
|
| 336 |
|
| 337 |
from hotel_api import get_room_map
|
| 338 |
room_map = await get_room_map()
|
| 339 |
+
room_info = room_map.get(category.lower())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 340 |
print(f"LOOKING FOR: {category.lower()}, FOUND: {room_info}")
|
| 341 |
|
| 342 |
|
|
|
|
| 465 |
|
| 466 |
elif booking_step == "collect_checkin" and not pb.get("check_in"):
|
| 467 |
pb["check_in"] = state.get("query", "").strip()
|
|
|
|
| 468 |
|
| 469 |
elif booking_step == "collect_checkout" and not pb.get("check_out"):
|
| 470 |
pb["check_out"] = state.get("query", "").strip()
|
|
|
|
| 471 |
|
| 472 |
#collecting meal plan
|
| 473 |
elif booking_step == "meal_plan":
|
|
|
|
| 643 |
safe_pb = state.get("pending_action") or {}
|
| 644 |
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"}
|
| 645 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 646 |
if not pb.get("availability_checked") and category and check_in and check_out:
|
| 647 |
rooms_payload = [{"roomNumber": 1, "guests": {"adults":adults,"children":0}}]
|
| 648 |
# safe_phone_id = state.get("phone_number_id")
|
| 649 |
# if not safe_phone_id:
|
| 650 |
+
# safe_phone_id = "1098093760057955"
|
| 651 |
safe_phone_id = "1220503234474080"
|
| 652 |
|
| 653 |
success,avail_data = await fetch_room_availability(
|
|
|
|
| 666 |
prices_list = avail_data.get("data", {}).get("reservationPrice",[])
|
| 667 |
found_room = None
|
| 668 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 669 |
from hotel_api import get_room_map
|
| 670 |
room_map = await get_room_map()
|
| 671 |
print(f"ROOM MAP KEYS: {list(room_map.keys())}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 672 |
room_info = None
|
| 673 |
+
for key,value in room_map.items():
|
| 674 |
+
if key in category.lower():
|
| 675 |
+
room_info = value
|
| 676 |
+
category = key
|
| 677 |
+
pb["category"] = key
|
| 678 |
+
break
|
| 679 |
+
# room_info = room_map.get(category.lower())
|
| 680 |
+
# print(f"LOOKING FOR: {category.lower()}, FOUND: {room_info}")
|
| 681 |
|
| 682 |
+
if not room_info:
|
| 683 |
+
pb["category"] = None #-> wipe bad data
|
| 684 |
return {
|
| 685 |
+
"response": f"Sorry, I don't recognize {category} category",
|
| 686 |
+
"pending_manager":"reservations",
|
| 687 |
+
"booking_step":"collect_category",
|
| 688 |
"pending_action": pb
|
| 689 |
}
|
| 690 |
|
|
|
|
| 912 |
|
| 913 |
|
| 914 |
if success:
|
| 915 |
+
status = f"Done! I have initiated the booking for {pb['guest_name']}. Please complete the form to confirm."
|
| 916 |
return {"response": status, "pending_manager": "", "pending_action": {}, "booking_step": ""}
|
| 917 |
else:
|
| 918 |
error_msg = data.get("data", {}).get("message", data.get("message", "an unknown API error"))
|
hotel_api.py
CHANGED
|
@@ -1,12 +1,10 @@
|
|
| 1 |
import httpx
|
| 2 |
from datetime import datetime, timedelta
|
| 3 |
-
import os
|
| 4 |
-
from dotenv import load_dotenv
|
| 5 |
-
load_dotenv()
|
| 6 |
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
|
|
|
| 10 |
|
| 11 |
async def fetch_inventory_availability(start_date: str, end_date: str):
|
| 12 |
url = f"{BASE_URL}/inventory/room/availability"
|
|
@@ -46,13 +44,11 @@ async def get_room_map():
|
|
| 46 |
if success:
|
| 47 |
for cat in data.get("categories",[]):
|
| 48 |
name = cat.get("categoryName", "").lower()
|
| 49 |
-
print(f"LOADED CATEGORY: {name}")
|
| 50 |
_room_map_cache[name] = {
|
| 51 |
"categoryName": cat.get("categoryName"),
|
| 52 |
"roomCode": cat.get("roomCode"),
|
| 53 |
"roomCategoryId": cat.get("roomCategoryId")
|
| 54 |
}
|
| 55 |
-
# print(f"FINAL ROOM MAP: {list(_room_map_cache.keys())}")
|
| 56 |
return _room_map_cache
|
| 57 |
|
| 58 |
|
|
@@ -210,8 +206,7 @@ async def update_rates(
|
|
| 210 |
category: str,
|
| 211 |
new_price: int,
|
| 212 |
start_date: str,
|
| 213 |
-
end_date: str
|
| 214 |
-
room_code=None
|
| 215 |
) -> tuple[bool, dict]:
|
| 216 |
"""
|
| 217 |
Updates the daily price of a room category on both Xipper and OTAs.
|
|
@@ -227,13 +222,12 @@ async def update_rates(
|
|
| 227 |
# 2. Format the string to match the strict payload rules
|
| 228 |
# If LLM gives "twin room":
|
| 229 |
room_type_name = category.title() # Becomes "Twin Room"
|
| 230 |
-
|
| 231 |
-
room_code = category.lower().replace(" ", "-") # Becomes "twin-room"
|
| 232 |
|
| 233 |
# In your payload, there are two rate plans: EP (Room Only) and CP (With Breakfast).
|
| 234 |
# We will assume EP gets the exact new price, and CP costs slightly more.
|
| 235 |
-
ep_code = "
|
| 236 |
-
cp_code = "
|
| 237 |
breakfast_addon = 20 # Just matching the +20 difference from your example payload
|
| 238 |
|
| 239 |
# 3. Build the exact payload you provided!
|
|
@@ -271,7 +265,6 @@ async def update_rates(
|
|
| 271 |
response = await client.post(url, json=payload)
|
| 272 |
response.raise_for_status()
|
| 273 |
data = response.json()
|
| 274 |
-
print(f"DATA: {data}")
|
| 275 |
|
| 276 |
# NEW: Check if the business logic inside the JSON failed!
|
| 277 |
if data.get("status") == "Fail":
|
|
|
|
| 1 |
import httpx
|
| 2 |
from datetime import datetime, timedelta
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
+
|
| 5 |
+
BASE_URL = "https://dev-api.xipper.in/api/v1.0.0"
|
| 6 |
+
HOTEL_ID = "XH33228365"
|
| 7 |
+
PHONE_NUMBER_ID = "1220503234474080"
|
| 8 |
|
| 9 |
async def fetch_inventory_availability(start_date: str, end_date: str):
|
| 10 |
url = f"{BASE_URL}/inventory/room/availability"
|
|
|
|
| 44 |
if success:
|
| 45 |
for cat in data.get("categories",[]):
|
| 46 |
name = cat.get("categoryName", "").lower()
|
|
|
|
| 47 |
_room_map_cache[name] = {
|
| 48 |
"categoryName": cat.get("categoryName"),
|
| 49 |
"roomCode": cat.get("roomCode"),
|
| 50 |
"roomCategoryId": cat.get("roomCategoryId")
|
| 51 |
}
|
|
|
|
| 52 |
return _room_map_cache
|
| 53 |
|
| 54 |
|
|
|
|
| 206 |
category: str,
|
| 207 |
new_price: int,
|
| 208 |
start_date: str,
|
| 209 |
+
end_date: str
|
|
|
|
| 210 |
) -> tuple[bool, dict]:
|
| 211 |
"""
|
| 212 |
Updates the daily price of a room category on both Xipper and OTAs.
|
|
|
|
| 222 |
# 2. Format the string to match the strict payload rules
|
| 223 |
# If LLM gives "twin room":
|
| 224 |
room_type_name = category.title() # Becomes "Twin Room"
|
| 225 |
+
room_code = category.lower().replace(" ", "-") # Becomes "twin-room"
|
|
|
|
| 226 |
|
| 227 |
# In your payload, there are two rate plans: EP (Room Only) and CP (With Breakfast).
|
| 228 |
# We will assume EP gets the exact new price, and CP costs slightly more.
|
| 229 |
+
ep_code = f"{room_code}-s-ep"
|
| 230 |
+
cp_code = f"{room_code}-s-cp"
|
| 231 |
breakfast_addon = 20 # Just matching the +20 difference from your example payload
|
| 232 |
|
| 233 |
# 3. Build the exact payload you provided!
|
|
|
|
| 265 |
response = await client.post(url, json=payload)
|
| 266 |
response.raise_for_status()
|
| 267 |
data = response.json()
|
|
|
|
| 268 |
|
| 269 |
# NEW: Check if the business logic inside the JSON failed!
|
| 270 |
if data.get("status") == "Fail":
|
index.html
ADDED
|
@@ -0,0 +1,654 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/RecordRTC/5.6.2/RecordRTC.min.js"></script>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>Voice recorder · FastAPI /voice</title>
|
| 8 |
+
<style>
|
| 9 |
+
* {
|
| 10 |
+
box-sizing: border-box;
|
| 11 |
+
margin: 0;
|
| 12 |
+
padding: 0;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
body {
|
| 16 |
+
background: linear-gradient(145deg, #1b2a3a 0%, #0f1a26 100%);
|
| 17 |
+
min-height: 100vh;
|
| 18 |
+
display: flex;
|
| 19 |
+
justify-content: center;
|
| 20 |
+
align-items: center;
|
| 21 |
+
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
| 22 |
+
padding: 1.5rem;
|
| 23 |
+
margin: 0;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
.card {
|
| 27 |
+
background: rgba(30, 44, 58, 0.7);
|
| 28 |
+
backdrop-filter: blur(8px);
|
| 29 |
+
-webkit-backdrop-filter: blur(8px);
|
| 30 |
+
border-radius: 4rem 4rem 3rem 3rem;
|
| 31 |
+
box-shadow: 0 25px 50px -8px rgba(0,0,0,0.8), inset 0 2px 4px rgba(255,255,255,0.06);
|
| 32 |
+
padding: 3rem 2.5rem 3.5rem;
|
| 33 |
+
max-width: 600px;
|
| 34 |
+
width: 100%;
|
| 35 |
+
text-align: center;
|
| 36 |
+
border: 1px solid rgba(255,255,255,0.04);
|
| 37 |
+
transition: box-shadow 0.2s;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
h1 {
|
| 41 |
+
font-weight: 300;
|
| 42 |
+
font-size: 1.8rem;
|
| 43 |
+
letter-spacing: 2px;
|
| 44 |
+
color: #d6e6f5;
|
| 45 |
+
text-shadow: 0 2px 3px rgba(0,0,0,0.4);
|
| 46 |
+
margin-bottom: 0.25rem;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.sub {
|
| 50 |
+
color: #8aa9c9;
|
| 51 |
+
font-weight: 300;
|
| 52 |
+
font-size: 0.95rem;
|
| 53 |
+
border-bottom: 1px dashed #3a556b;
|
| 54 |
+
padding-bottom: 1.2rem;
|
| 55 |
+
margin-bottom: 2.2rem;
|
| 56 |
+
letter-spacing: 0.3px;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.sub code {
|
| 60 |
+
background: #1f3345;
|
| 61 |
+
padding: 0.2rem 0.8rem;
|
| 62 |
+
border-radius: 30px;
|
| 63 |
+
color: #aac2dd;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.mic-wrapper {
|
| 67 |
+
position: relative;
|
| 68 |
+
display: inline-block;
|
| 69 |
+
margin: 0.5rem 0 1.8rem;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
.mic-btn {
|
| 73 |
+
background: #2b445a;
|
| 74 |
+
border: none;
|
| 75 |
+
border-radius: 50%;
|
| 76 |
+
width: 150px;
|
| 77 |
+
height: 150px;
|
| 78 |
+
box-shadow: 0 12px 28px rgba(0,0,0,0.7), 0 0 0 2px #4b6d89 inset, 0 0 0 4px #1a2e3f inset;
|
| 79 |
+
cursor: pointer;
|
| 80 |
+
transition: all 0.15s ease;
|
| 81 |
+
display: flex;
|
| 82 |
+
align-items: center;
|
| 83 |
+
justify-content: center;
|
| 84 |
+
color: #d9edff;
|
| 85 |
+
fill: currentColor;
|
| 86 |
+
position: relative;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.mic-btn:hover {
|
| 90 |
+
background: #365a77;
|
| 91 |
+
transform: scale(1.02);
|
| 92 |
+
box-shadow: 0 16px 36px rgba(0,0,0,0.8), 0 0 0 2px #6b93b5 inset, 0 0 0 4px #1a2e3f inset;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.mic-btn:active {
|
| 96 |
+
transform: scale(0.94);
|
| 97 |
+
background: #1e3a52;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.mic-btn svg {
|
| 101 |
+
width: 70px;
|
| 102 |
+
height: 70px;
|
| 103 |
+
filter: drop-shadow(0 4px 6px rgba(0,0,0,0.3));
|
| 104 |
+
transition: transform 0.1s;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
.mic-btn.recording svg {
|
| 108 |
+
animation: pulse 1.2s infinite ease-in-out;
|
| 109 |
+
filter: drop-shadow(0 0 12px #ff7b7b);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.mic-btn.recording {
|
| 113 |
+
background: #6f3a4a;
|
| 114 |
+
box-shadow: 0 0 0 4px #b34a5a inset, 0 0 0 6px #3f1f2b inset, 0 12px 28px #250f16;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
@keyframes pulse {
|
| 118 |
+
0% { transform: scale(1); }
|
| 119 |
+
50% { transform: scale(1.08); }
|
| 120 |
+
100% { transform: scale(1); }
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.status-box {
|
| 124 |
+
background: #0e1a24cc;
|
| 125 |
+
backdrop-filter: blur(4px);
|
| 126 |
+
-webkit-backdrop-filter: blur(4px);
|
| 127 |
+
border-radius: 60px;
|
| 128 |
+
padding: 0.9rem 1.8rem;
|
| 129 |
+
margin: 1.2rem 0 1.8rem;
|
| 130 |
+
display: inline-block;
|
| 131 |
+
border: 1px solid #3e5c74;
|
| 132 |
+
color: #c2d9ef;
|
| 133 |
+
font-weight: 400;
|
| 134 |
+
font-size: 1rem;
|
| 135 |
+
letter-spacing: 0.5px;
|
| 136 |
+
min-width: 180px;
|
| 137 |
+
box-shadow: inset 0 2px 6px rgba(0,0,0,0.4);
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.status-box span {
|
| 141 |
+
font-weight: 300;
|
| 142 |
+
color: #aac2dd;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
.log-area {
|
| 146 |
+
background: #0f1923cc;
|
| 147 |
+
border-radius: 28px;
|
| 148 |
+
padding: 1rem 1.6rem;
|
| 149 |
+
margin: 1.8rem 0 0.8rem;
|
| 150 |
+
font-size: 0.9rem;
|
| 151 |
+
color: #b3ceea;
|
| 152 |
+
border-left: 3px solid #3d779b;
|
| 153 |
+
text-align: left;
|
| 154 |
+
min-height: 72px;
|
| 155 |
+
backdrop-filter: blur(2px);
|
| 156 |
+
word-break: break-word;
|
| 157 |
+
line-height: 1.5;
|
| 158 |
+
box-shadow: inset 0 4px 8px rgba(0,0,0,0.3);
|
| 159 |
+
display: flex;
|
| 160 |
+
align-items: center;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.log-area .log-placeholder {
|
| 164 |
+
opacity: 0.5;
|
| 165 |
+
font-style: italic;
|
| 166 |
+
font-weight: 300;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
.log-area .log-message {
|
| 170 |
+
color: #d6ecff;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
.log-area .log-error {
|
| 174 |
+
color: #ffa79e;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
.log-area .log-success {
|
| 178 |
+
color: #a6e0b0;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
.footnote {
|
| 182 |
+
margin-top: 1.5rem;
|
| 183 |
+
color: #5a7d99;
|
| 184 |
+
font-size: 0.75rem;
|
| 185 |
+
letter-spacing: 0.3px;
|
| 186 |
+
opacity: 0.7;
|
| 187 |
+
border-top: 1px solid #2f4b61;
|
| 188 |
+
padding-top: 1.2rem;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
.btn-reset {
|
| 192 |
+
background: transparent;
|
| 193 |
+
border: 1px solid #3d6079;
|
| 194 |
+
color: #b3ceea;
|
| 195 |
+
border-radius: 40px;
|
| 196 |
+
padding: 0.4rem 1.2rem;
|
| 197 |
+
font-size: 0.8rem;
|
| 198 |
+
margin-top: 0.4rem;
|
| 199 |
+
cursor: pointer;
|
| 200 |
+
transition: 0.2s;
|
| 201 |
+
opacity: 0.6;
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
.btn-reset:hover {
|
| 205 |
+
background: #2f4b61;
|
| 206 |
+
opacity: 1;
|
| 207 |
+
border-color: #6c93b0;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.endpoint-badge {
|
| 211 |
+
display: inline-block;
|
| 212 |
+
background: #1a2e3f;
|
| 213 |
+
padding: 0.3rem 1rem;
|
| 214 |
+
border-radius: 30px;
|
| 215 |
+
font-size: 0.8rem;
|
| 216 |
+
color: #8bb3d4;
|
| 217 |
+
margin-top: 0.5rem;
|
| 218 |
+
border: 1px solid #3a5a75;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
.endpoint-badge code {
|
| 222 |
+
background: #0d1a26;
|
| 223 |
+
padding: 0.1rem 0.5rem;
|
| 224 |
+
border-radius: 12px;
|
| 225 |
+
color: #7fb7e0;
|
| 226 |
+
}
|
| 227 |
+
</style>
|
| 228 |
+
</head>
|
| 229 |
+
<body>
|
| 230 |
+
<div class="card">
|
| 231 |
+
<h1>🎙️ VOICE RECORDER</h1>
|
| 232 |
+
<div class="sub">click mic → speak → send to <code>/voice</code> (FastAPI)</div>
|
| 233 |
+
|
| 234 |
+
<div class="mic-wrapper">
|
| 235 |
+
<button class="mic-btn" id="micButton" aria-label="Record voice">
|
| 236 |
+
<svg viewBox="0 0 24 24" width="70" height="70">
|
| 237 |
+
<path d="M12 16c-2.206 0-4-1.794-4-4V6c0-2.206 1.794-4 4-4s4 1.794 4 4v6c0 2.206-1.794 4-4 4zm8-4h-2c0 2.607-2.126 4.5-4 4.5S10 14.607 10 12H8c0 3.157 2.416 5.5 5 5.944V20h-2v2h6v-2h-2v-2.056c2.584-.444 5-2.787 5-5.944z"/>
|
| 238 |
+
</svg>
|
| 239 |
+
</button>
|
| 240 |
+
</div>
|
| 241 |
+
|
| 242 |
+
<div id="chat-container" style="margin-top: 20px;">
|
| 243 |
+
<input type="text" id="chatInput" placeholder="Type your message here..." style="width: 300px; padding: 10px;">
|
| 244 |
+
<button id="btn-send-chat" style="padding: 10px;">Send</button>
|
| 245 |
+
</div>
|
| 246 |
+
|
| 247 |
+
<div class="status-box" id="statusDisplay">
|
| 248 |
+
<span id="statusText">🔴 idle</span>
|
| 249 |
+
</div>
|
| 250 |
+
|
| 251 |
+
<div class="endpoint-badge">⬇️ POST to <code>http://localhost:8000/voice</code></div>
|
| 252 |
+
|
| 253 |
+
<div class="log-area" id="logArea">
|
| 254 |
+
<span class="log-placeholder" id="logPlaceholder">📋 ready · click mic to start recording</span>
|
| 255 |
+
<span class="log-message" id="logMessage" style="display: none;"></span>
|
| 256 |
+
</div>
|
| 257 |
+
|
| 258 |
+
<button class="btn-reset" id="resetLogBtn">↻ clear log</button>
|
| 259 |
+
<div class="footnote">audio: WebM (Opus) · sent as multipart/form-data</div>
|
| 260 |
+
</div>
|
| 261 |
+
|
| 262 |
+
<script>
|
| 263 |
+
(function() {
|
| 264 |
+
"use strict";
|
| 265 |
+
|
| 266 |
+
// DOM refs
|
| 267 |
+
const micBtn = document.getElementById('micButton');
|
| 268 |
+
const statusText = document.getElementById('statusText');
|
| 269 |
+
const logPlaceholder = document.getElementById('logPlaceholder');
|
| 270 |
+
const logMessage = document.getElementById('logMessage');
|
| 271 |
+
|
| 272 |
+
const chatInput = document.getElementById('chatInput');
|
| 273 |
+
const sendChatBtn = document.getElementById('btn-send-chat');
|
| 274 |
+
|
| 275 |
+
sendChatBtn.onclick = async function() {
|
| 276 |
+
const message = chatInput.value.trim();
|
| 277 |
+
if (!message) return;
|
| 278 |
+
|
| 279 |
+
// Clear the input box immediately
|
| 280 |
+
chatInput.value = '';
|
| 281 |
+
|
| 282 |
+
// Show the user's message in your log/UI
|
| 283 |
+
setLogMessage(`👤 You (Typed): ${message}`, 'info');
|
| 284 |
+
setStatus('⏳ Agent typing...', false);
|
| 285 |
+
|
| 286 |
+
try {
|
| 287 |
+
// We use the EXACT SAME user ID (phoneNumberId) as the voice endpoint
|
| 288 |
+
const response = await fetch('https://dingy-unsaddle-casualty.ngrok-free.dev/chat', {
|
| 289 |
+
method: 'POST',
|
| 290 |
+
headers: { 'Content-Type': 'application/json' },
|
| 291 |
+
body: JSON.stringify({
|
| 292 |
+
message: message,
|
| 293 |
+
// phoneNumberId: '1220503234474080' // Ensure this matches your voice ID!
|
| 294 |
+
})
|
| 295 |
+
});
|
| 296 |
+
|
| 297 |
+
const data = await response.json();
|
| 298 |
+
|
| 299 |
+
// Show the bot's reply in your log/UI
|
| 300 |
+
setLogMessage(`🤖 Agent: ${data.response}`, 'success');
|
| 301 |
+
setStatus('🟢 ready', false);
|
| 302 |
+
|
| 303 |
+
if (data.audio) {
|
| 304 |
+
let base64String = data.audio;
|
| 305 |
+
const base64Prefix = 'data:audio/';
|
| 306 |
+
if (base64String.startsWith(base64Prefix)) {
|
| 307 |
+
const commaIndex = base64String.indexOf(',');
|
| 308 |
+
if (commaIndex !== -1) {
|
| 309 |
+
base64String = base64String.substring(commaIndex + 1);
|
| 310 |
+
}
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
// Decode Base64 to binary
|
| 314 |
+
const binaryString = window.atob(base64String);
|
| 315 |
+
const bytes = new Uint8Array(binaryString.length);
|
| 316 |
+
for (let i = 0; i < binaryString.length; i++) {
|
| 317 |
+
bytes[i] = binaryString.charCodeAt(i);
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
// Determine MIME type (default to mp3 or wav depending on your TTS)
|
| 321 |
+
let mimeType = 'audio/wav';
|
| 322 |
+
if (data.audio.startsWith('data:audio/')) {
|
| 323 |
+
const mimeMatch = data.audio.match(/^data:(audio\/[^;]+);/);
|
| 324 |
+
if (mimeMatch) {
|
| 325 |
+
mimeType = mimeMatch[1];
|
| 326 |
+
}
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
const audioBlobResponse = new Blob([bytes], { type: mimeType });
|
| 330 |
+
const audioUrl = URL.createObjectURL(audioBlobResponse);
|
| 331 |
+
|
| 332 |
+
// Play it!
|
| 333 |
+
const audio = new Audio(audioUrl);
|
| 334 |
+
audio.play().catch(err => {
|
| 335 |
+
console.warn('Autoplay blocked by browser:', err);
|
| 336 |
+
});
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
} catch (error) {
|
| 340 |
+
console.error("Chat error:", error);
|
| 341 |
+
setLogMessage('❌ Failed to send message.', 'error');
|
| 342 |
+
setStatus('🔴 error', false);
|
| 343 |
+
}
|
| 344 |
+
};
|
| 345 |
+
|
| 346 |
+
// Allow pressing "Enter" to send
|
| 347 |
+
chatInput.addEventListener('keypress', function (e) {
|
| 348 |
+
if (e.key === 'Enter') sendChatBtn.click();
|
| 349 |
+
});
|
| 350 |
+
|
| 351 |
+
// state
|
| 352 |
+
let mediaRecorder = null;
|
| 353 |
+
let audioChunks = [];
|
| 354 |
+
let isRecording = false;
|
| 355 |
+
let stream = null;
|
| 356 |
+
|
| 357 |
+
// FastAPI endpoint URL - CHANGE THIS IF YOUR API RUNS ON A DIFFERENT PORT
|
| 358 |
+
const API_BASE_URL = 'https://dingy-unsaddle-casualty.ngrok-free.dev';
|
| 359 |
+
const API_ENDPOINT = `${API_BASE_URL}/voice`;
|
| 360 |
+
|
| 361 |
+
// helpers
|
| 362 |
+
function setStatus(text, isRecordingState = false) {
|
| 363 |
+
statusText.textContent = text;
|
| 364 |
+
if (isRecordingState) {
|
| 365 |
+
micBtn.classList.add('recording');
|
| 366 |
+
} else {
|
| 367 |
+
micBtn.classList.remove('recording');
|
| 368 |
+
}
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
function setLogMessage(msg, type = 'info') {
|
| 372 |
+
logPlaceholder.style.display = 'none';
|
| 373 |
+
logMessage.style.display = 'inline';
|
| 374 |
+
logMessage.className = 'log-message';
|
| 375 |
+
if (type === 'error') logMessage.className = 'log-error';
|
| 376 |
+
else if (type === 'success') logMessage.className = 'log-success';
|
| 377 |
+
logMessage.textContent = msg;
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
function resetLogToPlaceholder() {
|
| 381 |
+
logMessage.style.display = 'none';
|
| 382 |
+
logPlaceholder.style.display = 'inline';
|
| 383 |
+
logPlaceholder.textContent = '📋 ready · click mic to start recording';
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
// clear log button
|
| 387 |
+
document.getElementById('resetLogBtn').addEventListener('click', function() {
|
| 388 |
+
resetLogToPlaceholder();
|
| 389 |
+
});
|
| 390 |
+
|
| 391 |
+
async function sendAudioToEndpoint(audioBlob) {
|
| 392 |
+
try {
|
| 393 |
+
setStatus('⏳ sending to FastAPI...', false);
|
| 394 |
+
|
| 395 |
+
const formData = new FormData();
|
| 396 |
+
formData.append('audio', audioBlob, `voice-${Date.now()}.webm`);
|
| 397 |
+
|
| 398 |
+
const response = await fetch(API_ENDPOINT, {
|
| 399 |
+
method: 'POST',
|
| 400 |
+
body: formData,
|
| 401 |
+
headers: {
|
| 402 |
+
'ngrok-skip-browser-warning': 'true'
|
| 403 |
+
},
|
| 404 |
+
});
|
| 405 |
+
|
| 406 |
+
// First, read the response as JSON (as your backend returns JSON)
|
| 407 |
+
let data;
|
| 408 |
+
const contentType = response.headers.get('content-type') || '';
|
| 409 |
+
if (contentType.includes('application/json')) {
|
| 410 |
+
data = await response.json();
|
| 411 |
+
} else {
|
| 412 |
+
// Fallback: if not JSON, treat as text (error or plain response)
|
| 413 |
+
const text = await response.text();
|
| 414 |
+
throw new Error(`Unexpected response (not JSON): ${text.substring(0, 100)}`);
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
// Handle HTTP errors (if response.ok is false, but we still got JSON)
|
| 418 |
+
if (!response.ok) {
|
| 419 |
+
const errorMsg = data.detail || data.message || JSON.stringify(data);
|
| 420 |
+
throw new Error(`Server error ${response.status}: ${errorMsg}`);
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
// ---- Now process the successful response ----
|
| 424 |
+
const { transcript, detected_language, response: agentReply, audio: audioBase64, booking_link } = data;
|
| 425 |
+
|
| 426 |
+
// 1. Display the transcript and agent reply in the log area
|
| 427 |
+
let logText = `📝 "${transcript}" (${detected_language || 'unknown'})\n`;
|
| 428 |
+
logText += `🤖 ${agentReply || ''}`;
|
| 429 |
+
if (booking_link) {
|
| 430 |
+
logText += `\n🔗 Booking: ${booking_link}`;
|
| 431 |
+
}
|
| 432 |
+
setLogMessage(logText, 'success');
|
| 433 |
+
|
| 434 |
+
// 2. Handle the audio (Base64 -> Blob -> play)
|
| 435 |
+
if (audioBase64) {
|
| 436 |
+
// Remove data URI prefix if present (e.g., "data:audio/wav;base64,")
|
| 437 |
+
let base64String = audioBase64;
|
| 438 |
+
const base64Prefix = 'data:audio/';
|
| 439 |
+
if (base64String.startsWith(base64Prefix)) {
|
| 440 |
+
// Extract the part after the comma
|
| 441 |
+
const commaIndex = base64String.indexOf(',');
|
| 442 |
+
if (commaIndex !== -1) {
|
| 443 |
+
base64String = base64String.substring(commaIndex + 1);
|
| 444 |
+
}
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
// Decode Base64 to binary
|
| 448 |
+
const binaryString = window.atob(base64String);
|
| 449 |
+
const bytes = new Uint8Array(binaryString.length);
|
| 450 |
+
for (let i = 0; i < binaryString.length; i++) {
|
| 451 |
+
bytes[i] = binaryString.charCodeAt(i);
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
// Determine MIME type from the response (or guess from the base64 prefix)
|
| 455 |
+
let mimeType = 'audio/wav'; // default
|
| 456 |
+
if (audioBase64.startsWith('data:audio/')) {
|
| 457 |
+
const mimeMatch = audioBase64.match(/^data:(audio\/[^;]+);/);
|
| 458 |
+
if (mimeMatch) {
|
| 459 |
+
mimeType = mimeMatch[1];
|
| 460 |
+
}
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
const audioBlobResponse = new Blob([bytes], { type: mimeType });
|
| 464 |
+
const audioUrl = URL.createObjectURL(audioBlobResponse);
|
| 465 |
+
|
| 466 |
+
// Play the audio
|
| 467 |
+
const audio = new Audio(audioUrl);
|
| 468 |
+
const playPromise = audio.play();
|
| 469 |
+
if (playPromise !== undefined) {
|
| 470 |
+
playPromise.catch(err => {
|
| 471 |
+
console.warn('Autoplay blocked:', err);
|
| 472 |
+
setLogMessage(
|
| 473 |
+
`🔊 Audio ready but autoplay blocked. Click the mic again or tap the play button (if shown).\n${logText}`,
|
| 474 |
+
'info'
|
| 475 |
+
);
|
| 476 |
+
// You could optionally show a manual "Play" button here.
|
| 477 |
+
});
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
const sizeKB = (audioBlobResponse.size / 1024).toFixed(1);
|
| 481 |
+
setStatus(`✔️ audio playing (${sizeKB} KB)`, false);
|
| 482 |
+
} else {
|
| 483 |
+
// No audio returned – just show text
|
| 484 |
+
setStatus('✔️ done (no audio)', false);
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
} catch (err) {
|
| 488 |
+
console.error('Upload error:', err);
|
| 489 |
+
let errorMsg = err.message;
|
| 490 |
+
if (err.message.includes('Failed to fetch')) {
|
| 491 |
+
errorMsg = 'Cannot connect to FastAPI server. Make sure it\'s running on http://localhost:8000';
|
| 492 |
+
}
|
| 493 |
+
setLogMessage(`❌ Error: ${errorMsg}`, 'error');
|
| 494 |
+
setStatus('⚠️ error', false);
|
| 495 |
+
}
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
// ----- finish recording & send -----
|
| 499 |
+
function finishRecordingAndSend() {
|
| 500 |
+
if (mediaRecorder && mediaRecorder.state === 'recording') {
|
| 501 |
+
mediaRecorder.stop();
|
| 502 |
+
} else {
|
| 503 |
+
resetRecordingState();
|
| 504 |
+
}
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
// reset media recorder & stream
|
| 508 |
+
function resetRecordingState() {
|
| 509 |
+
if (mediaRecorder) {
|
| 510 |
+
if (mediaRecorder.state === 'recording') {
|
| 511 |
+
try { mediaRecorder.stop(); } catch (_) {}
|
| 512 |
+
}
|
| 513 |
+
mediaRecorder = null;
|
| 514 |
+
}
|
| 515 |
+
if (stream) {
|
| 516 |
+
stream.getTracks().forEach(track => track.stop());
|
| 517 |
+
stream = null;
|
| 518 |
+
}
|
| 519 |
+
audioChunks = [];
|
| 520 |
+
isRecording = false;
|
| 521 |
+
micBtn.classList.remove('recording');
|
| 522 |
+
setStatus('🔴 idle', false);
|
| 523 |
+
}
|
| 524 |
+
|
| 525 |
+
// ----- start recording -----
|
| 526 |
+
async function startRecording() {
|
| 527 |
+
// If already recording, stop and send
|
| 528 |
+
if (isRecording) {
|
| 529 |
+
finishRecordingAndSend();
|
| 530 |
+
return;
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
// Reset any previous state
|
| 534 |
+
resetRecordingState();
|
| 535 |
+
|
| 536 |
+
try {
|
| 537 |
+
// Request microphone access
|
| 538 |
+
stream = await navigator.mediaDevices.getUserMedia({
|
| 539 |
+
audio: {
|
| 540 |
+
echoCancellation: true,
|
| 541 |
+
noiseSuppression: true,
|
| 542 |
+
autoGainControl: true,
|
| 543 |
+
sampleRate: 16000,
|
| 544 |
+
}
|
| 545 |
+
});
|
| 546 |
+
|
| 547 |
+
// Use WebM format (widely supported)
|
| 548 |
+
const options = { mimeType: 'audio/webm;codecs=opus' };
|
| 549 |
+
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
|
| 550 |
+
console.warn('Opus not supported, using default audio/webm');
|
| 551 |
+
mediaRecorder = new MediaRecorder(stream);
|
| 552 |
+
} else {
|
| 553 |
+
mediaRecorder = new MediaRecorder(stream, options);
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
audioChunks = [];
|
| 557 |
+
|
| 558 |
+
mediaRecorder.ondataavailable = (event) => {
|
| 559 |
+
if (event.data.size > 0) {
|
| 560 |
+
audioChunks.push(event.data);
|
| 561 |
+
}
|
| 562 |
+
};
|
| 563 |
+
|
| 564 |
+
mediaRecorder.onstop = () => {
|
| 565 |
+
if (audioChunks.length === 0) {
|
| 566 |
+
setLogMessage('⚠️ No audio data captured', 'error');
|
| 567 |
+
setStatus('🔴 idle', false);
|
| 568 |
+
micBtn.classList.remove('recording');
|
| 569 |
+
isRecording = false;
|
| 570 |
+
return;
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
const blob = new Blob(audioChunks, {
|
| 574 |
+
type: mediaRecorder.mimeType || 'audio/webm'
|
| 575 |
+
});
|
| 576 |
+
|
| 577 |
+
audioChunks = [];
|
| 578 |
+
|
| 579 |
+
// Send to FastAPI endpoint
|
| 580 |
+
sendAudioToEndpoint(blob).finally(() => {
|
| 581 |
+
if (stream) {
|
| 582 |
+
stream.getTracks().forEach(track => track.stop());
|
| 583 |
+
stream = null;
|
| 584 |
+
}
|
| 585 |
+
isRecording = false;
|
| 586 |
+
micBtn.classList.remove('recording');
|
| 587 |
+
});
|
| 588 |
+
};
|
| 589 |
+
|
| 590 |
+
// Start recording (collect data in 1-second chunks)
|
| 591 |
+
mediaRecorder.start(1000);
|
| 592 |
+
isRecording = true;
|
| 593 |
+
micBtn.classList.add('recording');
|
| 594 |
+
setStatus('🔴 recording...', true);
|
| 595 |
+
setLogMessage('🎤 Speak now · click mic to stop and send to FastAPI', 'info');
|
| 596 |
+
|
| 597 |
+
} catch (err) {
|
| 598 |
+
console.error('Microphone error:', err);
|
| 599 |
+
let msg = '❌ Cannot access microphone: ';
|
| 600 |
+
if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') {
|
| 601 |
+
msg += 'Permission denied. Please allow microphone access.';
|
| 602 |
+
} else if (err.name === 'NotFoundError' || err.name === 'DevicesNotFoundError') {
|
| 603 |
+
msg += 'No microphone found. Please connect a microphone.';
|
| 604 |
+
} else {
|
| 605 |
+
msg += err.message;
|
| 606 |
+
}
|
| 607 |
+
setLogMessage(msg, 'error');
|
| 608 |
+
setStatus('⛔ mic error', false);
|
| 609 |
+
resetRecordingState();
|
| 610 |
+
}
|
| 611 |
+
}
|
| 612 |
+
|
| 613 |
+
// ----- mic click handler -----
|
| 614 |
+
micBtn.addEventListener('click', startRecording);
|
| 615 |
+
|
| 616 |
+
// Clean up on page unload
|
| 617 |
+
window.addEventListener('beforeunload', function() {
|
| 618 |
+
if (stream) {
|
| 619 |
+
stream.getTracks().forEach(track => track.stop());
|
| 620 |
+
}
|
| 621 |
+
if (mediaRecorder && mediaRecorder.state === 'recording') {
|
| 622 |
+
mediaRecorder.stop();
|
| 623 |
+
}
|
| 624 |
+
});
|
| 625 |
+
|
| 626 |
+
// Initial status
|
| 627 |
+
setStatus('🔴 idle', false);
|
| 628 |
+
resetLogToPlaceholder();
|
| 629 |
+
|
| 630 |
+
// Check if FastAPI endpoint is reachable
|
| 631 |
+
async function checkEndpoint() {
|
| 632 |
+
try {
|
| 633 |
+
console.log(`Checking FastAPI endpoint: ${API_ENDPOINT}`);
|
| 634 |
+
const response = await fetch(API_ENDPOINT, {
|
| 635 |
+
method: 'POST',
|
| 636 |
+
mode: 'cors'
|
| 637 |
+
});
|
| 638 |
+
if (response.ok) {
|
| 639 |
+
console.log('✅ FastAPI /voice endpoint is reachable');
|
| 640 |
+
setLogMessage('✅ Connected to FastAPI server at localhost:8000', 'success');
|
| 641 |
+
}
|
| 642 |
+
} catch (e) {
|
| 643 |
+
console.warn('⚠️ Cannot reach /voice endpoint. Make sure FastAPI server is running on localhost:8000');
|
| 644 |
+
setLogMessage('⚠️ Cannot reach FastAPI server. Make sure it\'s running on localhost:8000', 'error');
|
| 645 |
+
}
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
// Check connection after a short delay
|
| 649 |
+
setTimeout(checkEndpoint, 1000);
|
| 650 |
+
|
| 651 |
+
})();
|
| 652 |
+
</script>
|
| 653 |
+
</body>
|
| 654 |
+
</html>
|
main.py
CHANGED
|
@@ -1,20 +1,20 @@
|
|
| 1 |
from fastapi.middleware.cors import CORSMiddleware
|
| 2 |
-
from fastapi import FastAPI,
|
| 3 |
from pydantic import BaseModel
|
| 4 |
from fastapi.responses import FileResponse
|
| 5 |
-
from translate import translate_to_target_language,
|
| 6 |
-
from
|
| 7 |
from sarvamai import SarvamAI
|
| 8 |
import tempfile
|
| 9 |
-
from pydub import AudioSegment
|
| 10 |
-
import io
|
| 11 |
from groq import Groq
|
| 12 |
-
from
|
|
|
|
| 13 |
import base64
|
|
|
|
|
|
|
| 14 |
import os
|
| 15 |
import time
|
| 16 |
import string
|
| 17 |
-
import asyncio
|
| 18 |
from dotenv import load_dotenv
|
| 19 |
load_dotenv()
|
| 20 |
|
|
@@ -32,7 +32,9 @@ print("CORS middleware added")
|
|
| 32 |
|
| 33 |
class ChatRequest(BaseModel):
|
| 34 |
message: str
|
|
|
|
| 35 |
|
|
|
|
| 36 |
groq_client = Groq(api_key=os.getenv("GROQ_API_KEY"))
|
| 37 |
sarvam_client = SarvamAI(api_subscription_key=os.getenv("SARVAM_API_KEY"))
|
| 38 |
deepgram_client = DeepgramClient(os.getenv("DEEPGRAM_API_KEY"))
|
|
@@ -53,350 +55,214 @@ SARVAM_LANG_MAP = {
|
|
| 53 |
async def chat(request: ChatRequest):
|
| 54 |
try:
|
| 55 |
user_text = request.message
|
|
|
|
| 56 |
config = {"configurable": {"thread_id": "hotel_owner_1"}}
|
| 57 |
-
result = await graph.ainvoke({"query": user_text}, config=config)
|
| 58 |
-
response = result.get("response", "sorry, i couldn't process that request")
|
| 59 |
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
Return ONLY the code, nothing else."""
|
| 63 |
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
detected_language =
|
| 67 |
-
|
| 68 |
|
| 69 |
-
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
for attempt in range(3):
|
| 73 |
try:
|
|
|
|
| 74 |
tts_response = sarvam_client.text_to_speech.convert(
|
| 75 |
-
text=
|
| 76 |
target_language_code=sarvam_lang,
|
| 77 |
speaker="abhilash",
|
| 78 |
model="bulbul:v2"
|
| 79 |
)
|
|
|
|
|
|
|
|
|
|
| 80 |
break
|
| 81 |
except Exception as e:
|
|
|
|
| 82 |
if attempt == 2:
|
| 83 |
raise HTTPException(status_code=503, detail=f"TTS unavailable: {e}")
|
| 84 |
|
| 85 |
audio_bytes = base64.b64decode(tts_response.audios[0])
|
| 86 |
audio_base64 = base64.b64encode(audio_bytes).decode("utf-8")
|
| 87 |
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
except Exception as e:
|
| 90 |
-
import traceback
|
| 91 |
print(f"Chat endpoint error: {e}")
|
| 92 |
-
traceback.print_exc()
|
| 93 |
return {"response": "Sorry, I encountered an error."}
|
| 94 |
|
| 95 |
|
| 96 |
@app.get("/")
|
| 97 |
def root():
|
| 98 |
-
return FileResponse("
|
| 99 |
-
|
| 100 |
|
| 101 |
@app.post("/voice")
|
| 102 |
async def voice(audio: UploadFile = File(...)):
|
| 103 |
-
temp_file =
|
| 104 |
try:
|
| 105 |
audio_bytes = await audio.read()
|
| 106 |
|
| 107 |
-
|
| 108 |
-
|
|
|
|
|
|
|
|
|
|
| 109 |
|
| 110 |
try:
|
| 111 |
raw_audio = AudioSegment.from_file(io.BytesIO(audio_bytes))
|
|
|
|
| 112 |
clean_audio = raw_audio.set_frame_rate(16000).set_channels(1)
|
| 113 |
except Exception as e:
|
| 114 |
-
raise HTTPException(status_code=400,
|
| 115 |
-
|
| 116 |
-
f = tempfile.NamedTemporaryFile(delete=False,
|
| 117 |
temp_file = f.name
|
| 118 |
-
f.
|
| 119 |
clean_audio.export(temp_file, format="wav")
|
| 120 |
-
|
|
|
|
| 121 |
total_start = time.time()
|
| 122 |
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
| 124 |
for attempt in range(3):
|
| 125 |
try:
|
| 126 |
stt_start = time.time()
|
| 127 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
options = PrerecordedOptions(
|
| 129 |
-
model="nova-2",
|
| 130 |
-
language="hi
|
| 131 |
-
smart_format=True,
|
| 132 |
-
punctuate=True
|
| 133 |
)
|
|
|
|
| 134 |
response = deepgram_client.listen.rest.v("1").transcribe_file(payload, options)
|
|
|
|
|
|
|
| 135 |
transcript_data = response["results"]["channels"][0]["alternatives"][0]
|
| 136 |
raw_transcript = transcript_data["transcript"]
|
| 137 |
print(f"Deepgram Heard: {raw_transcript}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
print(f"STT Latency: {time.time() - stt_start:.2f}s")
|
| 139 |
break
|
| 140 |
except Exception as e:
|
| 141 |
if attempt == 2:
|
| 142 |
-
raise HTTPException(status_code=503,
|
|
|
|
| 143 |
|
| 144 |
try:
|
|
|
|
| 145 |
clean_english_query = translate_input_to_english(raw_transcript)
|
|
|
|
| 146 |
except Exception as e:
|
| 147 |
-
raise HTTPException(status_code=503,
|
|
|
|
|
|
|
| 148 |
|
| 149 |
-
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
| 151 |
clean_english_query = english_text.translate(str.maketrans('', '', string.punctuation)).strip()
|
| 152 |
|
| 153 |
if not clean_english_query:
|
| 154 |
-
return {"transcript":
|
| 155 |
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
try:
|
| 158 |
-
|
|
|
|
|
|
|
|
|
|
| 159 |
except Exception as e:
|
|
|
|
|
|
|
|
|
|
| 160 |
import traceback
|
| 161 |
traceback.print_exc()
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
|
| 164 |
graph_response = result.get("response", "Sorry, I couldn't process that request")
|
| 165 |
-
|
| 166 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
|
| 168 |
for attempt in range(3):
|
| 169 |
try:
|
|
|
|
| 170 |
tts_response = sarvam_client.text_to_speech.convert(
|
| 171 |
text=final_response,
|
| 172 |
target_language_code=sarvam_lang,
|
| 173 |
speaker="abhilash",
|
| 174 |
model="bulbul:v2"
|
| 175 |
)
|
|
|
|
| 176 |
break
|
| 177 |
except Exception as e:
|
| 178 |
if attempt == 2:
|
| 179 |
-
raise HTTPException(status_code=503,
|
| 180 |
|
| 181 |
audio_bytes = base64.b64decode(tts_response.audios[0])
|
| 182 |
audio_base64 = base64.b64encode(audio_bytes).decode("utf-8")
|
|
|
|
|
|
|
|
|
|
| 183 |
print(f"Total Latency: {time.time() - total_start:.2f}s")
|
| 184 |
|
|
|
|
|
|
|
| 185 |
return {
|
| 186 |
"transcript": clean_english_query,
|
| 187 |
"detected_language": detected_language,
|
| 188 |
"response": final_response,
|
| 189 |
"audio": audio_base64,
|
| 190 |
-
"booking_link":
|
| 191 |
}
|
| 192 |
-
|
| 193 |
finally:
|
| 194 |
-
if
|
| 195 |
os.remove(temp_file)
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
from fastapi import WebSocket, WebSocketDisconnect
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
async def process_and_respond(websocket: WebSocket, full_transcript: str, detected_language: str):
|
| 202 |
-
"""Takes the final transcript, runs LLM + TTS, sends audio back."""
|
| 203 |
-
english_text = full_transcript.strip()
|
| 204 |
-
if not english_text:
|
| 205 |
-
await websocket.send_json({"response": "Sorry, I couldn't hear anything. Please try again."})
|
| 206 |
-
return
|
| 207 |
-
|
| 208 |
-
await websocket.send_json({"status": "processing", "transcript": english_text})
|
| 209 |
-
|
| 210 |
-
config = {"configurable": {"thread_id": "hotel_owner_1"}}
|
| 211 |
-
try:
|
| 212 |
-
graph_start = time.time()
|
| 213 |
-
result = await graph.ainvoke({"query": english_text}, config=config)
|
| 214 |
-
print(f"Graph Latency: {time.time() - graph_start:.2f}s")
|
| 215 |
-
except Exception as e:
|
| 216 |
-
import traceback
|
| 217 |
-
traceback.print_exc()
|
| 218 |
-
result = {"response": "I'm sorry, I encountered an internal glitch. Could you repeat that?"}
|
| 219 |
-
|
| 220 |
-
graph_response = result.get("response", "Sorry, I couldn't process that request")
|
| 221 |
-
print(f"Graph output: {graph_response}")
|
| 222 |
-
|
| 223 |
-
final_response = await asyncio.to_thread(translate_to_target_language, graph_response, detected_language)
|
| 224 |
-
sarvam_lang = SARVAM_LANG_MAP.get(detected_language.lower(), "hi-IN")
|
| 225 |
-
|
| 226 |
-
for attempt in range(3):
|
| 227 |
-
try:
|
| 228 |
-
tts_start = time.time()
|
| 229 |
-
tts_response = await asyncio.to_thread(
|
| 230 |
-
sarvam_client.text_to_speech.convert,
|
| 231 |
-
text=final_response,
|
| 232 |
-
target_language_code=sarvam_lang,
|
| 233 |
-
speaker="abhilash",
|
| 234 |
-
model="bulbul:v2"
|
| 235 |
-
)
|
| 236 |
-
print(f"TTS Latency: {time.time() - tts_start:.2f}s")
|
| 237 |
-
break
|
| 238 |
-
except Exception as e:
|
| 239 |
-
if attempt == 2:
|
| 240 |
-
await websocket.send_json({"response": "Audio generation failed, please try again."})
|
| 241 |
-
return
|
| 242 |
-
|
| 243 |
-
audio_bytes = base64.b64decode(tts_response.audios[0])
|
| 244 |
-
audio_base64 = base64.b64encode(audio_bytes).decode("utf-8")
|
| 245 |
-
|
| 246 |
-
try:
|
| 247 |
-
await websocket.send_json({
|
| 248 |
-
"transcript": english_text,
|
| 249 |
-
"detected_language": detected_language,
|
| 250 |
-
"response": final_response,
|
| 251 |
-
"audio": audio_base64
|
| 252 |
-
})
|
| 253 |
-
except Exception:
|
| 254 |
-
print("Client disconnected before response could be sent")
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
@app.websocket("/ws/voice")
|
| 258 |
-
async def voice_ws(websocket: WebSocket):
|
| 259 |
-
await websocket.accept()
|
| 260 |
-
|
| 261 |
-
# Shared state between Deepgram callbacks and the main loop
|
| 262 |
-
full_transcript = ""
|
| 263 |
-
detected_language = "hi"
|
| 264 |
-
utterance_end_event = asyncio.Event() # fires when Deepgram detects end of speech
|
| 265 |
-
is_processing = False # prevent overlapping pipeline runs
|
| 266 |
-
|
| 267 |
-
# ── Deepgram callbacks ────────────────────────────────────────────────────
|
| 268 |
-
|
| 269 |
-
async def on_transcript(self, result, **kwargs):
|
| 270 |
-
nonlocal full_transcript, detected_language
|
| 271 |
-
transcript = result.channel.alternatives[0].transcript
|
| 272 |
-
if not transcript:
|
| 273 |
-
return
|
| 274 |
-
# Only accumulate is_final results to avoid duplicates
|
| 275 |
-
if result.is_final:
|
| 276 |
-
full_transcript += " " + transcript
|
| 277 |
-
print(f"Deepgram [final]: {transcript}")
|
| 278 |
-
|
| 279 |
-
# Translate on the fly so detected_language is ready when utterance ends
|
| 280 |
-
try:
|
| 281 |
-
translated = await asyncio.to_thread(translate_input_to_english, full_transcript.strip())
|
| 282 |
-
detected_language = translated.get("language", "hi")
|
| 283 |
-
english = translated.get("english_text", full_transcript.strip())
|
| 284 |
-
# Replace accumulated transcript with clean english version
|
| 285 |
-
full_transcript = english
|
| 286 |
-
except Exception as e:
|
| 287 |
-
print(f"Translation error: {e}")
|
| 288 |
-
|
| 289 |
-
# Send live transcript to frontend so user sees what was heard
|
| 290 |
-
try:
|
| 291 |
-
await websocket.send_json({"live_transcript": full_transcript})
|
| 292 |
-
except Exception:
|
| 293 |
-
pass
|
| 294 |
-
else:
|
| 295 |
-
# Send interim results so frontend can show "hearing..." feedback
|
| 296 |
-
try:
|
| 297 |
-
await websocket.send_json({"interim_transcript": transcript})
|
| 298 |
-
except Exception:
|
| 299 |
-
pass
|
| 300 |
-
|
| 301 |
-
async def on_utterance_end(self, utterance_end, **kwargs):
|
| 302 |
-
"""
|
| 303 |
-
Deepgram fires this after endpointing_ms of silence — means user stopped talking.
|
| 304 |
-
We set the event here; the main loop picks it up and runs the pipeline.
|
| 305 |
-
"""
|
| 306 |
-
nonlocal is_processing
|
| 307 |
-
print(f"Deepgram UtteranceEnd fired — transcript so far: '{full_transcript.strip()}'")
|
| 308 |
-
if full_transcript.strip() and not is_processing:
|
| 309 |
-
utterance_end_event.set()
|
| 310 |
-
|
| 311 |
-
async def on_error(self, error, **kwargs):
|
| 312 |
-
print(f"Deepgram error: {error}")
|
| 313 |
-
try:
|
| 314 |
-
await websocket.send_json({"error": str(error)})
|
| 315 |
-
except Exception:
|
| 316 |
-
pass
|
| 317 |
-
|
| 318 |
-
# ── Connect to Deepgram ───────────────────────────────────────────────────
|
| 319 |
-
|
| 320 |
-
dg_connection = deepgram_client.listen.asyncwebsocket.v("1")
|
| 321 |
-
dg_connection.on(LiveTranscriptionEvents.Transcript, on_transcript)
|
| 322 |
-
dg_connection.on(LiveTranscriptionEvents.UtteranceEnd, on_utterance_end)
|
| 323 |
-
dg_connection.on(LiveTranscriptionEvents.Error, on_error)
|
| 324 |
-
|
| 325 |
-
options = LiveOptions(
|
| 326 |
-
model="nova-2",
|
| 327 |
-
language="hi",
|
| 328 |
-
smart_format=True,
|
| 329 |
-
punctuate=True,
|
| 330 |
-
encoding="linear16",
|
| 331 |
-
sample_rate=16000,
|
| 332 |
-
channels=1,
|
| 333 |
-
# Server-side VAD settings:
|
| 334 |
-
# endpointing_ms — how long Deepgram waits after last speech before
|
| 335 |
-
# firing UtteranceEnd. 1200ms is a good balance for natural speech.
|
| 336 |
-
endpointing=1200,
|
| 337 |
-
# utterance_end_ms — extra buffer after endpointing before the event fires.
|
| 338 |
-
utterance_end_ms="1500",
|
| 339 |
-
# interim_results — send partial transcripts so we can show live feedback.
|
| 340 |
-
interim_results=True,
|
| 341 |
-
# vad_events — enables SpeechStarted event (optional, good for UI feedback).
|
| 342 |
-
vad_events=True,
|
| 343 |
-
)
|
| 344 |
-
|
| 345 |
-
started = await dg_connection.start(options)
|
| 346 |
-
print(f"Deepgram started: {started}")
|
| 347 |
-
if not started:
|
| 348 |
-
print("ERROR: Deepgram WebSocket failed to start")
|
| 349 |
-
await websocket.send_json({"error": "Speech service unavailable. Please try again."})
|
| 350 |
-
await websocket.close()
|
| 351 |
-
return
|
| 352 |
-
|
| 353 |
-
# ── Main loop: receive audio from browser, forward to Deepgram ───────────
|
| 354 |
-
# Browser now just streams raw PCM continuously — no END message needed.
|
| 355 |
-
# Server-side VAD (via utterance_end_event) drives the pipeline.
|
| 356 |
-
|
| 357 |
-
async def receive_audio():
|
| 358 |
-
"""Reads audio chunks from browser and forwards to Deepgram."""
|
| 359 |
-
try:
|
| 360 |
-
while True:
|
| 361 |
-
message = await websocket.receive()
|
| 362 |
-
if "bytes" in message and message["bytes"]:
|
| 363 |
-
await dg_connection.send(message["bytes"])
|
| 364 |
-
elif "text" in message and message["text"] == "STOP":
|
| 365 |
-
# Browser sends STOP only when user clicks mic off
|
| 366 |
-
break
|
| 367 |
-
except WebSocketDisconnect:
|
| 368 |
-
pass
|
| 369 |
-
|
| 370 |
-
try:
|
| 371 |
-
# Run audio receiver in background so utterance_end_event can be
|
| 372 |
-
# awaited at the same time in the foreground
|
| 373 |
-
audio_task = asyncio.create_task(receive_audio())
|
| 374 |
-
|
| 375 |
-
while not audio_task.done():
|
| 376 |
-
# Wait for either Deepgram to detect end of utterance, or audio to stop
|
| 377 |
-
try:
|
| 378 |
-
await asyncio.wait_for(utterance_end_event.wait(), timeout=0.5)
|
| 379 |
-
except asyncio.TimeoutError:
|
| 380 |
-
continue
|
| 381 |
-
|
| 382 |
-
if utterance_end_event.is_set():
|
| 383 |
-
utterance_end_event.clear()
|
| 384 |
-
is_processing = True
|
| 385 |
-
|
| 386 |
-
# Snapshot and reset transcript for next turn
|
| 387 |
-
turn_transcript = full_transcript.strip()
|
| 388 |
-
turn_language = detected_language
|
| 389 |
-
full_transcript = ""
|
| 390 |
-
|
| 391 |
-
if turn_transcript:
|
| 392 |
-
await process_and_respond(websocket, turn_transcript, turn_language)
|
| 393 |
-
|
| 394 |
-
is_processing = False
|
| 395 |
-
|
| 396 |
-
audio_task.cancel()
|
| 397 |
-
|
| 398 |
-
except WebSocketDisconnect:
|
| 399 |
-
print("Client disconnected")
|
| 400 |
-
finally:
|
| 401 |
-
await dg_connection.finish()
|
| 402 |
-
print("Deepgram connection closed")
|
|
|
|
| 1 |
from fastapi.middleware.cors import CORSMiddleware
|
| 2 |
+
from fastapi import FastAPI,UploadFile,File,HTTPException
|
| 3 |
from pydantic import BaseModel
|
| 4 |
from fastapi.responses import FileResponse
|
| 5 |
+
from translate import translate_to_target_language,translate_input_to_english
|
| 6 |
+
from graph_test import graph
|
| 7 |
from sarvamai import SarvamAI
|
| 8 |
import tempfile
|
|
|
|
|
|
|
| 9 |
from groq import Groq
|
| 10 |
+
# from elevenlabs.client import ElevenLabs
|
| 11 |
+
from deepgram import DeepgramClient,FileSource,PrerecordedOptions
|
| 12 |
import base64
|
| 13 |
+
from pydub import AudioSegment
|
| 14 |
+
import io
|
| 15 |
import os
|
| 16 |
import time
|
| 17 |
import string
|
|
|
|
| 18 |
from dotenv import load_dotenv
|
| 19 |
load_dotenv()
|
| 20 |
|
|
|
|
| 32 |
|
| 33 |
class ChatRequest(BaseModel):
|
| 34 |
message: str
|
| 35 |
+
# phoneNumberId: str
|
| 36 |
|
| 37 |
+
# elevenlabs_client = ElevenLabs(api_key=os.getenv("ELEVENLABS_API_KEY"))
|
| 38 |
groq_client = Groq(api_key=os.getenv("GROQ_API_KEY"))
|
| 39 |
sarvam_client = SarvamAI(api_subscription_key=os.getenv("SARVAM_API_KEY"))
|
| 40 |
deepgram_client = DeepgramClient(os.getenv("DEEPGRAM_API_KEY"))
|
|
|
|
| 55 |
async def chat(request: ChatRequest):
|
| 56 |
try:
|
| 57 |
user_text = request.message
|
| 58 |
+
|
| 59 |
config = {"configurable": {"thread_id": "hotel_owner_1"}}
|
|
|
|
|
|
|
| 60 |
|
| 61 |
+
result = await graph.ainvoke({"query": user_text},config=config)
|
| 62 |
+
# print(f"Full result: {result}")
|
|
|
|
| 63 |
|
| 64 |
+
response = result.get("response","sorry, i couldn't process that request")
|
| 65 |
+
# print("reached here 1")
|
| 66 |
+
# detected_language = response.get("language", "hi")
|
| 67 |
+
sarvam_lang = "hi-IN"
|
| 68 |
|
| 69 |
+
|
| 70 |
+
# final_response = translate_to_target_language(response,detected_language)
|
| 71 |
+
# print(f"final response: {final_response}")
|
| 72 |
+
|
| 73 |
+
# sarvam_lang = SARVAM_LANG_MAP.get(detected_language.lower(),"hi-IN")
|
| 74 |
|
| 75 |
for attempt in range(3):
|
| 76 |
try:
|
| 77 |
+
tts_start = time.time()
|
| 78 |
tts_response = sarvam_client.text_to_speech.convert(
|
| 79 |
+
text=response,
|
| 80 |
target_language_code=sarvam_lang,
|
| 81 |
speaker="abhilash",
|
| 82 |
model="bulbul:v2"
|
| 83 |
)
|
| 84 |
+
print(f"TTS Latency: {time.time() - tts_start:.2f}s")
|
| 85 |
+
# print(f"TTS response: {tts_response}")
|
| 86 |
+
# print(f"Audio list: {tts_response.audios}")
|
| 87 |
break
|
| 88 |
except Exception as e:
|
| 89 |
+
# print(f"❌ TTS attempt {attempt} failed: {e}") # ← add this
|
| 90 |
if attempt == 2:
|
| 91 |
raise HTTPException(status_code=503, detail=f"TTS unavailable: {e}")
|
| 92 |
|
| 93 |
audio_bytes = base64.b64decode(tts_response.audios[0])
|
| 94 |
audio_base64 = base64.b64encode(audio_bytes).decode("utf-8")
|
| 95 |
|
| 96 |
+
|
| 97 |
+
return {
|
| 98 |
+
"response": response,
|
| 99 |
+
"audio": audio_base64
|
| 100 |
+
}
|
| 101 |
except Exception as e:
|
|
|
|
| 102 |
print(f"Chat endpoint error: {e}")
|
|
|
|
| 103 |
return {"response": "Sorry, I encountered an error."}
|
| 104 |
|
| 105 |
|
| 106 |
@app.get("/")
|
| 107 |
def root():
|
| 108 |
+
return FileResponse("index.html")
|
|
|
|
| 109 |
|
| 110 |
@app.post("/voice")
|
| 111 |
async def voice(audio: UploadFile = File(...)):
|
| 112 |
+
# temp_file = "temp_audio.wav"
|
| 113 |
try:
|
| 114 |
audio_bytes = await audio.read()
|
| 115 |
|
| 116 |
+
with open("received.webm", "wb") as f:
|
| 117 |
+
f.write(audio_bytes)
|
| 118 |
+
|
| 119 |
+
if len(audio_bytes) > 5*1024*1024:
|
| 120 |
+
return HTTPException(status_code=400,detail="Audio file is too large. Max 5 MB file is allowed")
|
| 121 |
|
| 122 |
try:
|
| 123 |
raw_audio = AudioSegment.from_file(io.BytesIO(audio_bytes))
|
| 124 |
+
|
| 125 |
clean_audio = raw_audio.set_frame_rate(16000).set_channels(1)
|
| 126 |
except Exception as e:
|
| 127 |
+
raise HTTPException(status_code=400,detail=f"failed to process audio format: {e}")
|
| 128 |
+
|
| 129 |
+
f = tempfile.NamedTemporaryFile(delete=False,suffix=".wav")
|
| 130 |
temp_file = f.name
|
| 131 |
+
# f.write(audio_bytes)
|
| 132 |
clean_audio.export(temp_file, format="wav")
|
| 133 |
+
f.close()
|
| 134 |
+
|
| 135 |
total_start = time.time()
|
| 136 |
|
| 137 |
+
#this is one of the most wrost way never use this way to make stt better
|
| 138 |
+
# hotel_vocab = "Kanhaiya, Aman, Maharani, Deluxe, Standard, Suite, Presidential, EP, CP, MAP, AP, room only, breakfast, booking, check-in, check-out, advance"
|
| 139 |
+
|
| 140 |
+
with open(temp_file,"rb") as f:
|
| 141 |
for attempt in range(3):
|
| 142 |
try:
|
| 143 |
stt_start = time.time()
|
| 144 |
+
|
| 145 |
+
payload: FileSource = {
|
| 146 |
+
"buffer": f.read(),
|
| 147 |
+
}
|
| 148 |
+
# The Magic Parameters
|
| 149 |
options = PrerecordedOptions(
|
| 150 |
+
model="nova-2", # The fastest, most accurate model
|
| 151 |
+
language="hi", # Deepgram's Hindi model flawlessly handles Hinglish
|
| 152 |
+
smart_format=True, # Forces dates, times, and phone numbers into digits!
|
| 153 |
+
punctuate=True # Adds periods and commas so LangGraph reads it easily
|
| 154 |
)
|
| 155 |
+
|
| 156 |
response = deepgram_client.listen.rest.v("1").transcribe_file(payload, options)
|
| 157 |
+
|
| 158 |
+
# Extract the text from the JSON response
|
| 159 |
transcript_data = response["results"]["channels"][0]["alternatives"][0]
|
| 160 |
raw_transcript = transcript_data["transcript"]
|
| 161 |
print(f"Deepgram Heard: {raw_transcript}")
|
| 162 |
+
# transcript = elevenlabs_client.speech_to_text.convert(
|
| 163 |
+
# file=f,
|
| 164 |
+
# model_id="scribe_v2"
|
| 165 |
+
# )
|
| 166 |
+
# transcript = groq_client.audio.transcriptions.create(
|
| 167 |
+
# model="whisper-large-v3",
|
| 168 |
+
# file=f,
|
| 169 |
+
# response_format="verbose_json",
|
| 170 |
+
# prompt=hotel_vocab,
|
| 171 |
+
# temperature=0.0
|
| 172 |
+
# )
|
| 173 |
+
|
| 174 |
print(f"STT Latency: {time.time() - stt_start:.2f}s")
|
| 175 |
break
|
| 176 |
except Exception as e:
|
| 177 |
if attempt == 2:
|
| 178 |
+
raise HTTPException(status_code=503,detail="STT service unavailable")
|
| 179 |
+
# raw_transcript = transcript.text.strip()
|
| 180 |
|
| 181 |
try:
|
| 182 |
+
trans_start = time.time()
|
| 183 |
clean_english_query = translate_input_to_english(raw_transcript)
|
| 184 |
+
print(f"Translation Latency: {time.time() - trans_start:.2f}s")
|
| 185 |
except Exception as e:
|
| 186 |
+
raise HTTPException(status_code=503,detail="Translation not possible")
|
| 187 |
+
|
| 188 |
+
detected_language = clean_english_query.get("language", "hi")
|
| 189 |
|
| 190 |
+
print(f"Original: {raw_transcript} | Translated: {clean_english_query} | Language: {detected_language}")
|
| 191 |
+
# if detected_language in ["ur", "urdu"]:
|
| 192 |
+
# detected_language = "hi"
|
| 193 |
+
|
| 194 |
+
english_text = clean_english_query.get("english_text","")
|
| 195 |
clean_english_query = english_text.translate(str.maketrans('', '', string.punctuation)).strip()
|
| 196 |
|
| 197 |
if not clean_english_query:
|
| 198 |
+
return {"transcript":"","response": "Sorry, I couldn't here you. Please try again.","audio_base64": None}
|
| 199 |
|
| 200 |
+
# detected_language = transcript.language
|
| 201 |
+
# hinglish_query = translate_to_hinglish(clean_english_query)
|
| 202 |
+
# print(f"Hinglish Query: {hinglish_query}")
|
| 203 |
+
# print(f"Detected language: {detected_language}")
|
| 204 |
+
# print(f"whisper transcript: {query}")
|
| 205 |
+
|
| 206 |
+
config = {"configurable": {"thread_id":"hotel_owner_1"}}
|
| 207 |
try:
|
| 208 |
+
graph_start = time.time()
|
| 209 |
+
result = await graph.ainvoke({"query": clean_english_query},config=config)
|
| 210 |
+
print(f"Graph Latency: {time.time() - graph_start:.2f}s")
|
| 211 |
+
|
| 212 |
except Exception as e:
|
| 213 |
+
# IF GRAPH CRASHES, CATCH IT AND PRINT THE EXACT LINE!
|
| 214 |
+
print("\n" + "🔥"*25)
|
| 215 |
+
print(f"CRASH IN LANGGRAPH: {e}")
|
| 216 |
import traceback
|
| 217 |
traceback.print_exc()
|
| 218 |
+
print("🔥"*25 + "\n")
|
| 219 |
+
|
| 220 |
+
result = {
|
| 221 |
+
"response": "I'm sorry, I encountered an internal glitch. Could you repeat that?"
|
| 222 |
+
}
|
| 223 |
|
| 224 |
graph_response = result.get("response", "Sorry, I couldn't process that request")
|
| 225 |
+
print(f"graph output: {graph_response}")
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
final_response = translate_to_target_language(graph_response,detected_language)
|
| 229 |
+
print(f"final response: {final_response}")
|
| 230 |
+
|
| 231 |
+
sarvam_lang = SARVAM_LANG_MAP.get(detected_language.lower(),"hi-IN")
|
| 232 |
|
| 233 |
for attempt in range(3):
|
| 234 |
try:
|
| 235 |
+
tts_start = time.time()
|
| 236 |
tts_response = sarvam_client.text_to_speech.convert(
|
| 237 |
text=final_response,
|
| 238 |
target_language_code=sarvam_lang,
|
| 239 |
speaker="abhilash",
|
| 240 |
model="bulbul:v2"
|
| 241 |
)
|
| 242 |
+
print(f"TTS Latency: {time.time() - tts_start:.2f}s")
|
| 243 |
break
|
| 244 |
except Exception as e:
|
| 245 |
if attempt == 2:
|
| 246 |
+
raise HTTPException(status_code=503,detail="TTS service unavailable")
|
| 247 |
|
| 248 |
audio_bytes = base64.b64decode(tts_response.audios[0])
|
| 249 |
audio_base64 = base64.b64encode(audio_bytes).decode("utf-8")
|
| 250 |
+
with open("test_output.wav", "wb") as f:
|
| 251 |
+
f.write(audio_bytes)
|
| 252 |
+
|
| 253 |
print(f"Total Latency: {time.time() - total_start:.2f}s")
|
| 254 |
|
| 255 |
+
booking_link = result.get("booking_link","")
|
| 256 |
+
|
| 257 |
return {
|
| 258 |
"transcript": clean_english_query,
|
| 259 |
"detected_language": detected_language,
|
| 260 |
"response": final_response,
|
| 261 |
"audio": audio_base64,
|
| 262 |
+
"booking_link": booking_link
|
| 263 |
}
|
| 264 |
+
|
| 265 |
finally:
|
| 266 |
+
if os.path.exists(temp_file):
|
| 267 |
os.remove(temp_file)
|
| 268 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
packages.txt
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
ffmpeg
|
|
|
|
|
|
requirements.txt
CHANGED
|
Binary files a/requirements.txt and b/requirements.txt differ
|
|
|
tools/__init__.py
DELETED
|
File without changes
|
tools/category_matcher.py
DELETED
|
@@ -1,23 +0,0 @@
|
|
| 1 |
-
from agents.llm import llm
|
| 2 |
-
import json
|
| 3 |
-
|
| 4 |
-
async def match_category(category: str, room_map: dict) -> str | None:
|
| 5 |
-
prompt = f"""
|
| 6 |
-
The user said they want "{category}" room.
|
| 7 |
-
Available room categories: {list(room_map.keys())}
|
| 8 |
-
Match to the closest category. Return ONLY JSON:
|
| 9 |
-
{{"matched_category": "exact_key_from_list_or_null"}}
|
| 10 |
-
"""
|
| 11 |
-
try:
|
| 12 |
-
response = await llm.ainvoke(prompt)
|
| 13 |
-
clean = response.content.replace("```json","").replace("```","").strip()
|
| 14 |
-
if hasattr(response, 'usage_metadata') and response.usage_metadata:
|
| 15 |
-
print(f"Tokens used: {response.usage_metadata}")
|
| 16 |
-
data = json.loads(clean)
|
| 17 |
-
matched = data.get("matched_category")
|
| 18 |
-
if matched and matched in room_map:
|
| 19 |
-
return matched
|
| 20 |
-
return None
|
| 21 |
-
except Exception as e:
|
| 22 |
-
print(f"Category match error: {e}")
|
| 23 |
-
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tools/date_utiles.py
DELETED
|
@@ -1,39 +0,0 @@
|
|
| 1 |
-
from datetime import datetime, timedelta
|
| 2 |
-
import json
|
| 3 |
-
|
| 4 |
-
async def sanitize_date(date_str, llm):
|
| 5 |
-
if not date_str:
|
| 6 |
-
return None
|
| 7 |
-
|
| 8 |
-
d = str(date_str).strip()
|
| 9 |
-
today_dt = datetime.now()
|
| 10 |
-
today_str = today_dt.strftime("%Y-%m-%d")
|
| 11 |
-
today_day = today_dt.strftime("%A")
|
| 12 |
-
|
| 13 |
-
# quick check if already in correct format
|
| 14 |
-
try:
|
| 15 |
-
datetime.strptime(d, "%Y-%m-%d")
|
| 16 |
-
return d
|
| 17 |
-
except:
|
| 18 |
-
pass
|
| 19 |
-
|
| 20 |
-
prompt = f"""Today is {today_day}, {today_str}.
|
| 21 |
-
Convert this date expression to YYYY-MM-DD format: "{d}"
|
| 22 |
-
|
| 23 |
-
This could be in any language (Hindi, Hinglish, Tamil, English, etc).
|
| 24 |
-
Examples: "aaj"/"today"/"आज" = today, "kal"/"tomorrow"/"कல்" = tomorrow,
|
| 25 |
-
"parso" = day after tomorrow, "agle Friday" = next Friday.
|
| 26 |
-
|
| 27 |
-
Return ONLY a JSON object: {{"date": "YYYY-MM-DD"}}
|
| 28 |
-
If you cannot determine the date, return: {{"date": null}}"""
|
| 29 |
-
|
| 30 |
-
try:
|
| 31 |
-
response = await llm.ainvoke(prompt)
|
| 32 |
-
clean = response.content.replace("```json", "").replace("```", "").strip()
|
| 33 |
-
if hasattr(response, 'usage_metadata') and response.usage_metadata:
|
| 34 |
-
print(f"Tokens used: {response.usage_metadata}")
|
| 35 |
-
data = json.loads(clean)
|
| 36 |
-
return data.get("date")
|
| 37 |
-
except Exception as e:
|
| 38 |
-
print(f"Date sanitize error: {e}")
|
| 39 |
-
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tools/pricing_utils.py
DELETED
|
@@ -1,39 +0,0 @@
|
|
| 1 |
-
from datetime import datetime, timedelta
|
| 2 |
-
|
| 3 |
-
def build_date_wise_pricing(check_in: str, nights: int, base_price: float, total_tax: float) -> list:
|
| 4 |
-
nightly_base = round(base_price / nights, 2) if nights > 0 else base_price
|
| 5 |
-
nightly_tax = round(total_tax / nights, 2) if nights > 0 else total_tax
|
| 6 |
-
|
| 7 |
-
date_wise = []
|
| 8 |
-
try:
|
| 9 |
-
cin_date = datetime.strptime(check_in, "%Y-%m-%d")
|
| 10 |
-
for i in range(nights):
|
| 11 |
-
current_date = cin_date + timedelta(days=i)
|
| 12 |
-
date_wise.append({
|
| 13 |
-
"date": current_date.strftime("%d-%m-%Y"),
|
| 14 |
-
"price": nightly_base,
|
| 15 |
-
"tax": nightly_tax
|
| 16 |
-
})
|
| 17 |
-
except Exception as e:
|
| 18 |
-
print(f"Date generation error: {e}")
|
| 19 |
-
|
| 20 |
-
return date_wise
|
| 21 |
-
|
| 22 |
-
def apply_custom_price(pb: dict) -> dict:
|
| 23 |
-
if pb.get("custom_price"):
|
| 24 |
-
custom_total = int(pb["custom_price"])
|
| 25 |
-
if "pricing" not in pb:
|
| 26 |
-
pb["pricing"] = {}
|
| 27 |
-
pb["pricing"]["totalPrice"] = custom_total
|
| 28 |
-
return pb
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
MEAL_PLAN_MAP = {
|
| 32 |
-
"EP": "Room Only",
|
| 33 |
-
"CP": "Room With Breakfast",
|
| 34 |
-
"MAP": "Room With Breakfast + Lunch/Dinner",
|
| 35 |
-
"AP": "Room With All Meals"
|
| 36 |
-
}
|
| 37 |
-
|
| 38 |
-
def get_meal_name(meal_abbreviation: str) -> str:
|
| 39 |
-
return MEAL_PLAN_MAP.get(meal_abbreviation, "Room Only")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
translate.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
| 1 |
from groq import Groq
|
| 2 |
-
from openai import OpenAI
|
| 3 |
from langsmith import traceable
|
| 4 |
import os
|
| 5 |
import json
|
|
@@ -7,7 +6,6 @@ from dotenv import load_dotenv
|
|
| 7 |
load_dotenv()
|
| 8 |
|
| 9 |
client = Groq(api_key=os.getenv("GROQ_API_KEY"))
|
| 10 |
-
# client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
|
| 11 |
|
| 12 |
@traceable(name="translate_input_to_english")
|
| 13 |
def translate_input_to_english(raw_transcript):
|
|
@@ -30,13 +28,8 @@ def translate_input_to_english(raw_transcript):
|
|
| 30 |
messages=[{"role": "user", "content": prompt}],
|
| 31 |
temperature=0.1
|
| 32 |
)
|
| 33 |
-
# response = client.chat.completions.create(
|
| 34 |
-
# model="gpt-4.1-nano",
|
| 35 |
-
# messages=[{"role": "user", "content": prompt}],
|
| 36 |
-
# temperature=0.1
|
| 37 |
-
# )
|
| 38 |
|
| 39 |
-
print(
|
| 40 |
raw_response = response.choices[0].message.content.strip()
|
| 41 |
|
| 42 |
raw_response = raw_response.replace('json', '').replace('```', '').strip()
|
|
@@ -50,14 +43,6 @@ def translate_input_to_english(raw_transcript):
|
|
| 50 |
@traceable(name="translate_to_target_language")
|
| 51 |
def translate_to_target_language(text, target_lang):
|
| 52 |
|
| 53 |
-
lang_map = {
|
| 54 |
-
"hi": "hindi", "ta": "tamil", "mr": "marathi",
|
| 55 |
-
"bn": "bengali", "te": "telugu", "gu": "gujarati",
|
| 56 |
-
"en": "english", "kn": "kannada"
|
| 57 |
-
}
|
| 58 |
-
target_lang = lang_map.get(target_lang.lower().strip(), target_lang.lower().strip())
|
| 59 |
-
|
| 60 |
-
|
| 61 |
supported_langs = ["hindi", "tamil", "marathi", "bengali", "telugu", "gujarati", "english","kannada"]
|
| 62 |
if target_lang.lower().strip() not in supported_langs:
|
| 63 |
target_lang = "hindi"
|
|
@@ -85,13 +70,8 @@ def translate_to_target_language(text, target_lang):
|
|
| 85 |
messages=[{"role": "user", "content": prompt}],
|
| 86 |
temperature=0.3
|
| 87 |
)
|
| 88 |
-
# response = client.chat.completions.create(
|
| 89 |
-
# model="gpt-4.1-nano",
|
| 90 |
-
# messages=[{"role": "user", "content": prompt}],
|
| 91 |
-
# temperature=0.1
|
| 92 |
-
# )
|
| 93 |
|
| 94 |
-
print(
|
| 95 |
translated_text = response.choices[0].message.content.strip()
|
| 96 |
|
| 97 |
native_to_eng = {
|
|
|
|
| 1 |
from groq import Groq
|
|
|
|
| 2 |
from langsmith import traceable
|
| 3 |
import os
|
| 4 |
import json
|
|
|
|
| 6 |
load_dotenv()
|
| 7 |
|
| 8 |
client = Groq(api_key=os.getenv("GROQ_API_KEY"))
|
|
|
|
| 9 |
|
| 10 |
@traceable(name="translate_input_to_english")
|
| 11 |
def translate_input_to_english(raw_transcript):
|
|
|
|
| 28 |
messages=[{"role": "user", "content": prompt}],
|
| 29 |
temperature=0.1
|
| 30 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
+
print(response.usage)
|
| 33 |
raw_response = response.choices[0].message.content.strip()
|
| 34 |
|
| 35 |
raw_response = raw_response.replace('json', '').replace('```', '').strip()
|
|
|
|
| 43 |
@traceable(name="translate_to_target_language")
|
| 44 |
def translate_to_target_language(text, target_lang):
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
supported_langs = ["hindi", "tamil", "marathi", "bengali", "telugu", "gujarati", "english","kannada"]
|
| 47 |
if target_lang.lower().strip() not in supported_langs:
|
| 48 |
target_lang = "hindi"
|
|
|
|
| 70 |
messages=[{"role": "user", "content": prompt}],
|
| 71 |
temperature=0.3
|
| 72 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
+
print(response.usage)
|
| 75 |
translated_text = response.choices[0].message.content.strip()
|
| 76 |
|
| 77 |
native_to_eng = {
|