humanvprojectceo commited on
Commit
81a0940
·
verified ·
1 Parent(s): 8e11847

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +180 -188
app.py CHANGED
@@ -1,72 +1,60 @@
1
  import os
2
  import json
3
  import time
4
- import threading
5
- from flask import Flask, request, jsonify, render_template
6
  from google import genai
7
  from google.genai import types
8
 
9
- app = Flask(__name__, template_folder="templates")
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
- file_lock = threading.Lock()
21
-
22
- # بارگذاری منوی پیش‌فرض در اولین اجرا
23
  if not os.path.exists(MENU_FILE):
24
- default_menu = [
25
- {"name": "اسپرسو", "description": "قهوه غلیظ و اصیل ۱۰۰٪ عربیکا", "price": "۵۰,۰۰۰", "available": True},
26
- {"name": "لاته", "description": "ترکیب اسپرسو، شیر گرم و فوم شیر", "price": "۶۵,۰۰۰", "available": True},
27
- {"name": "کیک شکلاتی خیس", "description": "کیک شکلاتی تازه با سس شکلات داغ", "price": "۸۰,۰۰۰", "available": True},
28
- {"name": "چای ماسالا", "description": "چای هندی معطر با ادویه‌های مخصوص و شیر", "price": "۷۰,۰۰۰", "available": True},
29
- {"name": "کروسان ساده", "description": "شیرینی کره‌ای ترد و لایه لایه", "price": "۴۵,۰۰۰", "available": True}
30
- ]
31
  with open(MENU_FILE, "w", encoding="utf-8") as f:
32
- json.dump(default_menu, f, ensure_ascii=False, indent=4)
33
 
34
  if not os.path.exists(ORDERS_FILE):
35
  with open(ORDERS_FILE, "w", encoding="utf-8") as f:
36
  json.dump([], f, ensure_ascii=False, indent=4)
37
 
38
 
39
- # --- توابع مدیریت داده‌ها (Thread-Safe) ---
 
 
 
 
 
 
40
 
41
  def load_menu():
42
- with file_lock:
43
- try:
44
- with open(MENU_FILE, "r", encoding="utf-8") as f:
45
- return json.load(f)
46
- except Exception:
47
- return []
48
 
49
  def save_menu(menu_data):
50
- with file_lock:
51
- with open(MENU_FILE, "w", encoding="utf-8") as f:
52
- json.dump(menu_data, f, ensure_ascii=False, indent=4)
53
 
54
  def load_orders():
55
- with file_lock:
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 file_lock:
64
- with open(ORDERS_FILE, "w", encoding="utf-8") as f:
65
- json.dump(orders_data, f, ensure_ascii=False, indent=4)
66
-
67
-
68
- # پیش‌نویس سفارش‌های موقت (کلید: شماره میز، مقدار: اطلاعات سفارش)
69
- global_drafts = {}
70
 
71
 
72
  # --- دستورالعمل‌های سیستمی نیلا (System Instructions) ---
@@ -78,7 +66,7 @@ Rules you must strictly follow:
78
  1. Speak in Farsi (Persian) in a warm, polite, and welcoming tone. Use emojis suitable for a cozy cafe (☕️, 🍰, 🥐, 🌸, etc.).
79
  2. If anyone asks who you are or who created you, say: "من نیلا هستم، دستیار هوشمند کافه AI که توسط تیم الگوریتم داده نسترن توسعه پیدا کردم."
80
  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.
81
- 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 (e.g. if they want something sweet, search for cakes, milkshakes, or sweet drinks).
82
  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.
83
  6. Do not proceed to place an order unless they have specified what they want.
84
  7. Always act as an elegant, polite cafe waitress.
@@ -100,44 +88,43 @@ Rules you must strictly follow:
100
  """
101
 
102
 
103
- # --- ابزارهای مشتری (Customer Function Tools) ---
104
 
105
  def get_menu() -> str:
106
  """
107
  Returns the current menu items. Use this to see what drinks, foods, or sweets are available in the cafe.
108
- Returns:
109
- A JSON string containing list of available items with their name, description, and price.
110
  """
111
  menu = load_menu()
112
  available_items = [item for item in menu if item.get("available", True)]
 
 
113
  return json.dumps(available_items, ensure_ascii=False)
114
 
115
  def prepare_order_draft(table_number: str, items: list) -> str:
116
  """
117
  Prepares a draft order list for the customer to confirm on their screen.
118
- Call this when the customer specifies what items they want to order.
119
 
120
  Args:
121
  table_number: The table number (e.g. '3')
122
- items: A list of items, where each item is a dictionary containing 'name' (string) and 'quantity' (integer).
123
- Example: [{"name": "اسپرسو", "quantity": 2}]
124
  """
125
  global_drafts[table_number] = {
126
  "table": table_number,
127
  "items": items,
128
  "timestamp": time.time()
129
  }
130
- return f"پیش‌نویس سفارش برای میز {table_number} آماده شد. لطفاً برای ثبت نهایی دکمه تایید سفارش را لمس کنید."
131
-
132
-
133
- # --- ابزارهای مدیریت کافه (Cafe Admin Function Tools) ---
134
 
135
  def get_full_menu() -> str:
136
  """
137
  Returns the complete menu of the cafe, including both available and unavailable items.
138
- Use this to inspect all menu entries.
139
  """
140
  menu = load_menu()
 
 
141
  return json.dumps(menu, ensure_ascii=False)
142
 
143
  def set_item_availability(item_name: str, available: bool) -> str:
@@ -156,7 +143,7 @@ def set_item_availability(item_name: str, available: bool) -> str:
156
  if item["name"].strip().lower() == normalized_input or normalized_input in item["name"].strip().lower():
157
  item["available"] = available
158
  found = True
159
- item_name = item["name"] # بروزرسانی به نام دقیق منو برای گزارش دهی دقیق‌تر
160
  break
161
 
162
  if found:
@@ -167,97 +154,124 @@ def set_item_availability(item_name: str, available: bool) -> str:
167
  return f"آیتم '{item_name}' در منوی کافه یافت نشد."
168
 
169
 
170
- # --- مسیرهای رندر صفحات فرانت‌اند (HTML Templates) ---
171
 
172
- @app.route("/")
173
- def index():
174
- # صفحه اصلی چت مشتری
175
- return render_template("customer.html")
176
 
177
- @app.route("/admin")
178
- def admin():
179
- # پنل مدیریت کافه
180
- return render_template("cafe.html")
181
 
 
 
 
 
182
 
183
- # --- مسیرهای API مشتری ---
184
 
185
- @app.route("/api/menu", methods=["GET"])
186
- def api_get_menu():
187
- # بازگرداندن کل منو برای پاپ‌آپ فرانت‌اند مشتری
188
- return jsonify(load_menu())
189
 
190
- @app.route("/api/customer/chat", methods=["POST"])
191
- def api_customer_chat():
192
  api_key = os.environ.get("NILLA_CUSTOMER")
193
  if not api_key:
194
- return jsonify({"error": "کلید متغیر محیطی NILLA_CUSTOMER یافت نشد."}), 500
195
-
196
  data = request.json or {}
197
- table_number = data.get("table_number")
198
- messages = data.get("messages", [])
199
-
200
- if not table_number:
201
- return jsonify({"error": "شماره میز ارسال نشده است."}), 400
202
-
203
- # پاک کردن موقت پیش‌نویس این میز در این دور گفتگو تا متوجه تغییرات جدید شویم
 
 
204
  global_drafts.pop(table_number, None)
205
-
206
- try:
207
- client = genai.Client(api_key=api_key)
208
-
209
- # تبدیل ساختار پیام‌ها به فرمت SDK جدید گوگل
210
- contents = []
211
- for msg in messages:
212
- role = "user" if msg["role"] == "user" else "model"
213
- contents.append(
214
- types.Content(
215
- role=role,
216
- parts=[types.Part.from_text(text=msg["content"])]
 
 
 
 
217
  )
218
- )
 
 
 
219
 
220
- # تزریق میز فعال به انتهای دستورالعمل برای تضمین عدم اشتباه هوش مصنوعی
221
- dynamic_instruction = CUSTOMER_SYSTEM_INSTRUCTION + f"\nYou are currently interacting with table: {table_number}. Always prepare draft orders strictly for this table."
222
-
223
- config = types.GenerateContentConfig(
224
- system_instruction=dynamic_instruction,
225
- tools=[get_menu, prepare_order_draft],
226
- thinking_config=types.ThinkingConfig(thinking_level="MINIMAL")
227
- )
228
-
229
- response = client.models.generate_content(
230
- model="gemini-3.1-flash-lite",
231
- contents=contents,
232
- config=config
233
- )
234
-
235
- # بررسی اینکه آیا در طول پردازش چت، پیش‌نویسی ثبت شده است یا خیر
236
- draft = global_drafts.get(table_number, None)
237
-
238
- return jsonify({
239
- "success": True,
240
- "response": response.text,
241
- "draft_order": draft
242
- })
243
-
244
- except Exception as e:
245
- return jsonify({"error": f"خطایی رخ داده است: {str(e)}"}), 500
246
 
247
- @app.route("/api/confirm_order", methods=["POST"])
248
- def api_confirm_order():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  data = request.json or {}
250
  table_number = data.get("table_number")
251
- items = data.get("items") # فرانت‌اند می‌تواند مستقیم لیست نهایی شده را ارسال کند
252
 
253
  if not table_number:
254
  return jsonify({"error": "شماره میز مشخص نیست."}), 400
255
 
256
- # اگر فرانت‌اند آیتم‌ها را نفرستاده باشد، تلاش می‌کند از حافظه پیش‌نویس سرور بخواند
257
  if not items:
 
258
  draft = global_drafts.get(table_number)
259
  if not draft:
260
- return jsonify({"error": "پیش‌نویس فعالی برای این میز یافت نشد. لطفاً مجدداً تلاش فرمایید."}), 404
261
  items = draft.get("items")
262
 
263
  orders = load_orders()
@@ -271,32 +285,32 @@ def api_confirm_order():
271
  orders.append(new_order)
272
  save_orders(orders)
273
 
274
- # حذف از لیست پیش‌نویس‌های فعال
 
275
  global_drafts.pop(table_number, None)
 
276
 
277
- return jsonify({"success": True, "message": "سفارش شما با موفقیت ثبت شد و به پنل مدیریت ارسال گردید."})
278
 
279
 
280
- # --- مسیرهای API مدیریت کافه ---
281
 
282
- @app.route("/api/admin/orders", methods=["GET"])
283
- def api_admin_orders():
284
- # بازگرداندن فقط سفارش‌های فعال (معلق) جهت نمایش در پنل کافه
285
  orders = load_orders()
286
  active_orders = [o for o in orders if o.get("status", "pending") == "pending"]
287
  return jsonify(active_orders)
288
 
289
- @app.route("/api/admin/complete_order", methods=["POST"])
290
- def api_admin_complete_order():
291
  data = request.json or {}
292
  order_id = data.get("order_id")
293
 
294
  if not order_id:
295
- return jsonify({"error": "شناسه سفارش ارسال نشده است."}), 400
296
 
297
  orders = load_orders()
298
  updated = False
299
-
300
  for o in orders:
301
  if o.get("id") == order_id:
302
  o["status"] = "completed"
@@ -305,22 +319,20 @@ def api_admin_complete_order():
305
 
306
  if updated:
307
  save_orders(orders)
308
- return jsonify({"success": True, "message": "سفارش با موفقیت تحویل داده و برداشته شد."})
309
- else:
310
- return jsonify({"error": "سفارش یافت نشد."}), 404
311
 
312
- @app.route("/api/admin/chat", methods=["POST"])
313
- def api_admin_chat():
314
  api_key = os.environ.get("NILLA_CAFE")
315
  if not api_key:
316
- return jsonify({"error": "کلید متغیر محیطی NILLA_CAFE یافت نشد."}), 500
317
 
318
  data = request.json or {}
319
  messages = data.get("messages", [])
320
 
321
  try:
322
  client = genai.Client(api_key=api_key)
323
-
324
  contents = []
325
  for msg in messages:
326
  role = "user" if msg["role"] == "user" else "model"
@@ -337,7 +349,8 @@ def api_admin_chat():
337
  thinking_config=types.ThinkingConfig(thinking_level="MINIMAL")
338
  )
339
 
340
- response = client.models.generate_content(
 
341
  model="gemini-3.1-flash-lite",
342
  contents=contents,
343
  config=config
@@ -349,90 +362,69 @@ def api_admin_chat():
349
  })
350
 
351
  except Exception as e:
352
- return jsonify({"error": f"خطایی در بخش ادمین رخ داده است: {str(e)}"}), 500
353
 
354
- @app.route("/api/admin/extract_menu", methods=["POST"])
355
- def api_extract_menu():
356
  api_key = os.environ.get("NILLA_CAFE")
357
  if not api_key:
358
- return jsonify({"error": "کلید متغیر محیطی NILLA_CAFE برای بخش استخراج منو تنظیم نشده است."}), 500
359
 
360
  if "file" not in request.files:
361
- return jsonify({"error": "فایلی جهت استخراج فرستاده نشده است."}), 400
362
 
363
  file = request.files["file"]
364
  if file.filename == "":
365
- return jsonify({"error": "فایل فاقد نام است."}), 400
366
 
367
  try:
368
  file_bytes = file.read()
369
  mime_type = file.mimetype or "image/jpeg"
370
 
371
- # قالب‌بندی خروجی هوش مصنوعی به صورت JSON دقیق
372
- extraction_prompt = """
373
- Please analyze this menu file or image and extract all drinks, food, and dessert items.
374
- For each item, extract:
375
- 1. name (name of the drink or food, in Farsi)
376
- 2. description (ingredients or short description, in Farsi)
377
- 3. price (price of the item, formatted nicely, in Farsi)
378
-
379
- Return ONLY a raw JSON array of objects. Do not wrap it in markdown block quotes (do not use ```json ... ```).
380
- Example of expected output structure:
381
  [
382
- {"name": "اسپرسو", "description": "قهوه غلیظ و خالص اسپرسو", "price": "۵۰,۰۰۰"},
383
- {"name": "کیک هویج", "description": "کیک هویج و گردو به همراه خامه مخصوص", "price": "۸۵,۰۰۰"}
384
  ]
385
  """
386
 
387
  client = genai.Client(api_key=api_key)
388
-
389
- # استفاده از متد جدید و استاندارد انواع داده‌های باینری در SDK گوگل
390
  file_part = types.Part.from_bytes(data=file_bytes, mime_type=mime_type)
391
 
392
- response = client.models.generate_content(
 
393
  model="gemini-3.1-flash-lite",
394
- contents=[file_part, extraction_prompt],
395
  config=types.GenerateContentConfig(
396
  response_mime_type="application/json"
397
  )
398
  )
399
 
400
- # پارس کردن برای تایید صحت خروجی ساختاریافته هوش مصنوعی
401
- extracted_list = json.loads(response.text)
402
-
403
- # همگام سازی اتوماتیک فیلد موجودی (پیش‌فرض موجود)
404
- for item in extracted_list:
405
- if "available" not in item:
406
- item["available"] = True
407
-
408
- return jsonify({"success": True, "extracted_menu": extracted_list})
409
 
410
  except Exception as e:
411
- return jsonify({"error": f"عدم موفقیت هوش مصنوعی در تحلیل و استخراج منو: {str(e)}"}), 500
412
 
413
- @app.route("/api/admin/save_menu", methods=["POST"])
414
- def api_admin_save_menu():
415
  data = request.json or {}
416
  menu_list = data.get("menu")
417
 
418
  if not isinstance(menu_list, list):
419
- return jsonify({"error": "ساختار داده منو نامعتبر است."}), 400
420
 
421
- # اعتبارسنجی فیلدهای ضروری منو پیش از ذخیره‌سازی
422
- for item in menu_list:
423
- if "name" not in item:
424
- return jsonify({"error": "تمامی آیتم‌های منو باید فیلد نام (name) داشته باشند."}), 400
425
- if "description" not in item:
426
- item["description"] = ""
427
- if "price" not in item:
428
- item["price"] = "توافقی"
429
- if "available" not in item:
430
- item["available"] = True
431
-
432
  save_menu(menu_list)
433
- return jsonify({"success": True, "message": "منوی جدید با موفقیت جایگزین و ثبت گردید."})
434
 
435
 
436
  if __name__ == "__main__":
437
- # اجرای وب اپلیکیشن در پورت محلی جهت تست در خارج از داکر
438
  app.run(host="0.0.0.0", port=7860, debug=True)
 
1
  import os
2
  import json
3
  import time
4
+ import asyncio
5
+ from flask import Flask, request, Response, jsonify, render_template, redirect
6
  from google import genai
7
  from google.genai import types
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
+ # --- توابع مدیریت داده‌های پایگاه‌داده محلی منو و سفارشات ---
36
 
37
  def load_menu():
38
+ try:
39
+ with open(MENU_FILE, "r", encoding="utf-8") as f:
40
+ return json.load(f)
41
+ except Exception:
42
+ return []
 
43
 
44
  def save_menu(menu_data):
45
+ with open(MENU_FILE, "w", encoding="utf-8") as f:
46
+ json.dump(menu_data, f, ensure_ascii=False, indent=4)
 
47
 
48
  def load_orders():
49
+ try:
50
+ with open(ORDERS_FILE, "r", encoding="utf-8") as f:
51
+ return json.load(f)
52
+ except Exception:
53
+ return []
 
54
 
55
  def save_orders(orders_data):
56
+ with open(ORDERS_FILE, "w", encoding="utf-8") as f:
57
+ json.dump(orders_data, f, ensure_ascii=False, indent=4)
 
 
 
 
 
58
 
59
 
60
  # --- دستورالعمل‌های سیستمی نیلا (System Instructions) ---
 
66
  1. Speak in Farsi (Persian) in a warm, polite, and welcoming tone. Use emojis suitable for a cozy cafe (☕️, 🍰, 🥐, 🌸, etc.).
67
  2. If anyone asks who you are or who created you, say: "من نیلا هستم، دستیار هوشمند کافه AI که توسط تیم الگوریتم داده نسترن توسعه پیدا کردم."
68
  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.
69
+ 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.
70
  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.
71
  6. Do not proceed to place an order unless they have specified what they want.
72
  7. Always act as an elegant, polite cafe waitress.
 
88
  """
89
 
90
 
91
+ # --- توابع فراخوانی ابزارها (Function Calling Tools) ---
92
 
93
  def get_menu() -> str:
94
  """
95
  Returns the current menu items. Use this to see what drinks, foods, or sweets are available in the cafe.
96
+ If empty or no items are available, it returns that there is no menu yet.
 
97
  """
98
  menu = load_menu()
99
  available_items = [item for item in menu if item.get("available", True)]
100
+ if not available_items:
101
+ return "در حال حاضر هیچ منویی آپلود نشده است و منوی فعال موجود نیست."
102
  return json.dumps(available_items, ensure_ascii=False)
103
 
104
  def prepare_order_draft(table_number: str, items: list) -> str:
105
  """
106
  Prepares a draft order list for the customer to confirm on their screen.
107
+ Call this when the customer has decided what they want to order.
108
 
109
  Args:
110
  table_number: The table number (e.g. '3')
111
+ items: A list of dicts, where each dict contains 'name' (string) and 'quantity' (integer).
112
+ Example: [{"name": "اسپرسو", "quantity": 1}]
113
  """
114
  global_drafts[table_number] = {
115
  "table": table_number,
116
  "items": items,
117
  "timestamp": time.time()
118
  }
119
+ return f"من پیش‌نویس سفارش شما رو برای میز {table_number} آماده کردم."
 
 
 
120
 
121
  def get_full_menu() -> str:
122
  """
123
  Returns the complete menu of the cafe, including both available and unavailable items.
 
124
  """
125
  menu = load_menu()
126
+ if not menu:
127
+ return "منوی کافه خالی است."
128
  return json.dumps(menu, ensure_ascii=False)
129
 
130
  def set_item_availability(item_name: str, available: bool) -> str:
 
143
  if item["name"].strip().lower() == normalized_input or normalized_input in item["name"].strip().lower():
144
  item["available"] = available
145
  found = True
146
+ item_name = item["name"]
147
  break
148
 
149
  if found:
 
154
  return f"آیتم '{item_name}' در منوی کافه یافت نشد."
155
 
156
 
157
+ # --- روت‌های مسیریابی آدرسها (Domain URL Mapping) ---
158
 
159
+ @app.route('/')
160
+ async def home_redirect():
161
+ # ریدایرکت روت اصلی به میز ۱ به عنوان پیش‌فرض
162
+ return redirect('/customer/1')
163
 
164
+ @app.route('/customer/<table_number>')
165
+ async def customer_chat_page(table_number):
166
+ # سرو کردن صفحه چت مشتری برای میز انتخابی
167
+ return render_template("customer.html", table_number=table_number)
168
 
169
+ @app.route('/dashboard')
170
+ async def admin_dashboard_page():
171
+ # سرو کردن پنل مدیریت کافه
172
+ return render_template("cafe.html")
173
 
 
174
 
175
+ # --- مسیرهای ناهمگام سرویس‌های API مشتری ---
 
 
 
176
 
177
+ @app.route('/api/customer/chat_stream/<table_number>', methods=['POST'])
178
+ async def api_customer_chat_stream(table_number):
179
  api_key = os.environ.get("NILLA_CUSTOMER")
180
  if not api_key:
181
+ return jsonify({"error": "کلید NILLA_CUSTOMER در سرور یافت نشد."}), 500
182
+
183
  data = request.json or {}
184
+ user_message = data.get("message")
185
+ if not user_message:
186
+ return jsonify({"error": "پیام خالی است."}), 400
187
+
188
+ # بررسی و تعریف آرایه سابقه چت برای میز جاری
189
+ if table_number not in table_chat_histories:
190
+ table_chat_histories[table_number] = []
191
+
192
+ # پاک کردن سفارش موقت قبلی جهت استخراج فاکتور جدید
193
  global_drafts.pop(table_number, None)
194
+
195
+ # افزودن پیام مشتری به آرشیو سوابق میز
196
+ table_chat_histories[table_number].append({"role": "user", "content": user_message})
197
+ active_tasks[table_number] = True
198
+
199
+ async def generate_chunks():
200
+ try:
201
+ # تبدیل ساختار آرشیو سوابق به انواع داده‌ای مورد پذیرش SDK جدید
202
+ contents = []
203
+ for msg in table_chat_histories[table_number]:
204
+ role = "user" if msg["role"] == "user" else "model"
205
+ contents.append(
206
+ types.Content(
207
+ role=role,
208
+ parts=[types.Part.from_text(text=msg["content"])]
209
+ )
210
  )
211
+
212
+ # راه‌اندازی کلاینت ناهمگام گوگل با متد aio
213
+ client = genai.Client(api_key=api_key)
214
+ full_generated_text = ""
215
 
216
+ dynamic_instruction = CUSTOMER_SYSTEM_INSTRUCTION + f"\nYou are currently serving table: {table_number}. Maintain context based on this table number."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
 
218
+ config = types.GenerateContentConfig(
219
+ system_instruction=dynamic_instruction,
220
+ tools=[get_menu, prepare_order_draft],
221
+ thinking_config=types.ThinkingConfig(thinking_level="MINIMAL")
222
+ )
223
+
224
+ # فراخوانی استریم ناهمگام گوگل
225
+ async for chunk in await client.aio.models.generate_content_stream(
226
+ model="gemini-3.1-flash-lite",
227
+ contents=contents,
228
+ config=config
229
+ ):
230
+ # اگر کلاینت دکمه توقف را زده باشد، استریم بلافاصله خاتمه یابد
231
+ if not active_tasks.get(table_number, True):
232
+ break
233
+
234
+ if chunk.text:
235
+ full_generated_text += chunk.text
236
+ yield f"data: {json.dumps({'type': 'text', 'content': chunk.text}, ensure_ascii=False)}\n\n"
237
+
238
+ # ذخیره پاسخ مدل در پایان جریان چت برای پیوستگی گفتگوهای بعد
239
+ if full_generated_text:
240
+ table_chat_histories[table_number].append({"role": "model", "content": full_generated_text})
241
+
242
+ # بررسی اینکه آیا ابزار ثبت سفارش (prepare_order_draft) صدا زده شده بود
243
+ draft = global_drafts.get(table_number)
244
+ if draft:
245
+ # ارسال فاکتور استخراج شده به سمت فرانت‌اند جهت نمایش دکمه تایید نهایی دو مرحله‌ای
246
+ yield f"data: {json.dumps({'type': 'draft', 'items': draft['items']}, ensure_ascii=False)}\n\n"
247
+
248
+ except Exception as e:
249
+ yield f"data: {json.dumps({'type': 'error', 'content': str(e)}, ensure_ascii=False)}\n\n"
250
+ finally:
251
+ active_tasks.pop(table_number, None)
252
+
253
+ return Response(generate_chunks(), mimetype='text/event-stream')
254
+
255
+ @app.route('/api/customer/stop/<table_number>', methods=['POST'])
256
+ async def api_customer_stop(table_number):
257
+ # متوقف کردن زنده فرآیند استریمینگ در سمت بک‌اند
258
+ active_tasks[table_number] = False
259
+ return jsonify({"success": True, "message": "فرآیند تولید متوقف شد."})
260
+
261
+ @app.route('/api/confirm_order', methods=['POST'])
262
+ async def api_confirm_order():
263
  data = request.json or {}
264
  table_number = data.get("table_number")
265
+ items = data.get("items")
266
 
267
  if not table_number:
268
  return jsonify({"error": "شماره میز مشخص نیست."}), 400
269
 
 
270
  if not items:
271
+ # در صورت نبود آیتم‌ها تلاش مجدد برای خواندن از درفت سرور انجام می‌شود
272
  draft = global_drafts.get(table_number)
273
  if not draft:
274
+ return jsonify({"error": "اطلاعات پیش‌نویس یافت نشد."}), 404
275
  items = draft.get("items")
276
 
277
  orders = load_orders()
 
285
  orders.append(new_order)
286
  save_orders(orders)
287
 
288
+ # --- پاکسازی نهایی جهت آزادسازی منابع سیستم (نیاز ادمین) ---
289
+ table_chat_histories.pop(table_number, None)
290
  global_drafts.pop(table_number, None)
291
+ active_tasks.pop(table_number, None)
292
 
293
+ return jsonify({"success": True, "message": "سفارش ثبت شد و سشن میز کاملاً آزاد گردید."})
294
 
295
 
296
+ # --- مسیرهای ناهمگام سرویس‌های API مدیریت کافه ---
297
 
298
+ @app.route('/api/admin/orders', methods=['GET'])
299
+ async def api_admin_orders():
 
300
  orders = load_orders()
301
  active_orders = [o for o in orders if o.get("status", "pending") == "pending"]
302
  return jsonify(active_orders)
303
 
304
+ @app.route('/api/admin/complete_order', methods=['POST'])
305
+ async def api_admin_complete_order():
306
  data = request.json or {}
307
  order_id = data.get("order_id")
308
 
309
  if not order_id:
310
+ return jsonify({"error": "شناسه سفارش نامعتبر"}), 400
311
 
312
  orders = load_orders()
313
  updated = False
 
314
  for o in orders:
315
  if o.get("id") == order_id:
316
  o["status"] = "completed"
 
319
 
320
  if updated:
321
  save_orders(orders)
322
+ return jsonify({"success": True, "message": "سفارش با موفقیت ��رداشته شد."})
323
+ return jsonify({"error": "سفارش پیدا نشد."}), 404
 
324
 
325
+ @app.route('/api/admin/chat', methods=['POST'])
326
+ async def api_admin_chat():
327
  api_key = os.environ.get("NILLA_CAFE")
328
  if not api_key:
329
+ return jsonify({"error": "کلید NILLA_CAFE یافت نشد."}), 500
330
 
331
  data = request.json or {}
332
  messages = data.get("messages", [])
333
 
334
  try:
335
  client = genai.Client(api_key=api_key)
 
336
  contents = []
337
  for msg in messages:
338
  role = "user" if msg["role"] == "user" else "model"
 
349
  thinking_config=types.ThinkingConfig(thinking_level="MINIMAL")
350
  )
351
 
352
+ # استفاده از متد فراخوانی ناهمگام aio برای جلوگیری از بلاک شدن سایر کاربران
353
+ response = await client.aio.models.generate_content(
354
  model="gemini-3.1-flash-lite",
355
  contents=contents,
356
  config=config
 
362
  })
363
 
364
  except Exception as e:
365
+ return jsonify({"error": f"خطا در بخش سرور: {str(e)}"}), 500
366
 
367
+ @app.route('/api/admin/extract_menu', methods=['POST'])
368
+ async def api_extract_menu():
369
  api_key = os.environ.get("NILLA_CAFE")
370
  if not api_key:
371
+ return jsonify({"error": "کلید NILLA_CAFE یافت نشد."}), 500
372
 
373
  if "file" not in request.files:
374
+ return jsonify({"error": "فایل ارسال نشده"}), 400
375
 
376
  file = request.files["file"]
377
  if file.filename == "":
378
+ return jsonify({"error": "نام فایل خالی است."}), 400
379
 
380
  try:
381
  file_bytes = file.read()
382
  mime_type = file.mimetype or "image/jpeg"
383
 
384
+ prompt = """
385
+ Analyze this menu image or file. Extract all menu items, descriptions, and prices.
386
+ Return ONLY a raw JSON array of objects without markdown formatting.
387
+ Format:
 
 
 
 
 
 
388
  [
389
+ {"name": "اسپرسو", "description": "قهوه غلیظ خالص", "price": "۵۰,۰۰۰"},
390
+ {"name": "کیک ساده", "description": "شیرینی خانگی لذیذ", "price": "۴۰,۰۰۰"}
391
  ]
392
  """
393
 
394
  client = genai.Client(api_key=api_key)
 
 
395
  file_part = types.Part.from_bytes(data=file_bytes, mime_type=mime_type)
396
 
397
+ # استخراج منوی آپلود شده به صورت ناهمگام
398
+ response = await client.aio.models.generate_content(
399
  model="gemini-3.1-flash-lite",
400
+ contents=[file_part, prompt],
401
  config=types.GenerateContentConfig(
402
  response_mime_type="application/json"
403
  )
404
  )
405
 
406
+ extracted_menu = json.loads(response.text)
407
+ for item in extracted_menu:
408
+ item["available"] = True
409
+
410
+ return jsonify({"success": True, "extracted_menu": extracted_menu})
 
 
 
 
411
 
412
  except Exception as e:
413
+ return jsonify({"error": f"خطا در استخراج ساختار: {str(e)}"}), 500
414
 
415
+ @app.route('/api/admin/save_menu', methods=['POST'])
416
+ async def api_admin_save_menu():
417
  data = request.json or {}
418
  menu_list = data.get("menu")
419
 
420
  if not isinstance(menu_list, list):
421
+ return jsonify({"error": "فرمت داده ارسال شده نادرست است."}), 400
422
 
423
+ # ثبت نهایی منو در قالب فایل دیتابیس ثابت menu.json
 
 
 
 
 
 
 
 
 
 
424
  save_menu(menu_list)
425
+ return jsonify({"success": True, "message": "منوی فعال با موفقیت ثبت شد."})
426
 
427
 
428
  if __name__ == "__main__":
429
+ # راه‌اندازی سرور
430
  app.run(host="0.0.0.0", port=7860, debug=True)