Download app.py from terHrm/HRM_study: direct link, hf CLI and curl.
- Browser
- Download file 15.6 kB
-
https://huggingface.co/spaces/terHrm/HRM_study/resolve/main/app.py
- Command line
-
hf download hf://spaces/terHrm/HRM_study/app.py
-
curl -L -o app.py https://huggingface.co/spaces/terHrm/HRM_study/resolve/main/app.py
15.6 kB
| 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() |