Dar066/Resturant_Ordering_Agent
0
1import os2import sqlite33import threading4import asyncio5import time6import uvicorn7import gradio as gr8from datetime import datetime9from fastapi import FastAPI, Request, Form10from fastapi.responses import PlainTextResponse11from twilio.twiml.messaging_response import MessagingResponse12from twilio.rest import Client13from langchain_groq import ChatGroq14from langchain_core.tools import Tool15from langchain_core.messages import ToolMessage16 17# ─────────────────────────────────────────18# 1. CONFIGURATION19# ─────────────────────────────────────────20GROQ_API_KEY = os.environ.get("GROQ_API_KEY")21TWILIO_ACCOUNT_SID = os.environ.get("TWILIO_ACCOUNT_SID")22TWILIO_AUTH_TOKEN = os.environ.get("TWILIO_AUTH_TOKEN")23TWILIO_WA_NUMBER = os.environ.get("TWILIO_WA_NUMBER") # whatsapp:+1415523888624DB_PATH = "restaurant.db"25 26# ─────────────────────────────────────────27# 2. DATABASE28# ─────────────────────────────────────────29def get_db():30 return sqlite3.connect(DB_PATH, check_same_thread=False)31 32def init_db():33 conn = get_db()34 cur = conn.cursor()35 cur.execute("""36 CREATE TABLE IF NOT EXISTS menu (37 id INTEGER PRIMARY KEY AUTOINCREMENT,38 item TEXT NOT NULL UNIQUE,39 price REAL NOT NULL40 )41 """)42 cur.execute("""43 CREATE TABLE IF NOT EXISTS orders (44 id INTEGER PRIMARY KEY AUTOINCREMENT,45 timestamp TEXT NOT NULL,46 customer TEXT NOT NULL,47 phone TEXT,48 item TEXT NOT NULL,49 quantity INTEGER NOT NULL,50 total REAL NOT NULL,51 status TEXT NOT NULL DEFAULT 'Awaited'52 )53 """)54 # Seed menu only if empty55 if not cur.execute("SELECT 1 FROM menu LIMIT 1").fetchone():56 cur.executemany(57 "INSERT OR IGNORE INTO menu (item, price) VALUES (?, ?)",58 [("Pizza", 800), ("Burger", 500), ("Pasta", 650),59 ("Fries", 200), ("Cola", 100), ("Salad", 350)]60 )61 conn.commit()62 conn.close()63 64init_db()65 66# ─────────────────────────────────────────67# 3. TOOL FUNCTIONS68# ─────────────────────────────────────────69def get_menu(_input=""):70 conn = get_db()71 rows = conn.execute("SELECT item, price FROM menu ORDER BY item").fetchall()72 conn.close()73 if not rows:74 return "Menu is empty."75 return "\n".join([item + ": Rs. " + str(int(price)) for item, price in rows])76 77 78def place_order(order_str):79 try:80 parts = [p.strip() for p in str(order_str).split(",")]81 customer = parts[0]82 item = parts[1]83 qty = int(parts[2])84 phone = parts[3] if len(parts) > 3 else ""85 except Exception:86 return "Please provide order as: CustomerName, ItemName, Quantity"87 conn = get_db()88 cur = conn.cursor()89 cur.execute(90 "SELECT item, price FROM menu WHERE LOWER(item)=LOWER(?)", (item,)91 )92 row = cur.fetchone()93 if row is None:94 conn.close()95 return item + " is not on the menu. Type 'menu' to see options."96 canonical = row[0]97 price = row[1]98 total = price * qty99 ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")100 cur.execute(101 "INSERT INTO orders (timestamp,customer,phone,item,quantity,total,status) "102 "VALUES (?,?,?,?,?,?,'Awaited')",103 (ts, customer, phone, canonical, qty, total)104 )105 conn.commit()106 conn.close()107 return ("Order confirmed! " + customer + " - " +108 canonical + " x" + str(qty) + " = Rs. " + str(int(total)) +109 " [Status: Awaited]")110 111 112def view_orders(_input=""):113 conn = get_db()114 rows = conn.execute(115 "SELECT timestamp, customer, item, quantity, total, status "116 "FROM orders ORDER BY id DESC LIMIT 10"117 ).fetchall()118 conn.close()119 if not rows:120 return "No orders yet."121 lines = ["Last 10 orders:"]122 for ts, cust, item, qty, total, status in rows:123 lines.append(124 "[" + ts + "] " + cust + " - " +125 item + " x" + str(qty) +126 " = Rs." + str(int(total)) +127 " [" + status + "]"128 )129 return "\n".join(lines)130 131 132# ─────────────────────────────────────────133# 4. LANGCHAIN AGENT134# ─────────────────────────────────────────135llm = ChatGroq(136 model="llama-3.3-70b-versatile",137 api_key=GROQ_API_KEY,138 temperature=0139)140 141tools = [142 Tool(name="GetMenu", func=get_menu,143 description="Shows restaurant menu and prices."),144 Tool(name="PlaceOrder", func=place_order,145 description="Places an order. Input: CustomerName, ItemName, Quantity, PhoneNumber"),146 Tool(name="ViewOrders", func=view_orders,147 description="Shows last 10 orders with status."),148]149 150tool_map = {t.name: t for t in tools}151llm_with_tools = llm.bind_tools(tools)152 153def run_agent(user_input, phone=""):154 messages = [155 ("system",156 "You are a helpful restaurant assistant. "157 "Use tools to check the menu or place orders. "158 "When placing an order always include the customer phone as 4th argument: "159 "CustomerName, ItemName, Quantity, " + phone),160 ("user", user_input)161 ]162 response = None163 for _ in range(5):164 response = llm_with_tools.invoke(messages)165 messages.append(response)166 if not response.tool_calls:167 break168 for tc in response.tool_calls:169 args = tc["args"]170 arg_input = list(args.values())[0] if args else ""171 obs = tool_map[tc["name"]].func(arg_input)172 messages.append(ToolMessage(content=str(obs), tool_call_id=tc["id"]))173 return response.content if response else "Sorry, I could not process that."174 175 176# ─────────────────────────────────────────177# 5. FASTAPI WEBHOOK178# ─────────────────────────────────────────179import concurrent.futures180executor = concurrent.futures.ThreadPoolExecutor(max_workers=4)181 182wa_app = FastAPI()183 184@wa_app.post("/webhook/whatsapp")185async def whatsapp_webhook(186 request: Request,187 Body: str = Form(default=""),188 From: str = Form(default="")189):190 # Handle Twilio verification ping (empty body)191 if not Body and not From:192 resp = MessagingResponse()193 return PlainTextResponse(str(resp), media_type="text/xml")194 195 sender = From196 message = Body.strip()197 print("[WA IN] " + sender + ": " + message)198 199 try:200 loop = asyncio.get_event_loop()201 phone = sender.replace("whatsapp:", "")202 reply = await loop.run_in_executor(203 executor,204 lambda: run_agent(message, phone)205 )206 if not reply:207 reply = "Sorry, could not process that."208 except Exception as e:209 reply = "Sorry, something went wrong."210 print("[ERROR] " + str(e))211 212 print("[WA OUT] " + sender + ": " + reply)213 resp = MessagingResponse()214 resp.message(reply)215 return PlainTextResponse(str(resp), media_type="text/xml")216 217# ─────────────────────────────────────────218# 6. ORDER STATUS UPDATER219# ─────────────────────────────────────────220def get_all_orders():221 conn = get_db()222 rows = conn.execute(223 "SELECT id, timestamp, customer, phone, item, quantity, total, status "224 "FROM orders ORDER BY id DESC"225 ).fetchall()226 conn.close()227 return rows228 229def update_order_status(order_id, new_status):230 conn = get_db()231 232 # Get customer details BEFORE updating233 row = conn.execute(234 "SELECT customer, phone, item, quantity FROM orders WHERE id=?",235 (order_id,)236 ).fetchone()237 238 if not row:239 conn.close()240 print("[NOTIFY] Order " + str(order_id) + " not found.")241 return242 243 customer, phone, item, qty = row244 245 # Now update status246 conn.execute(247 "UPDATE orders SET status=? WHERE id=?",248 (new_status, order_id)249 )250 conn.commit()251 conn.close()252 253 print("[STATUS UPDATE] Order " + str(order_id) + " -> " + new_status)254 255 # Send WhatsApp notification256 if phone and TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN:257 try:258 client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)259 msg = (260 "Hi " + customer + "! Update on your order:\n"261 + item + " x" + str(qty) + "\n"262 + "New status: *" + new_status + "*"263 )264 result = client.messages.create(265 body=msg,266 from_=TWILIO_WA_NUMBER,267 to="whatsapp:+" + phone.lstrip("+")268 )269 print("[NOTIFY SENT] SID: " + result.sid + " to " + phone)270 except Exception as e:271 print("[NOTIFY ERROR] " + str(e))272 else:273 print("[NOTIFY SKIP] Missing phone or Twilio credentials.")274 275# ─────────────────────────────────────────276# 7. GRADIO DASHBOARD277# ─────────────────────────────────────────278def get_orders_df():279 rows = get_all_orders()280 if not rows:281 return []282 return [list(r) for r in rows]283 284def refresh_orders():285 return get_orders_df()286 287def update_status(order_id, new_status):288 if not order_id:289 return "Please enter an order ID.", get_orders_df()290 try:291 update_order_status(int(order_id), new_status)292 return (293 "Order " + str(order_id) + " updated to: " + new_status +294 " — WhatsApp notification sent.",295 get_orders_df()296 )297 except Exception as e:298 return "Error: " + str(e), get_orders_df()299 300def add_menu_item(item_name, price):301 if not item_name or not price:302 return "Please enter item name and price.", get_menu_df()303 try:304 conn = get_db()305 conn.execute(306 "INSERT OR IGNORE INTO menu (item, price) VALUES (?, ?)",307 (item_name.strip(), float(price))308 )309 conn.commit()310 conn.close()311 return item_name + " added at Rs. " + str(price), get_menu_df()312 except Exception as e:313 return "Error: " + str(e), get_menu_df()314 315def remove_menu_item(item_name):316 if not item_name:317 return "Please enter item name.", get_menu_df()318 try:319 conn = get_db()320 conn.execute("DELETE FROM menu WHERE LOWER(item)=LOWER(?)", (item_name.strip(),))321 conn.commit()322 conn.close()323 return item_name + " removed.", get_menu_df()324 except Exception as e:325 return "Error: " + str(e), get_menu_df()326 327def get_menu_df():328 conn = get_db()329 rows = conn.execute("SELECT id, item, price FROM menu ORDER BY item").fetchall()330 conn.close()331 return [list(r) for r in rows]332 333 334with gr.Blocks(title="RestaurantBot Dashboard") as dashboard:335 336 gr.Markdown("# RestaurantBot Staff Dashboard")337 338 with gr.Tab("Live Orders"):339 gr.Markdown("### All Orders")340 orders_table = gr.Dataframe(341 headers=["ID", "Time", "Customer", "Phone", "Item", "Qty", "Total", "Status"],342 value=get_orders_df(),343 interactive=False,344 wrap=True345 )346 refresh_btn = gr.Button("Refresh Orders")347 refresh_btn.click(fn=refresh_orders, outputs=orders_table)348 349 gr.Markdown("### Update Order Status")350 with gr.Row():351 order_id_input = gr.Textbox(label="Order ID", placeholder="e.g. 3")352 status_dropdown = gr.Dropdown(353 choices=["Awaited", "Preparing", "Ready", "Delivered", "Cancelled"],354 label="New Status",355 value="Ready"356 )357 update_btn = gr.Button("Update Status", variant="primary")358 update_result = gr.Textbox(label="Result", interactive=False)359 update_btn.click(360 fn=update_status,361 inputs=[order_id_input, status_dropdown],362 outputs=[update_result, orders_table]363 )364 365 with gr.Tab("Menu Management"):366 gr.Markdown("### Current Menu")367 menu_table = gr.Dataframe(368 headers=["ID", "Item", "Price (Rs.)"],369 value=get_menu_df(),370 interactive=False371 )372 373 gr.Markdown("### Add Item")374 with gr.Row():375 new_item_name = gr.Textbox(label="Item Name", placeholder="e.g. Shawarma")376 new_item_price = gr.Textbox(label="Price (Rs.)", placeholder="e.g. 450")377 add_btn = gr.Button("Add Item", variant="primary")378 add_result = gr.Textbox(label="Result", interactive=False)379 add_btn.click(380 fn=add_menu_item,381 inputs=[new_item_name, new_item_price],382 outputs=[add_result, menu_table]383 )384 385 gr.Markdown("### Remove Item")386 remove_item_name = gr.Textbox(label="Item Name to Remove", placeholder="e.g. Salad")387 remove_btn = gr.Button("Remove Item", variant="stop")388 remove_result = gr.Textbox(label="Result", interactive=False)389 remove_btn.click(390 fn=remove_menu_item,391 inputs=[remove_item_name],392 outputs=[remove_result, menu_table]393 )394 395 with gr.Tab("Test Agent"):396 gr.Markdown("### Test the bot directly")397 test_input = gr.Textbox(label="Your message", placeholder="What is on the menu?")398 test_output = gr.Textbox(label="Bot reply", interactive=False)399 test_btn = gr.Button("Send", variant="primary")400 test_btn.click(401 fn=lambda msg: run_agent(msg),402 inputs=test_input,403 outputs=test_output404 )405 406# ─────────────────────────────────────────407# 8. MOUNT GRADIO ONTO FASTAPI + RUN408# ─────────────────────────────────────────409from fastapi.middleware.cors import CORSMiddleware410from gradio.routes import mount_gradio_app411 412# Add middleware BEFORE app starts413wa_app.add_middleware(414 CORSMiddleware,415 allow_origins=["*"],416 allow_methods=["*"],417 allow_headers=["*"]418)419 420# Mount Gradio dashboard at root "/"421# WhatsApp webhook stays at "/webhook/whatsapp"422app = mount_gradio_app(wa_app, dashboard, path="/")423 424# Run everything with uvicorn directly — no threading needed425if __name__ == "__main__":426 import uvicorn427 uvicorn.run(app, host="0.0.0.0", port=7860)428 