Spaces:
Sleeping
Sleeping
Commit ·
fbe1c8a
1
Parent(s): 5ec50c1
Add teacher agent, docs, and project updates; ClareVoiceV1 remains untracked (nested repo)
Browse files- .gitignore +6 -0
- PUSH_TO_HF.md +61 -0
- api/config.py +12 -0
- api/routes_teacher.py +140 -0
- api/server.py +3 -0
- api/teacher_agent.py +116 -0
- api/weaviate_retrieve.py +61 -0
- docs/FRONTEND_BACKEND_AND_PODCAST.md +109 -0
- docs/TEACHER_AGENT.md +50 -0
- docs/VECTOR_DB_LOGIC_AND_MULTI_COURSE.md +222 -0
- requirements.txt +8 -1
- web/src/App.tsx +9 -0
- web/src/components/Header.tsx +32 -0
- web/src/components/TeacherDashboard.tsx +352 -0
- web/src/lib/api.ts +96 -0
.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 |
+
}
|