SarahXia0405 commited on
Commit
80d7aad
·
verified ·
1 Parent(s): 74f25bb

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +33 -706
app.py CHANGED
@@ -1,701 +1,28 @@
1
- import os
2
- import re
3
- import math
4
  from typing import List, Dict, Tuple, Optional
5
 
6
  import gradio as gr
7
- from openai import OpenAI
8
- from docx import Document
9
 
10
- # ---------- 环境变量 ----------
11
- OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
12
- if not OPENAI_API_KEY:
13
- raise RuntimeError(
14
- "OPENAI_API_KEY is not set. Please go to Settings → Secrets and add it."
15
- )
16
-
17
- client = OpenAI(api_key=OPENAI_API_KEY)
18
- DEFAULT_MODEL = "gpt-4.1-mini"
19
- EMBEDDING_MODEL = "text-embedding-3-small"
20
-
21
- # ---------- 默认 GenAI 课程大纲 ----------
22
- DEFAULT_COURSE_TOPICS = [
23
- "Week 0 – Welcome & What is Generative AI; course outcomes LO1–LO5.",
24
- "Week 1 – Foundations of GenAI: LLMs, Transformer & self-attention, perplexity.",
25
- "Week 2 – Foundation Models & multimodal models; data scale, bias & risks.",
26
- "Week 3 – Choosing Pre-trained Models; open-source vs proprietary; cost vs quality.",
27
- "Week 4 – Prompt Engineering: core principles; zero/few-shot; CoT; ReAct.",
28
- "Week 5 – Building a Simple Chatbot; memory (short vs long term); LangChain & UI.",
29
- "Week 6 – Review Week; cross-module consolidation & self-check prompts.",
30
- "Week 7 – Retrieval-Augmented Generation (RAG); embeddings; hybrid retrieval.",
31
- "Week 8 – Agents & Agentic RAG; planning, tools, knowledge augmentation.",
32
- "Week 9 – Evaluating GenAI Apps; hallucination, bias/fairness, metrics.",
33
- "Week 10 – Responsible AI; risks, governance, EU AI Act-style ideas.",
34
- ]
35
-
36
- # ---------- 学习模式 ----------
37
- LEARNING_MODES = [
38
- "Concept Explainer",
39
- "Socratic Tutor",
40
- "Exam Prep / Quiz",
41
- "Assignment Helper",
42
- "Quick Summary",
43
- ]
44
-
45
- LEARNING_MODE_INSTRUCTIONS = {
46
- "Concept Explainer": (
47
- "Explain concepts step by step. Use clear definitions, key formulas or structures, "
48
- "and one or two simple examples. Focus on clarity over depth. Regularly check if "
49
- "the student is following."
50
- ),
51
- "Socratic Tutor": (
52
- "Use a Socratic style. Ask the student ONE short question at a time, guide them to "
53
- "reason step by step, and only give full explanations after they try. Prioritize "
54
- "questions and hints over long lectures."
55
- ),
56
- "Exam Prep / Quiz": (
57
- "Behave like an exam prep coach. Often propose short quiz-style questions "
58
- "(multiple choice or short answer), then explain the solutions clearly. Emphasize "
59
- "common traps and how to avoid them."
60
- ),
61
- "Assignment Helper": (
62
- "Help with assignments WITHOUT giving full final solutions. Clarify requirements, "
63
- "break tasks into smaller steps, and provide hints, partial examples, or pseudo-code "
64
- "instead of complete code or final answers. Encourage the student to attempt each "
65
- "step before revealing more."
66
- ),
67
- "Quick Summary": (
68
- "Provide concise, bullet-point style summaries and cheat-sheet style notes. "
69
- "Focus on key ideas and avoid long paragraphs."
70
- ),
71
- }
72
-
73
- # ---------- 上传文件类型 ----------
74
- DOC_TYPES = [
75
- "Syllabus",
76
- "Lecture Slides / PPT",
77
- "Literature Review / Paper",
78
- "Other Course Document",
79
- ]
80
-
81
- # ---------- Clare 的基础 System Prompt ----------
82
- CLARE_SYSTEM_PROMPT = """
83
- You are Clare, an AI teaching assistant for Hanbridge University.
84
-
85
- Core identity:
86
- - You are patient, encouraging, and structured like a very good TA.
87
- - Your UI and responses should be in ENGLISH by default.
88
- - However, you can understand BOTH English and Chinese, and you may reply in Chinese
89
- if the student clearly prefers Chinese or asks you to.
90
-
91
- General responsibilities:
92
- 1. Help students understand course concepts step by step.
93
- 2. Ask short check-up questions to confirm understanding instead of giving huge long lectures.
94
- 3. When the student seems confused, break content into smaller chunks and use simple language first.
95
- 4. When the student is advanced, you can switch to more technical explanations.
96
-
97
- Safety and honesty:
98
- - If you don’t know, say you are not sure and suggest how to verify.
99
- - Do not fabricate references, exam answers, or grades.
100
- """
101
-
102
- # ---------- syllabus 解析 ----------
103
- def parse_syllabus_docx(file_path: str, max_lines: int = 15) -> List[str]:
104
- """
105
- 非常简单的 syllabus 解析:取前若干个非空段落当作主题行。
106
- 只是为了给 Clare 一些课程上下文,不追求超精确结构。
107
- """
108
- topics: List[str] = []
109
- try:
110
- doc = Document(file_path)
111
- for para in doc.paragraphs:
112
- text = para.text.strip()
113
- if not text:
114
- continue
115
- topics.append(text)
116
- if len(topics) >= max_lines:
117
- break
118
- except Exception as e:
119
- topics = [f"[Error parsing syllabus: {e}]"]
120
-
121
- return topics
122
-
123
-
124
- # ---------- 简单“弱项”检测 ----------
125
- WEAKNESS_KEYWORDS = [
126
- "don't understand",
127
- "do not understand",
128
- "not understand",
129
- "not sure",
130
- "confused",
131
- "hard to",
132
- "difficult",
133
- "struggle",
134
- "不会",
135
- "不懂",
136
- "看不懂",
137
- "搞不清",
138
- "很难",
139
- ]
140
-
141
- # ---------- 简单“掌握”检测 ----------
142
- MASTERY_KEYWORDS = [
143
- "got it",
144
- "makes sense",
145
- "now i see",
146
- "i see",
147
- "understand now",
148
- "clear now",
149
- "easy",
150
- "no problem",
151
- "没问题",
152
- "懂了",
153
- "明白了",
154
- "清楚了",
155
- ]
156
-
157
- def update_weaknesses_from_message(message: str, weaknesses: List[str]) -> List[str]:
158
- lower_msg = message.lower()
159
- if any(k in lower_msg for k in WEAKNESS_KEYWORDS):
160
- weaknesses = weaknesses or []
161
- weaknesses.append(message)
162
- return weaknesses
163
-
164
-
165
- def update_cognitive_state_from_message(
166
- message: str,
167
- state: Optional[Dict[str, int]],
168
- ) -> Dict[str, int]:
169
- """
170
- 简单认知状态统计:
171
- - 遇到困惑类关键词 → confusion +1
172
- - 遇到掌握类关键词 → mastery +1
173
- """
174
- if state is None:
175
- state = {"confusion": 0, "mastery": 0}
176
-
177
- lower_msg = message.lower()
178
- if any(k in lower_msg for k in WEAKNESS_KEYWORDS):
179
- state["confusion"] = state.get("confusion", 0) + 1
180
- if any(k in lower_msg for k in MASTERY_KEYWORDS):
181
- state["mastery"] = state.get("mastery", 0) + 1
182
- return state
183
-
184
-
185
- def describe_cognitive_state(state: Optional[Dict[str, int]]) -> str:
186
- if not state:
187
- return "unknown"
188
- confusion = state.get("confusion", 0)
189
- mastery = state.get("mastery", 0)
190
- if confusion >= 2 and confusion >= mastery + 1:
191
- return "student shows signs of HIGH cognitive load (often confused)."
192
- elif mastery >= 2 and mastery >= confusion + 1:
193
- return "student seems COMFORTABLE; material may be slightly easy."
194
- else:
195
- return "mixed or uncertain cognitive state."
196
-
197
-
198
- # ---------- 语言检测(用于 Auto 模式) ----------
199
- def detect_language(message: str, preference: str) -> str:
200
- """
201
- preference:
202
- - 'English' → 强制英文
203
- - '中文' → 强制中文
204
- - 'Auto' → 检测文本是否包含中文字符
205
- """
206
- if preference in ("English", "中文"):
207
- return preference
208
- # Auto 模式下简单检测是否含有中文字符
209
- if re.search(r"[\u4e00-\u9fff]", message):
210
- return "中文"
211
- return "English"
212
-
213
-
214
- # ---------- Session 状态展示 ----------
215
- def render_session_status(
216
- learning_mode: str,
217
- weaknesses: Optional[List[str]],
218
- cognitive_state: Optional[Dict[str, int]],
219
- ) -> str:
220
- lines: List[str] = []
221
- lines.append("### Session status\n")
222
- lines.append(f"- Learning mode: **{learning_mode}**")
223
- lines.append(f"- Cognitive state: {describe_cognitive_state(cognitive_state)}")
224
-
225
- if weaknesses:
226
- lines.append("- Recent difficulties (last 3):")
227
- for w in weaknesses[-3:]:
228
- lines.append(f" - {w}")
229
- else:
230
- lines.append("- Recent difficulties: *(none yet)*")
231
-
232
- return "\n".join(lines)
233
-
234
-
235
- # ---------- Same Question Check helpers ----------
236
- def _normalize_text(text: str) -> str:
237
- """
238
- 将文本转为小写、去除标点和多余空格,用于简单相似度计算。
239
- """
240
- text = text.lower().strip()
241
- # 去掉标点符号,只保留字母数字和空格
242
- text = re.sub(r"[^\w\s]", " ", text)
243
- text = re.sub(r"\s+", " ", text)
244
- return text
245
-
246
-
247
- def _jaccard_similarity(a: str, b: str) -> float:
248
- tokens_a = set(a.split())
249
- tokens_b = set(b.split())
250
- if not tokens_a or not tokens_b:
251
- return 0.0
252
- return len(tokens_a & tokens_b) / len(tokens_a | tokens_b)
253
-
254
-
255
- def cosine_similarity(a: List[float], b: List[float]) -> float:
256
- if not a or not b or len(a) != len(b):
257
- return 0.0
258
- dot = sum(x * y for x, y in zip(a, b))
259
- norm_a = math.sqrt(sum(x * x for x in a))
260
- norm_b = math.sqrt(sum(y * y for y in b))
261
- if norm_a == 0 or norm_b == 0:
262
- return 0.0
263
- return dot / (norm_a * norm_b)
264
-
265
-
266
- def get_embedding(text: str) -> Optional[List[float]]:
267
- """
268
- 调用 OpenAI Embedding API,将文本编码为向量。
269
- """
270
- try:
271
- resp = client.embeddings.create(
272
- model=EMBEDDING_MODEL,
273
- input=[text],
274
- )
275
- return resp.data[0].embedding
276
- except Exception as e:
277
- # 打到 Hugging Face 的 log,方便你在 Space Logs 里看
278
- print(f"[Embedding error] {repr(e)}")
279
- return None
280
-
281
-
282
- def find_similar_past_question(
283
- message: str,
284
- history: List[Tuple[str, str]],
285
- jaccard_threshold: float = 0.65,
286
- embedding_threshold: float = 0.85,
287
- max_turns_to_check: int = 6,
288
- ) -> Optional[Tuple[str, str, float]]:
289
- """
290
- 在最近若干轮历史对话中查找与当前问题相似的既往问题。
291
-
292
- 两级检测:
293
- 1. 先用 Jaccard 做快速近似匹配(文本几乎一样的情况)
294
- 2. 再用 OpenAI embedding 做语义相似度检测(改写、同义句)
295
-
296
- 返回:
297
- (past_question, past_answer, similarity_score) 或 None
298
- """
299
- # ---------- 第一步:Jaccard 快速检测 ----------
300
- norm_msg = _normalize_text(message)
301
- if not norm_msg:
302
- return None
303
-
304
- best_sim_j = 0.0
305
- best_pair_j: Optional[Tuple[str, str]] = None
306
- checked = 0
307
-
308
- for user_q, assistant_a in reversed(history):
309
- checked += 1
310
- if checked > max_turns_to_check:
311
- break
312
-
313
- norm_hist_q = _normalize_text(user_q)
314
- if not norm_hist_q:
315
- continue
316
-
317
- if norm_msg == norm_hist_q:
318
- # 完全相同,直接视为重复
319
- return user_q, assistant_a, 1.0
320
-
321
- sim_j = _jaccard_similarity(norm_msg, norm_hist_q)
322
- if sim_j > best_sim_j:
323
- best_sim_j = sim_j
324
- best_pair_j = (user_q, assistant_a)
325
-
326
- if best_pair_j and best_sim_j >= jaccard_threshold:
327
- # 词面高度相似,直接视为重复
328
- return best_pair_j[0], best_pair_j[1], best_sim_j
329
-
330
- # ---------- 第二步:Embedding 语义相似度 ----------
331
- # 如果历史太少,就没必要算 embedding
332
- if not history:
333
- return None
334
-
335
- msg_emb = get_embedding(message)
336
- if msg_emb is None:
337
- # embedding 调用失败,放弃语义检测
338
- return None
339
-
340
- best_sim_e = 0.0
341
- best_pair_e: Optional[Tuple[str, str]] = None
342
- checked = 0
343
-
344
- for user_q, assistant_a in reversed(history):
345
- checked += 1
346
- if checked > max_turns_to_check:
347
- break
348
-
349
- hist_emb = get_embedding(user_q)
350
- if hist_emb is None:
351
- continue
352
-
353
- sim_e = cosine_similarity(msg_emb, hist_emb)
354
- if sim_e > best_sim_e:
355
- best_sim_e = sim_e
356
- best_pair_e = (user_q, assistant_a)
357
-
358
- if best_pair_e and best_sim_e >= embedding_threshold:
359
- return best_pair_e[0], best_pair_e[1], best_sim_e
360
-
361
- return None
362
-
363
-
364
- # ---------- 构建 messages ----------
365
- def build_messages(
366
- user_message: str,
367
- history: List[Tuple[str, str]],
368
- language_preference: str,
369
- learning_mode: str,
370
- doc_type: str,
371
- course_outline: Optional[List[str]],
372
- weaknesses: Optional[List[str]],
373
- cognitive_state: Optional[Dict[str, int]],
374
- ) -> List[Dict[str, str]]:
375
- messages: List[Dict[str, str]] = [
376
- {"role": "system", "content": CLARE_SYSTEM_PROMPT}
377
- ]
378
-
379
- # 学习模式注入
380
- if learning_mode in LEARNING_MODE_INSTRUCTIONS:
381
- mode_instruction = LEARNING_MODE_INSTRUCTIONS[learning_mode]
382
- messages.append(
383
- {
384
- "role": "system",
385
- "content": f"Current learning mode: {learning_mode}. {mode_instruction}",
386
- }
387
- )
388
-
389
- # 课程大纲注入
390
- topics = course_outline if course_outline else DEFAULT_COURSE_TOPICS
391
- topics_text = " | ".join(topics)
392
- messages.append(
393
- {
394
- "role": "system",
395
- "content": (
396
- "Here is the course syllabus context. Use this to stay aligned "
397
- "with the course topics when answering: "
398
- + topics_text
399
- ),
400
- }
401
- )
402
-
403
- # 上传文件类型提示(仅作语境)
404
- if doc_type and doc_type != "Syllabus":
405
- messages.append(
406
- {
407
- "role": "system",
408
- "content": (
409
- f"The student also uploaded a {doc_type} document as supporting material. "
410
- "You do not see the full content directly, but you may assume it is relevant "
411
- "to the same course and topics."
412
- ),
413
- }
414
- )
415
-
416
- # 学生弱项提示(会话内记忆)
417
- if weaknesses:
418
- weak_text = " | ".join(weaknesses[-5:]) # 最近几条即可
419
- messages.append(
420
- {
421
- "role": "system",
422
- "content": (
423
- "The student seems to struggle with the following questions or topics. "
424
- "Be extra gentle and clear when these appear: " + weak_text
425
- ),
426
- }
427
- )
428
-
429
- # 认知状态提示(动态复杂度调整)
430
- if cognitive_state:
431
- confusion = cognitive_state.get("confusion", 0)
432
- mastery = cognitive_state.get("mastery", 0)
433
- if confusion >= 2 and confusion >= mastery + 1:
434
- messages.append(
435
- {
436
- "role": "system",
437
- "content": (
438
- "The student is currently under HIGH cognitive load. "
439
- "Use simpler language, shorter steps, and more concrete examples. "
440
- "Avoid long derivations in a single answer, and check understanding "
441
- "frequently."
442
- ),
443
- }
444
- )
445
- elif mastery >= 2 and mastery >= confusion + 1:
446
- messages.append(
447
- {
448
- "role": "system",
449
- "content": (
450
- "The student seems comfortable with the material. "
451
- "You may increase difficulty slightly, introduce deeper follow-up "
452
- "questions, and connect concepts across topics."
453
- ),
454
- }
455
- )
456
- else:
457
- messages.append(
458
- {
459
- "role": "system",
460
- "content": (
461
- "The student's cognitive state is mixed or uncertain. "
462
- "Keep explanations clear and moderately paced, and probe for "
463
- "understanding with short questions."
464
- ),
465
- }
466
- )
467
-
468
- # 语言偏好控制
469
- if language_preference == "English":
470
- messages.append(
471
- {"role": "system", "content": "Please answer in English."}
472
- )
473
- elif language_preference == "中文":
474
- messages.append(
475
- {"role": "system", "content": "请用中文回答学生的问题。"}
476
- )
477
-
478
- # 历史对话
479
- for user, assistant in history:
480
- messages.append({"role": "user", "content": user})
481
- if assistant is not None:
482
- messages.append({"role": "assistant", "content": assistant})
483
-
484
- # 当前输入
485
- messages.append({"role": "user", "content": user_message})
486
- return messages
487
-
488
-
489
- def chat_with_clare(
490
- message: str,
491
- history: List[Tuple[str, str]],
492
- model_name: str,
493
- language_preference: str,
494
- learning_mode: str,
495
- doc_type: str,
496
- course_outline: Optional[List[str]],
497
- weaknesses: Optional[List[str]],
498
- cognitive_state: Optional[Dict[str, int]],
499
- ):
500
- try:
501
- messages = build_messages(
502
- user_message=message,
503
- history=history,
504
- language_preference=language_preference,
505
- learning_mode=learning_mode,
506
- doc_type=doc_type,
507
- course_outline=course_outline,
508
- weaknesses=weaknesses,
509
- cognitive_state=cognitive_state,
510
- )
511
- response = client.chat.completions.create(
512
- model=model_name or DEFAULT_MODEL,
513
- messages=messages,
514
- temperature=0.5,
515
- )
516
- answer = response.choices[0].message.content
517
- except Exception as e:
518
- answer = f"⚠️ Error talking to the model: {e}"
519
-
520
- history = history + [(message, answer)]
521
- return answer, history
522
-
523
-
524
- # ---------- 导出对话为 Markdown ----------
525
- def export_conversation(
526
- history: List[Tuple[str, str]],
527
- course_outline: List[str],
528
- learning_mode_val: str,
529
- weaknesses: List[str],
530
- cognitive_state: Optional[Dict[str, int]],
531
- ) -> str:
532
- lines: List[str] = []
533
- lines.append("# Clare – Conversation Export\n")
534
- lines.append(f"- Learning mode: **{learning_mode_val}**\n")
535
- lines.append("- Course topics (short): " + "; ".join(course_outline[:5]) + "\n")
536
- lines.append(f"- Cognitive state snapshot: {describe_cognitive_state(cognitive_state)}\n")
537
-
538
- if weaknesses:
539
- lines.append("- Observed student difficulties:\n")
540
- for w in weaknesses[-5:]:
541
- lines.append(f" - {w}\n")
542
- lines.append("\n---\n\n")
543
-
544
- for user, assistant in history:
545
- lines.append(f"**Student:** {user}\n\n")
546
- lines.append(f"**Clare:** {assistant}\n\n")
547
- lines.append("---\n\n")
548
-
549
- return "".join(lines)
550
-
551
-
552
- # ---------- 生成 3 个 quiz 题目 ----------
553
- def generate_quiz_from_history(
554
- history: List[Tuple[str, str]],
555
- course_outline: List[str],
556
- weaknesses: List[str],
557
- cognitive_state: Optional[Dict[str, int]],
558
- model_name: str,
559
- language_preference: str,
560
- ) -> str:
561
- conversation_text = ""
562
- for user, assistant in history[-8:]: # 用最近几轮
563
- conversation_text += f"Student: {user}\nClare: {assistant}\n"
564
-
565
- topics_text = "; ".join(course_outline[:8])
566
- weakness_text = "; ".join(weaknesses[-5:]) if weaknesses else "N/A"
567
- cog_text = describe_cognitive_state(cognitive_state)
568
-
569
- messages = [
570
- {"role": "system", "content": CLARE_SYSTEM_PROMPT},
571
- {
572
- "role": "system",
573
- "content": (
574
- "Now your task is to create a **short concept quiz** for the student. "
575
- "Based on the conversation and course topics, generate **3 questions** "
576
- "(a mix of multiple-choice and short-answer is fine). After listing the "
577
- "questions, provide an answer key at the end under a heading 'Answer Key'. "
578
- "Number the questions Q1, Q2, Q3. Adjust the difficulty according to the "
579
- "student's cognitive state."
580
- ),
581
- },
582
- {
583
- "role": "system",
584
- "content": f"Course topics: {topics_text}",
585
- },
586
- {
587
- "role": "system",
588
- "content": f"Student known difficulties: {weakness_text}",
589
- },
590
- {
591
- "role": "system",
592
- "content": f"Student cognitive state: {cog_text}",
593
- },
594
- {
595
- "role": "user",
596
- "content": (
597
- "Here is the recent conversation between you and the student:\n\n"
598
- + conversation_text
599
- + "\n\nPlease create the quiz now."
600
- ),
601
- },
602
- ]
603
-
604
- if language_preference == "中文":
605
- messages.append(
606
- {
607
- "role": "system",
608
- "content": "请用中文给出问题和答案。",
609
- }
610
- )
611
-
612
- try:
613
- response = client.chat.completions.create(
614
- model=model_name or DEFAULT_MODEL,
615
- messages=messages,
616
- temperature=0.5,
617
- )
618
- quiz_text = response.choices[0].message.content
619
- except Exception as e:
620
- quiz_text = f"⚠️ Error generating quiz: {e}"
621
-
622
- return quiz_text
623
-
624
-
625
- # ---------- 概念总结(知识点摘要) ----------
626
- def summarize_conversation(
627
- history: List[Tuple[str, str]],
628
- course_outline: List[str],
629
- weaknesses: List[str],
630
- cognitive_state: Optional[Dict[str, int]],
631
- model_name: str,
632
- language_preference: str,
633
- ) -> str:
634
- conversation_text = ""
635
- for user, assistant in history[-10:]: # 最近 10 轮足够
636
- conversation_text += f"Student: {user}\nClare: {assistant}\n"
637
-
638
- topics_text = "; ".join(course_outline[:8])
639
- weakness_text = "; ".join(weaknesses[-5:]) if weaknesses else "N/A"
640
- cog_text = describe_cognitive_state(cognitive_state)
641
-
642
- messages = [
643
- {"role": "system", "content": CLARE_SYSTEM_PROMPT},
644
- {
645
- "role": "system",
646
- "content": (
647
- "Your task now is to produce a **concept-only summary** of this tutoring "
648
- "session. Only include knowledge points, definitions, key formulas, "
649
- "examples, and main takeaways. Do **not** include any personal remarks, "
650
- "jokes, or off-topic chat. Write in clear bullet points. This summary "
651
- "should be suitable for the student to paste into their study notes. "
652
- "Take into account what the student struggled with and their cognitive state."
653
- ),
654
- },
655
- {
656
- "role": "system",
657
- "content": f"Course topics context: {topics_text}",
658
- },
659
- {
660
- "role": "system",
661
- "content": f"Student known difficulties: {weakness_text}",
662
- },
663
- {
664
- "role": "system",
665
- "content": f"Student cognitive state: {cog_text}",
666
- },
667
- {
668
- "role": "user",
669
- "content": (
670
- "Here is the recent conversation between you and the student:\n\n"
671
- + conversation_text
672
- + "\n\nPlease summarize only the concepts and key ideas learned."
673
- ),
674
- },
675
- ]
676
-
677
- if language_preference == "中文":
678
- messages.append(
679
- {
680
- "role": "system",
681
- "content": "请用中文给出要点总结,只保留知识点和结论,使用条目符号。"
682
- }
683
- )
684
-
685
- try:
686
- response = client.chat.completions.create(
687
- model=model_name or DEFAULT_MODEL,
688
- messages=messages,
689
- temperature=0.4,
690
- )
691
- summary_text = response.choices[0].message.content
692
- except Exception as e:
693
- summary_text = f"⚠️ Error generating summary: {e}"
694
-
695
- return summary_text
696
 
697
 
698
- # ---------- Gradio UI ----------
699
  with gr.Blocks(title="Clare – Hanbridge AI Teaching Assistant") as demo:
700
  gr.Markdown(
701
  """
@@ -709,6 +36,7 @@ with gr.Blocks(title="Clare – Hanbridge AI Teaching Assistant") as demo:
709
  """
710
  )
711
 
 
712
  with gr.Row():
713
  model_name = gr.Textbox(
714
  label="Model name",
@@ -726,6 +54,7 @@ with gr.Blocks(title="Clare – Hanbridge AI Teaching Assistant") as demo:
726
  label="Learning mode",
727
  )
728
 
 
729
  with gr.Row():
730
  syllabus_file = gr.File(
731
  label="Upload course file (.docx)",
@@ -742,20 +71,18 @@ with gr.Blocks(title="Clare – Hanbridge AI Teaching Assistant") as demo:
742
  weakness_state = gr.State([])
743
  cognitive_state_state = gr.State({"confusion": 0, "mastery": 0})
744
 
745
- # syllabus 上传后更新课程大纲(仅当类型是 Syllabus 时解析)
746
  def update_outline(file, doc_type_val):
747
  if file is None:
748
  return DEFAULT_COURSE_TOPICS
749
- # Gradio File 默认传的是一个带 .name 的临时文件对象
750
  if doc_type_val == "Syllabus":
751
  try:
752
- file_path = file.name # 临时文件真实路径
753
  if file_path.lower().endswith(".docx"):
754
  topics = parse_syllabus_docx(file_path)
755
  return topics
756
  except Exception:
757
  return DEFAULT_COURSE_TOPICS
758
- # 其他类型文件目前不解析,只保留默认大纲
759
  return DEFAULT_COURSE_TOPICS
760
 
761
  syllabus_file.change(
@@ -764,6 +91,7 @@ with gr.Blocks(title="Clare – Hanbridge AI Teaching Assistant") as demo:
764
  outputs=[course_outline_state],
765
  )
766
 
 
767
  with gr.Row():
768
  chatbot = gr.Chatbot(
769
  label="Clare Chat",
@@ -798,7 +126,7 @@ with gr.Blocks(title="Clare – Hanbridge AI Teaching Assistant") as demo:
798
  lines=8,
799
  )
800
 
801
- # 主对话逻辑:Same Question Check + 更新弱项 + 认知状态 + 调用 Clare
802
  def respond(
803
  message,
804
  chat_history,
@@ -810,14 +138,14 @@ with gr.Blocks(title="Clare – Hanbridge AI Teaching Assistant") as demo:
810
  learning_mode_val,
811
  doc_type_val,
812
  ):
813
- # 先根据 Auto / English / 中文 决定本轮用什么语言
814
  resolved_lang = detect_language(message, language_pref_val)
815
 
816
- # 先更新弱项和认知状态
817
  weaknesses = update_weaknesses_from_message(message, weaknesses or [])
818
  cognitive_state = update_cognitive_state_from_message(message, cognitive_state)
819
 
820
- # ---------- Same Question Check ----------
821
  dup = find_similar_past_question(message, chat_history)
822
  if dup is not None:
823
  past_q, past_a, sim = dup
@@ -841,10 +169,9 @@ with gr.Blocks(title="Clare – Hanbridge AI Teaching Assistant") as demo:
841
 
842
  new_history = chat_history + [(message, answer)]
843
  status_text = render_session_status(learning_mode_val, weaknesses, cognitive_state)
844
- # 清空输入框,更新 history / 弱项 / 认知状态 / 状态栏
845
  return "", new_history, weaknesses, cognitive_state, status_text
846
 
847
- # ---------- 正常调用 Clare ----------
848
  answer, new_history = chat_with_clare(
849
  message=message,
850
  history=chat_history,
@@ -876,7 +203,7 @@ with gr.Blocks(title="Clare – Hanbridge AI Teaching Assistant") as demo:
876
  [user_input, chatbot, weakness_state, cognitive_state_state, session_status],
877
  )
878
 
879
- # 清空对话 & 状态 & 导出/quiz/summary
880
  def clear_all():
881
  empty_state = {"confusion": 0, "mastery": 0}
882
  status_text = render_session_status("Concept Explainer", [], empty_state)
@@ -889,7 +216,7 @@ with gr.Blocks(title="Clare – Hanbridge AI Teaching Assistant") as demo:
889
  queue=False,
890
  )
891
 
892
- # 导出对话按钮
893
  def on_export(chat_history, course_outline, learning_mode_val, weaknesses, cognitive_state):
894
  return export_conversation(
895
  chat_history,
@@ -905,7 +232,7 @@ with gr.Blocks(title="Clare – Hanbridge AI Teaching Assistant") as demo:
905
  [export_box],
906
  )
907
 
908
- # 生成 quiz 按钮
909
  def on_quiz(
910
  chat_history,
911
  course_outline,
@@ -936,7 +263,7 @@ with gr.Blocks(title="Clare – Hanbridge AI Teaching Assistant") as demo:
936
  [quiz_box],
937
  )
938
 
939
- # 概念总结按钮
940
  def on_summary(
941
  chat_history,
942
  course_outline,
 
1
+ # app.py
 
 
2
  from typing import List, Dict, Tuple, Optional
3
 
4
  import gradio as gr
 
 
5
 
6
+ from config import (
7
+ DEFAULT_MODEL,
8
+ DEFAULT_COURSE_TOPICS,
9
+ LEARNING_MODES,
10
+ DOC_TYPES,
11
+ )
12
+ from clare_core import (
13
+ parse_syllabus_docx,
14
+ update_weaknesses_from_message,
15
+ update_cognitive_state_from_message,
16
+ render_session_status,
17
+ find_similar_past_question,
18
+ detect_language,
19
+ chat_with_clare,
20
+ export_conversation,
21
+ generate_quiz_from_history,
22
+ summarize_conversation,
23
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
 
 
26
  with gr.Blocks(title="Clare – Hanbridge AI Teaching Assistant") as demo:
27
  gr.Markdown(
28
  """
 
36
  """
37
  )
38
 
39
+ # 顶部:模型、语言偏好、学习模式
40
  with gr.Row():
41
  model_name = gr.Textbox(
42
  label="Model name",
 
54
  label="Learning mode",
55
  )
56
 
57
+ # 课程文件上传
58
  with gr.Row():
59
  syllabus_file = gr.File(
60
  label="Upload course file (.docx)",
 
71
  weakness_state = gr.State([])
72
  cognitive_state_state = gr.State({"confusion": 0, "mastery": 0})
73
 
74
+ # 上传 syllabus 时更新课程大纲
75
  def update_outline(file, doc_type_val):
76
  if file is None:
77
  return DEFAULT_COURSE_TOPICS
 
78
  if doc_type_val == "Syllabus":
79
  try:
80
+ file_path = file.name
81
  if file_path.lower().endswith(".docx"):
82
  topics = parse_syllabus_docx(file_path)
83
  return topics
84
  except Exception:
85
  return DEFAULT_COURSE_TOPICS
 
86
  return DEFAULT_COURSE_TOPICS
87
 
88
  syllabus_file.change(
 
91
  outputs=[course_outline_state],
92
  )
93
 
94
+ # 左侧聊天,右侧 Session 状态栏
95
  with gr.Row():
96
  chatbot = gr.Chatbot(
97
  label="Clare Chat",
 
126
  lines=8,
127
  )
128
 
129
+ # 主对话逻辑
130
  def respond(
131
  message,
132
  chat_history,
 
138
  learning_mode_val,
139
  doc_type_val,
140
  ):
141
+ # 1) 决定本轮语言(Auto / English / 中文)
142
  resolved_lang = detect_language(message, language_pref_val)
143
 
144
+ # 2) 更新弱项 & 认知状态
145
  weaknesses = update_weaknesses_from_message(message, weaknesses or [])
146
  cognitive_state = update_cognitive_state_from_message(message, cognitive_state)
147
 
148
+ # 3) Same Question Check
149
  dup = find_similar_past_question(message, chat_history)
150
  if dup is not None:
151
  past_q, past_a, sim = dup
 
169
 
170
  new_history = chat_history + [(message, answer)]
171
  status_text = render_session_status(learning_mode_val, weaknesses, cognitive_state)
 
172
  return "", new_history, weaknesses, cognitive_state, status_text
173
 
174
+ # 4) 正常调用 Clare
175
  answer, new_history = chat_with_clare(
176
  message=message,
177
  history=chat_history,
 
203
  [user_input, chatbot, weakness_state, cognitive_state_state, session_status],
204
  )
205
 
206
+ # 清空对话 & 状态
207
  def clear_all():
208
  empty_state = {"confusion": 0, "mastery": 0}
209
  status_text = render_session_status("Concept Explainer", [], empty_state)
 
216
  queue=False,
217
  )
218
 
219
+ # 导出对话
220
  def on_export(chat_history, course_outline, learning_mode_val, weaknesses, cognitive_state):
221
  return export_conversation(
222
  chat_history,
 
232
  [export_box],
233
  )
234
 
235
+ # 生成 quiz
236
  def on_quiz(
237
  chat_history,
238
  course_outline,
 
263
  [quiz_box],
264
  )
265
 
266
+ # 概念总结
267
  def on_summary(
268
  chat_history,
269
  course_outline,