| import backend |
| import gradio as gr |
| import re, os, random, json, requests, math |
| import pandas as pd |
| from collections import defaultdict |
| from utilit import skills as all_skills |
| from groq import Groq |
|
|
| OLLAMA_URL = "http://localhost:11434/api/chat" |
| MODEL = "qwen2.5:1.5b" |
|
|
| HIERARCHY = { |
| "Arithmetic Operations": {1: ["whole numbers", "ordering", "number line"], 2: ["integers"], 3: ["decimals"], 4: ["order of operations"]}, |
| "Fractions Decimals Percents": {1: ["equivalent fractions", "conversion"], 2: ["fraction", "decimal"], 3: ["mixed fractions"], 4: ["percent", "ratio", "rate"]}, |
| "Algebra Basics": {1: ["expression", "terms"], 2: ["like terms", "distributive"], 3: ["simplifying"], 4: ["polynomial"]}, |
| "Equations and Inequalities": {1: ["one step", "two step"], 2: ["solving"], 3: ["multi", "more than two"], 4: ["inequality"]}, |
| "Linear Functions": {1: ["pattern", "table"], 2: ["slope"], 3: ["graph"], 4: ["write linear equation", "from situation"]}, |
| "Systems of Equations": {3: ["graphing"], 4: ["substitution", "system"]}, |
| "Geometry": {1: ["angles", "triangle", "polygon"], 2: ["perimeter", "area"], 3: ["volume"], 4: ["surface area", "3d"]}, |
| "Statistics": {1: ["mean", "median", "mode"], 2: ["range"], 3: ["plot", "histogram"], 4: ["scatter", "sampling"]}, |
| "Probability": {1: ["single"], 2: ["venn"], 3: ["two"], 4: ["combinatorics", "counting"]}, |
| "Advanced Algebra": {2: ["factoring"], 3: ["quadratic"], 4: ["quadratic formula"]} |
| } |
|
|
| CLUSTERS = { |
| "Arithmetic Operations": ["addition", "subtraction", "multiplication", "division", "order of operations", "computation", "estimation"], |
| "Fractions Decimals Percents": ["fraction", "decimal", "percent", "ratio", "rate"], |
| "Number Theory": ["prime", "factor", "multiple", "divisibility", "gcf", "lcm", "odd", "even"], |
| "Algebra Basics": ["expression", "like terms", "distributive", "polynomial", "simplifying"], |
| "Equations and Inequalities": ["equation", "inequality", "solving", "variable"], |
| "Linear Functions": ["linear", "slope", "y-intercept", "graph", "table"], |
| "Systems of Equations": ["system"], |
| "Geometry": ["angle", "triangle", "polygon", "circle", "perimeter", "area", "volume", "surface area", "prism", "cone", "sphere"], |
| "Statistics": ["mean", "median", "mode", "range", "plot", "histogram", "scatter"], |
| "Probability": ["probability", "venn", "combinatorics", "counting"], |
| "Transformations": ["rotation", "reflection", "translation", "symmetry"], |
| "Coordinate Geometry": ["coordinate", "distance", "midpoint"], |
| "Advanced Algebra": ["quadratic", "factoring", "trinomials"] |
| } |
|
|
| SKILL_VIDEOS = { |
| "whole numbers": "https://www.youtube.com/watch?v=m94WTzp14SA", |
| "ordering": "https://www.youtube.com/watch?v=0_fC9Lsn7-8", |
| "number line": "https://www.youtube.com/watch?v=6u3pU9V7NoQ", |
| "integers": "https://www.youtube.com/watch?v=vNguZreVv_g", |
| "decimals": "https://www.youtube.com/watch?v=kgv_UnE_iXQ", |
| "order of operations": "https://www.youtube.com/watch?v=dAgfnK528RA", |
| "equivalent fractions": "https://www.youtube.com/watch?v=qcHHbaKo_7E", |
| "conversion": "https://www.youtube.com/watch?v=do_IbHId2Os", |
| "fraction": "https://www.youtube.com/watch?v=CA9Xp80ZfIs", |
| "decimal": "https://www.youtube.com/watch?v=9VvU6Sow064", |
| "mixed fractions": "https://www.youtube.com/watch?v=1xuf6Z40Y8U", |
| "percent": "https://www.youtube.com/watch?v=JeVSmq1Nrpw", |
| "ratio": "https://www.youtube.com/watch?v=HpdMJaKa9P0", |
| "rate": "https://www.youtube.com/watch?v=LIYfbe7_EaA", |
| "expression": "https://www.youtube.com/watch?v=LpUeZ8T_6mE", |
| "terms": "https://www.youtube.com/watch?v=0AfS8vS4Zks", |
| "like terms": "https://www.youtube.com/watch?v=H_N1_Z9_X7Y", |
| "distributive": "https://www.youtube.com/watch?v=v-6MShC82ow", |
| "simplifying": "https://www.youtube.com/watch?v=3S9S8p_UuU4", |
| "polynomial": "https://www.youtube.com/watch?v=ffLLmV4mZwU", |
| "one step": "https://www.youtube.com/watch?v=L0_K895874A", |
| "two step": "https://www.youtube.com/watch?v=LDIiYKYvvdA", |
| "solving": "https://www.youtube.com/watch?v=bAerID24QJ0", |
| "multi": "https://www.youtube.com/watch?v=f15zA0PhSek", |
| "more than two": "https://www.youtube.com/watch?v=13YFOnR_kio", |
| "inequality": "https://www.youtube.com/watch?v=njpSAtIDZ30", |
| "pattern": "https://www.youtube.com/watch?v=8mGeU_uIs9I", |
| "table": "https://www.youtube.com/watch?v=7u_X6X8tU-A", |
| "slope": "https://www.youtube.com/watch?v=v8VSDg_WQlA", |
| "graph": "https://www.youtube.com/watch?v=f58_XvY9o30", |
| "write linear equation": "https://www.youtube.com/watch?v=eHPTyYbNmxw", |
| "from situation": "https://www.youtube.com/watch?v=m682S8xUuYg", |
| "graphing": "https://www.youtube.com/watch?v=S0_qX4VJhMQ", |
| "substitution": "https://www.youtube.com/watch?v=zsD965-X-Vw", |
| "system": "https://www.youtube.com/watch?v=fNk_zzaMoBA", |
| "angles": "https://www.youtube.com/watch?v=DGKwdHMiqCg", |
| "triangle": "https://www.youtube.com/watch?v=mLeNaZcy-hE", |
| "polygon": "https://www.youtube.com/watch?v=IaoZhhx_I9s", |
| "perimeter": "https://www.youtube.com/watch?v=AAY1bsazcgM", |
| "area": "https://www.youtube.com/watch?v=xCdxURXMdFY", |
| "volume": "https://www.youtube.com/watch?v=qJwecTgce6c", |
| "surface area": "https://www.youtube.com/watch?v=uK5E_T6fX7c", |
| "3d": "https://www.youtube.com/watch?v=ZqzAOZasN9A", |
| "mean": "https://www.youtube.com/watch?v=B1HEzQu68tY", |
| "median": "https://www.youtube.com/watch?v=B1HEzQu68tY", |
| "mode": "https://www.youtube.com/watch?v=B1HEzQu68tY", |
| "range": "https://www.youtube.com/watch?v=B1HEzQu68tY", |
| "plot": "https://www.youtube.com/watch?v=mH_S_zT3qH0", |
| "histogram": "https://www.youtube.com/watch?v=gY-mreidV-4", |
| "scatter": "https://www.youtube.com/watch?v=pGZ37A47nI0", |
| "sampling": "https://www.youtube.com/watch?v=pTuj57uXWlk", |
| "single": "https://www.youtube.com/watch?v=KzfWUEJjG18", |
| "venn": "https://www.youtube.com/watch?v=XdfS2-W8WnU", |
| "two": "https://www.youtube.com/watch?v=Y-XU9rL-pY0", |
| "combinatorics": "https://www.youtube.com/watch?v=Tr6Yp3_78uY", |
| "counting": "https://www.youtube.com/watch?v=O129Sscf6lE", |
| "factoring": "https://www.youtube.com/watch?v=f_Vp_74p_G8", |
| "quadratic": "https://www.youtube.com/watch?v=6QYatS7vV0o", |
| "quadratic formula": "https://www.youtube.com/watch?v=EeVqtpuGgu8" |
| } |
|
|
| hierarchy_skills_list = [] |
| for domain_data in HIERARCHY.values(): |
| for skill_group in domain_data.values(): |
| hierarchy_skills_list.extend(skill_group) |
|
|
| css = """ |
| body, .gradio-container { |
| font-size: 16px; |
| } |
| """ |
|
|
| groq_key = os.getenv("GROQ_KEY") |
| backend.init_db() |
| client = Groq(api_key=groq_key) |
| model_id = "llama-3.3-70b-versatile" |
|
|
| session_skills = [] |
|
|
| def assign_cluster(skill): |
| skill_lower = str(skill).lower() |
| for cluster, keywords in CLUSTERS.items(): |
| if any(keyword in skill_lower for keyword in keywords): |
| return cluster |
| return "Other" |
|
|
| def assign_hierarchy(skill): |
| skill_lower = str(skill).lower() |
| for domain, levels in HIERARCHY.items(): |
| for level, keywords in levels.items(): |
| if any(keyword in skill_lower for keyword in keywords): |
| return domain, level |
| return "Other", -1 |
|
|
| def find_most_similar_prediction(current_session_skills): |
| all_logs = backend.get_all_logs() |
| if not all_logs: |
| return [], "Database is empty." |
|
|
| student_histories = defaultdict(list) |
| for user_id, skill in all_logs: |
| student_histories[str(user_id)].append(str(skill)) |
| |
| if not student_histories: |
| return [], "No student data available." |
| |
| random_student = random.choice(list(student_histories.keys())) |
| selected_history = student_histories[random_student][-30:] |
| |
| formatted_history = [] |
| for s in selected_history: |
| domain, level = assign_hierarchy(s) |
| formatted_history.append(f"(skill={s}, cluster={assign_cluster(s)}, level={level})") |
|
|
| prompt = f"""You are a student knowledge model. |
| Reference Student history: {", ".join(formatted_history)} |
| The current user has expressed difficulty with: {", ".join(current_session_skills)} |
| Available hierarchy skills: {", ".join(hierarchy_skills_list)} |
| |
| Predict the NEXT 3 most likely skills (ranked) for the current user. |
| Reply ONLY with JSON: {{"skills": ["Skill_A", "Skill_B", "Skill_C"]}}""" |
|
|
| try: |
| response = requests.post( |
| OLLAMA_URL, |
| json={ |
| "model": MODEL, |
| "messages": [{"role": "user", "content": prompt}], |
| "stream": False, |
| "format": "json" |
| }, |
| timeout=15 |
| ) |
| |
| response.raise_for_status() |
| raw_data = response.json() |
| text_content = raw_data.get("message", {}).get("content", "{}") |
| parsed = json.loads(text_content) |
| preds = parsed.get("skills", []) |
| |
| for i, skill in enumerate(preds, 1): |
| print(f"Rank {i}: {skill}") |
| |
| return preds, f"Random match based on Student ID: {random_student}" |
|
|
| except Exception as e: |
| print(f"Detailed LLM Error: {e}") |
| return [], f"LLM processing error: {str(e)}" |
|
|
| def get_youtube_id(url): |
| pattern = r'(?:https?://)?(?:www\.)?(?:youtube\.com/(?:watch\?v=|embed/)|youtu\.be/)([a-zA-Z0-9_-]{11})' |
| match = re.search(pattern, url) |
| return match.group(1) if match else None |
|
|
| def display_video_and_predict(): |
| preds, info_msg = find_most_similar_prediction(session_skills) |
| |
| print(f"Final Output: {info_msg} Recommendations: {preds}") |
| |
| if not preds: |
| return "<p style='text-align:center;'>No recommendation available.</p>", f"### Personalized Path\n{info_msg}" |
|
|
| main_skill = preds[0] |
| main_url = SKILL_VIDEOS.get(main_skill, "https://www.youtube.com/watch?v=WUvTyaaNkzM") |
| video_id = get_youtube_id(main_url) |
| |
| video_html = f''' |
| <div style="display: flex; flex-direction: column; align-items: center;"> |
| <h3 style="margin-bottom: 10px;">Main Lesson: {main_skill.title()}</h3> |
| <iframe width="100%" height="450" src="https://www.youtube.com/embed/{video_id}?autoplay=1" frameborder="0" allowfullscreen></iframe> |
| </div> |
| ''' |
|
|
| other_markdown = f"### Personalized Path\n{info_msg}\n\n#### Other recommendation\n" |
| for skill in preds[1:]: |
| url = SKILL_VIDEOS.get(skill, "#") |
| other_markdown += f"* [{skill.title()}]({url})\n" |
|
|
| return video_html, other_markdown |
|
|
| def chat_local(message, history): |
| system_content = f"""You are a diagnostic assistant for a maths learning app. |
| Your ONLY job is to identify what the student does not understand in maths. |
| How to behave: |
| 1. Ask the student what topic or concept they are struggling with or don't understand. |
| 2. Ask short clarifying questions if needed to pinpoint the exact difficulty. |
| 3. Once you have enough information about one topic, you MUST respond EXACTLY in this format: |
| "Got it! I'm going to select some exercises to help you with that. [SKILL:skill_name_here]" |
| NEVER say "Got it!" without the [SKILL:] tag. The [SKILL:] tag is mandatory. |
| 4. After saying "Got it!", ask the student if they have any other topic they struggle with and keep the conversation going. |
| 5. Never explain, never teach, never solve anything. |
| Map what the student says to the most relevant skill from this list: {all_skills} |
| NEVER mention the skills list to the student, only include it in the [SKILL:] tag. |
| Rules: |
| - Maximum 2 sentences per response. |
| - Always end with a question. |
| - If the student asks you to explain something, say: "I'm not a tutor! Tell me what you don't understand and I'll find the right exercises for you." """ |
| |
| messages = [{"role": "system", "content": system_content}] |
| for m in history: |
| messages.append({"role": m["role"], "content": m["content"]}) |
| |
| try: |
| response = client.chat.completions.create( |
| model=model_id, |
| messages=messages, |
| max_tokens=300, |
| ) |
| return response.choices[0].message.content |
| except Exception as e: |
| print(f"API Error: {e}") |
| return None |
|
|
| def respond(message, chat_history, user_id): |
| global session_skills |
| chat_history.append({"role": "user", "content": message}) |
| bot_message = chat_local(message, chat_history) |
| chat_history.append({"role": "assistant", "content": bot_message or "Sorry, I couldn't generate a response."}) |
| |
| if bot_message and "Got it!" in bot_message: |
| match = re.search(r'\[SKILL:(.*?)\]', bot_message) |
| if match: |
| skill = match.group(1).strip() |
| if skill not in session_skills: |
| session_skills.append(skill) |
| backend.add_skill(user_id, skill) |
| chat_history[-1]["content"] = re.sub(r'\[SKILL:(.*?)\]', f"We'll be working on: {skill}", bot_message).strip() |
| |
| return "", chat_history |
|
|
| def handle_login(email, password): |
| user_id = backend.login(email, password) |
| if user_id: |
| return gr.update(visible=False), gr.update(visible=True), "", user_id |
| return gr.update(visible=True), gr.update(visible=False), "## <span style='color:red'>Invalid email or password</span>", None |
|
|
| def reset_skills(): |
| global session_skills |
| session_skills = [] |
|
|
| with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo: |
| with gr.Column(visible=True) as auth_block: |
| gr.Markdown("# Identification") |
| with gr.Tab("Sign In"): |
| u = gr.Textbox(label="Email") |
| p = gr.Textbox(label="Password", type="password") |
| btn_l = gr.Button("Sign In", variant="primary") |
| out_l = gr.Markdown() |
| with gr.Tab("Create account"): |
| nu = gr.Textbox(label="Username") |
| nemail = gr.Textbox(label="Email") |
| np = gr.Textbox(label="Password", type="password") |
| btn_r = gr.Button("Sign Up") |
| out_r = gr.Textbox(label="Status") |
| btn_r.click(backend.create_user, [nemail, np, nu], out_r) |
|
|
| with gr.Column(visible=False) as main_block: |
| gr.Markdown("# AI LEARNING ASSISTANT") |
| user_id_state = gr.State(None) |
| with gr.Row(): |
| with gr.Column(scale=1): |
| chatbot = gr.Chatbot(height=450) |
| msg = gr.Textbox(placeholder="Tell me what you don't understand...", label="Tutor Chat") |
| with gr.Row(): |
| send_btn = gr.Button("Send", variant="primary") |
| clear_btn = gr.Button("Clear") |
| logout_btn = gr.Button("Logout", variant="secondary") |
| with gr.Column(scale=1): |
| video_output = gr.HTML(value="<p style='text-align:center;'>Video will appear here.</p>") |
| prediction_output = gr.Markdown() |
| change_video = gr.Button("Get Video Lesson & Recommendation", variant="primary") |
|
|
| btn_l.click( |
| fn=handle_login, |
| inputs=[u, p], |
| outputs=[auth_block, main_block, out_l, user_id_state] |
| ) |
|
|
| send_btn.click(respond, [msg, chatbot, user_id_state], [msg, chatbot]) |
| msg.submit(respond, [msg, chatbot, user_id_state], [msg, chatbot]) |
| clear_btn.click(lambda: ([], reset_skills()), None, chatbot) |
| change_video.click(fn=display_video_and_predict, outputs=[video_output, prediction_output]) |
| |
| logout_btn.click(lambda: (gr.update(visible=True), gr.update(visible=False), "", None), None, [auth_block, main_block, out_l, user_id_state]) |
|
|
| demo.launch() |