from typing import Optional from services.intent_classifier_client import classify_message_with_model def normalize_text(text: str) -> str: return (text or "").strip().lower() def contains_any(text: str, keywords: list) -> bool: return any(k in text for k in keywords) def is_yes(text: str) -> bool: t = normalize_text(text) return t in [ "نعم", "اه", "أه", "ايوه", "أيوه", "yes", "y", "درست", "اه درست", "أيوه درست" ] def is_no(text: str) -> bool: t = normalize_text(text) return t in [ "لا", "لأ", "لاا", "no", "n", "مدرستش", "ما درستش", "لا مدرستش" ] def is_new_student(text: str) -> bool: t = normalize_text(text) return contains_any(t, [ "طالب جديد", "جديد", "عميل جديد", "اول مرة", "أول مرة", "لسه جديد", "مشترك جديد" ]) def is_current_student(text: str) -> bool: t = normalize_text(text) return contains_any(t, [ "طالب حالي", "حالي", "عميل حالي", "مشترك", "مشترك حالي", "أنا طالب", "انا طالب عندكم", "انا مشترك" ]) def is_adults(text: str) -> bool: t = normalize_text(text) return contains_any(t, [ "كبار", "adult", "adults", "الكبار", "كورسات الكبار" ]) def is_children(text: str) -> bool: t = normalize_text(text) return contains_any(t, [ "اطفال", "أطفال", "طفل", "children", "kids", "كورسات الأطفال", "كورسات الاطفال" ]) def is_support_request(text: str) -> bool: t = normalize_text(text) return contains_any(t, [ "استفسار", "سؤال", "عندي سؤال", "مشكلة", "مش فاهم", "عايز اسأل", "عايزة اسأل", "محتاج مساعدة", "محتاجه مساعدة", "support", "خدمة العملاء" ]) def is_next_level_booking(text: str) -> bool: t = normalize_text(text) return contains_any(t, [ "حجز", "احجز", "المستوى التالي", "مستوى تالي", "next level", "احجز المستوى", "حجز مستوى" ]) def is_complaint(text: str) -> bool: t = normalize_text(text) return contains_any(t, [ "شكوى", "اشتكي", "اشتك", "مشكلة كبيرة", "complaint" ]) def wants_direct_support(text: str) -> bool: t = normalize_text(text) return contains_any(t, [ "تواصل", "اكلم", "عايز حد يكلمني", "عايزة حد يكلمني", "عايز اكلم خدمة العملاء", "عايزة اكلم خدمة العملاء" ]) def wants_start(text: str) -> bool: t = normalize_text(text) return contains_any(t, [ "ابدأ", "ابدا", "مساعدة", "مساعده", "start", "menu", "القائمة" ]) def wants_restart(text: str) -> bool: t = normalize_text(text) return contains_any(t, [ "من جديد", "ابدأ من جديد", "restart", "مينيو", "القائمة", "ابدأ" ]) def wants_new_topic(text: str) -> bool: t = normalize_text(text) return contains_any(t, [ "عايز اسال عن حاجة تانية", "عايزة اسال عن حاجة تانية", "استفسار جديد", "موضوع تاني", "حاجة تانية" ]) def wants_courses_info(text: str) -> bool: t = normalize_text(text) return contains_any(t, [ "كورسات", "الكورسات", "ايه الكورسات", "ما هي الكورسات", "الأنواع", "الانواع", "عايز اعرف الكورسات", "عايزة اعرف الكورسات", "ايه الكورسات المتاحة", "الكورسات المتاحة" ]) def asks_about_prior_study_case(text: str) -> bool: t = normalize_text(text) return contains_any(t, [ "لو كنت درست", "لو كنت دارس", "لو درست قبل كده", "طب لو درست", "ولو درست", "اذا كنت درست", "إذا كنت درست", "اختبار تحديد مستوى", "تحديد مستوى" ]) def asks_about_beginner_case(text: str) -> bool: t = normalize_text(text) return contains_any(t, [ "لو مكنتش درست", "لو ما درستش", "لو مدرستش", "لو لسه جديد", "لو مبتدئ", "لو بادئ", "لو اول مرة", "لو أول مرة" ]) def detect_level(text: str) -> Optional[str]: t = normalize_text(text) if contains_any(t, ["1a", "a1", "a1.1", "1 a"]): return "1A" if contains_any(t, ["2a", "a2", "a1.2", "2 a"]): return "2A" if contains_any(t, ["1b", "b1", "b1.1", "1 b"]): return "1B" if contains_any(t, ["1c", "2b", "b2", "1c2/b", "1 c", "2 b"]): return "1C2/B" return None def detect_payment_method(text: str) -> Optional[str]: t = normalize_text(text) if contains_any(t, ["فرع", "فروع", "كاش", "cash"]): return "branch_or_cash" if contains_any(t, ["تحويل", "بنكي", "bank", "transfer"]): return "bank_transfer" if contains_any(t, ["فودافون", "vodafone", "vodafone cash"]): return "vodafone_cash" if contains_any(t, ["فيزا", "visa", "ماستر", "master", "credit card", "card"]): return "card" if contains_any(t, ["تقسيط", "value", "فاليو"]): return "installments" return None def _make_result(kind: str, value: Optional[str], confidence: float, entities: Optional[dict] = None, source: str = "rules", raw_model_output: Optional[str] = None): result = { "kind": kind, "value": value, "confidence": confidence, "entities": entities or {}, "source": source, } if raw_model_output is not None: result["raw_model_output"] = raw_model_output return result def _map_model_intent_to_result(state: str, text: str, model_intent: str, raw_output: str = ""): # ===== Topic switch labels ===== if model_intent == "restart": return _make_result("topic_switch", "restart", 0.90, {}, "model", raw_output) if model_intent == "new_topic": return _make_result("topic_switch", "new_topic", 0.88, {}, "model", raw_output) if model_intent == "complaint": return _make_result("topic_switch", "complaint", 0.92, {}, "model", raw_output) if model_intent == "direct_support": return _make_result("topic_switch", "direct_support", 0.90, {}, "model", raw_output) if model_intent == "courses_info": return _make_result("topic_switch", "courses_info", 0.88, {}, "model", raw_output) if model_intent == "children_courses": return _make_result( "topic_switch", "children_courses", 0.88, {"audience": "children"}, "model", raw_output ) if model_intent == "adults_courses": return _make_result( "topic_switch", "adults_courses", 0.88, {"audience": "adults"}, "model", raw_output ) if model_intent == "new_student": target_kind = "direct_answer" if state == "WAITING_USER_TYPE" else "topic_switch" return _make_result( target_kind, "new_student", 0.90, {"customer_type": "new"}, "model", raw_output ) if model_intent == "current_student": target_kind = "direct_answer" if state == "WAITING_USER_TYPE" else "topic_switch" return _make_result( target_kind, "current_student", 0.90, {"customer_type": "current"}, "model", raw_output ) # ===== Direct answers ===== if model_intent == "adults": return _make_result("direct_answer", "adults", 0.93, {"audience": "adults"}, "model", raw_output) if model_intent == "children": return _make_result("direct_answer", "children", 0.93, {"audience": "children"}, "model", raw_output) if model_intent == "prior_study_yes": return _make_result("direct_answer", "prior_study_yes", 0.94, {"prior_study": True}, "model", raw_output) if model_intent == "prior_study_no": return _make_result("direct_answer", "prior_study_no", 0.94, {"prior_study": False}, "model", raw_output) if model_intent == "confirm_schedule_reviewed": return _make_result("direct_answer", "confirm_schedule_reviewed", 0.90, {}, "model", raw_output) if model_intent == "proceed_booking": return _make_result("direct_answer", "proceed_booking", 0.90, {}, "model", raw_output) if model_intent == "confirm_pdf_reviewed": return _make_result("direct_answer", "confirm_pdf_reviewed", 0.90, {}, "model", raw_output) if model_intent == "confirm_placement_test_reviewed": return _make_result("direct_answer", "confirm_placement_test_reviewed", 0.90, {}, "model", raw_output) if model_intent == "current_student_support": return _make_result("direct_answer", "current_student_support", 0.90, {}, "model", raw_output) if model_intent == "current_student_next_level": return _make_result("direct_answer", "current_student_next_level", 0.90, {}, "model", raw_output) if model_intent == "support_question_text": return _make_result( "direct_answer", "support_question_text", 0.85, {"support_question": text}, "model", raw_output ) if model_intent == "level_selected": level = detect_level(text) if level: return _make_result( "direct_answer", "level_selected", 0.92, {"selected_level": level}, "model", raw_output ) if model_intent == "payment_method_selected": payment_method = detect_payment_method(text) if payment_method: return _make_result( "direct_answer", "payment_method_selected", 0.92, {"payment_method": payment_method}, "model", raw_output ) if model_intent == "complaint_form_submitted": return _make_result("direct_answer", "complaint_form_submitted", 0.90, {}, "model", raw_output) if model_intent == "thanks": return _make_result("direct_answer", "thanks", 0.95, {}, "model", raw_output) # ===== State switches ===== if model_intent == "switch_to_prior_study_true": return _make_result( "state_switch", "switch_to_prior_study_true", 0.90, {"prior_study": True}, "model", raw_output ) if model_intent == "switch_to_prior_study_false": return _make_result( "state_switch", "switch_to_prior_study_false", 0.90, {"prior_study": False}, "model", raw_output ) if model_intent == "support_needed": return _make_result("state_switch", "support_needed", 0.86, {}, "model", raw_output) return None def _fallback_rule_based_classification(state: str, text: str, flow_data: dict | None = None): """ ده اللوجيك القديم كما هو تقريبًا، عشان ما نكسرش أي حاجة. """ flow_data = flow_data or {} t = normalize_text(text) # ===== Global topic switches ===== if wants_restart(t): return _make_result("topic_switch", "restart", 0.99, {}) if wants_new_topic(t): return _make_result("topic_switch", "new_topic", 0.95, {}) if is_complaint(t): return _make_result("topic_switch", "complaint", 0.98, {}) if wants_direct_support(t): return _make_result("topic_switch", "direct_support", 0.95, {}) if wants_courses_info(t): return _make_result("topic_switch", "courses_info", 0.90, {}) if is_children(t): return _make_result("topic_switch", "children_courses", 0.88, {"audience": "children"}) if is_adults(t): return _make_result("topic_switch", "adults_courses", 0.88, {"audience": "adults"}) if is_new_student(t): return _make_result("topic_switch", "new_student", 0.90, {"customer_type": "new"}) if is_current_student(t): return _make_result("topic_switch", "current_student", 0.90, {"customer_type": "current"}) # ===== State-specific understanding ===== if state == "WAITING_USER_TYPE": if is_new_student(t): return _make_result("direct_answer", "new_student", 0.95, {"customer_type": "new"}) if is_current_student(t): return _make_result("direct_answer", "current_student", 0.95, {"customer_type": "current"}) if state == "WAITING_AUDIENCE": if is_adults(t): return _make_result("direct_answer", "adults", 0.95, {"audience": "adults"}) if is_children(t): return _make_result("direct_answer", "children", 0.95, {"audience": "children"}) if state == "WAITING_PRIOR_STUDY": if is_yes(t): return _make_result("direct_answer", "prior_study_yes", 0.96, {"prior_study": True}) if is_no(t): return _make_result("direct_answer", "prior_study_no", 0.96, {"prior_study": False}) if state in [ "WAITING_BEGINNER_SCHEDULE_CHOICE", "WAITING_PDF_102_CONFIRMATION", "WAITING_PLACEMENT_TEST_CONFIRMATION", ]: if asks_about_prior_study_case(t): return _make_result("state_switch", "switch_to_prior_study_true", 0.92, {"prior_study": True}) if asks_about_beginner_case(t): return _make_result("state_switch", "switch_to_prior_study_false", 0.92, {"prior_study": False}) if state == "WAITING_BEGINNER_SCHEDULE_CHOICE": if contains_any(t, ["تم", "اخترت", "اختارت", "جاهز", "جاهزة"]): return _make_result("direct_answer", "confirm_schedule_reviewed", 0.92, {}) if contains_any(t, ["عايز احجز", "عايزة احجز", "احجز", "حجز", "اشترك", "اشتراك"]): return _make_result("direct_answer", "proceed_booking", 0.90, {}) if is_support_request(t): return _make_result("state_switch", "support_needed", 0.88, {}) if state == "WAITING_PDF_102_CONFIRMATION": if contains_any(t, ["تم", "خلصت", "قريت", "اطلعت", "جاهز", "جاهزة"]): return _make_result("direct_answer", "confirm_pdf_reviewed", 0.92, {}) if is_support_request(t): return _make_result("state_switch", "support_needed", 0.88, {}) if state == "WAITING_PLACEMENT_TEST_CONFIRMATION": if contains_any(t, ["تم", "اخترت", "اختارت", "جاهز", "جاهزة"]): return _make_result("direct_answer", "confirm_placement_test_reviewed", 0.92, {}) if is_support_request(t): return _make_result("state_switch", "support_needed", 0.88, {}) if state == "WAITING_CURRENT_STUDENT_ACTION": if is_support_request(t): return _make_result("direct_answer", "current_student_support", 0.92, {}) if is_next_level_booking(t): return _make_result("direct_answer", "current_student_next_level", 0.92, {}) if state == "WAITING_SUPPORT_QUESTION": if t: return _make_result( "direct_answer", "support_question_text", 0.85, {"support_question": text} ) if state == "WAITING_LEVEL_SELECTION": level = detect_level(t) if level: return _make_result("direct_answer", "level_selected", 0.95, {"selected_level": level}) if is_support_request(t) or contains_any(t, ["مش عارف", "مش متأكد", "مش متاكدة"]): return _make_result("state_switch", "support_needed", 0.85, {}) if state == "WAITING_PAYMENT_METHOD": payment_method = detect_payment_method(t) if payment_method: return _make_result( "direct_answer", "payment_method_selected", 0.95, {"payment_method": payment_method} ) if is_support_request(t): return _make_result("state_switch", "support_needed", 0.85, {}) if state == "WAITING_COMPLAINT_FORM": if contains_any(t, ["تم", "خلصت", "سجلت", "قدمت", "بعت"]): return _make_result("direct_answer", "complaint_form_submitted", 0.90, {}) if state == "HANDOFF_DONE": if contains_any(t, ["شكرا", "متشكر", "تسلم", "ميرسي"]): return _make_result("direct_answer", "thanks", 0.95, {}) if is_support_request(t): return _make_result("topic_switch", "direct_support", 0.90, {}) return _make_result("unclear", None, 0.30, {}) def classify_message(state: str, text: str, flow_data: dict | None = None): """ Returns structured classification: { "kind": "direct_answer" | "state_switch" | "topic_switch" | "unclear", "value": str | None, "confidence": float, "entities": dict, "source": "model" | "rules" } """ flow_data = flow_data or {} # 1) Try model classifier first try: model_res = classify_message_with_model( user_message=text, state=state, flow_data=flow_data, ) if model_res and model_res.get("intent"): mapped = _map_model_intent_to_result( state=state, text=text, model_intent=model_res["intent"], raw_output=model_res.get("raw_output", "") ) if mapped: return mapped except Exception as e: print(f"[message_understanding] model classifier failed: {repr(e)}") # 2) Fallback to existing rule-based logic return _fallback_rule_based_classification(state, text, flow_data)