ADK-Bot / knowledge /message_understanding.py
Mr-Help's picture
Update knowledge/message_understanding.py
3ecfbbf verified
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)