Mr-Help commited on
Commit
a722165
·
verified ·
1 Parent(s): 9c0101f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +400 -63
app.py CHANGED
@@ -147,6 +147,75 @@ def normalize_arabic(text: str) -> str:
147
  return text
148
 
149
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  def detect_intent(text: str) -> str:
151
  t = normalize_arabic(text)
152
 
@@ -160,8 +229,6 @@ def detect_intent(text: str) -> str:
160
  "حد يرد",
161
  "اكلم",
162
  "اتواصل",
163
- "رقم",
164
- "واتساب",
165
  "support",
166
  "agent",
167
  ]
@@ -175,7 +242,7 @@ def detect_intent(text: str) -> str:
175
  if any(x in t for x in ["weekend", "ويكند", "الجمعة", "السبت", "عطله", "عطلة"]):
176
  return "WEEKEND_COURSES"
177
 
178
- if any(x in t for x in ["children", "kids", "اطفال", "طفل", "سن", "سنين"]):
179
  return "CHILDREN_COURSES"
180
 
181
  if any(x in t for x in ["express", "اكسبريس", "سريع", "super intensive"]):
@@ -194,13 +261,13 @@ def detect_intent(text: str) -> str:
194
  "المركز",
195
  "adk",
196
  "ädk",
197
- "فروع",
198
- "branch",
199
  "اتاسس",
200
  "اتأسس",
201
  "تاريخ",
202
  "goethe",
203
  "معتمد",
 
 
204
  ]
205
  ):
206
  return "CENTER_INFO"
@@ -215,7 +282,9 @@ def detect_intent(text: str) -> str:
215
  "الكورسات المتاحه",
216
  "الكورسات المتاحة",
217
  "عايز كورس",
 
218
  "عايز اتعلم",
 
219
  "الماني عندكم",
220
  "ألماني",
221
  "الماني",
@@ -245,95 +314,257 @@ def detect_course_subtype(text: str) -> Optional[str]:
245
  return "weekend"
246
  if any(x in t for x in ["online", "اونلاين", "اون لاين", "zoom", "زووم", "من البيت"]):
247
  return "online"
248
- if any(x in t for x in ["children", "kids", "اطفال", "طفل", "سن", "سنين"]):
249
  return "children"
250
 
251
  return None
252
 
253
 
254
- def fallback_customer_service_reply() -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  return (
256
  "تمام — عشان نضمن لك إجابة دقيقة، فريق خدمة العملاء هيقدر يفيدك أكتر.\n"
257
- "تحب أوجّه استفسارك لهم بخصوص أونلاين ولا حضور؟"
258
  )
259
 
260
 
261
- def build_courses_menu_reply() -> str:
 
 
 
 
 
 
262
  return (
263
  "عندنا الأنواع دي من كورسات الألماني:\n"
264
  "Regular / Intensive / Express / Weekend / Online / Children\n"
265
- "تحب تعرف تفاصيل أنهي نوع؟"
266
  )
267
 
268
 
269
- def build_center_info_reply() -> str:
270
  center = KB["center"]
 
 
 
 
 
 
271
  return (
272
  f"ÄDK اتأسس سنة {center['established']}، و{center['branches']}.\n"
273
  f"الكورسات للكبار {center['adults_levels']}، وللأطفال {center['children_levels']}.\n"
274
  f"وكمان هو {center['goethe']}.\n"
275
- "لو عندك سؤال محدد، أقدر أوصله لخدمة العملاء."
276
  )
277
 
278
 
279
- def build_course_details_reply(course_key: str) -> str:
280
- course = KB["courses"][course_key]
281
- details = course["details"]
282
 
283
  if course_key == "express":
 
 
 
 
 
 
 
 
 
 
 
 
284
  return (
285
- "الـ Express مناسب لو عايز تتعلم بسرعة.\n"
286
  f"{details[0]}، و{details[1]}.\n"
287
  f"ممكن تحجز Stage كاملة، والنظام {details[3]}، {details[4]}.\n"
288
  f"مدة الـ stage {details[5].replace('مدة الـ stage ', '')}.\n"
289
- "تحب تعرف الفرق بينه وبين الـ Intensive؟"
290
  )
291
 
292
  if course_key == "intensive":
 
 
 
 
 
 
293
  return (
294
  "الـ Intensive ممكن تحجز فيه Stage كاملة من A1 لحد B1.\n"
295
  f"النظام {details[1]}، و{details[2]}.\n"
296
  f"مدة الـ stage {details[3].replace('مدة الـ stage ', '')}.\n"
297
- "تحب أقارن لك بينه وبين الـ Regular؟"
298
  )
299
 
300
  if course_key == "regular":
 
 
 
 
 
 
 
 
 
 
 
 
301
  return (
302
- "الـ Regular مناسب لو تحب تمشي level level.\n"
303
  f"ممكن تحجز كل level لوحده من A1.1 لحد B1.3.\n"
304
  f"النظام {details[1]}، و{details[2]}.\n"
305
  f"مدة الكورس {details[3].replace('مدة الكورس ', '')}.\n"
306
- "تحب تعرف لو ده أنسب لك ولا الـ Intensive؟"
307
  )
308
 
309
  if course_key == "weekend":
 
 
 
 
 
 
310
  return (
311
  "الـ Weekend مناسب لو إنت مشغول خلال الأسبوع بسبب شغل أو دراسة.\n"
312
  "وبيمشي بنفس outline وساعات الـ Regular.\n"
313
- "تحب حضور في الويكند ولا كنت بتدور على أونلاين؟"
314
  )
315
 
316
  if course_key == "online":
 
 
 
 
 
 
317
  return (
318
  "عندنا كورسات أونلاين، والحضور بيكون online.\n"
319
- "لو مناسب لك النظام ده، أقدر أساعدك تختار النوع الأقرب لاحتياجك.\n"
320
- "تحب أقولك على الأنواع المتاحة؟"
321
  )
322
 
323
  if course_key == "children":
 
 
 
 
 
 
324
  return (
325
  "عندنا كورسات أطفال من سن 5 لحد 15 سنة.\n"
326
  "الهدف منها تنمية مهارات الطفل واللغة.\n"
327
  "وكمان بتشجع التفكير الحر والنقدي والاستقلالية.\n"
328
- "تحب أعرفك على نظام الأطفال بشكل أقرب؟"
329
  )
330
 
331
- return fallback_customer_service_reply()
332
 
333
 
334
- def sanitize_reply(text: str) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
  if not text or not text.strip():
336
- return fallback_customer_service_reply()
337
 
338
  text = text.strip()
339
 
@@ -359,8 +590,8 @@ def sanitize_reply(text: str) -> str:
359
  text = "\n".join(lines)
360
 
361
  english_words = re.findall(r"[A-Za-z]{4,}", text)
362
- if len(english_words) > 8:
363
- return fallback_customer_service_reply()
364
 
365
  blocked_patterns = [
366
  r"\b\d+\s*جنيه\b",
@@ -377,65 +608,88 @@ def sanitize_reply(text: str) -> str:
377
  lowered = normalize_arabic(text)
378
  for pattern in blocked_patterns:
379
  if re.search(pattern, lowered, flags=re.IGNORECASE):
380
- return fallback_customer_service_reply()
381
 
382
  return text
383
 
384
 
385
- def generate_rule_based_reply(intent: str, user_text: str) -> str:
 
 
 
 
386
  subtype = detect_course_subtype(user_text)
387
 
388
  if intent == "GREETING":
 
 
 
 
 
 
 
 
 
 
 
 
389
  return (
390
- "أهلا بيك في ÄDK.\n"
391
  "أقدر أساعدك في أنواع الكورسات، تفاصيل نوع معين، أو معلومات عن المركز.\n"
392
- "تحب تبدأ بإيه؟"
393
  )
394
 
395
  if intent == "HUMAN_AGENT":
 
 
 
 
 
 
396
  return (
397
  "تمام — هسجّل استفسارك لفريق خدمة العملاء عشان يردوا عليك بشكل أدق.\n"
398
- "تحب استفسارك يكون عن أونلاين ولا حضور؟"
399
  )
400
 
401
  if intent == "COURSES_MENU":
402
- return build_courses_menu_reply()
403
 
404
  if intent == "CENTER_INFO":
405
- return build_center_info_reply()
406
 
407
  if intent == "ONLINE_COURSES":
408
- return build_course_details_reply("online")
409
 
410
  if intent == "WEEKEND_COURSES":
411
- return build_course_details_reply("weekend")
412
 
413
  if intent == "CHILDREN_COURSES":
414
- return build_course_details_reply("children")
415
 
416
  if intent == "COURSE_TYPE_DETAILS":
417
  if subtype in ["express", "intensive", "regular"]:
418
- return build_course_details_reply(subtype)
419
- return build_courses_menu_reply()
420
 
421
  return ""
422
 
423
 
424
  # =========================
425
- # LLM Fallback (ONLY for OTHER)
426
  # =========================
427
 
428
- def build_system_prompt() -> str:
429
  return """
430
  أنت مساعد واتساب رسمي لمركز ÄDK.
431
- المطلوب:
432
- - ترد بالعربية المصرية بشكل مهذب ورسمي وخفيف.
433
- - الرد يكون قصير جدًا: من 2 إلى 4 سطور.
434
- - ممنوع اختراع أي معلومة غير الموجودة هنا.
435
- - ممنوع ذكر أسعار أو مواعيد أو عناوين أو أرقام فروع.
436
- - لو السؤال خارج المعلومات المتاحة، استخدم النص التالي حرفيًا:
437
- "تمام عشان نضمن لك إجابة دقيقة، فريق خدمة العملاء هيقدر يفيدك أكتر.
438
- تحب أوجّه استفسارك لهم بخصوص أونلاين ولا حضور؟"
 
439
 
440
  المعلومات المتاحة فقط:
441
  1) المركز:
@@ -455,9 +709,9 @@ def build_system_prompt() -> str:
455
  """.strip()
456
 
457
 
458
- def process_with_llm(user_text: str) -> str:
459
  if not api_key:
460
- return fallback_customer_service_reply()
461
 
462
  client = openai.OpenAI(api_key=api_key, base_url=base_url)
463
 
@@ -465,20 +719,70 @@ def process_with_llm(user_text: str) -> str:
465
  resp = client.chat.completions.create(
466
  model=model_link,
467
  messages=[
468
- {"role": "system", "content": build_system_prompt()},
469
  {"role": "user", "content": user_text},
470
  ],
471
  max_tokens=120,
472
- temperature=0.0,
473
- top_p=0.3,
474
  stream=False,
475
  )
476
 
477
  answer = resp.choices[0].message.content.strip()
478
- return sanitize_reply(answer)
479
 
480
  except Exception:
481
- return fallback_customer_service_reply()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
482
 
483
 
484
  # =========================
@@ -487,23 +791,56 @@ def process_with_llm(user_text: str) -> str:
487
 
488
  def process_text(user_text: str):
489
  user_text = (user_text or "").strip()
 
490
 
491
  if not user_text:
492
  return {
493
  "ok": True,
494
  "intent": "OTHER",
 
 
495
  "reply": "أرسل سؤالك عن الكورسات أو عن المركز، وأنا أساعدك.",
496
  }
497
 
498
  intent = detect_intent(user_text)
499
 
500
- answer = generate_rule_based_reply(intent, user_text)
 
501
  if answer:
502
- answer = sanitize_reply(answer)
503
- return {"ok": True, "intent": intent, "reply": answer}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
504
 
505
- llm_answer = process_with_llm(user_text)
506
- return {"ok": True, "intent": intent, "reply": llm_answer}
 
 
 
 
 
 
 
507
 
508
 
509
  # =========================
 
147
  return text
148
 
149
 
150
+ def detect_gender(text: str) -> Optional[str]:
151
+ """
152
+ Returns:
153
+ - 'male'
154
+ - 'female'
155
+ - None
156
+ """
157
+ t = normalize_arabic(text)
158
+
159
+ female_markers = [
160
+ "انا مهتمه",
161
+ "مهتمه",
162
+ "عايزه",
163
+ "عاوزه",
164
+ "عاوزة",
165
+ "حابه",
166
+ "حابة",
167
+ "محتاجه",
168
+ "محتاجة",
169
+ "عايزه اقدم",
170
+ "عايزه اعرف",
171
+ "عايزه اسال",
172
+ "عايزة",
173
+ "عايزة اعرف",
174
+ "عايزة اقدم",
175
+ "عايزة اسال",
176
+ "انا ام",
177
+ "انا ماما",
178
+ "انا والده",
179
+ "بنتي",
180
+ "لبنتي",
181
+ ]
182
+
183
+ male_markers = [
184
+ "انا مهتم",
185
+ "مهتم",
186
+ "عايز",
187
+ "حابب",
188
+ "محتاج",
189
+ "عايز اقدم",
190
+ "عايز اعرف",
191
+ "عايز اسال",
192
+ "انا اب",
193
+ "انا بابا",
194
+ "انا والد",
195
+ "ابني",
196
+ "لابني",
197
+ ]
198
+
199
+ # female first to avoid "مهتم" matching inside "مهتمه"
200
+ for marker in female_markers:
201
+ if marker in t:
202
+ return "female"
203
+
204
+ for marker in male_markers:
205
+ if marker in t:
206
+ return "male"
207
+
208
+ return None
209
+
210
+
211
+ def choose_variant(gender: Optional[str], male_text: str, female_text: str, neutral_text: str) -> str:
212
+ if gender == "male":
213
+ return male_text
214
+ if gender == "female":
215
+ return female_text
216
+ return neutral_text
217
+
218
+
219
  def detect_intent(text: str) -> str:
220
  t = normalize_arabic(text)
221
 
 
229
  "حد يرد",
230
  "اكلم",
231
  "اتواصل",
 
 
232
  "support",
233
  "agent",
234
  ]
 
242
  if any(x in t for x in ["weekend", "ويكند", "الجمعة", "السبت", "عطله", "عطلة"]):
243
  return "WEEKEND_COURSES"
244
 
245
+ if any(x in t for x in ["children", "kids", "اطفال", "طفل", "سنين", "سن كام", "سن قد ايه"]):
246
  return "CHILDREN_COURSES"
247
 
248
  if any(x in t for x in ["express", "اكسبريس", "سريع", "super intensive"]):
 
261
  "المركز",
262
  "adk",
263
  "ädk",
 
 
264
  "اتاسس",
265
  "اتأسس",
266
  "تاريخ",
267
  "goethe",
268
  "معتمد",
269
+ "نبذه",
270
+ "نبذة",
271
  ]
272
  ):
273
  return "CENTER_INFO"
 
282
  "الكورسات المتاحه",
283
  "الكورسات المتاحة",
284
  "عايز كورس",
285
+ "عايزه كورس",
286
  "عايز اتعلم",
287
+ "عايزه اتعلم",
288
  "الماني عندكم",
289
  "ألماني",
290
  "الماني",
 
314
  return "weekend"
315
  if any(x in t for x in ["online", "اونلاين", "اون لاين", "zoom", "زووم", "من البيت"]):
316
  return "online"
317
+ if any(x in t for x in ["children", "kids", "اطفال", "طفل", "سنين", "سن كام", "سن قد ايه"]):
318
  return "children"
319
 
320
  return None
321
 
322
 
323
+ def detect_other_subtype(text: str) -> Optional[str]:
324
+ t = normalize_arabic(text)
325
+
326
+ if any(x in t for x in ["العنوان", "مكانكم فين", "مكانكو فين", "فين المكان", "لوكيشن", "الموقع فين"]):
327
+ return "ADDRESS_QUERY"
328
+
329
+ if any(x in t for x in ["مواعيد", "ميعاد", "الميعاد", "امتى", "متى", "الفتره", "الفترة", "صباحي", "مسائي", "بعد الظهر", "بالليل"]):
330
+ return "SCHEDULE_QUERY"
331
+
332
+ if any(x in t for x in ["التقديم", "اقدم", "اقدملكم", "اسجل", "اسجل ازاي", "التسجيل", "التحاق", "احجز", "الحجز"]):
333
+ return "REGISTRATION_QUERY"
334
+
335
+ if any(x in t for x in ["السعر", "الاسعار", "الأسعار", "بكام", "كم", "التكلفه", "التكلفة", "الرسوم"]):
336
+ return "PRICE_QUERY"
337
+
338
+ if any(x in t for x in ["فروع", "فرع", "branch", "branches"]):
339
+ return "BRANCH_QUERY"
340
+
341
+ if any(x in t for x in ["رقم", "واتساب", "تليفون", "اتواصل", "التواصل", "اكلم"]):
342
+ return "CONTACT_QUERY"
343
+
344
+ return None
345
+
346
+
347
+ def fallback_customer_service_reply(gender: Optional[str] = None) -> str:
348
+ line2 = choose_variant(
349
+ gender,
350
+ "تحب أوجّه استفسارك لفريق خدمة العملاء؟",
351
+ "تحبي أوجّه استفسارك لفريق خدمة العملاء؟",
352
+ "تحب أو تحبي أوجّه استفسارك لفريق خدمة العملاء؟"
353
+ )
354
  return (
355
  "تمام — عشان نضمن لك إجابة دقيقة، فريق خدمة العملاء هيقدر يفيدك أكتر.\n"
356
+ f"{line2}"
357
  )
358
 
359
 
360
+ def build_courses_menu_reply(gender: Optional[str] = None) -> str:
361
+ ask_line = choose_variant(
362
+ gender,
363
+ "تحب تعرف تفاصيل أنهي نوع؟",
364
+ "تحبي تعرفي تفاصيل أنهي نوع؟",
365
+ "تحب تعرف تفاصيل أنهي نوع؟"
366
+ )
367
  return (
368
  "عندنا الأنواع دي من كورسات الألماني:\n"
369
  "Regular / Intensive / Express / Weekend / Online / Children\n"
370
+ f"{ask_line}"
371
  )
372
 
373
 
374
+ def build_center_info_reply(gender: Optional[str] = None) -> str:
375
  center = KB["center"]
376
+ closing = choose_variant(
377
+ gender,
378
+ "لو عندك سؤال محدد، أقدر أوصله لخدمة العملاء.",
379
+ "لو عندك سؤال محدد، أقدر أوصله لخدمة العملاء.",
380
+ "لو عندك سؤال محدد، أقدر أوصله لخدمة العملاء."
381
+ )
382
  return (
383
  f"ÄDK اتأسس سنة {center['established']}، و{center['branches']}.\n"
384
  f"الكورسات للكبار {center['adults_levels']}، وللأطفال {center['children_levels']}.\n"
385
  f"وكمان هو {center['goethe']}.\n"
386
+ f"{closing}"
387
  )
388
 
389
 
390
+ def build_course_details_reply(course_key: str, gender: Optional[str] = None) -> str:
391
+ details = KB["courses"][course_key]["details"]
 
392
 
393
  if course_key == "express":
394
+ ask_line = choose_variant(
395
+ gender,
396
+ "تحب تعرف الفرق بينه وبين الـ Intensive؟",
397
+ "تحبي تعرفي الفرق بينه وبين الـ Intensive؟",
398
+ "تحب تعرف الفرق بينه وبين الـ Intensive؟"
399
+ )
400
+ start_line = choose_variant(
401
+ gender,
402
+ "الـ Express مناسب لو عايز تتعلم بسرعة.",
403
+ "الـ Express مناسب لو عايزة تتعلمي بسرعة.",
404
+ "الـ Express مناسب لو حابب أو حابة تتعلم بسرعة."
405
+ )
406
  return (
407
+ f"{start_line}\n"
408
  f"{details[0]}، و{details[1]}.\n"
409
  f"ممكن تحجز Stage كاملة، والنظام {details[3]}، {details[4]}.\n"
410
  f"مدة الـ stage {details[5].replace('مدة الـ stage ', '')}.\n"
411
+ f"{ask_line}"
412
  )
413
 
414
  if course_key == "intensive":
415
+ ask_line = choose_variant(
416
+ gender,
417
+ "تحب أقارن لك بينه وبين الـ Regular؟",
418
+ "تحبي أقارن لك بينه وبين الـ Regular؟",
419
+ "تحب أقارن لك بينه وبين الـ Regular؟"
420
+ )
421
  return (
422
  "الـ Intensive ممكن تحجز فيه Stage كاملة من A1 لحد B1.\n"
423
  f"النظام {details[1]}، و{details[2]}.\n"
424
  f"مدة الـ stage {details[3].replace('مدة الـ stage ', '')}.\n"
425
+ f"{ask_line}"
426
  )
427
 
428
  if course_key == "regular":
429
+ ask_line = choose_variant(
430
+ gender,
431
+ "تحب تعرف لو ده أنسب لك ولا الـ Intensive؟",
432
+ "تحبي تعرفي لو ده أنسب لك ولا الـ Intensive؟",
433
+ "تحب تعرف لو ده أنسب لك ولا الـ Intensive؟"
434
+ )
435
+ start_line = choose_variant(
436
+ gender,
437
+ "الـ Regular مناسب لو تحب تمشي level level.",
438
+ "الـ Regular مناسب لو تحبي تمشي level level.",
439
+ "الـ Regular مناسب لو حابب أو حابة تمشي level level."
440
+ )
441
  return (
442
+ f"{start_line}\n"
443
  f"ممكن تحجز كل level لوحده من A1.1 لحد B1.3.\n"
444
  f"النظام {details[1]}، و{details[2]}.\n"
445
  f"مدة الكورس {details[3].replace('مدة الكورس ', '')}.\n"
446
+ f"{ask_line}"
447
  )
448
 
449
  if course_key == "weekend":
450
+ ask_line = choose_variant(
451
+ gender,
452
+ "تحب حضور في الويكند ولا كنت بتفكر في أونلاين؟",
453
+ "تحبي حضور في الويكند ولا كنتِ بتفكري في أونلاين؟",
454
+ "تحب حضور في الويكند ولا كنت بتفكر في أونلاين؟"
455
+ )
456
  return (
457
  "الـ Weekend مناسب لو إنت مشغول خلال الأسبوع بسبب شغل أو دراسة.\n"
458
  "وبيمشي بنفس outline وساعات الـ Regular.\n"
459
+ f"{ask_line}"
460
  )
461
 
462
  if course_key == "online":
463
+ ask_line = choose_variant(
464
+ gender,
465
+ "تحب أقولك على الأنواع المتاحة؟",
466
+ "تحبي أقولك على الأنواع المتاحة؟",
467
+ "تحب أقولك على الأنواع المتاحة؟"
468
+ )
469
  return (
470
  "عندنا كورسات أونلاين، والحضور بيكون online.\n"
471
+ "ولو مناسب لك النظام ده، أقدر أساعدك تختار النوع الأقرب لاحتياجك.\n"
472
+ f"{ask_line}"
473
  )
474
 
475
  if course_key == "children":
476
+ ask_line = choose_variant(
477
+ gender,
478
+ "تحب أعرفك على نظام الأطفال بشكل أقرب؟",
479
+ "تحبي أعرفك على نظام الأطفال بشكل أقرب؟",
480
+ "تحب أعرفك على نظام الأطفال بشكل أقرب؟"
481
+ )
482
  return (
483
  "عندنا كورسات أطفال من سن 5 لحد 15 سنة.\n"
484
  "الهدف منها تنمية مهارات الطفل واللغة.\n"
485
  "وكمان بتشجع التفكير الحر والنقدي والاستقلالية.\n"
486
+ f"{ask_line}"
487
  )
488
 
489
+ return fallback_customer_service_reply(gender)
490
 
491
 
492
+ def generate_other_reply(user_text: str, gender: Optional[str] = None) -> str:
493
+ subtype = detect_other_subtype(user_text)
494
+
495
+ if subtype == "ADDRESS_QUERY":
496
+ ask_line = choose_variant(
497
+ gender,
498
+ "تحب أبلّغهم إنك مهتم بالحضور؟",
499
+ "تحبي أبلّغهم إنك مهتمة بالحضور؟",
500
+ "تحب أبلّغهم إنك مهتم أو مهتمة بالحضور؟"
501
+ )
502
+ return (
503
+ "تمام، بالنسبة للعنوان ده بيكون خاص بالكورسات الحضوري.\n"
504
+ "أقدر أوصّل استفسارك لفريق خدمة العملاء عشان يبعثوا لك الفرع المناسب.\n"
505
+ f"{ask_line}"
506
+ )
507
+
508
+ if subtype == "SCHEDULE_QUERY":
509
+ line2 = choose_variant(
510
+ gender,
511
+ "قولّي تفضّل أونلاين ولا حضور، وصباحي ولا مسائي، وأنا أوجّه استفسارك بشكل أوضح لخدمة العملاء.",
512
+ "قولّي تفضّلي أونلاين ولا حضور، وصباحي ولا مسائي، وأنا أوجّه استفسارك بشكل أوضح لخدمة العملاء.",
513
+ "قولّي تفضّل أونلاين ولا حضور، وصباحي ولا مسائي، وأنا أوجّه استفسارك بشكل أوضح لخدمة العملاء."
514
+ )
515
+ return (
516
+ "تمام، المواعيد بتختلف حسب إذا كنت مهتم بأونلاين أو حضور، وكمان حسب الفترة المناسبة لك.\n"
517
+ f"{line2}"
518
+ )
519
+
520
+ if subtype == "REGISTRATION_QUERY":
521
+ ask_line = choose_variant(
522
+ gender,
523
+ "تحب التقديم لكورس أونلاين ولا حضور؟",
524
+ "تحبي التقديم لكورس أونلاين ولا حضور؟",
525
+ "تحب التقديم لكورس أونلاين ولا حضور؟"
526
+ )
527
+ return (
528
+ "تمام، أقدر أوجّه استفسارك لفريق خدمة العملاء عشان يوضحوا لك خطوات التقديم أو التسجيل بشكل دقيق.\n"
529
+ f"{ask_line}"
530
+ )
531
+
532
+ if subtype == "PRICE_QUERY":
533
+ line2 = choose_variant(
534
+ gender,
535
+ "قولّي مهتم بأنهي نوع كورس، وأنا أوجّه استفسارك لخدمة العملاء بشكل أوضح.",
536
+ "قولّي مهتمة بأنهي نوع كورس، وأنا أوجّه استفسارك لخدمة العملاء بشكل أوضح.",
537
+ "قولّي مهتم بأنهي نوع كورس، وأنا أوجّه استفسارك لخدمة العملاء بشكل أوضح."
538
+ )
539
+ return (
540
+ "تمام، الأسعار بتختلف حسب نوع الكورس والنظام المناسب لك.\n"
541
+ f"{line2}"
542
+ )
543
+
544
+ if subtype == "BRANCH_QUERY":
545
+ ask_line = choose_variant(
546
+ gender,
547
+ "تحب أبلّغهم إنك مهتم بكورس حضور؟",
548
+ "تحبي أبلّغهم إنك مهتمة بكورس حضور؟",
549
+ "تحب أبلّغهم إنك مهتم بكورس حضور؟"
550
+ )
551
+ return (
552
+ "تمام، أقدر أوجّه استفسارك لفريق خدمة العملاء عشان يوضحوا لك الفرع المناسب.\n"
553
+ f"{ask_line}"
554
+ )
555
+
556
+ if subtype == "CONTACT_QUERY":
557
+ return (
558
+ "تمام، أقدر أوصّل استفسارك لفريق خدمة العملاء.\n"
559
+ "قولّي بس سؤالك عن الكورسات، التقديم، المواعيد، ولا الحضور؟"
560
+ )
561
+
562
+ return fallback_customer_service_reply(gender)
563
+
564
+
565
+ def sanitize_reply(text: str, gender: Optional[str] = None) -> str:
566
  if not text or not text.strip():
567
+ return fallback_customer_service_reply(gender)
568
 
569
  text = text.strip()
570
 
 
590
  text = "\n".join(lines)
591
 
592
  english_words = re.findall(r"[A-Za-z]{4,}", text)
593
+ if len(english_words) > 10:
594
+ return fallback_customer_service_reply(gender)
595
 
596
  blocked_patterns = [
597
  r"\b\d+\s*جنيه\b",
 
608
  lowered = normalize_arabic(text)
609
  for pattern in blocked_patterns:
610
  if re.search(pattern, lowered, flags=re.IGNORECASE):
611
+ return fallback_customer_service_reply(gender)
612
 
613
  return text
614
 
615
 
616
+ # =========================
617
+ # Rule-based replies
618
+ # =========================
619
+
620
+ def generate_rule_based_reply(intent: str, user_text: str, gender: Optional[str] = None) -> str:
621
  subtype = detect_course_subtype(user_text)
622
 
623
  if intent == "GREETING":
624
+ greet_line = choose_variant(
625
+ gender,
626
+ "أهلا بيك في ÄDK.",
627
+ "أهلا بيكي في ÄDK.",
628
+ "أهلا بيك في ÄDK."
629
+ )
630
+ ask_line = choose_variant(
631
+ gender,
632
+ "تحب تبدأ بإيه؟",
633
+ "تحبي تبدئي بإيه؟",
634
+ "تحب تبدأ بإيه؟"
635
+ )
636
  return (
637
+ f"{greet_line}\n"
638
  "أقدر أساعدك في أنواع الكورسات، تفاصيل نوع معين، أو معلومات عن المركز.\n"
639
+ f"{ask_line}"
640
  )
641
 
642
  if intent == "HUMAN_AGENT":
643
+ ask_line = choose_variant(
644
+ gender,
645
+ "تحب استفسارك يكون عن أونلاين ولا حضور؟",
646
+ "تحبي استفسارك يكون عن أونلاين ولا حضور؟",
647
+ "تحب استفسارك يكون عن أونلاين ولا حضور؟"
648
+ )
649
  return (
650
  "تمام — هسجّل استفسارك لفريق خدمة العملاء عشان يردوا عليك بشكل أدق.\n"
651
+ f"{ask_line}"
652
  )
653
 
654
  if intent == "COURSES_MENU":
655
+ return build_courses_menu_reply(gender)
656
 
657
  if intent == "CENTER_INFO":
658
+ return build_center_info_reply(gender)
659
 
660
  if intent == "ONLINE_COURSES":
661
+ return build_course_details_reply("online", gender)
662
 
663
  if intent == "WEEKEND_COURSES":
664
+ return build_course_details_reply("weekend", gender)
665
 
666
  if intent == "CHILDREN_COURSES":
667
+ return build_course_details_reply("children", gender)
668
 
669
  if intent == "COURSE_TYPE_DETAILS":
670
  if subtype in ["express", "intensive", "regular"]:
671
+ return build_course_details_reply(subtype, gender)
672
+ return build_courses_menu_reply(gender)
673
 
674
  return ""
675
 
676
 
677
  # =========================
678
+ # LLM Helpers
679
  # =========================
680
 
681
+ def build_llm_fallback_system_prompt() -> str:
682
  return """
683
  أنت مساعد واتساب رسمي لمركز ÄDK.
684
+
685
+ التعليمات:
686
+ - رد بالعربية المصرية بشكل مهذب ورسمي وخفيف.
687
+ - الرد يكون من 2 إلى 4 سطور فقط.
688
+ - لا تضف أي معلومة غير موجودة في المعلومات المتاحة.
689
+ - ممنوع ذكر أسعار فعلية أو مواعيد فعلية أو عناوين فعلية أو أرقام فروع.
690
+ - لو السؤال خارج المعلومات المتاحة، اقترح بشكل منطقي توجيه الاستفسار لفريق خدمة العملاء.
691
+ - لا تذكر أنك نموذج ذكاء اصطناعي.
692
+ - لا تكتب مقدمات طويلة.
693
 
694
  المعلومات المتاحة فقط:
695
  1) المركز:
 
709
  """.strip()
710
 
711
 
712
+ def process_with_llm(user_text: str, gender: Optional[str] = None) -> str:
713
  if not api_key:
714
+ return fallback_customer_service_reply(gender)
715
 
716
  client = openai.OpenAI(api_key=api_key, base_url=base_url)
717
 
 
719
  resp = client.chat.completions.create(
720
  model=model_link,
721
  messages=[
722
+ {"role": "system", "content": build_llm_fallback_system_prompt()},
723
  {"role": "user", "content": user_text},
724
  ],
725
  max_tokens=120,
726
+ temperature=0.1,
727
+ top_p=0.4,
728
  stream=False,
729
  )
730
 
731
  answer = resp.choices[0].message.content.strip()
732
+ return sanitize_reply(answer, gender)
733
 
734
  except Exception:
735
+ return fallback_customer_service_reply(gender)
736
+
737
+
738
+ def rewrite_reply_with_llm(reply: str, gender: Optional[str] = None) -> str:
739
+ """
740
+ LLM participates here only as a rewriter, not as a source of truth.
741
+ """
742
+ if not api_key or not reply:
743
+ return reply
744
+
745
+ client = openai.OpenAI(api_key=api_key, base_url=base_url)
746
+
747
+ gender_hint = "غير محدد"
748
+ if gender == "male":
749
+ gender_hint = "مذكر"
750
+ elif gender == "female":
751
+ gender_hint = "مؤنث"
752
+
753
+ prompt = f"""
754
+ أعد صياغة الرسالة التالية بالعربية المصرية بشكل طبيعي ولطيف ورسمي قليلًا.
755
+
756
+ قواعد مهمة جدًا:
757
+ - لا تضف أي معلومة جديدة
758
+ - لا تغير المعنى
759
+ - لا تضف أسعار أو مواعيد أو عناوين أو أرقام
760
+ - حافظ على الرد قصير: من 2 إلى 5 سطور
761
+ - لو كانت الصياغة الحالية جيدة، يمكنك إرجاعها كما هي
762
+ - حاول مراعاة الجنس اللغوي إن كان واضحًا: {gender_hint}
763
+
764
+ الرسالة:
765
+ {reply}
766
+ """.strip()
767
+
768
+ try:
769
+ resp = client.chat.completions.create(
770
+ model=model_link,
771
+ messages=[
772
+ {"role": "system", "content": "You rewrite Arabic business chat replies without adding information."},
773
+ {"role": "user", "content": prompt},
774
+ ],
775
+ max_tokens=140,
776
+ temperature=0.2,
777
+ top_p=0.5,
778
+ stream=False,
779
+ )
780
+
781
+ enhanced = resp.choices[0].message.content.strip()
782
+ return sanitize_reply(enhanced, gender)
783
+
784
+ except Exception:
785
+ return reply
786
 
787
 
788
  # =========================
 
791
 
792
  def process_text(user_text: str):
793
  user_text = (user_text or "").strip()
794
+ gender = detect_gender(user_text)
795
 
796
  if not user_text:
797
  return {
798
  "ok": True,
799
  "intent": "OTHER",
800
+ "gender": gender,
801
+ "source": "rule_based",
802
  "reply": "أرسل سؤالك عن الكورسات أو عن المركز، وأنا أساعدك.",
803
  }
804
 
805
  intent = detect_intent(user_text)
806
 
807
+ # 1) Known intents -> rule-based
808
+ answer = generate_rule_based_reply(intent, user_text, gender)
809
  if answer:
810
+ answer = sanitize_reply(answer, gender)
811
+ answer = rewrite_reply_with_llm(answer, gender)
812
+ answer = sanitize_reply(answer, gender)
813
+ return {
814
+ "ok": True,
815
+ "intent": intent,
816
+ "gender": gender,
817
+ "source": "rule_based+llm_rewrite",
818
+ "reply": answer
819
+ }
820
+
821
+ # 2) Smart fallback for OTHER
822
+ smart_other_answer = generate_other_reply(user_text, gender)
823
+ if smart_other_answer:
824
+ smart_other_answer = sanitize_reply(smart_other_answer, gender)
825
+ smart_other_answer = rewrite_reply_with_llm(smart_other_answer, gender)
826
+ smart_other_answer = sanitize_reply(smart_other_answer, gender)
827
+ return {
828
+ "ok": True,
829
+ "intent": intent,
830
+ "gender": gender,
831
+ "source": "smart_fallback+llm_rewrite",
832
+ "reply": smart_other_answer
833
+ }
834
 
835
+ # 3) Last resort LLM
836
+ llm_answer = process_with_llm(user_text, gender)
837
+ return {
838
+ "ok": True,
839
+ "intent": intent,
840
+ "gender": gender,
841
+ "source": "llm_fallback",
842
+ "reply": llm_answer
843
+ }
844
 
845
 
846
  # =========================