smart-advisor / src /rag /profile.py
sajaahmed5
Smart Advisor deployment
74e8a7b
Raw
History Blame Contribute Delete
5.44 kB
"""
Student profile extraction (LLM-based) and the onboarding question sequencer.
"""
import json
import re
from src.utils.config import groq_client, openrouter_client
from src.utils.schemas import StudentProfile
_EXTRACTION_SYSTEM = """
You are an assistant who extracts student information from a conversation message.
Return ONLY JSON object with these keys (if the information is not exist use null):
{
"gpa": floating number from 0 to 100 or null,
"academic_track": one of [علمي، صناعي، تكنولوجيا معلومات، أدبي، تجاري، أخرى] or null,
"likes_math": null or true/false,
"interest_areas": one of [علم البيانات، الذكاء الاصطناعي، أمن المعلومات، شبكات الحاسوب، هندسة الحاسوب، غير محدد] or null,
"degree_preference": one of [بكالوريوس، دبلوم، غير محدد] or null
}
Rules:
- Only extract a field if the student's message is actually answering the question about THAT field,
or explicitly volunteers that information. A short answer like "نعم"/"لا" only applies to the field
whose question was just asked — do not let it populate any other field.
- The student's message may be a short answer (e.g. "نعم", "لا", "أيوة", "yes", "no") to a question
the assistant just asked. Use the conversation context to determine which field this answer applies to,
and map Arabic/English affirmatives (نعم، أجل، أيوة، صح، yes) to true and negatives (لا، مش، no) to false.
- For degree_preference: if the question asked about degree/duration and the student answers with a
duration, map it accordingly — "سنتان"، "سنتين"، "2"، "two years" → دبلوم;
"اربع سنوات"، "4"، "أربعة"، "four years" → بكالوريوس.
- Do not invent information that is not in the message or implied by the immediate question context.
- Do not modify fields in the current file unless the student explicitly corrects information.
- Return only JSON, without explanation or markdown.
"""
def extract_profile(user_message: str, current: StudentProfile) -> StudentProfile:
"""Call LLM to extract profile fields from student message and merge with current profile."""
try:
resp = groq_client.chat.completions.create(
model="llama-3.1-8b-instant",
messages=[
{"role": "system", "content": _EXTRACTION_SYSTEM},
{"role": "user", "content": (
f"الملف الحالي: {current.model_dump_json()}\n\n"
f"رسالة الطالب: {user_message}"
)},
],
temperature=0.0,
max_tokens=400,
)
raw = re.sub(r"```json|```", "", resp.choices[0].message.content).strip()
extracted = json.loads(raw)
# NEW — strip whitespace from all extracted string/list values
cleaned = {}
for key, value in extracted.items():
if isinstance(value, str):
cleaned[key] = value.strip()
elif isinstance(value, list):
cleaned[key] = [v.strip() if isinstance(v, str) else v for v in value]
else:
cleaned[key] = value
# Merge: only fill None / empty-list fields from extraction
current_data = current.model_dump()
for key, value in cleaned.items():
if value is None:
continue
if key == "interest_areas" and isinstance(value, str):
value = [value]
existing = current_data.get(key)
if existing is None or (isinstance(existing, list) and len(existing) == 0):
current_data[key] = value
return StudentProfile(**current_data)
except Exception as e:
print(f"[Profile extraction error] {e}")
return current # return unchanged on any failure
# Maps missing profile fields -> natural Arabic questions.
# No LLM needed — pure lookup table for speed and reliability.
_FIELD_QUESTIONS: dict[str, str] = {
"gpa": (
"للبدء، ما معدلك في الثانوية العامة (التوجيهي)؟ "
"هذا يساعدني في معرفة البرامج التي تؤهل للقبول."
),
"academic_track": (
"ما فرعك الدراسي في الثانوية؟ "
"(علمي / صناعي / تكنولوجيا معلومات / أدبي / تجاري / أخرى)"
),
"likes_math": (
"هل تستمتع بالرياضيات والإحصاء؟ "
"أسألك لأن بعض التخصصات كعلم البيانات تعتمد عليهما بشكل كبير."
),
"interest_areas": (
"ما الذي يثير اهتمامك أكثر؟ "
"(تحليل البيانات / الذكاء الاصطناعي / أمن المعلومات / الشبكات / هندسة الحاسوب)"
),
"degree_preference": (
"هل تفضل الحصول على درجة البكالوريوس (4 سنوات) أم الدبلوم (سنتان)؟"
),
}
def next_onboarding_question(profile: StudentProfile) -> str | None:
"""Return the next onboarding question to ask, or None if profile is complete."""
for field in profile.missing_fields():
q = _FIELD_QUESTIONS.get(field)
if q:
return q
return None