Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import os
|
| 2 |
import json
|
| 3 |
import time
|
|
@@ -5,119 +6,174 @@ from flask import Flask, request, Response, jsonify, render_template, redirect,
|
|
| 5 |
from google import genai
|
| 6 |
from google.genai import types
|
| 7 |
from pydantic import BaseModel, Field
|
|
|
|
|
|
|
| 8 |
|
| 9 |
app = Flask(__name__, template_folder=".")
|
| 10 |
|
| 11 |
-
#
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
#
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
|
|
|
|
|
|
| 34 |
|
| 35 |
-
# ---
|
|
|
|
|
|
|
|
|
|
| 36 |
|
|
|
|
| 37 |
class OrderItem(BaseModel):
|
| 38 |
name: str = Field(description="نام دقیق و کامل آیتم از منوی فعال به زبان فارسی (مثال: کیک شکلاتی خیس)")
|
| 39 |
quantity: int = Field(description="تعداد درخواستی مشتری از این آیتم")
|
| 40 |
|
| 41 |
-
|
| 42 |
-
# --- توابع مدیریت دادههای پایگاهداده محلی منو و سفارشات ---
|
| 43 |
-
|
| 44 |
def load_menu():
|
|
|
|
|
|
|
| 45 |
try:
|
| 46 |
-
with
|
| 47 |
-
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
return []
|
|
|
|
|
|
|
| 50 |
|
| 51 |
def save_menu(menu_data):
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
def load_orders():
|
|
|
|
|
|
|
| 56 |
try:
|
| 57 |
-
with
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
return []
|
|
|
|
|
|
|
| 61 |
|
| 62 |
def save_orders(orders_data):
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
# --- دستورالعملهای سیستمی نیلا (System Instructions) ---
|
| 68 |
-
|
| 69 |
-
CUSTOMER_SYSTEM_INSTRUCTION = """Your name is Nila, a classic, warm, and highly professional AI cafe assistant for "Cafe AI". You were developed by "Nastaran Data Algorithm".
|
| 70 |
-
Your primary goal is to help customers at their tables select drinks, food, and sweets from the menu, and prepare their orders.
|
| 71 |
-
Rules you must strictly follow:
|
| 72 |
-
1. Speak in Farsi (Persian) in a warm, polite, and welcoming tone. Use emojis suitable for a cozy cafe (☕️, 🍰, 🥐, 🌸, etc.).
|
| 73 |
-
2. If anyone asks who you are or who created you, say: "من نیلا هستم، دستیار هوشمند کافه AI که توسط تیم الگوریتم داده نسترن توسعه پیدا کردم."
|
| 74 |
-
3. NEVER mention Google, Gemini, Google Cloud, AI Studio, or LLMs. If asked about your technology, say you run on proprietary models developed by Nastaran Data Algorithm.
|
| 75 |
-
4. Do not make up items that are not in the menu. Always use the `get_menu` tool to retrieve the current menu of available items and recommend options based on their taste. If get_menu returns that there is no menu, inform the customer politely that the menu is currently unavailable.
|
| 76 |
-
5. When they choose their items, confirm the items and quantities. Then, call the `prepare_order_draft` tool to prepare the draft order. Once prepared, tell them to click the "Confirm Order" (تایید نهایی سفارش) button on their screen to send it to the kitchen.
|
| 77 |
-
6. Do not proceed to place an order unless they have specified what they want.
|
| 78 |
-
7. Always act as an elegant, polite cafe waitress.
|
| 79 |
-
"""
|
| 80 |
-
|
| 81 |
-
ADMIN_SYSTEM_INSTRUCTION = """Your name is Nila, the smart cafe administrator assistant for "Cafe AI". You were developed by "Nastaran Data Algorithm".
|
| 82 |
-
Your role is to help the cafe staff manage their active menu. You can update item availabilities, edit menu details, and process commands.
|
| 83 |
-
Rules you must strictly follow:
|
| 84 |
-
1. Speak in Farsi (Persian) in a professional, clear, and polite tone.
|
| 85 |
-
2. If anyone asks who you are or who created you, say: "من نیلا هستم، دستیار هوشمند مدیریت که توسط تیم الگوریتم داده نسترن توسعه پیدا کردم."
|
| 86 |
-
3. NEVER mention Google, Gemini, or other non-cooperative entities.
|
| 87 |
-
4. You have tools to manage the menu:
|
| 88 |
-
- `get_full_menu()`: returns the entire menu list.
|
| 89 |
-
- `set_item_availability(item_name: str, available: bool)`: changes whether an item is available or sold out.
|
| 90 |
-
5. When the staff says an item is finished, sold out, or unavailable, call `set_item_availability` with `available=False` for that item.
|
| 91 |
-
6. When they say an item is back, available, or restocked, call `set_item_availability` with `available=True` for that item.
|
| 92 |
-
7. Always summarize your action clearly after calling the tool (e.g., "با موفقیت اسپرسو را به وضعیت ناموجود تغییر دادم.").
|
| 93 |
-
"""
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
# --- توابع فراخوانی ابزارها (Function Calling Tools) ---
|
| 97 |
|
|
|
|
| 98 |
def get_menu() -> str:
|
| 99 |
"""
|
| 100 |
Returns the current menu items. Use this to see what drinks, foods, or sweets are available in the cafe.
|
| 101 |
-
If empty or no items are available, it returns that there is no menu yet.
|
| 102 |
"""
|
| 103 |
menu = load_menu()
|
| 104 |
available_items = [item for item in menu if item.get("available", True)]
|
| 105 |
if not available_items:
|
| 106 |
return "در حال حاضر هیچ منویی آپلود نشده است و منوی فعال موجود نیست."
|
| 107 |
-
|
|
|
|
|
|
|
| 108 |
|
| 109 |
def prepare_order_draft(table_number: str, items: list[OrderItem]) -> str:
|
| 110 |
"""
|
| 111 |
Prepares a draft order list for the customer to confirm on their screen.
|
| 112 |
-
Call this when the customer has decided what they want to order.
|
| 113 |
-
|
| 114 |
-
Args:
|
| 115 |
-
table_number: The table number (e.g. '3')
|
| 116 |
-
items: A list of ordered items, each containing a name and a quantity.
|
| 117 |
"""
|
| 118 |
table_number_str = str(table_number)
|
| 119 |
items_list = []
|
| 120 |
-
# پردازش دفاعی برای پشتیبانی همزمان از کلاسهای Pydantic و ساختارهای دیکشنری خام
|
| 121 |
for item in items:
|
| 122 |
if isinstance(item, dict):
|
| 123 |
items_list.append({
|
|
@@ -130,14 +186,11 @@ def prepare_order_draft(table_number: str, items: list[OrderItem]) -> str:
|
|
| 130 |
"quantity": item.quantity
|
| 131 |
})
|
| 132 |
else:
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
except Exception:
|
| 139 |
-
pass
|
| 140 |
-
|
| 141 |
global_drafts[table_number_str] = {
|
| 142 |
"table": table_number_str,
|
| 143 |
"items": items_list,
|
|
@@ -157,32 +210,33 @@ def get_full_menu() -> str:
|
|
| 157 |
def set_item_availability(item_name: str, available: bool) -> str:
|
| 158 |
"""
|
| 159 |
Updates whether a menu item is available or sold out.
|
| 160 |
-
|
| 161 |
-
Args:
|
| 162 |
-
item_name: The exact name of the item in Persian (e.g. 'اسپرسو' or 'کیک شکلاتی')
|
| 163 |
-
available: True if the item is available, False if it is unavailable/sold out.
|
| 164 |
"""
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
return f"
|
| 182 |
-
|
|
|
|
| 183 |
|
| 184 |
-
# ---
|
|
|
|
|
|
|
|
|
|
| 185 |
|
|
|
|
| 186 |
@app.route('/')
|
| 187 |
def home():
|
| 188 |
return render_template("customer.html", table_number=None)
|
|
@@ -195,19 +249,17 @@ def customer_chat_page(table_number):
|
|
| 195 |
def admin_dashboard_page():
|
| 196 |
return render_template("cafe.html")
|
| 197 |
|
| 198 |
-
|
| 199 |
-
# --- مسیرهای سرویسهای API مشتری ---
|
| 200 |
-
|
| 201 |
@app.route('/api/customer/chat_stream/<table_number>', methods=['POST'])
|
| 202 |
def api_customer_chat_stream(table_number):
|
| 203 |
api_key = os.environ.get("NILLA_CUSTOMER") or os.environ.get("GEMINI_API_KEY")
|
| 204 |
if not api_key:
|
| 205 |
-
return jsonify({"error": "
|
| 206 |
|
| 207 |
data = request.json or {}
|
| 208 |
user_message = data.get("message")
|
| 209 |
if not user_message:
|
| 210 |
-
return jsonify({"error": "
|
| 211 |
|
| 212 |
if table_number not in table_chat_histories:
|
| 213 |
table_chat_histories[table_number] = []
|
|
@@ -218,7 +270,6 @@ def api_customer_chat_stream(table_number):
|
|
| 218 |
|
| 219 |
def generate_chunks():
|
| 220 |
try:
|
| 221 |
-
# بازسازی ساختار تاریخچه به جز پیام جاری
|
| 222 |
contents = []
|
| 223 |
for msg in table_chat_histories[table_number][:-1]:
|
| 224 |
role = "user" if msg["role"] == "user" else "model"
|
|
@@ -231,8 +282,8 @@ def api_customer_chat_stream(table_number):
|
|
| 231 |
|
| 232 |
client = genai.Client(api_key=api_key)
|
| 233 |
full_generated_text = ""
|
| 234 |
-
|
| 235 |
-
dynamic_instruction = CUSTOMER_SYSTEM_INSTRUCTION + f"\nYou are currently serving table: {table_number}.
|
| 236 |
|
| 237 |
config = types.GenerateContentConfig(
|
| 238 |
system_instruction=dynamic_instruction,
|
|
@@ -246,38 +297,33 @@ def api_customer_chat_stream(table_number):
|
|
| 246 |
config=config
|
| 247 |
)
|
| 248 |
|
| 249 |
-
# فراخوانی به صورت غیر استریمینگ به دلیل پایداری بسیار بالا در اجرای ابزارها
|
| 250 |
response = chat.send_message(user_message)
|
| 251 |
|
| 252 |
-
#
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
full_generated_text = "".join(text_parts)
|
| 259 |
-
except Exception:
|
| 260 |
-
pass
|
| 261 |
-
|
| 262 |
if not full_generated_text:
|
| 263 |
try:
|
| 264 |
full_generated_text = response.text or ""
|
| 265 |
except (ValueError, AttributeError):
|
| 266 |
full_generated_text = ""
|
| 267 |
|
| 268 |
-
#
|
| 269 |
-
chunk_size = 4
|
| 270 |
for i in range(0, len(full_generated_text), chunk_size):
|
| 271 |
if not active_tasks.get(table_number, True):
|
| 272 |
break
|
| 273 |
chunk = full_generated_text[i:i+chunk_size]
|
| 274 |
yield f"data: {json.dumps({'type': 'text', 'content': chunk}, ensure_ascii=False)}\n\n"
|
| 275 |
-
time.sleep(0.015)
|
| 276 |
|
| 277 |
if full_generated_text:
|
| 278 |
table_chat_histories[table_number].append({"role": "model", "content": full_generated_text})
|
| 279 |
|
| 280 |
-
#
|
| 281 |
draft = global_drafts.get(table_number)
|
| 282 |
if draft:
|
| 283 |
yield f"data: {json.dumps({'type': 'draft', 'items': draft['items']}, ensure_ascii=False)}\n\n"
|
|
@@ -292,82 +338,85 @@ def api_customer_chat_stream(table_number):
|
|
| 292 |
@app.route('/api/customer/stop/<table_number>', methods=['POST'])
|
| 293 |
def api_customer_stop(table_number):
|
| 294 |
active_tasks[table_number] = False
|
| 295 |
-
return jsonify({"success": True
|
| 296 |
|
| 297 |
@app.route('/api/confirm_order', methods=['POST'])
|
| 298 |
def api_confirm_order():
|
| 299 |
data = request.json or {}
|
| 300 |
table_number = data.get("table_number")
|
| 301 |
items = data.get("items")
|
| 302 |
-
|
| 303 |
if not table_number:
|
| 304 |
return jsonify({"error": "شماره میز مشخص نیست."}), 400
|
| 305 |
-
|
| 306 |
if not items:
|
| 307 |
draft = global_drafts.get(table_number)
|
| 308 |
if not draft:
|
| 309 |
return jsonify({"error": "اطلاعات پیشنویس یافت نشد."}), 404
|
| 310 |
items = draft.get("items")
|
| 311 |
-
|
| 312 |
-
orders = load_orders()
|
| 313 |
-
new_order = {
|
| 314 |
-
"id": len(orders) + 1,
|
| 315 |
-
"table": table_number,
|
| 316 |
-
"items": items,
|
| 317 |
-
"status": "pending",
|
| 318 |
-
"timestamp": time.time()
|
| 319 |
-
}
|
| 320 |
-
orders.append(new_order)
|
| 321 |
-
save_orders(orders)
|
| 322 |
-
|
| 323 |
-
# آزاد کردن سشن و سوابق حافظه موقت
|
| 324 |
-
table_chat_histories.pop(table_number, None)
|
| 325 |
-
global_drafts.pop(table_number, None)
|
| 326 |
-
active_tasks.pop(table_number, None)
|
| 327 |
-
|
| 328 |
-
return jsonify({"success": True, "message": "سفارش ثبت شد و سشن میز کاملاً آزاد گردید."})
|
| 329 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
|
| 331 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
|
|
|
|
| 333 |
@app.route('/api/admin/orders', methods=['GET'])
|
| 334 |
def api_admin_orders():
|
| 335 |
orders = load_orders()
|
| 336 |
-
active_orders = [o for o in orders if o.get("status"
|
| 337 |
return jsonify(active_orders)
|
| 338 |
|
| 339 |
@app.route('/api/admin/complete_order', methods=['POST'])
|
| 340 |
def api_admin_complete_order():
|
| 341 |
data = request.json or {}
|
| 342 |
order_id = data.get("order_id")
|
| 343 |
-
|
| 344 |
if not order_id:
|
| 345 |
return jsonify({"error": "شناسه سفارش نامعتبر"}), 400
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
return jsonify({"
|
| 358 |
-
|
|
|
|
| 359 |
|
| 360 |
@app.route('/api/admin/chat', methods=['POST'])
|
| 361 |
def api_admin_chat():
|
| 362 |
api_key = os.environ.get("NILLA_CAFE") or os.environ.get("GEMINI_API_KEY")
|
| 363 |
if not api_key:
|
| 364 |
-
return jsonify({"error": "
|
| 365 |
-
|
| 366 |
data = request.json or {}
|
| 367 |
messages = data.get("messages", [])
|
| 368 |
if not messages:
|
| 369 |
-
return jsonify({"error": "
|
| 370 |
-
|
| 371 |
try:
|
| 372 |
client = genai.Client(api_key=api_key)
|
| 373 |
contents = []
|
|
@@ -379,64 +428,138 @@ def api_admin_chat():
|
|
| 379 |
parts=[types.Part.from_text(text=msg["content"])]
|
| 380 |
)
|
| 381 |
)
|
| 382 |
-
|
| 383 |
config = types.GenerateContentConfig(
|
| 384 |
system_instruction=ADMIN_SYSTEM_INSTRUCTION,
|
| 385 |
tools=[get_full_menu, set_item_availability],
|
| 386 |
thinking_config=types.ThinkingConfig(thinking_budget=0)
|
| 387 |
)
|
| 388 |
-
|
| 389 |
last_user_message = messages[-1]["content"]
|
| 390 |
-
|
| 391 |
chat = client.chats.create(
|
| 392 |
model="gemini-3.1-flash-lite",
|
| 393 |
history=contents,
|
| 394 |
config=config
|
| 395 |
)
|
| 396 |
-
|
| 397 |
response = chat.send_message(last_user_message)
|
| 398 |
-
|
| 399 |
admin_response_text = ""
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
admin_response_text = "".join(text_parts)
|
| 406 |
-
except Exception:
|
| 407 |
-
pass
|
| 408 |
-
|
| 409 |
if not admin_response_text:
|
| 410 |
try:
|
| 411 |
admin_response_text = response.text or ""
|
| 412 |
except (ValueError, AttributeError):
|
| 413 |
admin_response_text = "تغییرات مورد نظر شما با موفقیت روی منو اعمال شد."
|
| 414 |
-
|
| 415 |
-
return jsonify({
|
| 416 |
-
"success": True,
|
| 417 |
-
"response": admin_response_text
|
| 418 |
-
})
|
| 419 |
-
|
| 420 |
except Exception as e:
|
| 421 |
-
return jsonify({"error":
|
| 422 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
@app.route('/api/admin/extract_menu', methods=['POST'])
|
| 424 |
def api_extract_menu():
|
| 425 |
api_key = os.environ.get("NILLA_CAFE") or os.environ.get("GEMINI_API_KEY")
|
| 426 |
if not api_key:
|
| 427 |
-
return jsonify({"error": "
|
| 428 |
-
|
| 429 |
if "file" not in request.files:
|
| 430 |
-
return jsonify({"error": "
|
| 431 |
-
|
| 432 |
file = request.files["file"]
|
| 433 |
if file.filename == "":
|
| 434 |
-
return jsonify({"error": "
|
| 435 |
-
|
| 436 |
try:
|
| 437 |
file_bytes = file.read()
|
| 438 |
mime_type = file.mimetype or "image/jpeg"
|
| 439 |
-
|
| 440 |
prompt = """
|
| 441 |
Analyze this menu image or file. Extract all menu items, descriptions, and prices.
|
| 442 |
Return ONLY a raw JSON array of objects without markdown formatting.
|
|
@@ -446,10 +569,10 @@ def api_extract_menu():
|
|
| 446 |
{"name": "کیک ساده", "description": "شیرینی خانگی لذیذ", "price": "۴۰,۰۰۰"}
|
| 447 |
]
|
| 448 |
"""
|
| 449 |
-
|
| 450 |
client = genai.Client(api_key=api_key)
|
| 451 |
file_part = types.Part.from_bytes(data=file_bytes, mime_type=mime_type)
|
| 452 |
-
|
| 453 |
response = client.models.generate_content(
|
| 454 |
model="gemini-3.1-flash-lite",
|
| 455 |
contents=[file_part, prompt],
|
|
@@ -457,27 +580,27 @@ def api_extract_menu():
|
|
| 457 |
response_mime_type="application/json"
|
| 458 |
)
|
| 459 |
)
|
| 460 |
-
|
| 461 |
extracted_menu = json.loads(response.text)
|
| 462 |
for item in extracted_menu:
|
| 463 |
item["available"] = True
|
| 464 |
-
|
| 465 |
return jsonify({"success": True, "extracted_menu": extracted_menu})
|
| 466 |
-
|
| 467 |
except Exception as e:
|
| 468 |
-
return jsonify({"error": f"خطا در استخراج
|
| 469 |
|
| 470 |
@app.route('/api/admin/save_menu', methods=['POST'])
|
| 471 |
def api_admin_save_menu():
|
|
|
|
| 472 |
data = request.json or {}
|
| 473 |
menu_list = data.get("menu")
|
| 474 |
-
|
| 475 |
if not isinstance(menu_list, list):
|
| 476 |
return jsonify({"error": "فرمت داده ارسال شده نادرست است."}), 400
|
| 477 |
-
|
| 478 |
-
save_menu(menu_list)
|
| 479 |
-
return jsonify({"success": True, "message": "منوی فعال با موفقیت ثبت شد."})
|
| 480 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 481 |
|
| 482 |
if __name__ == "__main__":
|
| 483 |
app.run(host="0.0.0.0", port=7860, debug=True)
|
|
|
|
| 1 |
+
# app.py
|
| 2 |
import os
|
| 3 |
import json
|
| 4 |
import time
|
|
|
|
| 6 |
from google import genai
|
| 7 |
from google.genai import types
|
| 8 |
from pydantic import BaseModel, Field
|
| 9 |
+
import psycopg2
|
| 10 |
+
import psycopg2.pool
|
| 11 |
|
| 12 |
app = Flask(__name__, template_folder=".")
|
| 13 |
|
| 14 |
+
# --- Database Setup (Neon PostgreSQL) ---
|
| 15 |
+
DATABASE_URL = os.environ.get("DATABASE_URL")
|
| 16 |
+
if not DATABASE_URL:
|
| 17 |
+
raise RuntimeError("DATABASE_URL environment variable is required (e.g. from Neon.tech)")
|
| 18 |
+
|
| 19 |
+
# Connection pool for better performance
|
| 20 |
+
try:
|
| 21 |
+
db_pool = psycopg2.pool.SimpleConnectionPool(1, 10, DATABASE_URL)
|
| 22 |
+
print("Database connection pool created")
|
| 23 |
+
except Exception as e:
|
| 24 |
+
print(f"Failed to create database pool: {e}")
|
| 25 |
+
raise
|
| 26 |
+
|
| 27 |
+
def get_db_connection():
|
| 28 |
+
"""Get a connection from the pool."""
|
| 29 |
+
return db_pool.getconn()
|
| 30 |
+
|
| 31 |
+
def release_db_connection(conn):
|
| 32 |
+
"""Return a connection to the pool."""
|
| 33 |
+
db_pool.putconn(conn)
|
| 34 |
+
|
| 35 |
+
def init_db():
|
| 36 |
+
"""Create tables if they don't exist."""
|
| 37 |
+
conn = get_db_connection()
|
| 38 |
+
try:
|
| 39 |
+
with conn.cursor() as cur:
|
| 40 |
+
# Menu table
|
| 41 |
+
cur.execute("""
|
| 42 |
+
CREATE TABLE IF NOT EXISTS menu (
|
| 43 |
+
id SERIAL PRIMARY KEY,
|
| 44 |
+
name TEXT NOT NULL,
|
| 45 |
+
description TEXT DEFAULT '',
|
| 46 |
+
price TEXT DEFAULT '',
|
| 47 |
+
available BOOLEAN DEFAULT TRUE,
|
| 48 |
+
created_at TIMESTAMP DEFAULT NOW()
|
| 49 |
+
);
|
| 50 |
+
""")
|
| 51 |
+
# Orders table
|
| 52 |
+
cur.execute("""
|
| 53 |
+
CREATE TABLE IF NOT EXISTS orders (
|
| 54 |
+
id SERIAL PRIMARY KEY,
|
| 55 |
+
table_number TEXT NOT NULL,
|
| 56 |
+
items JSONB NOT NULL,
|
| 57 |
+
status TEXT DEFAULT 'pending',
|
| 58 |
+
timestamp DOUBLE PRECISION DEFAULT 0,
|
| 59 |
+
created_at TIMESTAMP DEFAULT NOW()
|
| 60 |
+
);
|
| 61 |
+
""")
|
| 62 |
+
conn.commit()
|
| 63 |
+
except Exception as e:
|
| 64 |
+
print(f"Database initialization error: {e}")
|
| 65 |
+
conn.rollback()
|
| 66 |
+
finally:
|
| 67 |
+
release_db_connection(conn)
|
| 68 |
|
| 69 |
+
# Initialize database on startup
|
| 70 |
+
init_db()
|
| 71 |
|
| 72 |
+
# --- In-memory caches (thread-safe session data) ---
|
| 73 |
+
table_chat_histories = {} # conversation history per table
|
| 74 |
+
global_drafts = {} # draft orders per table
|
| 75 |
+
active_tasks = {} # stream cancellation tokens
|
| 76 |
|
| 77 |
+
# --- Pydantic Models ---
|
| 78 |
class OrderItem(BaseModel):
|
| 79 |
name: str = Field(description="نام دقیق و کامل آیتم از منوی فعال به زبان فارسی (مثال: کیک شکلاتی خیس)")
|
| 80 |
quantity: int = Field(description="تعداد درخواستی مشتری از این آیتم")
|
| 81 |
|
| 82 |
+
# --- Database helper functions ---
|
|
|
|
|
|
|
| 83 |
def load_menu():
|
| 84 |
+
"""Load all menu items from database."""
|
| 85 |
+
conn = get_db_connection()
|
| 86 |
try:
|
| 87 |
+
with conn.cursor() as cur:
|
| 88 |
+
cur.execute("SELECT id, name, description, price, available FROM menu ORDER BY id;")
|
| 89 |
+
rows = cur.fetchall()
|
| 90 |
+
menu = []
|
| 91 |
+
for row in rows:
|
| 92 |
+
menu.append({
|
| 93 |
+
"id": row[0],
|
| 94 |
+
"name": row[1],
|
| 95 |
+
"description": row[2],
|
| 96 |
+
"price": row[3],
|
| 97 |
+
"available": row[4]
|
| 98 |
+
})
|
| 99 |
+
return menu
|
| 100 |
+
except Exception as e:
|
| 101 |
+
print(f"load_menu error: {e}")
|
| 102 |
return []
|
| 103 |
+
finally:
|
| 104 |
+
release_db_connection(conn)
|
| 105 |
|
| 106 |
def save_menu(menu_data):
|
| 107 |
+
"""Replace the entire menu list (used when admin publishes a new menu)."""
|
| 108 |
+
conn = get_db_connection()
|
| 109 |
+
try:
|
| 110 |
+
with conn.cursor() as cur:
|
| 111 |
+
# Clear existing menu
|
| 112 |
+
cur.execute("DELETE FROM menu;")
|
| 113 |
+
# Insert new items
|
| 114 |
+
for item in menu_data:
|
| 115 |
+
cur.execute(
|
| 116 |
+
"INSERT INTO menu (name, description, price, available) VALUES (%s, %s, %s, %s)",
|
| 117 |
+
(item.get("name", ""), item.get("description", ""),
|
| 118 |
+
item.get("price", ""), item.get("available", True))
|
| 119 |
+
)
|
| 120 |
+
conn.commit()
|
| 121 |
+
return True
|
| 122 |
+
except Exception as e:
|
| 123 |
+
print(f"save_menu error: {e}")
|
| 124 |
+
conn.rollback()
|
| 125 |
+
return False
|
| 126 |
+
finally:
|
| 127 |
+
release_db_connection(conn)
|
| 128 |
|
| 129 |
def load_orders():
|
| 130 |
+
"""Load all orders from database."""
|
| 131 |
+
conn = get_db_connection()
|
| 132 |
try:
|
| 133 |
+
with conn.cursor() as cur:
|
| 134 |
+
cur.execute("SELECT id, table_number, items, status, timestamp FROM orders ORDER BY id;")
|
| 135 |
+
rows = cur.fetchall()
|
| 136 |
+
orders = []
|
| 137 |
+
for row in rows:
|
| 138 |
+
orders.append({
|
| 139 |
+
"id": row[0],
|
| 140 |
+
"table": row[1],
|
| 141 |
+
"items": row[2], # already JSON
|
| 142 |
+
"status": row[3],
|
| 143 |
+
"timestamp": row[4]
|
| 144 |
+
})
|
| 145 |
+
return orders
|
| 146 |
+
except Exception as e:
|
| 147 |
+
print(f"load_orders error: {e}")
|
| 148 |
return []
|
| 149 |
+
finally:
|
| 150 |
+
release_db_connection(conn)
|
| 151 |
|
| 152 |
def save_orders(orders_data):
|
| 153 |
+
"""Append a new order to database (used by confirm_order)."""
|
| 154 |
+
# This function is now only called to add one order at a time (the new_order dict)
|
| 155 |
+
# But we keep it for backward compatibility. We'll modify confirm_order to use insert directly.
|
| 156 |
+
pass # not used anymore, will insert directly in confirm_order endpoint
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
|
| 158 |
+
# --- Tool functions (will be called by Gemini) ---
|
| 159 |
def get_menu() -> str:
|
| 160 |
"""
|
| 161 |
Returns the current menu items. Use this to see what drinks, foods, or sweets are available in the cafe.
|
|
|
|
| 162 |
"""
|
| 163 |
menu = load_menu()
|
| 164 |
available_items = [item for item in menu if item.get("available", True)]
|
| 165 |
if not available_items:
|
| 166 |
return "در حال حاضر هیچ منویی آپلود نشده است و منوی فعال موجود نیست."
|
| 167 |
+
# Return only needed fields for AI (name, description, price)
|
| 168 |
+
simplified = [{"name": i["name"], "description": i["description"], "price": i["price"]} for i in available_items]
|
| 169 |
+
return json.dumps(simplified, ensure_ascii=False)
|
| 170 |
|
| 171 |
def prepare_order_draft(table_number: str, items: list[OrderItem]) -> str:
|
| 172 |
"""
|
| 173 |
Prepares a draft order list for the customer to confirm on their screen.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
"""
|
| 175 |
table_number_str = str(table_number)
|
| 176 |
items_list = []
|
|
|
|
| 177 |
for item in items:
|
| 178 |
if isinstance(item, dict):
|
| 179 |
items_list.append({
|
|
|
|
| 186 |
"quantity": item.quantity
|
| 187 |
})
|
| 188 |
else:
|
| 189 |
+
# fallback
|
| 190 |
+
items_list.append({
|
| 191 |
+
"name": str(item),
|
| 192 |
+
"quantity": 1
|
| 193 |
+
})
|
|
|
|
|
|
|
|
|
|
| 194 |
global_drafts[table_number_str] = {
|
| 195 |
"table": table_number_str,
|
| 196 |
"items": items_list,
|
|
|
|
| 210 |
def set_item_availability(item_name: str, available: bool) -> str:
|
| 211 |
"""
|
| 212 |
Updates whether a menu item is available or sold out.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
"""
|
| 214 |
+
conn = get_db_connection()
|
| 215 |
+
try:
|
| 216 |
+
with conn.cursor() as cur:
|
| 217 |
+
# Find item by name (case-insensitive)
|
| 218 |
+
cur.execute("SELECT id, name FROM menu WHERE LOWER(name) = %s;", (item_name.strip().lower(),))
|
| 219 |
+
row = cur.fetchone()
|
| 220 |
+
if row:
|
| 221 |
+
cur.execute("UPDATE menu SET available = %s WHERE id = %s;", (available, row[0]))
|
| 222 |
+
conn.commit()
|
| 223 |
+
status = "موجود" if available else "ناموجود"
|
| 224 |
+
return f"وضعیت آیتم '{row[1]}' به موفقیت به '{status}' تغییر یافت."
|
| 225 |
+
else:
|
| 226 |
+
return f"آیتم '{item_name}' در منوی کافه یافت نشد."
|
| 227 |
+
except Exception as e:
|
| 228 |
+
print(f"set_item_availability error: {e}")
|
| 229 |
+
conn.rollback()
|
| 230 |
+
return f"خطا در بروزرسانی آیتم: {str(e)}"
|
| 231 |
+
finally:
|
| 232 |
+
release_db_connection(conn)
|
| 233 |
|
| 234 |
+
# --- System Instructions (unchanged) ---
|
| 235 |
+
CUSTOMER_SYSTEM_INSTRUCTION = """Your name is Nila, a classic, warm, and highly professional AI cafe assistant for "Cafe AI". You were developed by "Nastaran Data Algorithm".
|
| 236 |
+
... (keep existing)"""
|
| 237 |
+
ADMIN_SYSTEM_INSTRUCTION = """Your name is Nila, the smart cafe administrator assistant for "Cafe AI". ... (keep existing)"""
|
| 238 |
|
| 239 |
+
# --- Routes ---
|
| 240 |
@app.route('/')
|
| 241 |
def home():
|
| 242 |
return render_template("customer.html", table_number=None)
|
|
|
|
| 249 |
def admin_dashboard_page():
|
| 250 |
return render_template("cafe.html")
|
| 251 |
|
| 252 |
+
# --- Customer API ---
|
|
|
|
|
|
|
| 253 |
@app.route('/api/customer/chat_stream/<table_number>', methods=['POST'])
|
| 254 |
def api_customer_chat_stream(table_number):
|
| 255 |
api_key = os.environ.get("NILLA_CUSTOMER") or os.environ.get("GEMINI_API_KEY")
|
| 256 |
if not api_key:
|
| 257 |
+
return jsonify({"error": "API key not found"}), 500
|
| 258 |
|
| 259 |
data = request.json or {}
|
| 260 |
user_message = data.get("message")
|
| 261 |
if not user_message:
|
| 262 |
+
return jsonify({"error": "Empty message"}), 400
|
| 263 |
|
| 264 |
if table_number not in table_chat_histories:
|
| 265 |
table_chat_histories[table_number] = []
|
|
|
|
| 270 |
|
| 271 |
def generate_chunks():
|
| 272 |
try:
|
|
|
|
| 273 |
contents = []
|
| 274 |
for msg in table_chat_histories[table_number][:-1]:
|
| 275 |
role = "user" if msg["role"] == "user" else "model"
|
|
|
|
| 282 |
|
| 283 |
client = genai.Client(api_key=api_key)
|
| 284 |
full_generated_text = ""
|
| 285 |
+
|
| 286 |
+
dynamic_instruction = CUSTOMER_SYSTEM_INSTRUCTION + f"\nYou are currently serving table: {table_number}."
|
| 287 |
|
| 288 |
config = types.GenerateContentConfig(
|
| 289 |
system_instruction=dynamic_instruction,
|
|
|
|
| 297 |
config=config
|
| 298 |
)
|
| 299 |
|
|
|
|
| 300 |
response = chat.send_message(user_message)
|
| 301 |
|
| 302 |
+
# Extract text
|
| 303 |
+
if response.candidates and response.candidates[0].content and response.candidates[0].content.parts:
|
| 304 |
+
parts = response.candidates[0].content.parts
|
| 305 |
+
text_parts = [part.text for part in parts if hasattr(part, "text") and part.text]
|
| 306 |
+
if text_parts:
|
| 307 |
+
full_generated_text = "".join(text_parts)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
if not full_generated_text:
|
| 309 |
try:
|
| 310 |
full_generated_text = response.text or ""
|
| 311 |
except (ValueError, AttributeError):
|
| 312 |
full_generated_text = ""
|
| 313 |
|
| 314 |
+
# Simulate streaming
|
| 315 |
+
chunk_size = 4
|
| 316 |
for i in range(0, len(full_generated_text), chunk_size):
|
| 317 |
if not active_tasks.get(table_number, True):
|
| 318 |
break
|
| 319 |
chunk = full_generated_text[i:i+chunk_size]
|
| 320 |
yield f"data: {json.dumps({'type': 'text', 'content': chunk}, ensure_ascii=False)}\n\n"
|
| 321 |
+
time.sleep(0.015)
|
| 322 |
|
| 323 |
if full_generated_text:
|
| 324 |
table_chat_histories[table_number].append({"role": "model", "content": full_generated_text})
|
| 325 |
|
| 326 |
+
# Check draft
|
| 327 |
draft = global_drafts.get(table_number)
|
| 328 |
if draft:
|
| 329 |
yield f"data: {json.dumps({'type': 'draft', 'items': draft['items']}, ensure_ascii=False)}\n\n"
|
|
|
|
| 338 |
@app.route('/api/customer/stop/<table_number>', methods=['POST'])
|
| 339 |
def api_customer_stop(table_number):
|
| 340 |
active_tasks[table_number] = False
|
| 341 |
+
return jsonify({"success": True})
|
| 342 |
|
| 343 |
@app.route('/api/confirm_order', methods=['POST'])
|
| 344 |
def api_confirm_order():
|
| 345 |
data = request.json or {}
|
| 346 |
table_number = data.get("table_number")
|
| 347 |
items = data.get("items")
|
| 348 |
+
|
| 349 |
if not table_number:
|
| 350 |
return jsonify({"error": "شماره میز مشخص نیست."}), 400
|
| 351 |
+
|
| 352 |
if not items:
|
| 353 |
draft = global_drafts.get(table_number)
|
| 354 |
if not draft:
|
| 355 |
return jsonify({"error": "اطلاعات پیشنویس یافت نشد."}), 404
|
| 356 |
items = draft.get("items")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
|
| 358 |
+
# Insert order into database
|
| 359 |
+
conn = get_db_connection()
|
| 360 |
+
try:
|
| 361 |
+
with conn.cursor() as cur:
|
| 362 |
+
cur.execute(
|
| 363 |
+
"INSERT INTO orders (table_number, items, status, timestamp) VALUES (%s, %s, 'pending', %s) RETURNING id;",
|
| 364 |
+
(table_number, json.dumps(items), time.time())
|
| 365 |
+
)
|
| 366 |
+
new_id = cur.fetchone()[0]
|
| 367 |
+
conn.commit()
|
| 368 |
+
|
| 369 |
+
# Clear session
|
| 370 |
+
table_chat_histories.pop(table_number, None)
|
| 371 |
+
global_drafts.pop(table_number, None)
|
| 372 |
+
active_tasks.pop(table_number, None)
|
| 373 |
|
| 374 |
+
return jsonify({"success": True, "message": "سفارش ثبت شد و سشن میز کاملاً آزاد گردید."})
|
| 375 |
+
except Exception as e:
|
| 376 |
+
conn.rollback()
|
| 377 |
+
return jsonify({"error": f"خطا در ثبت سفارش: {str(e)}"}), 500
|
| 378 |
+
finally:
|
| 379 |
+
release_db_connection(conn)
|
| 380 |
|
| 381 |
+
# --- Admin API ---
|
| 382 |
@app.route('/api/admin/orders', methods=['GET'])
|
| 383 |
def api_admin_orders():
|
| 384 |
orders = load_orders()
|
| 385 |
+
active_orders = [o for o in orders if o.get("status") == "pending"]
|
| 386 |
return jsonify(active_orders)
|
| 387 |
|
| 388 |
@app.route('/api/admin/complete_order', methods=['POST'])
|
| 389 |
def api_admin_complete_order():
|
| 390 |
data = request.json or {}
|
| 391 |
order_id = data.get("order_id")
|
|
|
|
| 392 |
if not order_id:
|
| 393 |
return jsonify({"error": "شناسه سفارش نامعتبر"}), 400
|
| 394 |
+
|
| 395 |
+
conn = get_db_connection()
|
| 396 |
+
try:
|
| 397 |
+
with conn.cursor() as cur:
|
| 398 |
+
cur.execute("UPDATE orders SET status = 'completed' WHERE id = %s;", (order_id,))
|
| 399 |
+
conn.commit()
|
| 400 |
+
if cur.rowcount == 0:
|
| 401 |
+
return jsonify({"error": "سفارش پیدا نشد."}), 404
|
| 402 |
+
return jsonify({"success": True, "message": "سفارش با موفقیت برداشته شد."})
|
| 403 |
+
except Exception as e:
|
| 404 |
+
conn.rollback()
|
| 405 |
+
return jsonify({"error": str(e)}), 500
|
| 406 |
+
finally:
|
| 407 |
+
release_db_connection(conn)
|
| 408 |
|
| 409 |
@app.route('/api/admin/chat', methods=['POST'])
|
| 410 |
def api_admin_chat():
|
| 411 |
api_key = os.environ.get("NILLA_CAFE") or os.environ.get("GEMINI_API_KEY")
|
| 412 |
if not api_key:
|
| 413 |
+
return jsonify({"error": "API key not found"}), 500
|
| 414 |
+
|
| 415 |
data = request.json or {}
|
| 416 |
messages = data.get("messages", [])
|
| 417 |
if not messages:
|
| 418 |
+
return jsonify({"error": "Empty history"}), 400
|
| 419 |
+
|
| 420 |
try:
|
| 421 |
client = genai.Client(api_key=api_key)
|
| 422 |
contents = []
|
|
|
|
| 428 |
parts=[types.Part.from_text(text=msg["content"])]
|
| 429 |
)
|
| 430 |
)
|
| 431 |
+
|
| 432 |
config = types.GenerateContentConfig(
|
| 433 |
system_instruction=ADMIN_SYSTEM_INSTRUCTION,
|
| 434 |
tools=[get_full_menu, set_item_availability],
|
| 435 |
thinking_config=types.ThinkingConfig(thinking_budget=0)
|
| 436 |
)
|
| 437 |
+
|
| 438 |
last_user_message = messages[-1]["content"]
|
| 439 |
+
|
| 440 |
chat = client.chats.create(
|
| 441 |
model="gemini-3.1-flash-lite",
|
| 442 |
history=contents,
|
| 443 |
config=config
|
| 444 |
)
|
| 445 |
+
|
| 446 |
response = chat.send_message(last_user_message)
|
| 447 |
+
|
| 448 |
admin_response_text = ""
|
| 449 |
+
if response.candidates and response.candidates[0].content and response.candidates[0].content.parts:
|
| 450 |
+
parts = response.candidates[0].content.parts
|
| 451 |
+
text_parts = [part.text for part in parts if hasattr(part, "text") and part.text]
|
| 452 |
+
if text_parts:
|
| 453 |
+
admin_response_text = "".join(text_parts)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 454 |
if not admin_response_text:
|
| 455 |
try:
|
| 456 |
admin_response_text = response.text or ""
|
| 457 |
except (ValueError, AttributeError):
|
| 458 |
admin_response_text = "تغییرات مورد نظر شما با موفقیت روی منو اعمال شد."
|
| 459 |
+
|
| 460 |
+
return jsonify({"success": True, "response": admin_response_text})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
except Exception as e:
|
| 462 |
+
return jsonify({"error": str(e)}), 500
|
| 463 |
|
| 464 |
+
# --- Admin Menu CRUD ---
|
| 465 |
+
@app.route('/api/admin/menu', methods=['GET'])
|
| 466 |
+
def api_admin_get_menu():
|
| 467 |
+
"""Get full menu for admin panel."""
|
| 468 |
+
menu = load_menu()
|
| 469 |
+
return jsonify(menu)
|
| 470 |
+
|
| 471 |
+
@app.route('/api/admin/menu/item', methods=['POST'])
|
| 472 |
+
def api_admin_add_menu_item():
|
| 473 |
+
"""Add a new menu item manually."""
|
| 474 |
+
data = request.json or {}
|
| 475 |
+
name = data.get("name", "").strip()
|
| 476 |
+
if not name:
|
| 477 |
+
return jsonify({"error": "نام آیتم الزامی است."}), 400
|
| 478 |
+
description = data.get("description", "")
|
| 479 |
+
price = data.get("price", "")
|
| 480 |
+
available = data.get("available", True)
|
| 481 |
+
|
| 482 |
+
conn = get_db_connection()
|
| 483 |
+
try:
|
| 484 |
+
with conn.cursor() as cur:
|
| 485 |
+
cur.execute(
|
| 486 |
+
"INSERT INTO menu (name, description, price, available) VALUES (%s, %s, %s, %s) RETURNING id;",
|
| 487 |
+
(name, description, price, available)
|
| 488 |
+
)
|
| 489 |
+
new_id = cur.fetchone()[0]
|
| 490 |
+
conn.commit()
|
| 491 |
+
return jsonify({"success": True, "id": new_id, "message": "آیتم با موفقیت افزوده شد."})
|
| 492 |
+
except Exception as e:
|
| 493 |
+
conn.rollback()
|
| 494 |
+
return jsonify({"error": str(e)}), 500
|
| 495 |
+
finally:
|
| 496 |
+
release_db_connection(conn)
|
| 497 |
+
|
| 498 |
+
@app.route('/api/admin/menu/item/<int:item_id>', methods=['PUT'])
|
| 499 |
+
def api_admin_update_menu_item(item_id):
|
| 500 |
+
"""Update a menu item's fields."""
|
| 501 |
+
data = request.json or {}
|
| 502 |
+
conn = get_db_connection()
|
| 503 |
+
try:
|
| 504 |
+
with conn.cursor() as cur:
|
| 505 |
+
# Build dynamic SET clause
|
| 506 |
+
allowed_fields = ["name", "description", "price", "available"]
|
| 507 |
+
updates = []
|
| 508 |
+
values = []
|
| 509 |
+
for field in allowed_fields:
|
| 510 |
+
if field in data:
|
| 511 |
+
updates.append(f"{field} = %s")
|
| 512 |
+
values.append(data[field])
|
| 513 |
+
if not updates:
|
| 514 |
+
return jsonify({"error": "هیچ فیلدی برای بروزرسانی ارسال نشده."}), 400
|
| 515 |
+
values.append(item_id)
|
| 516 |
+
query = f"UPDATE menu SET {', '.join(updates)} WHERE id = %s;"
|
| 517 |
+
cur.execute(query, values)
|
| 518 |
+
conn.commit()
|
| 519 |
+
if cur.rowcount == 0:
|
| 520 |
+
return jsonify({"error": "آیتم پیدا نشد."}), 404
|
| 521 |
+
return jsonify({"success": True, "message": "آیتم بروزرسانی شد."})
|
| 522 |
+
except Exception as e:
|
| 523 |
+
conn.rollback()
|
| 524 |
+
return jsonify({"error": str(e)}), 500
|
| 525 |
+
finally:
|
| 526 |
+
release_db_connection(conn)
|
| 527 |
+
|
| 528 |
+
@app.route('/api/admin/menu/item/<int:item_id>', methods=['DELETE'])
|
| 529 |
+
def api_admin_delete_menu_item(item_id):
|
| 530 |
+
"""Delete a menu item."""
|
| 531 |
+
conn = get_db_connection()
|
| 532 |
+
try:
|
| 533 |
+
with conn.cursor() as cur:
|
| 534 |
+
cur.execute("DELETE FROM menu WHERE id = %s;", (item_id,))
|
| 535 |
+
conn.commit()
|
| 536 |
+
if cur.rowcount == 0:
|
| 537 |
+
return jsonify({"error": "آیتم پیدا نشد."}), 404
|
| 538 |
+
return jsonify({"success": True, "message": "آیتم حذف شد."})
|
| 539 |
+
except Exception as e:
|
| 540 |
+
conn.rollback()
|
| 541 |
+
return jsonify({"error": str(e)}), 500
|
| 542 |
+
finally:
|
| 543 |
+
release_db_connection(conn)
|
| 544 |
+
|
| 545 |
+
# --- Legacy extraction & save (still used for bulk upload) ---
|
| 546 |
@app.route('/api/admin/extract_menu', methods=['POST'])
|
| 547 |
def api_extract_menu():
|
| 548 |
api_key = os.environ.get("NILLA_CAFE") or os.environ.get("GEMINI_API_KEY")
|
| 549 |
if not api_key:
|
| 550 |
+
return jsonify({"error": "API key not found"}), 500
|
| 551 |
+
|
| 552 |
if "file" not in request.files:
|
| 553 |
+
return jsonify({"error": "File missing"}), 400
|
| 554 |
+
|
| 555 |
file = request.files["file"]
|
| 556 |
if file.filename == "":
|
| 557 |
+
return jsonify({"error": "Empty filename"}), 400
|
| 558 |
+
|
| 559 |
try:
|
| 560 |
file_bytes = file.read()
|
| 561 |
mime_type = file.mimetype or "image/jpeg"
|
| 562 |
+
|
| 563 |
prompt = """
|
| 564 |
Analyze this menu image or file. Extract all menu items, descriptions, and prices.
|
| 565 |
Return ONLY a raw JSON array of objects without markdown formatting.
|
|
|
|
| 569 |
{"name": "کیک ساده", "description": "شیرینی خانگی لذیذ", "price": "۴۰,۰۰۰"}
|
| 570 |
]
|
| 571 |
"""
|
| 572 |
+
|
| 573 |
client = genai.Client(api_key=api_key)
|
| 574 |
file_part = types.Part.from_bytes(data=file_bytes, mime_type=mime_type)
|
| 575 |
+
|
| 576 |
response = client.models.generate_content(
|
| 577 |
model="gemini-3.1-flash-lite",
|
| 578 |
contents=[file_part, prompt],
|
|
|
|
| 580 |
response_mime_type="application/json"
|
| 581 |
)
|
| 582 |
)
|
| 583 |
+
|
| 584 |
extracted_menu = json.loads(response.text)
|
| 585 |
for item in extracted_menu:
|
| 586 |
item["available"] = True
|
| 587 |
+
|
| 588 |
return jsonify({"success": True, "extracted_menu": extracted_menu})
|
|
|
|
| 589 |
except Exception as e:
|
| 590 |
+
return jsonify({"error": f"خطا در استخراج: {str(e)}"}), 500
|
| 591 |
|
| 592 |
@app.route('/api/admin/save_menu', methods=['POST'])
|
| 593 |
def api_admin_save_menu():
|
| 594 |
+
"""Replace entire menu (used after extraction/manual editing)."""
|
| 595 |
data = request.json or {}
|
| 596 |
menu_list = data.get("menu")
|
|
|
|
| 597 |
if not isinstance(menu_list, list):
|
| 598 |
return jsonify({"error": "فرمت داده ارسال شده نادرست است."}), 400
|
|
|
|
|
|
|
|
|
|
| 599 |
|
| 600 |
+
if save_menu(menu_list):
|
| 601 |
+
return jsonify({"success": True, "message": "منوی فعال با موفقیت ثبت شد."})
|
| 602 |
+
else:
|
| 603 |
+
return jsonify({"error": "خطا در ذخیرهسازی منو."}), 500
|
| 604 |
|
| 605 |
if __name__ == "__main__":
|
| 606 |
app.run(host="0.0.0.0", port=7860, debug=True)
|