Update app.py
Browse files
app.py
CHANGED
|
@@ -156,28 +156,21 @@ def detect_gender(text: str) -> Optional[str]:
|
|
| 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 = [
|
|
@@ -186,17 +179,11 @@ def detect_gender(text: str) -> Optional[str]:
|
|
| 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"
|
|
@@ -373,17 +360,11 @@ def build_courses_menu_reply(gender: Optional[str] = None) -> str:
|
|
| 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 |
-
|
| 387 |
)
|
| 388 |
|
| 389 |
|
|
@@ -440,7 +421,7 @@ def build_course_details_reply(course_key: str, gender: Optional[str] = None) ->
|
|
| 440 |
)
|
| 441 |
return (
|
| 442 |
f"{start_line}\n"
|
| 443 |
-
|
| 444 |
f"النظام {details[1]}، و{details[2]}.\n"
|
| 445 |
f"مدة الكورس {details[3].replace('مدة الكورس ', '')}.\n"
|
| 446 |
f"{ask_line}"
|
|
@@ -500,22 +481,25 @@ def generate_other_reply(user_text: str, gender: Optional[str] = None) -> str:
|
|
| 500 |
"تحب أبلّغهم إنك مهتم أو مهتمة بالحضور؟"
|
| 501 |
)
|
| 502 |
return (
|
| 503 |
-
"تمام، بالنسبة للعنوان ده
|
| 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(
|
|
@@ -559,7 +543,7 @@ def generate_other_reply(user_text: str, gender: Optional[str] = None) -> str:
|
|
| 559 |
"قولّي بس سؤالك عن الكورسات، التقديم، المواعيد، ولا الحضور؟"
|
| 560 |
)
|
| 561 |
|
| 562 |
-
return
|
| 563 |
|
| 564 |
|
| 565 |
def sanitize_reply(text: str, gender: Optional[str] = None) -> str:
|
|
@@ -578,6 +562,8 @@ def sanitize_reply(text: str, gender: Optional[str] = None) -> str:
|
|
| 578 |
"يمكنني": "أقدر",
|
| 579 |
"العميل": "إنت",
|
| 580 |
"المستخدم": "إنت",
|
|
|
|
|
|
|
| 581 |
}
|
| 582 |
|
| 583 |
for old, new in replacements.items():
|
|
@@ -593,23 +579,6 @@ def sanitize_reply(text: str, gender: Optional[str] = None) -> str:
|
|
| 593 |
if len(english_words) > 10:
|
| 594 |
return fallback_customer_service_reply(gender)
|
| 595 |
|
| 596 |
-
blocked_patterns = [
|
| 597 |
-
r"\b\d+\s*جنيه\b",
|
| 598 |
-
r"\b\d+\s*egp\b",
|
| 599 |
-
r"\bالعنوان\b",
|
| 600 |
-
r"\bالمعاد\b",
|
| 601 |
-
r"\bالساعة\b",
|
| 602 |
-
r"\bالسعر\b",
|
| 603 |
-
r"\bالاسعار\b",
|
| 604 |
-
r"\bالأسعار\b",
|
| 605 |
-
r"\bفرع\s+\w+",
|
| 606 |
-
]
|
| 607 |
-
|
| 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 |
|
|
@@ -675,7 +644,7 @@ def generate_rule_based_reply(intent: str, user_text: str, gender: Optional[str]
|
|
| 675 |
|
| 676 |
|
| 677 |
# =========================
|
| 678 |
-
# LLM
|
| 679 |
# =========================
|
| 680 |
|
| 681 |
def build_llm_fallback_system_prompt() -> str:
|
|
@@ -683,13 +652,13 @@ def build_llm_fallback_system_prompt() -> str:
|
|
| 683 |
أنت مساعد واتساب رسمي لمركز ÄDK.
|
| 684 |
|
| 685 |
التعليمات:
|
| 686 |
-
- رد بالعربية المصرية بشكل مهذب و
|
| 687 |
- الرد يكون من 2 إلى 4 سطور فقط.
|
| 688 |
- لا تضف أي معلومة غير موجودة في المعلومات المتاحة.
|
| 689 |
- ممنوع ذكر أسعار فعلية أو مواعيد فعلية أو عناوين فعلية أو أرقام فروع.
|
| 690 |
-
- لو السؤال خارج المعلومات المتاحة، اقترح
|
|
|
|
| 691 |
- لا تذكر أنك نموذج ذكاء اصطناعي.
|
| 692 |
-
- لا تكتب مقدمات طويلة.
|
| 693 |
|
| 694 |
المعلومات المتاحة فقط:
|
| 695 |
1) المركز:
|
|
@@ -735,56 +704,6 @@ def process_with_llm(user_text: str, gender: Optional[str] = None) -> str:
|
|
| 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 |
# =========================
|
| 789 |
# Main Processing
|
| 790 |
# =========================
|
|
@@ -804,35 +723,32 @@ def process_text(user_text: str):
|
|
| 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
|
| 818 |
"reply": answer
|
| 819 |
}
|
| 820 |
|
| 821 |
-
# 2) Smart fallback for OTHER
|
| 822 |
-
|
| 823 |
-
if
|
| 824 |
-
smart_other_answer =
|
| 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
|
| 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,
|
|
|
|
| 156 |
"""
|
| 157 |
t = normalize_arabic(text)
|
| 158 |
|
| 159 |
+
# مهم: ما نستنتجش النوع من "ابني / بنتي" لأنها بتوصف الطفل مش المتكلم
|
| 160 |
female_markers = [
|
| 161 |
"انا مهتمه",
|
| 162 |
"مهتمه",
|
| 163 |
"عايزه",
|
| 164 |
"عاوزه",
|
| 165 |
"عاوزة",
|
| 166 |
+
"عايزة",
|
| 167 |
"حابه",
|
| 168 |
"حابة",
|
| 169 |
"محتاجه",
|
| 170 |
"محتاجة",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
"انا ام",
|
| 172 |
"انا ماما",
|
| 173 |
"انا والده",
|
|
|
|
|
|
|
| 174 |
]
|
| 175 |
|
| 176 |
male_markers = [
|
|
|
|
| 179 |
"عايز",
|
| 180 |
"حابب",
|
| 181 |
"محتاج",
|
|
|
|
|
|
|
|
|
|
| 182 |
"انا اب",
|
| 183 |
"انا بابا",
|
| 184 |
"انا والد",
|
|
|
|
|
|
|
| 185 |
]
|
| 186 |
|
|
|
|
| 187 |
for marker in female_markers:
|
| 188 |
if marker in t:
|
| 189 |
return "female"
|
|
|
|
| 360 |
|
| 361 |
def build_center_info_reply(gender: Optional[str] = None) -> str:
|
| 362 |
center = KB["center"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
return (
|
| 364 |
f"ÄDK اتأسس سنة {center['established']}، و{center['branches']}.\n"
|
| 365 |
f"الكورسات للكبار {center['adults_levels']}، وللأطفال {center['children_levels']}.\n"
|
| 366 |
f"وكمان هو {center['goethe']}.\n"
|
| 367 |
+
"لو عندك سؤال محدد، أقدر أوصله لخدمة العملاء."
|
| 368 |
)
|
| 369 |
|
| 370 |
|
|
|
|
| 421 |
)
|
| 422 |
return (
|
| 423 |
f"{start_line}\n"
|
| 424 |
+
"ممكن تحجز كل level لوحده من A1.1 لحد B1.3.\n"
|
| 425 |
f"النظام {details[1]}، و{details[2]}.\n"
|
| 426 |
f"مدة الكورس {details[3].replace('مدة الكورس ', '')}.\n"
|
| 427 |
f"{ask_line}"
|
|
|
|
| 481 |
"تحب أبلّغهم إنك مهتم أو مهتمة بالحضور؟"
|
| 482 |
)
|
| 483 |
return (
|
| 484 |
+
"تمام، بالنسبة للعنوان ده خاص بالكورسات الحضوري.\n"
|
| 485 |
"أقدر أوصّل استفسارك لفريق خدمة العملاء عشان يبعثوا لك الفرع المناسب.\n"
|
| 486 |
f"{ask_line}"
|
| 487 |
)
|
| 488 |
|
| 489 |
if subtype == "SCHEDULE_QUERY":
|
| 490 |
+
line1 = choose_variant(
|
| 491 |
+
gender,
|
| 492 |
+
"تمام، المواعيد بتختلف حسب إذا كنت مهتم بأونلاين أو حضور، وكمان حسب الفترة المناسبة لك.",
|
| 493 |
+
"تمام، المواعيد بتختلف حسب إذا كنتِ مهتمة بأونلاين أو حضور، وكمان حسب الفترة المناسبة لك.",
|
| 494 |
+
"تمام، المواعيد بتختلف حسب إذا كنت مهتم بأونلاين أو حضور، وكمان حسب الفترة المناسبة لك."
|
| 495 |
+
)
|
| 496 |
line2 = choose_variant(
|
| 497 |
gender,
|
| 498 |
"قولّي تفضّل أونلاين ولا حضور، وصباحي ولا مسائي، وأنا أوجّه استفسارك بشكل أوضح لخدمة العملاء.",
|
| 499 |
"قولّي تفضّلي أونلاين ولا حضور، وصباحي ولا مسائي، وأنا أوجّه استفسارك بشكل أوضح لخدمة العملاء.",
|
| 500 |
"قولّي تفضّل أونلاين ولا حضور، وصباحي ولا مسائي، وأنا أوجّه استفسارك بشكل أوضح لخدمة العملاء."
|
| 501 |
)
|
| 502 |
+
return f"{line1}\n{line2}"
|
|
|
|
|
|
|
|
|
|
| 503 |
|
| 504 |
if subtype == "REGISTRATION_QUERY":
|
| 505 |
ask_line = choose_variant(
|
|
|
|
| 543 |
"قولّي بس سؤالك عن الكورسات، التقديم، المواعيد، ولا الحضور؟"
|
| 544 |
)
|
| 545 |
|
| 546 |
+
return ""
|
| 547 |
|
| 548 |
|
| 549 |
def sanitize_reply(text: str, gender: Optional[str] = None) -> str:
|
|
|
|
| 562 |
"يمكنني": "أقدر",
|
| 563 |
"العميل": "إنت",
|
| 564 |
"المستخدم": "إنت",
|
| 565 |
+
"سأقوم": "هقوم",
|
| 566 |
+
"سوف": "ه",
|
| 567 |
}
|
| 568 |
|
| 569 |
for old, new in replacements.items():
|
|
|
|
| 579 |
if len(english_words) > 10:
|
| 580 |
return fallback_customer_service_reply(gender)
|
| 581 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 582 |
return text
|
| 583 |
|
| 584 |
|
|
|
|
| 644 |
|
| 645 |
|
| 646 |
# =========================
|
| 647 |
+
# LLM Fallback only
|
| 648 |
# =========================
|
| 649 |
|
| 650 |
def build_llm_fallback_system_prompt() -> str:
|
|
|
|
| 652 |
أنت مساعد واتساب رسمي لمركز ÄDK.
|
| 653 |
|
| 654 |
التعليمات:
|
| 655 |
+
- رد بالعربية المصرية بشكل مهذب وبسيط.
|
| 656 |
- الرد يكون من 2 إلى 4 سطور فقط.
|
| 657 |
- لا تضف أي معلومة غير موجودة في المعلومات المتاحة.
|
| 658 |
- ممنوع ذكر أسعار فعلية أو مواعيد فعلية أو عناوين فعلية أو أرقام فروع.
|
| 659 |
+
- لو السؤال خارج المعلومات المتاحة، اقترح توجيه الاستفسار لفريق خدمة العملاء بشكل منطقي.
|
| 660 |
+
- لا تكتب بصيغة فصحى شديدة.
|
| 661 |
- لا تذكر أنك نموذج ذكاء اصطناعي.
|
|
|
|
| 662 |
|
| 663 |
المعلومات المتاحة فقط:
|
| 664 |
1) المركز:
|
|
|
|
| 704 |
return fallback_customer_service_reply(gender)
|
| 705 |
|
| 706 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 707 |
# =========================
|
| 708 |
# Main Processing
|
| 709 |
# =========================
|
|
|
|
| 723 |
|
| 724 |
intent = detect_intent(user_text)
|
| 725 |
|
| 726 |
+
# 1) Known intents -> rule-based only
|
| 727 |
answer = generate_rule_based_reply(intent, user_text, gender)
|
| 728 |
if answer:
|
|
|
|
|
|
|
| 729 |
answer = sanitize_reply(answer, gender)
|
| 730 |
return {
|
| 731 |
"ok": True,
|
| 732 |
"intent": intent,
|
| 733 |
"gender": gender,
|
| 734 |
+
"source": "rule_based",
|
| 735 |
"reply": answer
|
| 736 |
}
|
| 737 |
|
| 738 |
+
# 2) Smart fallback for known OTHER subtypes
|
| 739 |
+
other_subtype = detect_other_subtype(user_text)
|
| 740 |
+
if other_subtype:
|
| 741 |
+
smart_other_answer = generate_other_reply(user_text, gender)
|
|
|
|
| 742 |
smart_other_answer = sanitize_reply(smart_other_answer, gender)
|
| 743 |
return {
|
| 744 |
"ok": True,
|
| 745 |
"intent": intent,
|
| 746 |
"gender": gender,
|
| 747 |
+
"source": "smart_fallback",
|
| 748 |
"reply": smart_other_answer
|
| 749 |
}
|
| 750 |
|
| 751 |
+
# 3) Last resort LLM only if nothing matched
|
| 752 |
llm_answer = process_with_llm(user_text, gender)
|
| 753 |
return {
|
| 754 |
"ok": True,
|