smart-advisor / src /rag /conversational.py
sajaahmed5
Smart Advisor deployment
74e8a7b
Raw
History Blame Contribute Delete
5.13 kB
"""
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