claudqunwang Cursor commited on
Commit
fbe1c8a
·
1 Parent(s): 5ec50c1

Add teacher agent, docs, and project updates; ClareVoiceV1 remains untracked (nested repo)

Browse files
.gitignore CHANGED
@@ -31,3 +31,9 @@ data/courses/**/raw/
31
 
32
  # HF Space clones (nested repos – push main project only)
33
  hf_space/
 
 
 
 
 
 
 
31
 
32
  # HF Space clones (nested repos – push main project only)
33
  hf_space/
34
+
35
+ # 临时目录:ClareVoice 独立仓库 push 用,push 完成后可删除
36
+ _clarevoice_push/
37
+
38
+ # ClareCourseWare Space 本地 clone,push 到 HF 后保留或删除
39
+ ClareCourseWare_push/
PUSH_TO_HF.md ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 推送到 Hugging Face Space(claudqunwang/ClareVoice)
2
+
3
+ ## 之前「整库 force 推送」和「只换 UI」的区别
4
+
5
+ | | 之前的做法(整库 force 推送) | 只换 UI(推荐) |
6
+ |---|---|---|
7
+ | **操作** | `git push huggingface main --force`,把本地整份项目推上去 | 先 clone HF 上现有 Space,只**新增** web 前端 + 后端 api,改 Dockerfile,再普通 push |
8
+ | **结果** | HF 上所有内容被本地项目**完全覆盖**,你在 HF 上改过的 app.py、config、Dockerfile、README 等都会变成本地版本 | HF 上现有文件**全部保留**,只多出 `web/`、`api/` 和一个新的 Dockerfile,运行时改用 React UI |
9
+ | **你的改动** | 会丢失 | 会保留(原有文件仍在仓库里) |
10
+
11
+ 所以:**不想覆盖在 HF 上的改动、只想换成 React UI 时**,不要用 force 推送整库,而是按下面「只换 UI」的步骤做。
12
+
13
+ ---
14
+
15
+ ## 做法一:只换 UI(保留你在 HF 上的所有改动)
16
+
17
+ 在**本机**执行(需已安装 git、node、python):
18
+
19
+ ```bash
20
+ # 1. 克隆 HF 上当前的 Space(保留你的历史与改动)
21
+ git clone https://huggingface.co/spaces/claudqunwang/ClareVoice
22
+ cd ClareVoice
23
+
24
+ # 2. 从本地 Clare 项目复制「仅 UI 相关」内容(不覆盖你已有的文件)
25
+ # 把下面的 /path/to/AI_Agent_Clare-main 改成你本机项目路径
26
+ PROJECT=/Users/qunwang/AI_Agent_Clare-main
27
+
28
+ cp -r "$PROJECT/web" .
29
+ cp -r "$PROJECT/api" .
30
+
31
+ # 3. 用「产品版」Dockerfile 替换当前的(改为构建 web + 跑 FastAPI 提供 React UI)
32
+ cp "$PROJECT/Dockerfile" .
33
+
34
+ # 4. 为 FastAPI 增加依赖(Space 原 requirements 可能没有)
35
+ echo "fastapi>=0.111.0" >> requirements.txt
36
+ echo "uvicorn[standard]>=0.30.0" >> requirements.txt
37
+ echo "python-multipart>=0.0.9" >> requirements.txt
38
+
39
+ # 5. 提交并推送(普通 push,不 force)
40
+ git add web/ api/ Dockerfile requirements.txt
41
+ git status # 确认只多了 web、api 和 Dockerfile
42
+ git commit -m "Switch to React product UI (web + api), keep existing files"
43
+ git push
44
+ ```
45
+
46
+ 这样 HF 上会多出 `web/`、`api/` 和新的 `Dockerfile`,**原有 app.py、config.py、clare_core.py 等都不会被覆盖**;之后 Space 会用新 Dockerfile 构建并显示 React UI。
47
+
48
+ **注意**:新 Dockerfile 的启动命令是 `uvicorn api.server:app`,即运行时用的是 **api/** 里的后端(api/config.py、api/clare_core.py 等)。若你之前在 HF 上只改了**根目录**的 config、clare_core,那些修改不会自动出现在新 UI 的后端里;若需要沿用,可以把根目录的配置或逻辑同步到 api/ 下,或再改 Dockerfile 的 working directory 让 api 使用根目录模块(需自行改 import 路径)。
49
+
50
+ ---
51
+
52
+ ## 做法二:整库覆盖(不保留 HF 上的改动)
53
+
54
+ 仅当你确定要**用本地项目完全替换** Space 内容时再用:
55
+
56
+ ```bash
57
+ cd /Users/qunwang/AI_Agent_Clare-main
58
+ git push -u huggingface main --force
59
+ ```
60
+
61
+ 这会用本地整份项目(含 GENAI COURSES、当前 README、.gitignore 等)覆盖 HF 上的 ClareVoice,你在 HF 上做过的任何修改都会丢失。
api/config.py CHANGED
@@ -200,3 +200,15 @@ Safety and honesty:
200
  - Do not fabricate references, exam answers, or grades.
201
  """
202
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  - Do not fabricate references, exam answers, or grades.
201
  """
202
 
203
+ # ============================
204
+ # Weaviate(教师 Agent / ClareVoice 共用,可选)
205
+ # ============================
206
+ WEAVIATE_URL = (os.getenv("WEAVIATE_URL") or "").strip()
207
+ WEAVIATE_API_KEY = (os.getenv("WEAVIATE_API_KEY") or "").strip()
208
+ WEAVIATE_COLLECTION = (os.getenv("WEAVIATE_COLLECTION") or "GenAICourses").strip()
209
+ if WEAVIATE_COLLECTION and not WEAVIATE_COLLECTION[0].isupper():
210
+ WEAVIATE_COLLECTION = "GenAICourses"
211
+ USE_WEAVIATE = bool(WEAVIATE_URL and WEAVIATE_API_KEY)
212
+ if USE_WEAVIATE and not WEAVIATE_URL.startswith("http"):
213
+ WEAVIATE_URL = "https://" + WEAVIATE_URL
214
+
api/routes_teacher.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # api/routes_teacher.py
2
+ """教师 Agent API:课程描述、文档建议、作业题库建议、学习评估分析。"""
3
+ from fastapi import APIRouter, HTTPException
4
+ from pydantic import BaseModel, Field
5
+
6
+ from api.teacher_agent import (
7
+ generate_course_description,
8
+ suggest_course_doc_content,
9
+ suggest_assignments_questions,
10
+ analyze_student_assessment,
11
+ )
12
+ from api.config import USE_WEAVIATE
13
+
14
+ router = APIRouter(tags=["teacher"])
15
+
16
+
17
+ class CourseDescriptionRequest(BaseModel):
18
+ topic: str = Field(..., min_length=1, description="课程主题/名称")
19
+ outline_hint: str | None = Field(None, description="可选:大纲或要点")
20
+
21
+
22
+ class CourseDescriptionResponse(BaseModel):
23
+ ok: bool = True
24
+ description: str
25
+ weaviate_used: bool = False
26
+
27
+
28
+ @router.post("/api/teacher/course-description", response_model=CourseDescriptionResponse)
29
+ def post_course_description(req: CourseDescriptionRequest):
30
+ """课程描述生成。"""
31
+ try:
32
+ desc = generate_course_description(
33
+ topic=req.topic.strip(),
34
+ outline_hint=req.outline_hint.strip() if req.outline_hint else None,
35
+ )
36
+ return CourseDescriptionResponse(
37
+ description=desc,
38
+ weaviate_used=USE_WEAVIATE,
39
+ )
40
+ except Exception as e:
41
+ raise HTTPException(status_code=500, detail=str(e))
42
+
43
+
44
+ class DocSuggestionRequest(BaseModel):
45
+ topic: str = Field(..., min_length=1, description="主题或章节")
46
+ current_doc_excerpt: str | None = Field(None, description="当前已有内容片段")
47
+ doc_type: str = Field("讲义/课件", description="文档类型")
48
+
49
+
50
+ class DocSuggestionResponse(BaseModel):
51
+ ok: bool = True
52
+ suggestion: str
53
+ weaviate_used: bool = False
54
+
55
+
56
+ @router.post("/api/teacher/doc-suggestion", response_model=DocSuggestionResponse)
57
+ def post_doc_suggestion(req: DocSuggestionRequest):
58
+ """课程文档内容建议。"""
59
+ try:
60
+ suggestion = suggest_course_doc_content(
61
+ topic=req.topic.strip(),
62
+ current_doc_excerpt=req.current_doc_excerpt.strip() if req.current_doc_excerpt else None,
63
+ doc_type=req.doc_type.strip() or "讲义/课件",
64
+ )
65
+ return DocSuggestionResponse(
66
+ suggestion=suggestion,
67
+ weaviate_used=USE_WEAVIATE,
68
+ )
69
+ except Exception as e:
70
+ raise HTTPException(status_code=500, detail=str(e))
71
+
72
+
73
+ class AssignmentQuestionsRequest(BaseModel):
74
+ topic: str = Field(..., min_length=1, description="主题")
75
+ week_or_module: str | None = Field(None, description="周次/模块")
76
+ question_type: str = Field("混合", description="题型:选择题、简答题、开放题、混合")
77
+
78
+
79
+ class AssignmentQuestionsResponse(BaseModel):
80
+ ok: bool = True
81
+ suggestion: str
82
+ weaviate_used: bool = False
83
+
84
+
85
+ @router.post("/api/teacher/assignment-questions", response_model=AssignmentQuestionsResponse)
86
+ def post_assignment_questions(req: AssignmentQuestionsRequest):
87
+ """作业和题库生成建议。"""
88
+ try:
89
+ suggestion = suggest_assignments_questions(
90
+ topic=req.topic.strip(),
91
+ week_or_module=req.week_or_module.strip() if req.week_or_module else None,
92
+ question_type=req.question_type.strip() or "混合",
93
+ )
94
+ return AssignmentQuestionsResponse(
95
+ suggestion=suggestion,
96
+ weaviate_used=USE_WEAVIATE,
97
+ )
98
+ except Exception as e:
99
+ raise HTTPException(status_code=500, detail=str(e))
100
+
101
+
102
+ class AssessmentAnalysisRequest(BaseModel):
103
+ assessment_summary: str = Field(..., min_length=1, description="学生表现/评估摘要")
104
+ course_topic_hint: str | None = Field(None, description="可选:相关课程主题")
105
+
106
+
107
+ class AssessmentAnalysisResponse(BaseModel):
108
+ ok: bool = True
109
+ analysis: str
110
+ weaviate_used: bool = False
111
+
112
+
113
+ @router.post("/api/teacher/assessment-analysis", response_model=AssessmentAnalysisResponse)
114
+ def post_assessment_analysis(req: AssessmentAnalysisRequest):
115
+ """学生学习评估分析。"""
116
+ try:
117
+ analysis = analyze_student_assessment(
118
+ assessment_summary=req.assessment_summary.strip(),
119
+ course_topic_hint=req.course_topic_hint.strip() if req.course_topic_hint else None,
120
+ )
121
+ return AssessmentAnalysisResponse(
122
+ analysis=analysis,
123
+ weaviate_used=USE_WEAVIATE,
124
+ )
125
+ except Exception as e:
126
+ raise HTTPException(status_code=500, detail=str(e))
127
+
128
+
129
+ @router.get("/api/teacher/status")
130
+ def get_teacher_status():
131
+ """教师 Agent 状态(是否已配置 Weaviate)。"""
132
+ return {
133
+ "weaviate_configured": USE_WEAVIATE,
134
+ "features": [
135
+ "course-description",
136
+ "doc-suggestion",
137
+ "assignment-questions",
138
+ "assessment-analysis",
139
+ ],
140
+ }
api/server.py CHANGED
@@ -31,6 +31,8 @@ from api.tts_podcast import (
31
 
32
  # ✅ NEW: course directory + workspace schema routes
33
  from api.routes_directory import router as directory_router
 
 
34
 
35
  # ✅ LangSmith (optional)
36
  try:
@@ -92,6 +94,7 @@ app.add_middleware(
92
 
93
  # ✅ NEW: include directory/workspace APIs BEFORE SPA fallback
94
  app.include_router(directory_router)
 
95
 
96
  # ----------------------------
97
  # Static hosting (Vite build)
 
31
 
32
  # ✅ NEW: course directory + workspace schema routes
33
  from api.routes_directory import router as directory_router
34
+ # ✅ 教师 Agent:课程描述、文档建议、作业题库、学习评估
35
+ from api.routes_teacher import router as teacher_router
36
 
37
  # ✅ LangSmith (optional)
38
  try:
 
94
 
95
  # ✅ NEW: include directory/workspace APIs BEFORE SPA fallback
96
  app.include_router(directory_router)
97
+ app.include_router(teacher_router)
98
 
99
  # ----------------------------
100
  # Static hosting (Vite build)
api/teacher_agent.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # api/teacher_agent.py
2
+ """
3
+ 教师端 AI Agent:基于与 Clare 相同的 Weaviate(GenAICourses)与 OpenAI,为教师提供:
4
+ - 课程描述生成
5
+ - 课程文档内容建议
6
+ - 作业和题库生成建议
7
+ - 学生学习评估分析
8
+ """
9
+ from typing import Optional
10
+
11
+ from .config import client, DEFAULT_MODEL, USE_WEAVIATE
12
+ from .weaviate_retrieve import retrieve_from_weaviate
13
+
14
+ TEACHER_SYSTEM = """你是一位面向高校教师的 AI 助教,擅长课程设计、教学材料建议与学习评估分析。
15
+ 你与 Clare 共用同一套 GENAI 课程知识库(Weaviate)。回答时请优先依据提供的「课程知识库摘录」,
16
+ 并结合通用教学经验,给出简洁、可操作的中文建议。若未提供知识库摘录,则基于通用教育学与学科知识回答。"""
17
+
18
+
19
+ def _call_llm(user_content: str, system_extra: str = "", max_tokens: int = 1500) -> str:
20
+ system = TEACHER_SYSTEM
21
+ if system_extra:
22
+ system = system + "\n\n" + system_extra
23
+ try:
24
+ resp = client.chat.completions.create(
25
+ model=DEFAULT_MODEL,
26
+ messages=[
27
+ {"role": "system", "content": system},
28
+ {"role": "user", "content": user_content},
29
+ ],
30
+ temperature=0.6,
31
+ max_tokens=max_tokens,
32
+ timeout=60,
33
+ )
34
+ return (resp.choices[0].message.content or "").strip()
35
+ except Exception as e:
36
+ print(f"[teacher_agent] LLM error: {repr(e)}")
37
+ return f"生成时出错:{repr(e)}。请稍后重试。"
38
+
39
+
40
+ def generate_course_description(
41
+ topic: str,
42
+ outline_hint: Optional[str] = None,
43
+ ) -> str:
44
+ """
45
+ 课程描述生成:根据课程主题(及可选大纲要点)生成一段可用于课程介绍/选课页的描述。
46
+ """
47
+ rag = retrieve_from_weaviate(topic, top_k=6) if USE_WEAVIATE else ""
48
+ user = f"请根据以下信息,生成一段简洁的**课程描述**(约 150–250 字),适合放在课程介绍或选课页面。\n\n"
49
+ user += f"**课程主题/名称:** {topic}\n\n"
50
+ if outline_hint:
51
+ user += f"**大纲或要点(可选):**\n{outline_hint}\n\n"
52
+ if rag:
53
+ user += "**参考知识库摘录(请据此保持与现有课程内容一致):**\n" + rag[:4000] + "\n\n"
54
+ user += "请直接输出课程描述正文,无需重复题目。"
55
+ return _call_llm(user, max_tokens=800)
56
+
57
+
58
+ def suggest_course_doc_content(
59
+ topic: str,
60
+ current_doc_excerpt: Optional[str] = None,
61
+ doc_type: str = "讲义/课件",
62
+ ) -> str:
63
+ """
64
+ 课程文档内容建议:针对某一主题或现有文档片段,给出可写入讲义/课件的内容建议。
65
+ """
66
+ rag = retrieve_from_weaviate(topic, top_k=8) if USE_WEAVIATE else ""
67
+ user = f"请针对以下**课程文档**(类型:{doc_type})给出**内容建议**:结构要点、关键概念、可选的例子或习题方向。\n\n"
68
+ user += f"**主题或章节:** {topic}\n\n"
69
+ if current_doc_excerpt:
70
+ user += f"**当前已有内容(片段):**\n{current_doc_excerpt[:2000]}\n\n"
71
+ if rag:
72
+ user += "**参考知识库摘录:**\n" + rag[:5000] + "\n\n"
73
+ user += "请用分点或短段落给出建议,便于教师直接采纳或改写。"
74
+ return _call_llm(user, max_tokens=1200)
75
+
76
+
77
+ def suggest_assignments_questions(
78
+ topic: str,
79
+ week_or_module: Optional[str] = None,
80
+ question_type: str = "混合",
81
+ ) -> str:
82
+ """
83
+ 作业和题库生成建议:根据主题(及可选周次/模块)给出作业题、练习题或考试题建议。
84
+ question_type 可为:选择题、简答题、开放题、混合 等。
85
+ """
86
+ rag = retrieve_from_weaviate(topic, top_k=8) if USE_WEAVIATE else ""
87
+ user = f"请根据以下课程信息,给出**作业与题库**的生成建议:包含题目类型、难度分布、2–3 道示例题(含参考答案要点)。\n\n"
88
+ user += f"**主题:** {topic}\n\n"
89
+ if week_or_module:
90
+ user += f"**周次/模块:** {week_or_module}\n\n"
91
+ user += f"**题型偏好:** {question_type}\n\n"
92
+ if rag:
93
+ user += "**参考知识库摘录:**\n" + rag[:5000] + "\n\n"
94
+ user += "请直接给出建议与示例题,便于教师录入题库或布置作业。"
95
+ return _call_llm(user, max_tokens=1500)
96
+
97
+
98
+ def analyze_student_assessment(
99
+ assessment_summary: str,
100
+ course_topic_hint: Optional[str] = None,
101
+ ) -> str:
102
+ """
103
+ 学生学习评估分析:根据教师提供的学生表现摘要(如作业/测验得分、常见错误、参与度等),
104
+ 给出简要分析结论与教学改进建议。
105
+ """
106
+ rag = ""
107
+ if course_topic_hint and USE_WEAVIATE:
108
+ rag = retrieve_from_weaviate(course_topic_hint, top_k=4)
109
+ user = "请根据以下**学生学习评估信息**,给出简要的**分析结论**与**教学改进建议**(分点、可操作)。\n\n"
110
+ user += f"**评估摘要:**\n{assessment_summary}\n\n"
111
+ if course_topic_hint:
112
+ user += f"**相关课程主题:** {course_topic_hint}\n\n"
113
+ if rag:
114
+ user += "**参考知识库摘录(可选,用于对齐课程目标):**\n" + rag[:3000] + "\n\n"
115
+ user += "请用中文输出,控制在 300–500 字。"
116
+ return _call_llm(user, max_tokens=800)
api/weaviate_retrieve.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # api/weaviate_retrieve.py
2
+ """
3
+ 与 ClareVoice 共用同一 Weaviate 数据库(GenAICourses)的检索封装。
4
+ 教师 Agent 和 Clare 均可调用,需与 build_weaviate_index 使用相同 embedding(HF all-MiniLM-L6-v2)。
5
+ """
6
+ import os
7
+ from typing import Optional
8
+
9
+ from .config import USE_WEAVIATE, WEAVIATE_URL, WEAVIATE_API_KEY, WEAVIATE_COLLECTION
10
+
11
+
12
+ def retrieve_from_weaviate(query: str, top_k: int = 8, timeout_sec: float = 45.0) -> str:
13
+ """
14
+ 从 Weaviate Cloud 的 GenAICourses 中检索与 query 相关的课程片段。
15
+ 使用 HuggingFace all-MiniLM-L6-v2 与建索引时一致。
16
+ 若未配置 Weaviate、query 过短、或依赖未安装,返回空字符串(教师 Agent 仍可运行,仅无 RAG)。
17
+ """
18
+ if not USE_WEAVIATE or not query or len(query.strip()) < 3:
19
+ return ""
20
+
21
+ def _call() -> str:
22
+ try:
23
+ import weaviate
24
+ from weaviate.classes.init import Auth
25
+ from llama_index.core import Settings, VectorStoreIndex
26
+ from llama_index.vector_stores.weaviate import WeaviateVectorStore
27
+ from llama_index.embeddings.huggingface import HuggingFaceEmbedding
28
+
29
+ Settings.embed_model = HuggingFaceEmbedding(
30
+ model_name=os.getenv("HF_EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2")
31
+ )
32
+ client = weaviate.connect_to_weaviate_cloud(
33
+ cluster_url=WEAVIATE_URL,
34
+ auth_credentials=Auth.api_key(WEAVIATE_API_KEY),
35
+ )
36
+ try:
37
+ if not client.is_ready():
38
+ return ""
39
+ vs = WeaviateVectorStore(
40
+ weaviate_client=client,
41
+ index_name=WEAVIATE_COLLECTION,
42
+ )
43
+ index = VectorStoreIndex.from_vector_store(vs)
44
+ nodes = index.as_retriever(similarity_top_k=top_k).retrieve(query)
45
+ return "\n\n---\n\n".join(n.get_content() for n in nodes) if nodes else ""
46
+ finally:
47
+ client.close()
48
+ except ImportError as e:
49
+ print(f"[weaviate_retrieve] 未安装 weaviate/llama_index,跳过 RAG: {e}")
50
+ return ""
51
+ except Exception as e:
52
+ print(f"[weaviate_retrieve] {repr(e)}")
53
+ return ""
54
+
55
+ try:
56
+ import concurrent.futures
57
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as ex:
58
+ return ex.submit(_call).result(timeout=timeout_sec)
59
+ except concurrent.futures.TimeoutError:
60
+ print(f"[weaviate_retrieve] timeout after {timeout_sec}s")
61
+ return ""
docs/FRONTEND_BACKEND_AND_PODCAST.md ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Clare AI Agent:前后端配合说明 & Podcast 集成到独立网站
2
+
3
+ ## 一、前后端需要配合做什么
4
+
5
+ 整体上:**前端负责 UI 和用户操作,后端负责会话状态、RAG、LLM 调用和音频生成**。具体分工如下。
6
+
7
+ ### 1. 会话与身份
8
+
9
+ | 能力 | 后端 | 前端 |
10
+ |------|------|------|
11
+ | 登录 | `POST /api/login` 校验并建立会话(内存里存 `user_id`) | 收集姓名/ID,调用 login,存 `user`(如 `user_id` = email) |
12
+ | 会话状态 | 按 `user_id` 存 history、course_outline、weaknesses、cognitive_state、上传文件等 | 所有需要会话的请求都带 `user_id`(如 `user.email`) |
13
+
14
+ ### 2. 对话与内容
15
+
16
+ | 能力 | 后端 | 前端 |
17
+ |------|------|------|
18
+ | 对话 | `POST /api/chat`:RAG 检索 + LLM 生成,返回回复 + 可选 references | 发 message、learning_mode、language_preference,渲染回复和引用 |
19
+ | 上传 | `POST /api/upload`:解析文件、更新该用户的 course_outline / RAG chunks | 选文件、doc_type,上传后刷新侧边栏/状态 |
20
+ | 导出 | `POST /api/export`:根据当前会话 history 生成 Markdown 文本 | 调 export,展示或下载文本 |
21
+ | 总结 | `POST /api/summary`:对当前会话做总结,返回 Markdown | 调 summary,展示在右侧或弹窗 |
22
+
23
+ ### 3. 音频(TTS / Podcast)
24
+
25
+ | 能力 | 后端 | 前端 |
26
+ |------|------|------|
27
+ | TTS | `POST /api/tts`:接收一段 text,调用 OpenAI TTS,返回 **MP3 二进制**(`audio/mpeg`) | 传 `user_id` + `text`,用 `response.blob()` 得到 Blob → `URL.createObjectURL(blob)` 给 `<audio src>` 播放或下载 |
28
+ | Podcast | `POST /api/podcast`:根据 `source=summary|conversation` 用当前会话生成脚本,再 TTS 合成,返回 **MP3 二进制**(`audio/mpeg`) | 传 `user_id` + `source`,同样用 Blob → Object URL 播放/下载 |
29
+
30
+ ### 4. 其他
31
+
32
+ - Memory Line、Profile 状态、Quiz 等:前端按需调对应 API,后端按 `user_id` 读会话状态并返回。
33
+ - 前端需统一配置 **API 基地址**(如 `VITE_API_BASE`),保证请求发到 Clare 后端。
34
+
35
+ **小结**:后端不写前端 UI,只提供 REST API 和会话状态;前端负责所有展示、表单和把「当前用户」(`user_id`) 带到每次请求里。Podcast 当前是「请求一次、返回一段 MP3 流」,不落盘。
36
+
37
+ ---
38
+
39
+ ## 二、把 Podcast 集成到「独立网站」的两种方式
40
+
41
+ 当前实现里,**Podcast 不会生成可下载的 MP3 文件链接**,而是接口**直接返回 MP3 字节流**,由调用方在内存里播放或触发下载。
42
+
43
+ 若你要在**独立网站**(与当前 React 应用分离的站点)里集成 podcast,有两种常见做法。
44
+
45
+ ### 方式 A:不落盘,直接用 API 返回的 MP3 流(当前能力即可)
46
+
47
+ **流程:**
48
+
49
+ 1. 独立网站(同源或配置好 CORS 的后端代理)向 Clare 后端发:
50
+ `POST /api/podcast`,body:`{ "user_id": "...", "source": "summary" | "conversation", "voice": "nova" }`。
51
+ 2. 后端生成 podcast,**直接在响应体里返回 MP3**(`Content-Type: audio/mpeg`)。
52
+ 3. 独立网站用 `fetch` 拿到 `response.blob()`,然后:
53
+ - **播放**:`URL.createObjectURL(blob)` 得到临时 URL,赋给 `<audio src="...">`;
54
+ - **下载**:用 `<a download>` + Object URL,或 Blob 转成文件让用户保存。
55
+
56
+ **特点:**
57
+
58
+ - **不需要**在后端生成「可下载的 MP3 文件」再让网站访问;
59
+ - 不占磁盘、不涉及文件 URL;适合「当前会话、一次性听/下」;
60
+ - 独立网站只要能用 JavaScript 调 Clare 的 API(或通过自己的后端转发)即可。
61
+
62
+ **结论:**
63
+ 若独立网站可以发 POST 并处理 Blob,**不需要**先生成可下载的 MP3 文件再给网站访问;直接使用接口返回的 MP3 流即可播放和下载。
64
+
65
+ ---
66
+
67
+ ### 方式 B:生成可下载的 MP3 文件,网站用「链接」播放/下载
68
+
69
+ 适用于:
70
+
71
+ - 需要**固定、可分享的链接**(例如发给别人、放 RSS、嵌入第三方播放器);
72
+ - 希望**缓存**同一脚本的 MP3,避免重复生成;
73
+ - 独立网站只能提供「一个 MP3 地址」,不能发 POST(例如纯静态页、外嵌 `<audio src="https://...">`)。
74
+
75
+ **做法:**
76
+
77
+ 1. **后端**在生成完 podcast 后:
78
+ - 把 MP3 写入**文件**(例如 `/static/podcasts/<session_id>_<timestamp>.mp3`)或对象存储(如 S3/OSS),
79
+ - 返回一个**可访问的 URL**,例如:
80
+ `https://your-api.com/static/podcasts/xxx.mp3` 或存储的 public URL。
81
+ 2. **独立网站**:
82
+ - 用该 URL 做 `<audio src="...">` 播放;
83
+ - 或用 `<a href="..." download>下载</a>` 提供下载。
84
+
85
+ 这样就是「**生成可下载的 MP3,再让网站通过文件 URL 访问/播放**」。
86
+
87
+ 若要采用方式 B,后端需要增加逻辑(例如):
88
+
89
+ - 在 `POST /api/podcast` 里,生成 MP3 后写入指定目录或上传到存储;
90
+ - 在响应里不再只返回 `audio/mpeg` 流,而是返回 JSON,例如:
91
+ `{ "url": "https://.../podcasts/xxx.mp3", "expires_in": 3600 }`;
92
+ 或保留当前「直接返回 MP3」的行为,并**额外**提供一个 `GET /api/podcast/<id>` 或静态路径,用于通过 URL 访问已生成的 MP3。
93
+
94
+ ---
95
+
96
+ ## 三、简要对比
97
+
98
+ | 维度 | 方式 A:直接用 API 返回的 MP3 流 | 方式 B:生成 MP3 文件 + URL |
99
+ |------|----------------------------------|-----------------------------|
100
+ | 是否需要生成可下载文件 | **否** | **是**(写盘或对象存储) |
101
+ | 网站如何播放/下载 | 用 Blob + Object URL 或 `<a download>` | 用返回的 URL 做 `<audio src>` / 下载链接 |
102
+ | 是否可分享/固定链接 | 否(每次请求新生成) | 是 |
103
+ | 是否适合纯静态页「只填 URL」 | 否(需能发 POST) | 是 |
104
+ | 当前 Clare 是否支持 | **是**(现有 `/api/podcast` 即可) | 需在后端增加「落盘/存储 + 返回 URL」 |
105
+
106
+ **总结:**
107
+
108
+ - **不需要**「先生成可下载的 MP3 再让网站访问」:用现有 `/api/podcast`,在独立网站里用 POST + Blob 即可播放和下载(方式 A)。
109
+ - **需要**「可分享链接、外嵌播放器、纯 URL 访问」时:再在后端增加「生成 MP3 文件并返回 URL」的能力(方式 B)。
docs/TEACHER_AGENT.md ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 教师 Agent(AI 智能建课)
2
+
3
+ 面向教师的 AI 助教,与 Clare 共用同一 Weaviate 数据库(GenAICourses),辅助设计与改进课程。
4
+
5
+ ## 功能
6
+
7
+ | 功能 | 说明 |
8
+ |------|------|
9
+ | **课程描述生成** | 根据课程主题(及可选大纲)生成可用于课程介绍/选课页的简洁描述 |
10
+ | **课程文档内容建议** | 针对某一主题或现有文档片段,给出讲义/课件的内容与结构建议 |
11
+ | **作业和题库生成建议** | 根据主题与周次,给出作业题、练习题或考试题建议与示例 |
12
+ | **学生学习评估分析** | 根据学生表现摘要,给出分析结论与教学改进建议 |
13
+
14
+ ## 使用方式
15
+
16
+ 1. **Web 界面**:启动后端与前端后,在页面右上角点击 **「AI 智能建课」** 进入教师仪表盘;使用完毕可点击 **「返回学习」** 回到 Clare 对话。
17
+ 2. **API**:直接调用以下接口(需已登录或按需鉴权):
18
+ - `POST /api/teacher/course-description` — 课程描述
19
+ - `POST /api/teacher/doc-suggestion` — 文档建议
20
+ - `POST /api/teacher/assignment-questions` — 作业/题库建议
21
+ - `POST /api/teacher/assessment-analysis` — 学习评估分析
22
+ - `GET /api/teacher/status` — 状态(是否已配置 Weaviate)
23
+
24
+ ## Weaviate 配置(可选)
25
+
26
+ 与 ClareVoice 相同:在环境变量或 `.env` 中配置:
27
+
28
+ - `WEAVIATE_URL`:Weaviate Cloud 集群地址(如 `https://xxx.c0.us-west3.gcp.weaviate.cloud`)
29
+ - `WEAVIATE_API_KEY`:API Key
30
+ - `WEAVIATE_COLLECTION`:可选,默认 `GenAICourses`
31
+
32
+ 配置后,教师 Agent 会从 GenAICourses 中检索相关课程片段作为 RAG 上下文,生成更贴合课程内容的建议。未配置时仍可运行,仅不使用知识库检索。
33
+
34
+ ## 依赖(启用 Weaviate 时)
35
+
36
+ 若需在**本仓库**(非 ClareVoice Space)使用 Weaviate,请安装:
37
+
38
+ ```bash
39
+ pip install weaviate-client>=4.0.0 llama-index-core>=0.10.0 llama-index-vector-stores-weaviate>=0.2.0 llama-index-embeddings-huggingface>=0.1.0 sentence-transformers>=2.2.0
40
+ ```
41
+
42
+ 索引需使用 **HuggingFace** `sentence-transformers/all-MiniLM-L6-v2` 构建(与 ClareVoice / `build_weaviate_index.py` 一致)。
43
+
44
+ ## 代码结构
45
+
46
+ - `api/config.py` — Weaviate 相关环境变量(`USE_WEAVIATE` 等)
47
+ - `api/weaviate_retrieve.py` — 共用 Weaviate 检索
48
+ - `api/teacher_agent.py` — 四个功能的 LLM + RAG 逻辑
49
+ - `api/routes_teacher.py` — FastAPI 路由
50
+ - `web/src/components/TeacherDashboard.tsx` — 教师仪表盘 UI(四张卡片)
docs/VECTOR_DB_LOGIC_AND_MULTI_COURSE.md ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ClareVoice 向量数据库逻辑 · 多课程支持 · 优化计划
2
+
3
+ 本文档**只描述 ClareVoice(HF Space)**的向量库设计与扩展思路。
4
+
5
+ ---
6
+
7
+ ## 一、当前向量数据库是怎么做的
8
+
9
+ **ClareVoice 使用 Weaviate Cloud 作为课程知识库**,同时会话级 RAG 用内存 + FAISS。双路检索结果在对话时拼接。
10
+
11
+ ### 1.1 Weaviate + 内存双路检索
12
+
13
+ | 数据源 | 存储位置 | 用途 | 检索入口 |
14
+ |--------|----------|------|----------|
15
+ | **Weaviate Cloud** | 持久化向量库(GCP) | GENAI 课程文档(151+ 文档,由 `build_weaviate_index.py` 一次性写入) | `_retrieve_from_weaviate(question)` |
16
+ | **内存 + FAISS** | 进程内 `SESSIONS[user_id]["rag_chunks"]` | Module 10 预读 + 用户上传文件 | `retrieve_relevant_chunks()` |
17
+
18
+ **Weaviate 流程简述:**
19
+
20
+ 1. **建索引(一次性)**
21
+ - 脚本:`hf_space/GenAICoursesDB_space/build_weaviate_index.py`
22
+ - 读取 `GENAI COURSES` 目录(.md / .pdf / .txt / .py / .ipynb / .docx),用 LlamaIndex `SimpleDirectoryReader` 加载 → `VectorStoreIndex.from_documents()` + `WeaviateVectorStore`,写入 Weaviate Cloud 的 **collection `GenAICourses`**。
23
+ - Embedding:默认 `sentence-transformers/all-MiniLM-L6-v2`(与 ClareVoice 运行时一致);可选 `EMBEDDING_PROVIDER=openai` 用 `text-embedding-3-small`。
24
+
25
+ 2. **运行时检索**(`hf_space/ClareVoice/app.py`)
26
+ - 配置:`WEAVIATE_URL`、`WEAVIATE_API_KEY`、`WEAVIATE_COLLECTION`(默认 `GenAICourses`),`USE_WEAVIATE_DIRECT=True` 时启用。
27
+ - `_get_weaviate_embed_model()`:懒加载并缓存 **HuggingFaceEmbedding(all-MiniLM-L6-v2)**(与建索引时一致)。
28
+ - `_retrieve_from_weaviate(question, top_k=5)`:连接 Weaviate Cloud → `WeaviateVectorStore` + `VectorStoreIndex.from_vector_store(vs)` → `as_retriever(similarity_top_k=top_k).retrieve(question)` → 将 node 内容用 `---` 拼接成字符串返回。
29
+ - 启动时 `_warmup_weaviate_embed()` 在后台线程预热 embedding 模型,减少首次检索超时。
30
+ - 未配置 Weaviate 时回退到调用 **GenAICoursesDB Space** 的 `/retrieve` 接口。
31
+
32
+ 3. **对话时的组合**
33
+ - 先按会话用 `retrieve_relevant_chunks(sess["rag_chunks"])` 得到「Module 10 + 上传」的 context。
34
+ - 若配置了 Weaviate,再调 `_retrieve_from_weaviate(message)`,把返回的课程检索文本**拼在**上面 context 后面,一起交给 LLM。
35
+
36
+ ---
37
+
38
+ ### 1.2 数据从哪来、存在哪
39
+
40
+ | 来源 | 何时写入 | 存在哪 |
41
+ |------|----------|--------|
42
+ | Module 10 预读 | 进程启动时 | 全局 cache → 每会话 `rag_chunks` |
43
+ | 用户上传文件 | Gradio 上传回调 | 追加到 `sess["rag_chunks"]` |
44
+ | GENAI 课程文档 | 本地运行 `build_weaviate_index.py` | **Weaviate Cloud** collection `GenAICourses` |
45
+
46
+ ### 1.3 Chunk 长什么样(内存/FAISS 侧)
47
+
48
+ 会话级 `rag_chunks` 里每个元素是一个 dict,例如:
49
+
50
+ ```python
51
+ {
52
+ "text": "一段正文...",
53
+ "source_file": "module10_responsible_ai.pdf", # 或上传的文件名
54
+ "section": "pdf_unstructured#1", # 段落/页码标识
55
+ "doc_type": "Literature Review / Paper", # Syllabus / Lecture Slides / ...
56
+ "embedding": [0.1, -0.02, ...] # 维度由 rag_engine 所用 embedding 模型决定
57
+ }
58
+ ```
59
+
60
+ - **Embedding**:ClareVoice 的 `rag_engine` 里用 `clare_core.get_embedding()`(或同配置的 embedding)为每个 chunk 生成向量。
61
+ - **解析**:PDF 优先 `unstructured.io`,失败则 pypdf;DOCX/PPTX 用 python-docx/pptx;按空行分段落再按约 1400 字符打包成 chunk。
62
+
63
+ **Weaviate 侧**:由 LlamaIndex 写入的 document/node,带向量与元数据;检索时取 `node.get_content()` 拼成字符串,无单独 chunk dict 结构要求。
64
+
65
+ ### 1.4 检索流程(retrieve_relevant_chunks,仅内存/FAISS)
66
+
67
+ 1. **入参**
68
+ - `query`:用户问题
69
+ - `chunks`:当前会话的 `sess["rag_chunks"]`
70
+ - 可选:`allowed_source_files`、`allowed_doc_types`(例如只查「刚上传的那份文件」)
71
+
72
+ 2. **先做范围过滤**
73
+ - 若有 `allowed_source_files` / `allowed_doc_types`,只保留符合的 chunks,再参与后续步骤。
74
+
75
+ 3. **向量检索(默认开启)**
76
+ - 用当前配置的 embedding 模型对 query 生成向量;
77
+ - 用当前过滤后的 chunks 临时建一个 **VectorStore**(FAISS IndexFlatL2 或列表余弦);
78
+ - 取 top `k*2` 个候选(默认 k=4),再按 `vector_similarity_threshold`(默认 0.7)过滤。
79
+
80
+ 4. **Rerank**
81
+ - 对候选做 **token 重叠**(query 与 chunk 的 token 交集);
82
+ - 综合分 = `0.7 * 向量相似度 + 0.3 * 归一化 token 重叠`,再取 top k。
83
+
84
+ 5. **无结果或分数过低**
85
+ - 回退到**纯 token 重叠**检索(按词匹配,无向量)。
86
+
87
+ 6. **拼上下文**
88
+ - 对最终 top chunks 做 token 截断(单 chunk 500、总 context 2000),拼成一段 `context_text` 返回给 LLM,同���返回 `used_chunks` 供前端展示引用。
89
+
90
+ ### 1.5 可选:GenAICoursesDB 回退
91
+
92
+ - 若**未配置 Weaviate**,ClareVoice 用 `GENAI_COURSES_SPACE` 调用 GenAICoursesDB Space 的 `/retrieve` 接口,作为课程知识库的「远程 RAG」回退。
93
+
94
+ ### 1.6 小结(当前逻辑)
95
+
96
+ - **Weaviate Cloud**:存 GENAI 课程知识库(`build_weaviate_index.py` 建索引,`_retrieve_from_weaviate` 检索)。
97
+ - **内存 + FAISS**:会话级 `rag_chunks`(Module 10 预读 + 用户上传),`retrieve_relevant_chunks()` 检索。
98
+ - **对话时**:先取 FAISS 的 context,再调 `_retrieve_from_weaviate` 把课程检索结果拼在后面,一起交给 LLM。
99
+ - **多课程**:当前 Weaviate 单 collection(GenAICourses);支持多门课时可在 Weaviate 内按 `course_id` 或分 collection 扩展(见下文)。
100
+
101
+ ---
102
+
103
+ ## 二、要支持多门课程,需要怎么做
104
+
105
+ 目标:**多门课程并存、按课程(或 workspace)隔离检索**,且不破坏现有单会话、单课程使用方式。
106
+ **ClareVoice 已有 Weaviate**,多课程可在此基础上扩展(多 collection 或单 collection + course_id 过滤)。
107
+
108
+ ### 2.1 数据模型上区分「课程」
109
+
110
+ - 为 chunk / 文档增加**课程维度**,例如:
111
+ - `course_id`:对应 `course_directory` 里的一门课;
112
+ - 或 `workspace_id`:对应某个 workspace(若一个 workspace 绑定一门课)。
113
+ - 会话侧增加「当前使用的课程/workspace」:
114
+ - 例如 `sess["course_id"]` 或 `sess["workspace_id"]`,由前端「选课/选 workspace」或默认规则设定。
115
+
116
+ 这样:
117
+ - **建索引时**:每个 chunk 带上 `course_id`(或 `workspace_id`);
118
+ - **检索时**:只在该会话当前 `course_id`(或当前 workspace 绑定的课程)对应的数据里搜。
119
+
120
+ ### 2.2 两种实现路径
121
+
122
+ **路径 A:内存 + 按课程分桶(仅会话级)**
123
+
124
+ - 会话结构扩展为例如:`sess["rag_chunks_by_course"] = { "course_ai_001": [...], "course_ml_002": [...] }`。
125
+ - 或保持 `sess["rag_chunks"]` 为「当前课程」的列表,在**切换课程**时从「课程级缓存」里加载对应列表。
126
+ - **课程级 chunk 从哪来**:
127
+ - **方案 A1**:仍以「上传」为主——用户选一门课再上传,上传时带 `course_id`,写入 `rag_chunks_by_course[course_id]`。
128
+ - **方案 A2**:预置「课程资料」——每门课有预解析好的 chunk 列表,登录/选课后加载到会话或课程缓存。
129
+ - **检索**:只对当前课程的 chunk 列表做现有 `retrieve_relevant_chunks()`,逻辑不变。
130
+
131
+ **路径 B:Weaviate 扩展(推荐)**
132
+
133
+ - ClareVoice 已用 **Weaviate Cloud** 存 GENAI 课程(单 collection `GenAICourses`)。多课程可:
134
+ - **方案 B1**:多 collection——每门课一个 collection,如 `GenAICourses`、`Course_ML_101`;`_retrieve_from_weaviate` 根据 `sess["course_id"]` 或配置选择 `index_name`。
135
+ - **方案 B2**:单 collection + 元数据过滤——在 Weaviate 的 class 上增加属性 `course_id`(或 `courseId`),建索引时写入;检索时用 Weaviate 的 **filter**(如 `where course_id == "course_ai_001"`)再做向量检索。
136
+ - **写入**:
137
+ - 预置课程:沿用或扩展 `build_weaviate_index.py`,按课程目录/配置写入,带 `course_id`;
138
+ - 用户上传:解析 → embedding → 写入 Weaviate,并带上当前 `course_id`(需在 ClareVoice 中接 Weaviate 写接口)。
139
+ - **检索**:
140
+ - query embedding + **filter (course_id = 当前课程)**;
141
+ - 再在应用层做 token rerank / 截断,拼 context。
142
+ - **会话**:存 `user_id`、`course_id`;多门课共享同一 Weaviate 集群,靠 collection 或 filter 隔离。
143
+
144
+ ### 2.3 与 Gradio 前端配合
145
+
146
+ - **选课/选 workspace**:
147
+ - 若 UI 有选课或 workspace,在会话中维护 `sess["course_id"]` / `sess["workspace_id"]`,上传与检索时只作用当前课程。
148
+ - **上传**:
149
+ - 上传回调里从当前会话读 `course_id`(若有),写入 chunk 池或 Weaviate 时带课程维度。
150
+ - **对话**:
151
+ - 多课程下从「当前课程对应的 chunk 源」取数(内存分桶或 Weaviate filter),再调 `retrieve_relevant_chunks()` 或 `_retrieve_from_weaviate(..., course_id=...)`。
152
+
153
+ ### 2.4 小结:多课程支持要做的事
154
+
155
+ | 项目 | 说明 |
156
+ |------|------|
157
+ | Chunk 增加 course_id(或 workspace_id) | 建索引/写入时必带;检索时按此过滤。 |
158
+ | 会话增加当前课程 | `sess["course_id"]` 或 `sess["workspace_id"]`,由选课/选 workspace 设定。 |
159
+ | 上传与选课绑定 | 上传时带 course_id,写入对应课程 chunk 池或向量库。 |
160
+ | 检索只查当前课程 | 内存方案:只对当前课程的 chunk 列表检索;向量库方案:filter by course_id。 |
161
+ | 可选:课程预置资料 | 每门课预解析+embedding,放入课程缓存或持久化向量库。 |
162
+
163
+ ---
164
+
165
+ ## 三、向量数据库的优化计划与想法
166
+
167
+ 在「写清当前逻辑」和「多课程支持」之上,可以���阶段做以下优化。
168
+
169
+ ### 3.1 短期(不引入新服务)
170
+
171
+ - **按会话/按课程复用 FAISS 索引**
172
+ - 当前是**每次** `retrieve_relevant_chunks` 都 `VectorStore()` + `build_index(chunks)`,重复建索引。
173
+ - 改为:对同一份 `chunks`(按会话或按 course_id 的列表)**建一次索引并缓存**,只有 chunks 变化(如新上传)时再重建。这样检索延迟和 CPU 都会明显下降。
174
+
175
+ - **Chunk 与 embedding 分离缓存**
176
+ - 对「未改动的文件」缓存其 chunk 列表(或甚至只缓存 embedding),避免重复解析、重复调 OpenAI Embeddings;上传同一文件时可直接复用。
177
+
178
+ - **检索参数可配置**
179
+ - `top_k`、`vector_similarity_threshold`、`RAG_CONTEXT_TOKEN_LIMIT` 等放进 config 或 API,便于按课程/场景调优(如考试模式用更大 top_k)。
180
+
181
+ - **多课程内存分桶**
182
+ - 实现 2.1 / 2.2 路径 A:`rag_chunks_by_course` + 选课接口,先支持多门课隔离,为后续迁到向量库打基础。
183
+
184
+ ### 3.2 中期(扩展 Weaviate / 持久化向量库)
185
+
186
+ - **ClareVoice 已接 Weaviate Cloud**;可在此基础上:
187
+ - Schema 增加 `course_id`(及可选 `workspace_id`、`uploaded_at`);
188
+ - 多 collection 或单 collection + filter,实现多课程隔离;
189
+ - 上传时写入 Weaviate(需在服务端接 Weaviate 写接口),预置课程继续用 `build_weaviate_index.py` 或同类脚本。
190
+
191
+ - **统一检索接口**
192
+ - 封装一层「RAG 检索」:内部根据配置选择「内存 FAISS」或「Weaviate」(及可选 GenAICoursesDB 回退);对上层仍返回 `(context_text, used_chunks)`,便于后续扩展与多课程切换。
193
+
194
+ - **课程级预建索引**
195
+ - 每门课预解析 + 批量 embedding → 写入 Weaviate(按 course_id 或分 collection);用户选课后直接查。
196
+
197
+ ### 3.3 中长期(效果与规模)
198
+
199
+ - **混合检索(Hybrid)**
200
+ - 向量检索 + 关键词(BM25/lexical)再融合(如 RRF),减少纯向量「语义漂移」或专有名词漏检。
201
+
202
+ - **Rerank 模型**
203
+ - 向量初筛后用小型的 cross-encoder 或 rerank API 精排,再截断进 context,提升引用准确度。
204
+
205
+ - **分片与规模**
206
+ - 按 `course_id` 分 collection 或分索引,便于按课程做备份、迁移、删除;单集合过大时再考虑按时间或按文档分片。
207
+
208
+ - **可观测性**
209
+ - 记录检索延迟、命中 chunk 数、分数分布;可选 A/B 不同 top_k 或阈值,便于调参。
210
+
211
+ ---
212
+
213
+ ## 四、总结表
214
+
215
+ | 维度 | ClareVoice 当前 | 多课程(建议) | 优化方向 |
216
+ |------|-----------------|----------------|----------|
217
+ | 课程知识库 | **Weaviate Cloud**(GenAICourses) | 多 collection 或 course_id 过滤 | 多课程 schema、统一检索接口 |
218
+ | 会话 RAG | 内存 rag_chunks + FAISS | 按 course_id 分桶或统一走 Weaviate | 索引复用、课程级预建 |
219
+ | 检索 | Weaviate + FAISS 双路,结果拼接 | 先按 course_id 过滤再检索 | Hybrid、rerank、可调参 |
220
+ | 数据来源 | Module10 预读 + 上传 + **Weaviate 课程** | + 课程预置资料、多课程上传 | 批量预置、可观测 |
221
+
222
+ 按上述步骤:先把「当前向量库逻辑」和「多课程支持」在设计与实现上对齐,再分阶段做「缓存/索引复用 → 扩展 Weaviate 多课程 → 混合检索与规模化」,即可在支持多门课的同时逐步优化 ClareVoice 向量数据库的表现与可维护性。
requirements.txt CHANGED
@@ -23,4 +23,11 @@ gradio_client>=1.0.0
23
  faiss-cpu>=1.7.4
24
  unstructured[pdf]>=0.12.0
25
  # Optional dependencies for unstructured (if needed)
26
- # unstructured[local-inference]>=0.12.0 # Uncomment if using local models
 
 
 
 
 
 
 
 
23
  faiss-cpu>=1.7.4
24
  unstructured[pdf]>=0.12.0
25
  # Optional dependencies for unstructured (if needed)
26
+ # unstructured[local-inference]>=0.12.0 # Uncomment if using local models
27
+
28
+ # 教师 Agent + ClareVoice 共用 Weaviate(可选,需配置 WEAVIATE_URL / WEAVIATE_API_KEY)
29
+ # weaviate-client>=4.0.0
30
+ # llama-index-core>=0.10.0
31
+ # llama-index-vector-stores-weaviate>=0.2.0
32
+ # llama-index-embeddings-huggingface>=0.1.0
33
+ # sentence-transformers>=2.2.0
web/src/App.tsx CHANGED
@@ -11,6 +11,7 @@ import { Button } from "./components/ui/button";
11
  import { Toaster } from "./components/ui/sonner";
12
  import { toast } from "sonner";
13
  import { LeftSidebar } from "./components/sidebar/LeftSidebar";
 
14
 
15
  // backend API bindings
16
  import { apiChat, apiUpload, apiMemoryline, apiQuizStart } from "./lib/api";
@@ -378,6 +379,7 @@ function App() {
378
 
379
  const [isTyping, setIsTyping] = useState(false);
380
  const [leftSidebarOpen, setLeftSidebarOpen] = useState(false);
 
381
  const [leftPanelVisible, setLeftPanelVisible] = useState(true);
382
  const [showProfileEditor, setShowProfileEditor] = useState(false);
383
  const [showOnboarding, setShowOnboarding] = useState(false);
@@ -1361,6 +1363,9 @@ function App() {
1361
  setShowReviewBanner(false);
1362
  localStorage.setItem("reviewBannerDismissed", "true");
1363
  }}
 
 
 
1364
  />
1365
  </div>
1366
 
@@ -1483,6 +1488,9 @@ function App() {
1483
 
1484
  <main className="flex flex-1 min-w-0 min-h-0 overflow-hidden flex-col">
1485
  <div className="flex-1 min-h-0 min-w-0 overflow-hidden">
 
 
 
1486
  <ChatArea
1487
  messages={messages}
1488
  onSendMessage={handleSendMessage}
@@ -1526,6 +1534,7 @@ function App() {
1526
  // ✅ bio is still allowed to be updated by chat/Clare
1527
  onProfileBioUpdate={(bio) => updateUser({ bio })}
1528
  />
 
1529
  </div>
1530
  </main>
1531
  </div>
 
11
  import { Toaster } from "./components/ui/sonner";
12
  import { toast } from "sonner";
13
  import { LeftSidebar } from "./components/sidebar/LeftSidebar";
14
+ import { TeacherDashboard } from "./components/TeacherDashboard";
15
 
16
  // backend API bindings
17
  import { apiChat, apiUpload, apiMemoryline, apiQuizStart } from "./lib/api";
 
379
 
380
  const [isTyping, setIsTyping] = useState(false);
381
  const [leftSidebarOpen, setLeftSidebarOpen] = useState(false);
382
+ const [showTeacherDashboard, setShowTeacherDashboard] = useState(false);
383
  const [leftPanelVisible, setLeftPanelVisible] = useState(true);
384
  const [showProfileEditor, setShowProfileEditor] = useState(false);
385
  const [showOnboarding, setShowOnboarding] = useState(false);
 
1363
  setShowReviewBanner(false);
1364
  localStorage.setItem("reviewBannerDismissed", "true");
1365
  }}
1366
+ showTeacherView={showTeacherDashboard}
1367
+ onShowTeacherDashboard={() => setShowTeacherDashboard(true)}
1368
+ onBackFromTeacher={() => setShowTeacherDashboard(false)}
1369
  />
1370
  </div>
1371
 
 
1488
 
1489
  <main className="flex flex-1 min-w-0 min-h-0 overflow-hidden flex-col">
1490
  <div className="flex-1 min-h-0 min-w-0 overflow-hidden">
1491
+ {showTeacherDashboard ? (
1492
+ <TeacherDashboard onBack={() => setShowTeacherDashboard(false)} />
1493
+ ) : (
1494
  <ChatArea
1495
  messages={messages}
1496
  onSendMessage={handleSendMessage}
 
1534
  // ✅ bio is still allowed to be updated by chat/Clare
1535
  onProfileBioUpdate={(bio) => updateUser({ bio })}
1536
  />
1537
+ )}
1538
  </div>
1539
  </main>
1540
  </div>
web/src/components/Header.tsx CHANGED
@@ -52,6 +52,11 @@ interface HeaderProps {
52
  reviewStarOpacity?: number; // 0..1
53
  reviewEnergyPct?: number; // 0..100
54
  onStarClick?: () => void; // recommended: switch to Review
 
 
 
 
 
55
  }
56
 
57
  export function Header({
@@ -72,6 +77,9 @@ export function Header({
72
  reviewStarOpacity,
73
  reviewEnergyPct,
74
  onStarClick,
 
 
 
75
  }: HeaderProps) {
76
  const [showProfileEditor, setShowProfileEditor] = useState(false);
77
 
@@ -191,6 +199,30 @@ export function Header({
191
  {isDarkMode ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
192
  </Button>
193
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  {user && currentWorkspace ? (
195
  <>
196
  <DropdownMenu>
 
52
  reviewStarOpacity?: number; // 0..1
53
  reviewEnergyPct?: number; // 0..100
54
  onStarClick?: () => void; // recommended: switch to Review
55
+
56
+ // ✅ 教师 Agent:AI 智能建课 入口
57
+ showTeacherView?: boolean;
58
+ onShowTeacherDashboard?: () => void;
59
+ onBackFromTeacher?: () => void;
60
  }
61
 
62
  export function Header({
 
77
  reviewStarOpacity,
78
  reviewEnergyPct,
79
  onStarClick,
80
+ showTeacherView,
81
+ onShowTeacherDashboard,
82
+ onBackFromTeacher,
83
  }: HeaderProps) {
84
  const [showProfileEditor, setShowProfileEditor] = useState(false);
85
 
 
199
  {isDarkMode ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
200
  </Button>
201
 
202
+ {showTeacherView && onBackFromTeacher ? (
203
+ <TooltipProvider>
204
+ <Tooltip>
205
+ <TooltipTrigger asChild>
206
+ <Button variant="outline" size="sm" onClick={onBackFromTeacher} className="gap-1.5">
207
+ 返回学习
208
+ </Button>
209
+ </TooltipTrigger>
210
+ <TooltipContent>返回 Clare 学习助手</TooltipContent>
211
+ </Tooltip>
212
+ </TooltipProvider>
213
+ ) : onShowTeacherDashboard ? (
214
+ <TooltipProvider>
215
+ <Tooltip>
216
+ <TooltipTrigger asChild>
217
+ <Button variant="outline" size="sm" onClick={onShowTeacherDashboard} className="gap-1.5">
218
+ AI 智能建课
219
+ </Button>
220
+ </TooltipTrigger>
221
+ <TooltipContent>教师:课程描述、文档建议、作业题库、学习评估</TooltipContent>
222
+ </Tooltip>
223
+ </TooltipProvider>
224
+ ) : null}
225
+
226
  {user && currentWorkspace ? (
227
  <>
228
  <DropdownMenu>
web/src/components/TeacherDashboard.tsx ADDED
@@ -0,0 +1,352 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // web/src/components/TeacherDashboard.tsx
2
+ // AI 智能建课:课程描述、文档建议、作业题库建议、学习评估分析
3
+ import React, { useState, useEffect } from "react";
4
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
5
+ import { Button } from "./ui/button";
6
+ import { Input } from "./ui/input";
7
+ import { Textarea } from "./ui/textarea";
8
+ import { Label } from "./ui/label";
9
+ import { ArrowLeft, Loader2, Lightbulb, FileText, ClipboardList, BarChart3 } from "lucide-react";
10
+ import {
11
+ apiTeacherStatus,
12
+ apiTeacherCourseDescription,
13
+ apiTeacherDocSuggestion,
14
+ apiTeacherAssignmentQuestions,
15
+ apiTeacherAssessmentAnalysis,
16
+ type TeacherStatus,
17
+ } from "../lib/api";
18
+ import { toast } from "sonner";
19
+
20
+ type FeatureId = "course-description" | "doc-suggestion" | "assignment-questions" | "assessment-analysis";
21
+
22
+ const FEATURES: { id: FeatureId; title: string; desc: string; icon: React.ReactNode }[] = [
23
+ {
24
+ id: "course-description",
25
+ title: "课程描述生成",
26
+ desc: "根据课程主题生成可用于课程介绍或选课页的简洁描述",
27
+ icon: <FileText className="h-6 w-6" />,
28
+ },
29
+ {
30
+ id: "doc-suggestion",
31
+ title: "课程文档内容建议",
32
+ desc: "针对某一主题或现有文档片段,给出讲义/课件的内容与结构建议",
33
+ icon: <Lightbulb className="h-6 w-6" />,
34
+ },
35
+ {
36
+ id: "assignment-questions",
37
+ title: "作业和题库生成建议",
38
+ desc: "根据主题与周次,给出作业题、练习题或考试题建议与示例",
39
+ icon: <ClipboardList className="h-6 w-6" />,
40
+ },
41
+ {
42
+ id: "assessment-analysis",
43
+ title: "学生学习评估分析",
44
+ desc: "根据学生表现摘要,给出分析结论与教学改进建议",
45
+ icon: <BarChart3 className="h-6 w-6" />,
46
+ },
47
+ ];
48
+
49
+ type Props = {
50
+ onBack: () => void;
51
+ };
52
+
53
+ export function TeacherDashboard({ onBack }: Props) {
54
+ const [status, setStatus] = useState<TeacherStatus | null>(null);
55
+ const [loading, setLoading] = useState<FeatureId | null>(null);
56
+ const [results, setResults] = useState<Record<FeatureId, string>>({
57
+ "course-description": "",
58
+ "doc-suggestion": "",
59
+ "assignment-questions": "",
60
+ "assessment-analysis": "",
61
+ });
62
+
63
+ const [topicDescription, setTopicDescription] = useState("");
64
+ const [outlineHint, setOutlineHint] = useState("");
65
+ const [topicDoc, setTopicDoc] = useState("");
66
+ const [docExcerpt, setDocExcerpt] = useState("");
67
+ const [docType, setDocType] = useState("讲义/课件");
68
+ const [topicAssignment, setTopicAssignment] = useState("");
69
+ const [weekOrModule, setWeekOrModule] = useState("");
70
+ const [questionType, setQuestionType] = useState("混合");
71
+ const [assessmentSummary, setAssessmentSummary] = useState("");
72
+ const [courseTopicHint, setCourseTopicHint] = useState("");
73
+
74
+ useEffect(() => {
75
+ apiTeacherStatus()
76
+ .then(setStatus)
77
+ .catch(() => setStatus({ weaviate_configured: false, features: [] }));
78
+ }, []);
79
+
80
+ const handleCourseDescription = async () => {
81
+ if (!topic.trim()) {
82
+ toast.error("请输入课程主题");
83
+ return;
84
+ }
85
+ setLoading("course-description");
86
+ setResults((r) => ({ ...r, "course-description": "" }));
87
+ try {
88
+ const res = await apiTeacherCourseDescription({
89
+ topic: topic.trim(),
90
+ outline_hint: outlineHint.trim() || undefined,
91
+ });
92
+ setResults((r) => ({ ...r, "course-description": res.description }));
93
+ if (res.weaviate_used) toast.success("已结合课程知识库生成");
94
+ } catch (e: any) {
95
+ toast.error(e?.message || "生成失败");
96
+ } finally {
97
+ setLoading(null);
98
+ }
99
+ };
100
+
101
+ const handleDocSuggestion = async () => {
102
+ if (!topicDoc.trim()) {
103
+ toast.error("请输入主题或章节");
104
+ return;
105
+ }
106
+ setLoading("doc-suggestion");
107
+ setResults((r) => ({ ...r, "doc-suggestion": "" }));
108
+ try {
109
+ const res = await apiTeacherDocSuggestion({
110
+ topic: topicDoc.trim(),
111
+ current_doc_excerpt: docExcerpt.trim() || undefined,
112
+ doc_type: docType.trim() || "讲义/课件",
113
+ });
114
+ setResults((r) => ({ ...r, "doc-suggestion": res.suggestion }));
115
+ if (res.weaviate_used) toast.success("已结合课程知识库");
116
+ } catch (e: any) {
117
+ toast.error(e?.message || "生成失败");
118
+ } finally {
119
+ setLoading(null);
120
+ }
121
+ };
122
+
123
+ const handleAssignmentQuestions = async () => {
124
+ if (!topicAssignment.trim()) {
125
+ toast.error("请输入主题");
126
+ return;
127
+ }
128
+ setLoading("assignment-questions");
129
+ setResults((r) => ({ ...r, "assignment-questions": "" }));
130
+ try {
131
+ const res = await apiTeacherAssignmentQuestions({
132
+ topic: topicAssignment.trim(),
133
+ week_or_module: weekOrModule.trim() || undefined,
134
+ question_type: questionType.trim() || "混合",
135
+ });
136
+ setResults((r) => ({ ...r, "assignment-questions": res.suggestion }));
137
+ if (res.weaviate_used) toast.success("已结合课程���识库");
138
+ } catch (e: any) {
139
+ toast.error(e?.message || "生成失败");
140
+ } finally {
141
+ setLoading(null);
142
+ }
143
+ };
144
+
145
+ const handleAssessmentAnalysis = async () => {
146
+ if (!assessmentSummary.trim()) {
147
+ toast.error("请输入学生评估摘要");
148
+ return;
149
+ }
150
+ setLoading("assessment-analysis");
151
+ setResults((r) => ({ ...r, "assessment-analysis": "" }));
152
+ try {
153
+ const res = await apiTeacherAssessmentAnalysis({
154
+ assessment_summary: assessmentSummary.trim(),
155
+ course_topic_hint: courseTopicHint.trim() || undefined,
156
+ });
157
+ setResults((r) => ({ ...r, "assessment-analysis": res.analysis }));
158
+ if (res.weaviate_used) toast.success("已结合课程知识库");
159
+ } catch (e: any) {
160
+ toast.error(e?.message || "分析失败");
161
+ } finally {
162
+ setLoading(null);
163
+ }
164
+ };
165
+
166
+ return (
167
+ <div className="h-full flex flex-col bg-background">
168
+ <div className="flex-shrink-0 flex items-center gap-4 px-4 py-3 border-b border-border">
169
+ <Button variant="ghost" size="icon" onClick={onBack} title="返回">
170
+ <ArrowLeft className="h-5 w-5" />
171
+ </Button>
172
+ <div>
173
+ <h1 className="text-lg font-semibold">AI 智能建课</h1>
174
+ <p className="text-sm text-muted-foreground">
175
+ 基于 GENAI 课程知识库,辅助设计与改进课程
176
+ {status?.weaviate_configured && (
177
+ <span className="ml-2 text-green-600 dark:text-green-400">· 知识库已连接</span>
178
+ )}
179
+ </p>
180
+ </div>
181
+ </div>
182
+
183
+ <div className="flex-1 overflow-auto p-4 md:p-6">
184
+ <div className="grid gap-6 md:grid-cols-2 max-w-5xl mx-auto">
185
+ {FEATURES.map((f) => (
186
+ <Card key={f.id} className="flex flex-col">
187
+ <CardHeader className="pb-2">
188
+ <div className="flex items-center gap-2">
189
+ <span className="text-muted-foreground">{f.icon}</span>
190
+ <CardTitle className="text-base">{f.title}</CardTitle>
191
+ </div>
192
+ <CardDescription>{f.desc}</CardDescription>
193
+ </CardHeader>
194
+ <CardContent className="flex-1 flex flex-col gap-3">
195
+ {f.id === "course-description" && (
196
+ <>
197
+ <div>
198
+ <Label>课程主题 / 名称 *</Label>
199
+ <Input
200
+ placeholder="例如:检索增强生成 (RAG)"
201
+ value={topicDescription}
202
+ onChange={(e) => setTopicDescription(e.target.value)}
203
+ className="mt-1"
204
+ />
205
+ </div>
206
+ <div>
207
+ <Label>大纲或要点(可选)</Label>
208
+ <Textarea
209
+ placeholder="可粘贴现有大纲要点"
210
+ value={outlineHint}
211
+ onChange={(e) => setOutlineHint(e.target.value)}
212
+ rows={2}
213
+ className="mt-1"
214
+ />
215
+ </div>
216
+ <Button
217
+ onClick={handleCourseDescription}
218
+ disabled={loading === "course-description"}
219
+ >
220
+ {loading === "course-description" ? (
221
+ <Loader2 className="h-4 w-4 animate-spin mr-2" />
222
+ ) : null}
223
+ 开始使用
224
+ </Button>
225
+ </>
226
+ )}
227
+ {f.id === "doc-suggestion" && (
228
+ <>
229
+ <div>
230
+ <Label>主题或章节 *</Label>
231
+ <Input
232
+ placeholder="例如:Prompt Engineering 核心原则"
233
+ value={topicDoc}
234
+ onChange={(e) => setTopicDoc(e.target.value)}
235
+ className="mt-1"
236
+ />
237
+ </div>
238
+ <div>
239
+ <Label>当前已有内容片段(可选)</Label>
240
+ <Textarea
241
+ placeholder="可粘贴现有讲义片段"
242
+ value={docExcerpt}
243
+ onChange={(e) => setDocExcerpt(e.target.value)}
244
+ rows={2}
245
+ className="mt-1"
246
+ />
247
+ </div>
248
+ <div>
249
+ <Label>文档类型</Label>
250
+ <Input
251
+ value={docType}
252
+ onChange={(e) => setDocType(e.target.value)}
253
+ className="mt-1"
254
+ />
255
+ </div>
256
+ <Button
257
+ onClick={handleDocSuggestion}
258
+ disabled={loading === "doc-suggestion"}
259
+ >
260
+ {loading === "doc-suggestion" ? (
261
+ <Loader2 className="h-4 w-4 animate-spin mr-2" />
262
+ ) : null}
263
+ 开始使用
264
+ </Button>
265
+ </>
266
+ )}
267
+ {f.id === "assignment-questions" && (
268
+ <>
269
+ <div>
270
+ <Label>主题 *</Label>
271
+ <Input
272
+ placeholder="例如:RAG 与向量检索"
273
+ value={topicAssignment}
274
+ onChange={(e) => setTopicAssignment(e.target.value)}
275
+ className="mt-1"
276
+ />
277
+ </div>
278
+ <div>
279
+ <Label>周次 / 模块(可选)</Label>
280
+ <Input
281
+ placeholder="例如:Week 7"
282
+ value={weekOrModule}
283
+ onChange={(e) => setWeekOrModule(e.target.value)}
284
+ className="mt-1"
285
+ />
286
+ </div>
287
+ <div>
288
+ <Label>题型偏好</Label>
289
+ <Input
290
+ placeholder="选择题、简答题、开放题、混合"
291
+ value={questionType}
292
+ onChange={(e) => setQuestionType(e.target.value)}
293
+ className="mt-1"
294
+ />
295
+ </div>
296
+ <Button
297
+ onClick={handleAssignmentQuestions}
298
+ disabled={loading === "assignment-questions"}
299
+ >
300
+ {loading === "assignment-questions" ? (
301
+ <Loader2 className="h-4 w-4 animate-spin mr-2" />
302
+ ) : null}
303
+ 开始使用
304
+ </Button>
305
+ </>
306
+ )}
307
+ {f.id === "assessment-analysis" && (
308
+ <>
309
+ <div>
310
+ <Label>学生评估摘要 *</Label>
311
+ <Textarea
312
+ placeholder="例如:本周作业得分分布、常见错误、参与度简述…"
313
+ value={assessmentSummary}
314
+ onChange={(e) => setAssessmentSummary(e.target.value)}
315
+ rows={4}
316
+ className="mt-1"
317
+ />
318
+ </div>
319
+ <div>
320
+ <Label>相关课程主题(可选)</Label>
321
+ <Input
322
+ placeholder="便于结合知识库分析"
323
+ value={courseTopicHint}
324
+ onChange={(e) => setCourseTopicHint(e.target.value)}
325
+ className="mt-1"
326
+ />
327
+ </div>
328
+ <Button
329
+ onClick={handleAssessmentAnalysis}
330
+ disabled={loading === "assessment-analysis"}
331
+ >
332
+ {loading === "assessment-analysis" ? (
333
+ <Loader2 className="h-4 w-4 animate-spin mr-2" />
334
+ ) : null}
335
+ 开始使用
336
+ </Button>
337
+ </>
338
+ )}
339
+
340
+ {results[f.id] && (
341
+ <div className="mt-2 p-3 rounded-md bg-muted/60 text-sm whitespace-pre-wrap border border-border">
342
+ {results[f.id]}
343
+ </div>
344
+ )}
345
+ </CardContent>
346
+ </Card>
347
+ ))}
348
+ </div>
349
+ </div>
350
+ </div>
351
+ );
352
+ }
web/src/lib/api.ts CHANGED
@@ -392,3 +392,99 @@ export async function apiPodcast(payload: {
392
  }
393
  return res.blob();
394
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
  }
393
  return res.blob();
394
  }
395
+
396
+ // --------------------
397
+ // 教师 Agent API(AI 智能建课)
398
+ // --------------------
399
+ const TEACHER_TIMEOUT_MS = 90000;
400
+
401
+ export type TeacherStatus = {
402
+ weaviate_configured: boolean;
403
+ features: string[];
404
+ };
405
+
406
+ export async function apiTeacherStatus(): Promise<TeacherStatus> {
407
+ const base = getBaseUrl();
408
+ const res = await fetchWithTimeout(`${base}/api/teacher/status`, {}, 10000);
409
+ const data = await parseJsonSafe(res);
410
+ if (!res.ok) throw new Error(errMsg(data, "Teacher status failed"));
411
+ return data as TeacherStatus;
412
+ }
413
+
414
+ export async function apiTeacherCourseDescription(payload: {
415
+ topic: string;
416
+ outline_hint?: string | null;
417
+ }): Promise<{ description: string; weaviate_used: boolean }> {
418
+ const base = getBaseUrl();
419
+ const res = await fetchWithTimeout(
420
+ `${base}/api/teacher/course-description`,
421
+ {
422
+ method: "POST",
423
+ headers: { "Content-Type": "application/json" },
424
+ body: JSON.stringify(payload),
425
+ },
426
+ TEACHER_TIMEOUT_MS
427
+ );
428
+ const data = await parseJsonSafe(res);
429
+ if (!res.ok) throw new Error(errMsg(data, "Course description failed"));
430
+ return data as { description: string; weaviate_used: boolean };
431
+ }
432
+
433
+ export async function apiTeacherDocSuggestion(payload: {
434
+ topic: string;
435
+ current_doc_excerpt?: string | null;
436
+ doc_type?: string;
437
+ }): Promise<{ suggestion: string; weaviate_used: boolean }> {
438
+ const base = getBaseUrl();
439
+ const res = await fetchWithTimeout(
440
+ `${base}/api/teacher/doc-suggestion`,
441
+ {
442
+ method: "POST",
443
+ headers: { "Content-Type": "application/json" },
444
+ body: JSON.stringify(payload),
445
+ },
446
+ TEACHER_TIMEOUT_MS
447
+ );
448
+ const data = await parseJsonSafe(res);
449
+ if (!res.ok) throw new Error(errMsg(data, "Doc suggestion failed"));
450
+ return data as { suggestion: string; weaviate_used: boolean };
451
+ }
452
+
453
+ export async function apiTeacherAssignmentQuestions(payload: {
454
+ topic: string;
455
+ week_or_module?: string | null;
456
+ question_type?: string;
457
+ }): Promise<{ suggestion: string; weaviate_used: boolean }> {
458
+ const base = getBaseUrl();
459
+ const res = await fetchWithTimeout(
460
+ `${base}/api/teacher/assignment-questions`,
461
+ {
462
+ method: "POST",
463
+ headers: { "Content-Type": "application/json" },
464
+ body: JSON.stringify(payload),
465
+ },
466
+ TEACHER_TIMEOUT_MS
467
+ );
468
+ const data = await parseJsonSafe(res);
469
+ if (!res.ok) throw new Error(errMsg(data, "Assignment questions failed"));
470
+ return data as { suggestion: string; weaviate_used: boolean };
471
+ }
472
+
473
+ export async function apiTeacherAssessmentAnalysis(payload: {
474
+ assessment_summary: string;
475
+ course_topic_hint?: string | null;
476
+ }): Promise<{ analysis: string; weaviate_used: boolean }> {
477
+ const base = getBaseUrl();
478
+ const res = await fetchWithTimeout(
479
+ `${base}/api/teacher/assessment-analysis`,
480
+ {
481
+ method: "POST",
482
+ headers: { "Content-Type": "application/json" },
483
+ body: JSON.stringify(payload),
484
+ },
485
+ TEACHER_TIMEOUT_MS
486
+ );
487
+ const data = await parseJsonSafe(res);
488
+ if (!res.ok) throw new Error(errMsg(data, "Assessment analysis failed"));
489
+ return data as { analysis: string; weaviate_used: boolean };
490
+ }