humanvprojectceo commited on
Commit
694cbf8
·
verified ·
1 Parent(s): 7d9cf50

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +332 -209
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
- DATA_DIR = "data"
13
- MENU_FILE = os.path.join(DATA_DIR, "menu.json")
14
- ORDERS_FILE = os.path.join(DATA_DIR, "orders.json")
15
-
16
- # ساخت دایرکتوری در صورت عدم وجود
17
- os.makedirs(DATA_DIR, exist_ok=True)
18
-
19
- # مقداردهی اولیه فایل‌ها با فرمت مناسب در صورت خالی بودن
20
- if not os.path.exists(MENU_FILE):
21
- with open(MENU_FILE, "w", encoding="utf-8") as f:
22
- json.dump([], f, ensure_ascii=False, indent=4)
23
-
24
- if not os.path.exists(ORDERS_FILE):
25
- with open(ORDERS_FILE, "w", encoding="utf-8") as f:
26
- json.dump([], f, ensure_ascii=False, indent=4)
27
-
28
-
29
- # --- مخازن ذخیره موقت وضعیت در حافظه (Thread-Safe & Multi-Client Session) ---
30
- table_chat_histories = {} # تاریخچه گفتگو بر اساس شماره میز
31
- global_drafts = {} # پیش‌نویس‌های موقت سفارش بر اساس شماره میز
32
- active_tasks = {} # کنترل پردازش‌های فعال جهت متوقف کردن استریمینگ
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
 
 
34
 
35
- # --- تعریف کلاس‌های پایداری اسکیما با Pydantic ---
 
 
 
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 open(MENU_FILE, "r", encoding="utf-8") as f:
47
- return json.load(f)
48
- except Exception:
 
 
 
 
 
 
 
 
 
 
 
 
49
  return []
 
 
50
 
51
  def save_menu(menu_data):
52
- with open(MENU_FILE, "w", encoding="utf-8") as f:
53
- json.dump(menu_data, f, ensure_ascii=False, indent=4)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
  def load_orders():
 
 
56
  try:
57
- with open(ORDERS_FILE, "r", encoding="utf-8") as f:
58
- return json.load(f)
59
- except Exception:
 
 
 
 
 
 
 
 
 
 
 
 
60
  return []
 
 
61
 
62
  def save_orders(orders_data):
63
- with open(ORDERS_FILE, "w", encoding="utf-8") as f:
64
- json.dump(orders_data, f, ensure_ascii=False, indent=4)
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
- return json.dumps(available_items, ensure_ascii=False)
 
 
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
- try:
134
- items_list.append({
135
- "name": getattr(item, "name", str(item)),
136
- "quantity": getattr(item, "quantity", 1)
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
- menu = load_menu()
166
- found = False
167
- normalized_input = item_name.strip().lower()
168
-
169
- for item in menu:
170
- if item["name"].strip().lower() == normalized_input or normalized_input in item["name"].strip().lower():
171
- item["available"] = available
172
- found = True
173
- item_name = item["name"]
174
- break
175
-
176
- if found:
177
- save_menu(menu)
178
- status = "موجود" if available else "ناموجود"
179
- return f"وضعیت آیتم '{item_name}' به موفقیت به '{status}' تغییر یافت."
180
- else:
181
- return f"آیتم '{item_name}' در منوی کافه یافت نشد."
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": "کلید API در سرور یافت نشد. لطفاً NILLA_CUSTOMER یا GEMINI_API_KEY را بررسی کنید."}), 500
206
 
207
  data = request.json or {}
208
  user_message = data.get("message")
209
  if not user_message:
210
- return jsonify({"error": "پیام خالی است."}), 400
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}. Maintain context based on this 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
- try:
254
- if response.candidates and response.candidates[0].content and response.candidates[0].content.parts:
255
- parts = response.candidates[0].content.parts
256
- text_parts = [part.text for part in parts if hasattr(part, "text") and part.text]
257
- if text_parts:
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, "message": "فرآیند تولید متوقف شد."})
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
- # --- سرویس‌های API مدیریت کافه ---
 
 
 
 
 
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", "pending") == "pending"]
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
- orders = load_orders()
348
- updated = False
349
- for o in orders:
350
- if o.get("id") == order_id:
351
- o["status"] = "completed"
352
- updated = True
353
- break
354
-
355
- if updated:
356
- save_orders(orders)
357
- return jsonify({"success": True, "message": "سفارش با موفقیت برداشته شد."})
358
- return jsonify({"error": "سفارش پیدا نشد."}), 404
 
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": "کلید API یافت نشد."}), 500
365
-
366
  data = request.json or {}
367
  messages = data.get("messages", [])
368
  if not messages:
369
- return jsonify({"error": "تاریخچه پیام‌ها خالی است."}), 400
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
- try:
401
- if response.candidates and response.candidates[0].content and response.candidates[0].content.parts:
402
- parts = response.candidates[0].content.parts
403
- text_parts = [part.text for part in parts if hasattr(part, "text") and part.text]
404
- if text_parts:
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": f"خطا در بخش سرور: {str(e)}"}), 500
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": "کلید API یافت نشد."}), 500
428
-
429
  if "file" not in request.files:
430
- return jsonify({"error": "فایل ارسال نشده"}), 400
431
-
432
  file = request.files["file"]
433
  if file.filename == "":
434
- return jsonify({"error": "نام فایل خالی است."}), 400
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"خطا در استخراج ساختار: {str(e)}"}), 500
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)