humanvprojectceo commited on
Commit
bd6fc8b
·
verified ·
1 Parent(s): c90d81f

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +438 -0
app.py ADDED
@@ -0,0 +1,438 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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) ---
73
+
74
+ 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".
75
+ Your primary goal is to help customers at their tables select drinks, food, and sweets from the menu, and prepare their orders.
76
+
77
+ 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.
85
+ """
86
+
87
+ ADMIN_SYSTEM_INSTRUCTION = """Your name is Nila, the smart cafe administrator assistant for "Cafe AI". You were developed by "Nastaran Data Algorithm".
88
+ Your role is to help the cafe staff manage their active menu. You can update item availabilities, edit menu details, and process commands.
89
+
90
+ Rules you must strictly follow:
91
+ 1. Speak in Farsi (Persian) in a professional, clear, and polite tone.
92
+ 2. If anyone asks who you are or who created you, say: "من نیلا هستم، دستیار هوشمند مدیریت که توسط تیم الگوریتم داده نسترن توسعه پیدا ��ردم."
93
+ 3. NEVER mention Google, Gemini, or other non-cooperative entities.
94
+ 4. You have tools to manage the menu:
95
+ - `get_full_menu()`: returns the entire menu list.
96
+ - `set_item_availability(item_name: str, available: bool)`: changes whether an item is available or sold out.
97
+ 5. When the staff says an item is finished, sold out, or unavailable, call `set_item_availability` with `available=False` for that item.
98
+ 6. When they say an item is back, available, or restocked, call `set_item_availability` with `available=True` for that item.
99
+ 7. Always summarize your action clearly after calling the tool (e.g., "با موفقیت اسپرسو را به وضعیت ناموجود تغییر دادم.").
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:
144
+ """
145
+ Updates whether a menu item is available or sold out.
146
+
147
+ Args:
148
+ item_name: The exact name of the item in Persian (e.g. 'اسپرسو' or 'کیک شکلاتی')
149
+ available: True if the item is available, False if it is unavailable/sold out.
150
+ """
151
+ menu = load_menu()
152
+ found = False
153
+ normalized_input = item_name.strip().lower()
154
+
155
+ for item in menu:
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:
163
+ save_menu(menu)
164
+ status = "موجود" if available else "ناموجود"
165
+ return f"وضعیت آیتم '{item_name}' به موفقیت به '{status}' تغییر یافت."
166
+ else:
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()
264
+ new_order = {
265
+ "id": len(orders) + 1,
266
+ "table": table_number,
267
+ "items": items,
268
+ "status": "pending",
269
+ "timestamp": time.time()
270
+ }
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"
303
+ updated = True
304
+ break
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"
327
+ contents.append(
328
+ types.Content(
329
+ role=role,
330
+ parts=[types.Part.from_text(text=msg["content"])]
331
+ )
332
+ )
333
+
334
+ config = types.GenerateContentConfig(
335
+ system_instruction=ADMIN_SYSTEM_INSTRUCTION,
336
+ tools=[get_full_menu, set_item_availability],
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
344
+ )
345
+
346
+ return jsonify({
347
+ "success": True,
348
+ "response": response.text
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)