Spaces:
Running
Running
| """ | |
| Classifies whether a student's message is conversational (greeting, thanks, | |
| small talk) vs. a real academic question needing retrieval. | |
| """ | |
| import json | |
| import re | |
| from src.utils.config import openrouter_client, groq_client | |
| _CONVERSATIONAL_PATTERNS = [ | |
| "ุดูุฑุง", "ุดูุฑุงู", "ุชู ุงู ", "ู ุงุดู", "ู ุฑุญุจุง", "ุฃููุง", "ุงููุง", "ูุณูู ู", | |
| "ุญุณูุง", "ุญุณูุงู", "ู ู ุชุงุฒ", "ุงูุณูุงู ุนูููู ", "ูุนูููู ุงูุณูุงู ", | |
| "ููู ุญุงูู", "ุดู ุงุฎุจุงุฑู", "ู ุน ุงูุณูุงู ุฉ", "ุจุงู", "ุทูุจ", | |
| ] | |
| def _quick_conversational_check(user_message: str) -> bool | None: | |
| """Returns True/False if confidently classifiable without an LLM call, | |
| None if genuinely ambiguous and needs the LLM fallback.""" | |
| stripped = user_message.strip() | |
| if len(stripped) <= 3: | |
| return True # extremely short messages are almost always filler | |
| if any(p in stripped for p in _CONVERSATIONAL_PATTERNS) and len(stripped) < 25: | |
| return True | |
| if "ุ" in stripped or "?" in stripped or len(stripped) > 25: | |
| return False # a question mark or longer message is very likely a real question | |
| return None # ambiguous โ defer to LLM | |
| _CONVERSATIONAL_SYSTEM = """You are a classifier for an academic advisor chatbot. | |
| Decide if the student's message is a real academic question that needs retrieval, or just conversational | |
| (greeting, thanks, acknowledgement, filler, or off-topic small talk). | |
| Return ONLY JSON in this exact format: | |
| { | |
| "is_conversational": true or false, | |
| "response": "a short polite Arabic reply if is_conversational is true, otherwise null" | |
| } | |
| Examples of conversational (is_conversational: true): | |
| - "ุดูุฑุงู", "ุชู ุงู ", "ู ุงุดู", "ู ุฑุญุจุง", "ุฃููุงู", "ูุณูู ู", "ุญุณูุงู", "ู ู ุชุงุฒ", "ุงูุณูุงู ุนูููู " | |
| - "ููู ุญุงูู", "ู ู ุฃูุช", "ู ุง ุงุณู ู" | |
| - Any greeting, farewell, or expression of thanks | |
| Examples of real questions (is_conversational: false): | |
| - "ู ุง ูู ุงูู ูุญ ุงูุฏุฑุงุณูุฉ", "ู ุง ุฎุทุฉ ุงูุฏุฑุงุณุฉ", "ู ุง ุดุฑุท ุงููุจูู" | |
| - "ูู ุฃูุง ู ูุจูู ุจู ุนุฏู 75%", "ู ุง ุงูู ุณุงูุงุช ูู ุงูุณูุฉ ุงูุฃููู" | |
| - Any question about the program, courses, admission, or career | |
| Rules: | |
| - Return only JSON, no markdown, no explanation. | |
| - The response field must always be in Arabic. | |
| - Keep the response short (1-2 sentences), warm, and relevant to what the student said.""" | |
| def is_conversational(user_message: str) -> tuple[bool, str | None]: | |
| """ | |
| Returns (True, polite_response) if message is a greeting/thanks/non-question. | |
| Returns (False, None) if message is a real academic question. | |
| """ | |
| quick = _quick_conversational_check(user_message) | |
| if quick is False: | |
| return False, None | |
| if quick is True: | |
| # Still return a plausible generic Arabic reply โ cheap, no LLM call | |
| return True, "ุฃููุงู ุจู! ููู ูู ูููู ู ุณุงุนุฏุชูุ" | |
| messages = [ | |
| {"role": "system", "content": """You are a classifier for an academic advisor chatbot. | |
| Decide if the student's message is a real academic question that needs retrieval, or just conversational | |
| (greeting, thanks, acknowledgement, filler, or off-topic small talk). | |
| Return ONLY JSON in this exact format: | |
| { | |
| "is_conversational": true or false, | |
| "response": "a short polite Arabic reply if is_conversational is true, otherwise null" | |
| } | |
| Examples of conversational (is_conversational: true): | |
| - "ุดูุฑุงู", "ุชู ุงู ", "ู ุงุดู", "ู ุฑุญุจุง", "ุฃููุงู", "ูุณูู ู", "ุญุณูุงู", "ู ู ุชุงุฒ", "ุงูุณูุงู ุนูููู " | |
| - "ููู ุญุงูู", "ู ู ุฃูุช", "ู ุง ุงุณู ู" | |
| - Any greeting, farewell, or expression of thanks | |
| Examples of real questions (is_conversational: false): | |
| - "ู ุง ูู ุงูู ูุญ ุงูุฏุฑุงุณูุฉ", "ู ุง ุฎุทุฉ ุงูุฏุฑุงุณุฉ", "ู ุง ุดุฑุท ุงููุจูู" | |
| - "ูู ุฃูุง ู ูุจูู ุจู ุนุฏู 75%", "ู ุง ุงูู ุณุงูุงุช ูู ุงูุณูุฉ ุงูุฃููู" | |
| - Any question about the program, courses, admission, or career | |
| Rules: | |
| - Return only JSON, no markdown, no explanation. | |
| - The response field must always be in Arabic. | |
| - Keep the response short (1-2 sentences), warm, and relevant to what the student said."""}, | |
| {"role": "user", "content": user_message} | |
| ] | |
| def _try(client, model): | |
| resp = client.chat.completions.create( | |
| model=model, messages=messages, temperature=0.0, max_tokens=100 | |
| ) | |
| raw = re.sub(r"```json|```", "", resp.choices[0].message.content).strip() | |
| return json.loads(raw) | |
| try: | |
| result = _try(groq_client, "llama-3.1-8b-instant") | |
| except Exception as e: | |
| print(f"[is_conversational/Groq error] {e} โ falling back to gpt-4o-mini") | |
| try: | |
| result = _try(openrouter_client, "openai/gpt-4o-mini") | |
| except Exception as e2: | |
| print(f"[is_conversational/fallback error] {e2} โ treating as real question") | |
| return False, None # โ this is the line that actually prevents the crash | |
| if result.get("is_conversational"): | |
| return True, result.get("response") | |
| return False, None |