Spaces:
Sleeping
Sleeping
| # app.py | |
| from typing import List, Dict, Tuple, Optional | |
| import gradio as gr | |
| from config import ( | |
| DEFAULT_MODEL, | |
| DEFAULT_COURSE_TOPICS, | |
| LEARNING_MODES, | |
| DOC_TYPES, | |
| ) | |
| from clare_core import ( | |
| parse_syllabus_docx, | |
| update_weaknesses_from_message, | |
| update_cognitive_state_from_message, | |
| render_session_status, | |
| find_similar_past_question, | |
| detect_language, | |
| chat_with_clare, | |
| export_conversation, | |
| generate_quiz_from_history, | |
| get_empty_input_prompt, | |
| summarize_conversation, | |
| ) | |
| from rag_engine import ( | |
| build_rag_chunks_from_file, | |
| retrieve_relevant_chunks, | |
| ) | |
| with gr.Blocks(title="Clare – Hanbridge AI Teaching Assistant") as demo: | |
| gr.Markdown( | |
| """ | |
| # 🎓 Clare – AI Teaching Assistant | |
| **Hanbridge University** · English UI · Supports both English & Chinese questions. | |
| - Ask in English → Clare answers in English. | |
| - Ask in Chinese → Clare can answer in Chinese. | |
| - Use different **learning modes** to change Clare's teaching style. | |
| - Optionally upload your **course syllabus / slides / literature (.docx)** so Clare stays aligned with your course. | |
| """ | |
| ) | |
| # 顶部:模型、语言偏好、学习模式 | |
| with gr.Row(): | |
| model_name = gr.Textbox( | |
| label="Model name", | |
| value=DEFAULT_MODEL, | |
| info="For example: gpt-4.1-mini, gpt-4.1, gpt-4o, etc.", | |
| ) | |
| language_preference = gr.Radio( | |
| choices=["Auto", "English", "中文"], | |
| value="Auto", | |
| label="Preferred answer language", | |
| ) | |
| learning_mode = gr.Radio( | |
| choices=LEARNING_MODES, | |
| value="Concept Explainer", | |
| label="Learning mode", | |
| ) | |
| # 课程文件上传 | |
| with gr.Row(): | |
| syllabus_file = gr.File( | |
| label="Upload course file (.docx)", | |
| file_types=[".docx"], | |
| ) | |
| doc_type = gr.Dropdown( | |
| choices=DOC_TYPES, | |
| value="Syllabus", | |
| label="File type", | |
| ) | |
| # 状态:课程大纲 + 学生弱项 + 认知状态 + RAG chunks | |
| course_outline_state = gr.State(DEFAULT_COURSE_TOPICS) | |
| weakness_state = gr.State([]) | |
| cognitive_state_state = gr.State({"confusion": 0, "mastery": 0}) | |
| rag_chunks_state = gr.State([]) # Session 级 RAG 向量库 | |
| # 上传 syllabus 时更新课程大纲 + RAG chunks | |
| def update_course_and_rag(file, doc_type_val): | |
| # 更新课程大纲(保持原有逻辑) | |
| if file is None: | |
| topics = DEFAULT_COURSE_TOPICS | |
| else: | |
| if doc_type_val == "Syllabus": | |
| try: | |
| file_path = file.name | |
| if file_path.lower().endswith(".docx"): | |
| topics = parse_syllabus_docx(file_path) | |
| else: | |
| topics = DEFAULT_COURSE_TOPICS | |
| except Exception: | |
| topics = DEFAULT_COURSE_TOPICS | |
| else: | |
| topics = DEFAULT_COURSE_TOPICS | |
| # 构建 RAG chunks | |
| rag_chunks = build_rag_chunks_from_file(file, doc_type_val) | |
| return topics, rag_chunks | |
| syllabus_file.change( | |
| fn=update_course_and_rag, | |
| inputs=[syllabus_file, doc_type], | |
| outputs=[course_outline_state, rag_chunks_state], | |
| ) | |
| # 左侧聊天,右侧 Session 状态栏 | |
| with gr.Row(): | |
| chatbot = gr.Chatbot( | |
| label="Clare Chat", | |
| height=450, | |
| ) | |
| session_status = gr.Markdown( | |
| value=render_session_status("Concept Explainer", [], {"confusion": 0, "mastery": 0}), | |
| label="Session status", | |
| ) | |
| user_input = gr.Textbox( | |
| label="Your question", | |
| placeholder="Ask Clare anything about your course, assignment, or study plan...", | |
| ) | |
| with gr.Row(): | |
| clear_btn = gr.Button("Reset conversation") | |
| export_btn = gr.Button("Export conversation") | |
| quiz_btn = gr.Button("Generate 3 quiz questions") | |
| summary_btn = gr.Button("Summarize concepts") | |
| export_box = gr.Textbox( | |
| label="Conversation export (Markdown)", | |
| lines=8, | |
| ) | |
| quiz_box = gr.Textbox( | |
| label="Generated quiz (with answer key)", | |
| lines=8, | |
| ) | |
| summary_box = gr.Textbox( | |
| label="Concept summary (for study notes)", | |
| lines=8, | |
| ) | |
| # 主对话逻辑 | |
| def respond( | |
| message, | |
| chat_history, | |
| course_outline, | |
| weaknesses, | |
| cognitive_state, | |
| rag_chunks, | |
| model_name_val, | |
| language_pref_val, | |
| learning_mode_val, | |
| doc_type_val, | |
| ): | |
| # 1) 决定本轮语言(Auto / English / 中文) | |
| resolved_lang = detect_language(message or "", language_pref_val) | |
| # 2) 空输入防护 | |
| if not message or not message.strip(): | |
| empty_msg = get_empty_input_prompt(resolved_lang) | |
| new_history = chat_history + [("", empty_msg)] | |
| status_text = render_session_status(learning_mode_val, weaknesses or [], cognitive_state) | |
| return "", new_history, weaknesses, cognitive_state, status_text | |
| # 3) 更新弱项 & 认知状态 | |
| weaknesses = update_weaknesses_from_message(message, weaknesses or []) | |
| cognitive_state = update_cognitive_state_from_message(message, cognitive_state) | |
| # 4) Same Question Check(session 内复用答案) | |
| dup = find_similar_past_question(message, chat_history) | |
| if dup is not None: | |
| past_q, past_a, sim = dup | |
| prefix_en = ( | |
| "I noticed this question is very similar to one you asked earlier, " | |
| "so I'm showing the previous explanation again. " | |
| "If there's a specific part that's still unclear, tell me and I can " | |
| "re-explain it in a different way.\n\n" | |
| "**Earlier answer:**\n" | |
| ) | |
| prefix_zh = ( | |
| "我注意到你现在的问题和之前问过的非常相似," | |
| "所以我先把当时的回答再展示一次。" | |
| "如果还有具体不清楚的地方,可以告诉我,我会换一种方式解释。\n\n" | |
| "**之前的回答:**\n" | |
| ) | |
| if resolved_lang == "中文": | |
| answer = prefix_zh + past_a | |
| else: | |
| answer = prefix_en + past_a | |
| new_history = chat_history + [(message, answer)] | |
| status_text = render_session_status(learning_mode_val, weaknesses, cognitive_state) | |
| return "", new_history, weaknesses, cognitive_state, status_text | |
| # 5) RAG:基于上传文档做检索 | |
| rag_context = retrieve_relevant_chunks(message, rag_chunks or []) | |
| # 6) 正常调用 Clare(带上 RAG context) | |
| answer, new_history = chat_with_clare( | |
| message=message, | |
| history=chat_history, | |
| model_name=model_name_val, | |
| language_preference=resolved_lang, | |
| learning_mode=learning_mode_val, | |
| doc_type=doc_type_val, | |
| course_outline=course_outline, | |
| weaknesses=weaknesses, | |
| cognitive_state=cognitive_state, | |
| rag_context=rag_context, | |
| ) | |
| status_text = render_session_status(learning_mode_val, weaknesses, cognitive_state) | |
| return "", new_history, weaknesses, cognitive_state, status_text | |
| user_input.submit( | |
| respond, | |
| [ | |
| user_input, | |
| chatbot, | |
| course_outline_state, | |
| weakness_state, | |
| cognitive_state_state, | |
| rag_chunks_state, | |
| model_name, | |
| language_preference, | |
| learning_mode, | |
| doc_type, | |
| ], | |
| [user_input, chatbot, weakness_state, cognitive_state_state, session_status], | |
| ) | |
| # 清空对话 & 状态 | |
| def clear_all(): | |
| empty_state = {"confusion": 0, "mastery": 0} | |
| status_text = render_session_status("Concept Explainer", [], empty_state) | |
| return ( | |
| [], # chatbot | |
| [], # weaknesses | |
| empty_state, # cognitive_state | |
| [], # rag_chunks | |
| "", # export_box | |
| "", # quiz_box | |
| "", # summary_box | |
| status_text, # session_status | |
| ) | |
| clear_btn.click( | |
| clear_all, | |
| None, | |
| [ | |
| chatbot, | |
| weakness_state, | |
| cognitive_state_state, | |
| rag_chunks_state, | |
| export_box, | |
| quiz_box, | |
| summary_box, | |
| session_status, | |
| ], | |
| queue=False, | |
| ) | |
| # 导出对话 | |
| def on_export(chat_history, course_outline, learning_mode_val, weaknesses, cognitive_state): | |
| return export_conversation( | |
| chat_history, | |
| course_outline, | |
| learning_mode_val, | |
| weaknesses or [], | |
| cognitive_state, | |
| ) | |
| export_btn.click( | |
| on_export, | |
| [chatbot, course_outline_state, learning_mode, weakness_state, cognitive_state_state], | |
| [export_box], | |
| ) | |
| # 生成 quiz | |
| def on_quiz( | |
| chat_history, | |
| course_outline, | |
| weaknesses, | |
| cognitive_state, | |
| model_name_val, | |
| language_pref_val, | |
| ): | |
| return generate_quiz_from_history( | |
| chat_history, | |
| course_outline, | |
| weaknesses or [], | |
| cognitive_state, | |
| model_name_val, | |
| language_pref_val, | |
| ) | |
| quiz_btn.click( | |
| on_quiz, | |
| [ | |
| chatbot, | |
| course_outline_state, | |
| weakness_state, | |
| cognitive_state_state, | |
| model_name, | |
| language_preference, | |
| ], | |
| [quiz_box], | |
| ) | |
| # 概念总结 | |
| def on_summary( | |
| chat_history, | |
| course_outline, | |
| weaknesses, | |
| cognitive_state, | |
| model_name_val, | |
| language_pref_val, | |
| ): | |
| return summarize_conversation( | |
| chat_history, | |
| course_outline, | |
| weaknesses or [], | |
| cognitive_state, | |
| model_name_val, | |
| language_pref_val, | |
| ) | |
| summary_btn.click( | |
| on_summary, | |
| [ | |
| chatbot, | |
| course_outline_state, | |
| weakness_state, | |
| cognitive_state_state, | |
| model_name, | |
| language_preference, | |
| ], | |
| [summary_box], | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() | |