claudqunwang commited on
Commit
2a4f012
·
1 Parent(s): 6a91b07

Add structured AI courseware APIs

Browse files
ARCHITECTURE.md CHANGED
@@ -261,7 +261,7 @@ RUN pip install -r requirements.txt
261
  **托管**: Google Cloud Platform (GCP)
262
 
263
  **配置**:
264
- - Cluster URL: `https://xxx.c0.us-west3.gcp.weaviate.cloud`
265
  - Authentication: API Key
266
  - Collection Schema: 自动创建(LlamaIndex 管理)
267
 
 
261
  **托管**: Google Cloud Platform (GCP)
262
 
263
  **配置**:
264
+ - Cluster URL: `https://riiqvgc7tuum6cgwhik9ra.c0.us-west3.gcp.weaviate.cloud`
265
  - Authentication: API Key
266
  - Collection Schema: 自动创建(LlamaIndex 管理)
267
 
api/routes_courseware_ai.py ADDED
@@ -0,0 +1,563 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ """
4
+ AI Courseware AI 接口 (Plan / Prepare / Reflect / Improve)。
5
+
6
+ 按照《AI Courseware AI 接口文档 v1.0》提供 6 个结构化 JSON API:
7
+ - /ai/courseware/syllabus/generate
8
+ - /ai/courseware/flow/generate
9
+ - /ai/courseware/flow/regenerate-partial
10
+ - /ai/courseware/plan/detail/generate
11
+ - /ai/courseware/reflection/generate
12
+ - /ai/courseware/improvement/generate
13
+ """
14
+
15
+ import json
16
+ import time
17
+ from typing import List, Optional, Any
18
+
19
+ from fastapi import APIRouter, HTTPException
20
+ from pydantic import BaseModel, Field
21
+
22
+ from api.config import client, DEFAULT_MODEL
23
+
24
+
25
+ router = APIRouter(prefix="/ai/courseware", tags=["ai-courseware"])
26
+
27
+ PROMPT_VERSION = "v1.0"
28
+
29
+
30
+ class Meta(BaseModel):
31
+ """所有成功响应必须携带的 meta 信息。"""
32
+
33
+ model: str
34
+ model_version: Optional[str] = None
35
+ prompt_version: str
36
+ temperature: float
37
+ tokens_used: int
38
+ latency_ms: int
39
+
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # 通用 OpenAI 调用辅助
43
+ # ---------------------------------------------------------------------------
44
+
45
+ def _call_openai_json(
46
+ op_name: str,
47
+ system_prompt: str,
48
+ user_prompt: str,
49
+ temperature: float = 0.4,
50
+ ) -> tuple[dict, Meta]:
51
+ """调用 OpenAI 并期望得到 JSON 对象;若解析失败则抛 422。"""
52
+ started = time.time()
53
+ try:
54
+ resp = client.chat.completions.create(
55
+ model=DEFAULT_MODEL,
56
+ messages=[
57
+ {"role": "system", "content": system_prompt},
58
+ {"role": "user", "content": user_prompt},
59
+ ],
60
+ temperature=temperature,
61
+ )
62
+ except Exception as e:
63
+ raise HTTPException(status_code=500, detail=f"{op_name} OpenAI error: {e}")
64
+
65
+ latency_ms = int((time.time() - started) * 1000)
66
+ model_name = getattr(resp, "model", DEFAULT_MODEL)
67
+ usage = getattr(resp, "usage", None)
68
+ tokens_used = int(getattr(usage, "total_tokens", 0) or 0)
69
+
70
+ content = (resp.choices[0].message.content or "").strip()
71
+ try:
72
+ data = json.loads(content)
73
+ if not isinstance(data, dict):
74
+ raise ValueError("top-level JSON is not an object")
75
+ except Exception as e:
76
+ raise HTTPException(
77
+ status_code=422,
78
+ detail=f"INVALID_GENERATION for {op_name}: cannot parse JSON ({e}); raw={content[:400]}",
79
+ )
80
+
81
+ meta = Meta(
82
+ model=model_name,
83
+ model_version=None,
84
+ prompt_version=PROMPT_VERSION,
85
+ temperature=temperature,
86
+ tokens_used=tokens_used,
87
+ latency_ms=latency_ms,
88
+ )
89
+ return data, meta
90
+
91
+
92
+ # ---------------------------------------------------------------------------
93
+ # 2.1 生成大纲草案 (Generate Syllabus Preview)
94
+ # ---------------------------------------------------------------------------
95
+
96
+
97
+ class SyllabusContext(BaseModel):
98
+ courseName: str
99
+ learningOutcome: str
100
+ studentLevel: str
101
+ teachingFocus: str
102
+ courseLength: int = Field(..., ge=1, le=52)
103
+
104
+
105
+ class SyllabusGenerateRequest(BaseModel):
106
+ requestId: str
107
+ context: SyllabusContext
108
+
109
+
110
+ class WeekSyllabus(BaseModel):
111
+ weekNumber: int
112
+ title: str
113
+ learningObjectives: List[str]
114
+ topics: List[str]
115
+
116
+
117
+ class SyllabusGenerateData(BaseModel):
118
+ syllabus: List[WeekSyllabus]
119
+
120
+
121
+ class SyllabusGenerateResponse(BaseModel):
122
+ data: SyllabusGenerateData
123
+ meta: Meta
124
+
125
+
126
+ @router.post("/syllabus/generate", response_model=SyllabusGenerateResponse)
127
+ def generate_syllabus(req: SyllabusGenerateRequest):
128
+ """Phase 1 (Plan):根据课程元数据生成 Weeks/Modules 大纲草案。"""
129
+ ctx = req.context
130
+ sys_prompt = (
131
+ "You are an instructional designer. "
132
+ "Given course metadata, generate a weekly syllabus as JSON. "
133
+ "Output ONLY a JSON object with key 'syllabus', matching the provided schema."
134
+ )
135
+ user_prompt = f"""
136
+ Course name: {ctx.courseName}
137
+ Learning outcome: {ctx.learningOutcome}
138
+ Student level: {ctx.studentLevel}
139
+ Teaching focus: {ctx.teachingFocus}
140
+ Total weeks/modules: {ctx.courseLength}
141
+
142
+ Return JSON:
143
+ {{
144
+ "syllabus": [
145
+ {{
146
+ "weekNumber": 1,
147
+ "title": "string",
148
+ "learningObjectives": ["string", "..."],
149
+ "topics": ["string", "..."]
150
+ }},
151
+ ...
152
+ ]
153
+ }}
154
+ Make sure the array length equals courseLength ({ctx.courseLength}) and weekNumber starts from 1.
155
+ """
156
+
157
+ data_dict, meta = _call_openai_json("syllabus.generate", sys_prompt, user_prompt, temperature=0.4)
158
+ if "syllabus" not in data_dict:
159
+ raise HTTPException(status_code=422, detail="INVALID_GENERATION: 'syllabus' missing in JSON.")
160
+
161
+ return SyllabusGenerateResponse(data=SyllabusGenerateData(**data_dict), meta=meta)
162
+
163
+
164
+ # ---------------------------------------------------------------------------
165
+ # 2.2 生成完整教学流程 (Generate Lesson Flow)
166
+ # ---------------------------------------------------------------------------
167
+
168
+
169
+ class ModuleContext(BaseModel):
170
+ title: str
171
+ learningObjectives: List[str]
172
+ topics: List[str]
173
+ durationMinutes: int = Field(..., ge=10, le=600)
174
+
175
+
176
+ class FlowGenerateRequest(BaseModel):
177
+ requestId: str
178
+ moduleContext: ModuleContext
179
+ systemPrompts: Optional[List[str]] = None
180
+
181
+
182
+ class LessonStep(BaseModel):
183
+ type: str
184
+ title: str
185
+ estimated_duration: int = Field(..., ge=1)
186
+ ai_understanding: str
187
+
188
+
189
+ class FlowGenerateData(BaseModel):
190
+ steps: List[LessonStep]
191
+
192
+
193
+ class FlowGenerateResponse(BaseModel):
194
+ data: FlowGenerateData
195
+ meta: Meta
196
+
197
+
198
+ @router.post("/flow/generate", response_model=FlowGenerateResponse)
199
+ def generate_lesson_flow(req: FlowGenerateRequest):
200
+ """Phase 2 (Prepare):为单个 Module 生成完整教学步骤列表。"""
201
+ mc = req.moduleContext
202
+ extra_system = "\n".join(req.systemPrompts or [])
203
+ sys_prompt = (
204
+ "You are an AI courseware copilot. "
205
+ "Generate a detailed lesson flow for one module as JSON. "
206
+ "Output ONLY a JSON object with key 'steps', each step matching the schema."
207
+ )
208
+ user_prompt = f"""
209
+ Module title: {mc.title}
210
+ Learning objectives: {mc.learningObjectives}
211
+ Topics: {mc.topics}
212
+ Duration (minutes): {mc.durationMinutes}
213
+
214
+ Additional long-term system prompts (optional, may be empty):
215
+ {extra_system}
216
+
217
+ Return JSON:
218
+ {{
219
+ "steps": [
220
+ {{
221
+ "type": "Teacher Explanation | Interactive Quiz | Group Discussion | ...",
222
+ "title": "string",
223
+ "estimated_duration": 10,
224
+ "ai_understanding": "short explanation of why this step exists and what it should achieve"
225
+ }},
226
+ ...
227
+ ]
228
+ }}
229
+ The total sum of estimated_duration should be roughly close to durationMinutes ({mc.durationMinutes}).
230
+ """
231
+
232
+ data_dict, meta = _call_openai_json("flow.generate", sys_prompt, user_prompt, temperature=0.5)
233
+ if "steps" not in data_dict:
234
+ raise HTTPException(status_code=422, detail="INVALID_GENERATION: 'steps' missing in JSON.")
235
+
236
+ return FlowGenerateResponse(data=FlowGenerateData(**data_dict), meta=meta)
237
+
238
+
239
+ # ---------------------------------------------------------------------------
240
+ # 2.3 局部重构教学流程 (Regenerate Partial Flow)
241
+ # ---------------------------------------------------------------------------
242
+
243
+
244
+ class SimpleStep(BaseModel):
245
+ id: str
246
+ title: str
247
+ duration: int
248
+
249
+
250
+ class CurrentFlow(BaseModel):
251
+ lockedSteps: List[SimpleStep]
252
+ unlockedSteps: List[SimpleStep]
253
+
254
+
255
+ class FlowPartialRequest(BaseModel):
256
+ requestId: str
257
+ prompt: str
258
+ currentFlow: CurrentFlow
259
+
260
+
261
+ class CopilotProposedStep(BaseModel):
262
+ type: str
263
+ title: str
264
+ estimated_duration: int
265
+ ai_understanding: str
266
+
267
+
268
+ class FlowPartialData(BaseModel):
269
+ explanation: str
270
+ proposedSteps: List[CopilotProposedStep]
271
+
272
+
273
+ class FlowPartialResponse(BaseModel):
274
+ data: FlowPartialData
275
+ meta: Meta
276
+
277
+
278
+ @router.post("/flow/regenerate-partial", response_model=FlowPartialResponse)
279
+ def regenerate_partial_flow(req: FlowPartialRequest):
280
+ """Phase 2 (Prepare):在锁定部分步骤后,根据 Prompt 重写剩余步骤。"""
281
+ locked = [s.model_dump() for s in req.currentFlow.lockedSteps]
282
+ unlocked = [s.model_dump() for s in req.currentFlow.unlockedSteps]
283
+ sys_prompt = (
284
+ "You are an AI copilot for lesson flow editing. "
285
+ "Given lockedSteps and unlockedSteps plus a user prompt, "
286
+ "propose replacement steps ONLY for unlockedSteps and explain your reasoning."
287
+ "Return JSON with 'explanation' and 'proposedSteps'."
288
+ )
289
+ user_prompt = f"""
290
+ User prompt:
291
+ {req.prompt}
292
+
293
+ Current flow:
294
+ lockedSteps: {locked}
295
+ unlockedSteps: {unlocked}
296
+
297
+ Return JSON:
298
+ {{
299
+ "explanation": "short natural language explanation for the teacher UI bubble",
300
+ "proposedSteps": [
301
+ {{
302
+ "type": "Teacher Explanation | Interactive Quiz | Group Discussion | ...",
303
+ "title": "string",
304
+ "estimated_duration": 10,
305
+ "ai_understanding": "short explanation of this step"
306
+ }}
307
+ ]
308
+ }}
309
+ """
310
+
311
+ data_dict, meta = _call_openai_json(
312
+ "flow.regenerate-partial", sys_prompt, user_prompt, temperature=0.5
313
+ )
314
+ if "explanation" not in data_dict or "proposedSteps" not in data_dict:
315
+ raise HTTPException(
316
+ status_code=422,
317
+ detail="INVALID_GENERATION: 'explanation' or 'proposedSteps' missing in JSON.",
318
+ )
319
+
320
+ return FlowPartialResponse(data=FlowPartialData(**data_dict), meta=meta)
321
+
322
+
323
+ # ---------------------------------------------------------------------------
324
+ # 2.4 生成/润色实体教案长文 (Generate/Polish Lesson Plan Detail)
325
+ # ---------------------------------------------------------------------------
326
+
327
+
328
+ class PlanDetailRequest(BaseModel):
329
+ requestId: str
330
+ finalizedSteps: List[Any]
331
+
332
+
333
+ class LessonSection(BaseModel):
334
+ section_id: str
335
+ type: str
336
+ content: str
337
+
338
+
339
+ class PlanDetailData(BaseModel):
340
+ sections: List[LessonSection]
341
+
342
+
343
+ class PlanDetailResponse(BaseModel):
344
+ data: PlanDetailData
345
+ meta: Meta
346
+
347
+
348
+ @router.post("/plan/detail/generate", response_model=PlanDetailResponse)
349
+ def generate_plan_detail(req: PlanDetailRequest):
350
+ """Phase 2 (Prepare):根据最终 Steps 生成/润色详细教案(富文本结构)。"""
351
+ sys_prompt = (
352
+ "You are an expert lesson-plan writer. "
353
+ "Given finalized lesson steps, generate detailed lesson plan sections as JSON. "
354
+ "Each section has section_id, type, content (Markdown allowed)."
355
+ )
356
+ user_prompt = f"""
357
+ Finalized steps (JSON array, opaque to you but describes lesson flow):
358
+ {json.dumps(req.finalizedSteps, ensure_ascii=False)}
359
+
360
+ Return JSON:
361
+ {{
362
+ "sections": [
363
+ {{
364
+ "section_id": "sec_obj_01",
365
+ "type": "Lesson Objectives",
366
+ "content": "By the end of this lesson, students will be able to..."
367
+ }},
368
+ {{
369
+ "section_id": "sec_content_01",
370
+ "type": "Content",
371
+ "content": "### 1. Topic heading\\nDetailed explanation..."
372
+ }}
373
+ ]
374
+ }}
375
+ """
376
+
377
+ data_dict, meta = _call_openai_json("plan.detail.generate", sys_prompt, user_prompt, temperature=0.4)
378
+ if "sections" not in data_dict:
379
+ raise HTTPException(status_code=422, detail="INVALID_GENERATION: 'sections' missing in JSON.")
380
+
381
+ return PlanDetailResponse(data=PlanDetailData(**data_dict), meta=meta)
382
+
383
+
384
+ # ---------------------------------------------------------------------------
385
+ # 2.5 生成反思看板数据 (Generate Reflection Report)
386
+ # ---------------------------------------------------------------------------
387
+
388
+
389
+ class TeachAnnotation(BaseModel):
390
+ category: str
391
+ selectedText: Optional[str] = None
392
+ feedback: Optional[str] = None
393
+
394
+
395
+ class QuizAggregations(BaseModel):
396
+ averageScore: Optional[float] = None
397
+ lowestTopic: Optional[str] = None
398
+
399
+
400
+ class ReflectionRequest(BaseModel):
401
+ requestId: str
402
+ teachAnnotations: List[TeachAnnotation] = Field(default_factory=list)
403
+ quizAggregations: Optional[QuizAggregations] = None
404
+
405
+
406
+ class ReflectionUnderstanding(BaseModel):
407
+ status: str
408
+ summary: str
409
+ bulletPoints: Optional[List[str]] = None
410
+
411
+
412
+ class ReflectionEngagement(BaseModel):
413
+ status: str
414
+ summary: Optional[str] = None
415
+
416
+
417
+ class ReflectionDifficulty(BaseModel):
418
+ status: str
419
+ challengingTopics: Optional[List[dict]] = None
420
+
421
+
422
+ class ReflectionMisconception(BaseModel):
423
+ status: str
424
+ issues: Optional[List[dict]] = None
425
+
426
+
427
+ class NextLessonSuggestion(BaseModel):
428
+ actionText: str
429
+ deepLinkType: Optional[str] = None
430
+
431
+
432
+ class ReflectionData(BaseModel):
433
+ understanding: ReflectionUnderstanding
434
+ engagement: ReflectionEngagement
435
+ difficulty: ReflectionDifficulty
436
+ misconceptions: ReflectionMisconception
437
+ nextLessonSuggestions: List[NextLessonSuggestion]
438
+
439
+
440
+ class ReflectionResponse(BaseModel):
441
+ data: ReflectionData
442
+ meta: Meta
443
+
444
+
445
+ @router.post("/reflection/generate", response_model=ReflectionResponse)
446
+ def generate_reflection(req: ReflectionRequest):
447
+ """Phase 4 (Reflect):基于反馈与测验数据生成反思看板。"""
448
+ annotations = [a.model_dump() for a in req.teachAnnotations]
449
+ qa = req.quizAggregations.model_dump() if req.quizAggregations else {}
450
+ sys_prompt = (
451
+ "You are an AI teaching analytics assistant. "
452
+ "Given teacher annotations and quiz aggregations, generate a reflection dashboard as JSON "
453
+ "with understanding, engagement, difficulty, misconceptions, and nextLessonSuggestions."
454
+ )
455
+ user_prompt = f"""
456
+ Teach annotations:
457
+ {json.dumps(annotations, ensure_ascii=False)}
458
+
459
+ Quiz aggregations:
460
+ {json.dumps(qa, ensure_ascii=False)}
461
+
462
+ Return JSON:
463
+ {{
464
+ "understanding": {{
465
+ "status": "Low | Medium | High",
466
+ "summary": "short text",
467
+ "bulletPoints": ["optional", "..."]
468
+ }},
469
+ "engagement": {{
470
+ "status": "Low | Medium | High",
471
+ "summary": "optional"
472
+ }},
473
+ "difficulty": {{
474
+ "status": "Low | Medium | High",
475
+ "challengingTopics": [{{"topic": "string", "percentStruggle": 45}}]
476
+ }},
477
+ "misconceptions": {{
478
+ "status": "Low | Medium | High",
479
+ "issues": [{{"title": "string", "description": "string", "evidence": "string"}}]
480
+ }},
481
+ "nextLessonSuggestions": [
482
+ {{
483
+ "actionText": "string",
484
+ "deepLinkType": "PREPARE_FLOW_EDIT | REFLECT_DASHBOARD | ..."
485
+ }}
486
+ ]
487
+ }}
488
+ """
489
+
490
+ data_dict, meta = _call_openai_json("reflection.generate", sys_prompt, user_prompt, temperature=0.4)
491
+ required = ["understanding", "engagement", "difficulty", "misconceptions", "nextLessonSuggestions"]
492
+ if any(k not in data_dict for k in required):
493
+ raise HTTPException(
494
+ status_code=422,
495
+ detail=f"INVALID_GENERATION: missing keys in JSON, required={required}",
496
+ )
497
+
498
+ return ReflectionResponse(data=ReflectionData(**data_dict), meta=meta)
499
+
500
+
501
+ # ---------------------------------------------------------------------------
502
+ # 2.6 生成跨期改进提案 (Generate Improvement Proposals)
503
+ # ---------------------------------------------------------------------------
504
+
505
+
506
+ class ImprovementRequest(BaseModel):
507
+ requestId: str
508
+ reflectionReports: List[Any]
509
+
510
+
511
+ class ImprovementProposal(BaseModel):
512
+ title: str
513
+ priority: str
514
+ affectedWeeks: Optional[str] = None
515
+ evidence: Optional[str] = None
516
+ rootCause: Optional[str] = None
517
+ proposedSolution: Optional[str] = None
518
+ expectedImpact: Optional[str] = None
519
+
520
+
521
+ class ImprovementData(BaseModel):
522
+ proposals: List[ImprovementProposal]
523
+
524
+
525
+ class ImprovementResponse(BaseModel):
526
+ data: ImprovementData
527
+ meta: Meta
528
+
529
+
530
+ @router.post("/improvement/generate", response_model=ImprovementResponse)
531
+ def generate_improvement(req: ImprovementRequest):
532
+ """Phase 5 (Improve):基于过往反思报告生成跨期改进提案。"""
533
+ reports = json.dumps(req.reflectionReports, ensure_ascii=False)
534
+ sys_prompt = (
535
+ "You are an AI curriculum improvement assistant. "
536
+ "Given past reflection reports, generate high-level improvement proposals as JSON."
537
+ )
538
+ user_prompt = f"""
539
+ Past reflection reports (opaque JSON array):
540
+ {reports}
541
+
542
+ Return JSON:
543
+ {{
544
+ "proposals": [
545
+ {{
546
+ "title": "Students struggle with gradient descent optimization",
547
+ "priority": "High Priority | Medium Priority | Low Priority",
548
+ "affectedWeeks": "Week 2, 3",
549
+ "evidence": "45% reported difficulty...",
550
+ "rootCause": "Introduced too quickly without visual math foundation.",
551
+ "proposedSolution": "Split gradient descent into two lessons: (1) Intuition (2) Math.",
552
+ "expectedImpact": "Increase scores by 15-20% and reduce frustration."
553
+ }}
554
+ ]
555
+ }}
556
+ """
557
+
558
+ data_dict, meta = _call_openai_json("improvement.generate", sys_prompt, user_prompt, temperature=0.4)
559
+ if "proposals" not in data_dict:
560
+ raise HTTPException(status_code=422, detail="INVALID_GENERATION: 'proposals' missing in JSON.")
561
+
562
+ return ImprovementResponse(data=ImprovementData(**data_dict), meta=meta)
563
+
api/server.py CHANGED
@@ -36,6 +36,7 @@ from api.routes_directory import router as directory_router
36
  from api.routes_teacher import router as teacher_router
37
  # ✅ Courseware:课程愿景、活动设计、课堂助教、QA 优化、教案与 PPT
38
  from api.routes_courseware import router as courseware_router
 
39
 
40
  # ✅ LangSmith (optional)
41
  try:
@@ -99,6 +100,7 @@ app.add_middleware(
99
  app.include_router(directory_router)
100
  app.include_router(teacher_router)
101
  app.include_router(courseware_router)
 
102
 
103
  # ----------------------------
104
  # Static hosting (Vite build)
 
36
  from api.routes_teacher import router as teacher_router
37
  # ✅ Courseware:课程愿景、活动设计、课堂助教、QA 优化、教案与 PPT
38
  from api.routes_courseware import router as courseware_router
39
+ from api.routes_courseware_ai import router as courseware_ai_router
40
 
41
  # ✅ LangSmith (optional)
42
  try:
 
100
  app.include_router(directory_router)
101
  app.include_router(teacher_router)
102
  app.include_router(courseware_router)
103
+ app.include_router(courseware_ai_router)
104
 
105
  # ----------------------------
106
  # Static hosting (Vite build)
docs/AI_COURSEWARE_API_USAGE.md ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## AI Courseware 结构化 API 使用说明(前后端协作)
2
+
3
+ 本说明基于《AI Courseware AI 接口文档 v1.0》,结合当前 Clare Courseware 实现,帮助前端与其他后端服务正确调用 AI 课程设计相关接口。
4
+
5
+ ---
6
+
7
+ ### 1. 服务 Base URL 与部署
8
+
9
+ - 所有接口均由 Clare 后端 (`api/server.py`) 暴露。
10
+ - 部署在本地开发环境时:
11
+ - Base URL 通常为 `http://localhost:8000`
12
+ - 部署在 Hugging Face Space 或其他环境时:
13
+ - 前端通过 Vite 环境变量 `VITE_API_BASE` 配置 Base URL
14
+ - 例如:`VITE_API_BASE=https://your-space.hf.space`
15
+
16
+ > 下文统一用 `{{BASE}}` 表示后端根地址(如 `http://localhost:8000`)。
17
+
18
+ ---
19
+
20
+ ### 2. 路由概览
21
+
22
+ #### 2.1 现有 Clare Teacher/Courseware 接口(非结构化文本)
23
+
24
+ 这些接口主要给当前 Clare 前端使用,返回 Markdown/文本:
25
+
26
+ - `POST {{BASE}}/api/courseware/vision`
27
+ - `POST {{BASE}}/api/courseware/activities`
28
+ - `POST {{BASE}}/api/courseware/copilot`
29
+ - `POST {{BASE}}/api/courseware/qa-optimize`
30
+ - `POST {{BASE}}/api/courseware/content`
31
+
32
+ 对应的前端封装函数在 `web/src/lib/api.ts` 中:
33
+
34
+ - `apiCoursewareVision`
35
+ - `apiCoursewareActivities`
36
+ - `apiCoursewareCopilot`
37
+ - `apiCoursewareQAOptimize`
38
+ - `apiCoursewareContent`
39
+
40
+ #### 2.2 新增 AI Courseware 结构化接口(建议给 LMS/独立前端用)
41
+
42
+ 新路由前缀:`/ai/courseware`,在 `api/routes_courseware_ai.py` 中实现,返回结构化 JSON (`data + meta`):
43
+
44
+ 1. **生成大纲草案**
45
+ - `POST {{BASE}}/ai/courseware/syllabus/generate`
46
+ 2. **生成单个模块的教学流程**
47
+ - `POST {{BASE}}/ai/courseware/flow/generate`
48
+ 3. **局部重构教学流程(Copilot)**
49
+ - `POST {{BASE}}/ai/courseware/flow/regenerate-partial`
50
+ 4. **生成/润色教案长文**
51
+ - `POST {{BASE}}/ai/courseware/plan/detail/generate`
52
+ 5. **生成反思看板数据**
53
+ - `POST {{BASE}}/ai/courseware/reflection/generate`
54
+ 6. **生成跨期改进提案**
55
+ - `POST {{BASE}}/ai/courseware/improvement/generate`
56
+
57
+ 所有成功响应都遵循统一包装:
58
+
59
+ ```json
60
+ {
61
+ "data": { ... }, // 每个接口自己的数据结构
62
+ "meta": {
63
+ "model": "gpt-4o",
64
+ "model_version": null,
65
+ "prompt_version": "v1.0",
66
+ "temperature": 0.4,
67
+ "tokens_used": 1234,
68
+ "latency_ms": 5678
69
+ }
70
+ }
71
+ ```
72
+
73
+ `meta` 由后端统一生成,包含模型、Prompt 版本、token 消耗和延迟,便于监控。
74
+
75
+ ---
76
+
77
+ ### 3. 前端 TypeScript 封装(`web/src/lib/api.ts`)
78
+
79
+ 前端已经在 `web/src/lib/api.ts` 中提供了 6 个结构化接口的封装函数,可直接在 React 组件中调用。
80
+
81
+ #### 3.1 生成大纲草案 `apiAiSyllabusGenerate`
82
+
83
+ ```ts
84
+ import {
85
+ apiAiSyllabusGenerate,
86
+ type AiSyllabusGenerateReq,
87
+ type AiSyllabusGenerateResp,
88
+ } from "@/lib/api";
89
+
90
+ const req: AiSyllabusGenerateReq = {
91
+ requestId: "req_cw_101",
92
+ context: {
93
+ courseName: "IST 345 Building Generative AI Application",
94
+ learningOutcome: "Understand LLMs and build a RAG application",
95
+ studentLevel: "BEGINNER",
96
+ teachingFocus: "PRACTICE_ORIENTED",
97
+ courseLength: 4,
98
+ },
99
+ };
100
+
101
+ const resp: AiSyllabusGenerateResp = await apiAiSyllabusGenerate(req);
102
+ // resp.data.syllabus -> Week 级别数组
103
+ // resp.meta -> 模型与性能信息
104
+ ```
105
+
106
+ #### 3.2 生成单个模块的教学流程 `apiAiFlowGenerate`
107
+
108
+ ```ts
109
+ import {
110
+ apiAiFlowGenerate,
111
+ type AiFlowGenerateReq,
112
+ type AiFlowGenerateResp,
113
+ } from "@/lib/api";
114
+
115
+ const req: AiFlowGenerateReq = {
116
+ requestId: "req_cw_102",
117
+ moduleContext: {
118
+ title: "Introduction to Generative AI",
119
+ learningObjectives: ["Understand basic LLM concepts"],
120
+ topics: ["History of AI", "Transformer Architecture"],
121
+ durationMinutes: 90,
122
+ },
123
+ systemPrompts: ["(可选) 来自 Improve 阶段的长期提示"],
124
+ };
125
+
126
+ const resp: AiFlowGenerateResp = await apiAiFlowGenerate(req);
127
+ // resp.data.steps -> 每个 step 含 type/title/estimated_duration/ai_understanding
128
+ ```
129
+
130
+ #### 3.3 局部重构教学流程 `apiAiFlowRegeneratePartial`
131
+
132
+ ```ts
133
+ import {
134
+ apiAiFlowRegeneratePartial,
135
+ type AiFlowPartialReq,
136
+ type AiFlowPartialResp,
137
+ } from "@/lib/api";
138
+
139
+ const req: AiFlowPartialReq = {
140
+ requestId: "req_cw_103",
141
+ prompt: "Split the 30-min explanation into two shorter parts with a quiz in between.",
142
+ currentFlow: {
143
+ lockedSteps: [{ id: "step_1", title: "Welcome", duration: 15 }],
144
+ unlockedSteps: [{ id: "step_2", title: "Heavy Explanation", duration: 30 }],
145
+ },
146
+ };
147
+
148
+ const resp: AiFlowPartialResp = await apiAiFlowRegeneratePartial(req);
149
+ // resp.data.explanation -> Copilot 气泡文案
150
+ // resp.data.proposedSteps -> 用于替换 unlockedSteps 的新步骤
151
+ ```
152
+
153
+ #### 3.4 教案长文 `apiAiPlanDetailGenerate`
154
+
155
+ ```ts
156
+ import {
157
+ apiAiPlanDetailGenerate,
158
+ type AiPlanDetailReq,
159
+ type AiPlanDetailResp,
160
+ } from "@/lib/api";
161
+
162
+ const req: AiPlanDetailReq = {
163
+ requestId: "req_cw_104",
164
+ finalizedSteps: stepsArray, // 来自 Flow 阶段确定的完整 steps
165
+ };
166
+
167
+ const resp: AiPlanDetailResp = await apiAiPlanDetailGenerate(req);
168
+ // resp.data.sections -> 多个 section(Lesson Objectives / Content 等),content 支持 Markdown
169
+ ```
170
+
171
+ #### 3.5 反思看板 `apiAiReflectionGenerate`
172
+
173
+ ```ts
174
+ import {
175
+ apiAiReflectionGenerate,
176
+ type AiReflectionReq,
177
+ type AiReflectionResp,
178
+ } from "@/lib/api";
179
+
180
+ const req: AiReflectionReq = {
181
+ requestId: "req_cw_105",
182
+ teachAnnotations: [
183
+ {
184
+ category: "NEEDS_MORE_TIME",
185
+ selectedText: "Gradient Descent",
186
+ feedback: "Students were confused by the math",
187
+ },
188
+ ],
189
+ quizAggregations: {
190
+ averageScore: 72,
191
+ lowestTopic: "Matrix Operations",
192
+ },
193
+ };
194
+
195
+ const resp: AiReflectionResp = await apiAiReflectionGenerate(req);
196
+ // resp.data.understanding / engagement / difficulty / misconceptions / nextLessonSuggestions
197
+ ```
198
+
199
+ #### 3.6 改进提案 `apiAiImprovementGenerate`
200
+
201
+ ```ts
202
+ import {
203
+ apiAiImprovementGenerate,
204
+ type AiImprovementReq,
205
+ type AiImprovementResp,
206
+ } from "@/lib/api";
207
+
208
+ const req: AiImprovementReq = {
209
+ requestId: "req_cw_106",
210
+ reflectionReports: previousReflectionReports, // 来自 2.5 的历史数据数组
211
+ };
212
+
213
+ const resp: AiImprovementResp = await apiAiImprovementGenerate(req);
214
+ // resp.data.proposals -> 每条包含 title/priority/affectedWeeks/evidence/rootCause/proposedSolution/expectedImpact
215
+ ```
216
+
217
+ ---
218
+
219
+ ### 4. 后端如何配置 OpenAI 模型(固定为 gpt‑4o)
220
+
221
+ 后端 `api/config.py` 使用环境变量 `CLARE_DEFAULT_MODEL` 控制默认模型:
222
+
223
+ ```python
224
+ DEFAULT_MODEL = (os.getenv("CLARE_DEFAULT_MODEL") or "gpt-4.1-mini").strip()
225
+ ```
226
+
227
+ 在 Hugging Face Space(或其他部署环境)中:
228
+
229
+ - 打开 **Settings → Variables and secrets**
230
+ - 设置:
231
+
232
+ ```text
233
+ CLARE_DEFAULT_MODEL = gpt-4o
234
+ ```
235
+
236
+ 这样,上述所有 `/ai/courseware/...` 接口默认都会使用 `gpt-4o`,同时 `meta.model` 字段会反映真实模型名称,便于前端或监控系统做统计。
237
+
238
+ ---
239
+
240
+ ### 5. 错误处理约定
241
+
242
+ - 若 OpenAI 调用失败:后端返回 `500`,`detail` 内含错误信息。
243
+ - 若 LLM 返回的内容不是合法 JSON 或缺少关键字段:
244
+ - 返回 `422 INVALID_GENERATION`,`detail` 会说明缺失的字段或解析失败原因。
245
+ - 前端封装函数(`apiAi*` 系列)若收到非 2xx,会抛出 `Error`,错误信息来源:
246
+ - `data.error` / `data.detail` / `data.message` / 自定义 fallback。
247
+
248
+ 前端在捕获异常时可以统一提示“AI 生成失败,请稍后重试”,并记录 `requestId` 以便后端排查。
249
+
docs/TEACHER_AGENT.md CHANGED
@@ -25,7 +25,7 @@
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
 
 
25
 
26
  与 ClareVoice 相同:在环境变量或 `.env` 中配置:
27
 
28
+ - `WEAVIATE_URL`:Weaviate Cloud 集群地址(如 `https://riiqvgc7tuum6cgwhik9ra.c0.us-west3.gcp.weaviate.cloud`)
29
  - `WEAVIATE_API_KEY`:API Key
30
  - `WEAVIATE_COLLECTION`:可选,默认 `GenAICourses`
31
 
web/src/lib/api.ts CHANGED
@@ -580,3 +580,300 @@ export async function apiCoursewareContent(payload: {
580
  if (!res.ok) throw new Error(errMsg(data, "Content generation failed"));
581
  return data as { content: string; weaviate_used: boolean };
582
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
580
  if (!res.ok) throw new Error(errMsg(data, "Content generation failed"));
581
  return data as { content: string; weaviate_used: boolean };
582
  }
583
+
584
+ // -------------------- AI Courseware structured APIs (Plan / Prepare / Reflect / Improve) --------------------
585
+
586
+ export type AiMeta = {
587
+ model: string;
588
+ model_version?: string | null;
589
+ prompt_version: string;
590
+ temperature: number;
591
+ tokens_used: number;
592
+ latency_ms: number;
593
+ };
594
+
595
+ // 2.1 Generate Syllabus Preview
596
+ export type AiSyllabusContext = {
597
+ courseName: string;
598
+ learningOutcome: string;
599
+ studentLevel: string;
600
+ teachingFocus: string;
601
+ courseLength: number;
602
+ };
603
+
604
+ export type AiSyllabusGenerateReq = {
605
+ requestId: string;
606
+ context: AiSyllabusContext;
607
+ };
608
+
609
+ export type AiWeekSyllabus = {
610
+ weekNumber: number;
611
+ title: string;
612
+ learningObjectives: string[];
613
+ topics: string[];
614
+ };
615
+
616
+ export type AiSyllabusGenerateResp = {
617
+ data: { syllabus: AiWeekSyllabus[] };
618
+ meta: AiMeta;
619
+ };
620
+
621
+ export async function apiAiSyllabusGenerate(
622
+ payload: AiSyllabusGenerateReq
623
+ ): Promise<AiSyllabusGenerateResp> {
624
+ const base = getBaseUrl();
625
+ const res = await fetchWithTimeout(
626
+ `${base}/ai/courseware/syllabus/generate`,
627
+ {
628
+ method: "POST",
629
+ headers: { "Content-Type": "application/json" },
630
+ body: JSON.stringify(payload),
631
+ },
632
+ TEACHER_TIMEOUT_MS
633
+ );
634
+ const data = await parseJsonSafe(res);
635
+ if (!res.ok) throw new Error(errMsg(data, "AI syllabus generate failed"));
636
+ return data as AiSyllabusGenerateResp;
637
+ }
638
+
639
+ // 2.2 Generate Lesson Flow
640
+ export type AiModuleContext = {
641
+ title: string;
642
+ learningObjectives: string[];
643
+ topics: string[];
644
+ durationMinutes: number;
645
+ };
646
+
647
+ export type AiFlowGenerateReq = {
648
+ requestId: string;
649
+ moduleContext: AiModuleContext;
650
+ systemPrompts?: string[] | null;
651
+ };
652
+
653
+ export type AiLessonStep = {
654
+ type: string;
655
+ title: string;
656
+ estimated_duration: number;
657
+ ai_understanding: string;
658
+ };
659
+
660
+ export type AiFlowGenerateResp = {
661
+ data: { steps: AiLessonStep[] };
662
+ meta: AiMeta;
663
+ };
664
+
665
+ export async function apiAiFlowGenerate(
666
+ payload: AiFlowGenerateReq
667
+ ): Promise<AiFlowGenerateResp> {
668
+ const base = getBaseUrl();
669
+ const res = await fetchWithTimeout(
670
+ `${base}/ai/courseware/flow/generate`,
671
+ {
672
+ method: "POST",
673
+ headers: { "Content-Type": "application/json" },
674
+ body: JSON.stringify(payload),
675
+ },
676
+ TEACHER_TIMEOUT_MS
677
+ );
678
+ const data = await parseJsonSafe(res);
679
+ if (!res.ok) throw new Error(errMsg(data, "AI flow generate failed"));
680
+ return data as AiFlowGenerateResp;
681
+ }
682
+
683
+ // 2.3 Regenerate Partial Flow
684
+ export type AiSimpleStep = {
685
+ id: string;
686
+ title: string;
687
+ duration: number;
688
+ };
689
+
690
+ export type AiCurrentFlow = {
691
+ lockedSteps: AiSimpleStep[];
692
+ unlockedSteps: AiSimpleStep[];
693
+ };
694
+
695
+ export type AiFlowPartialReq = {
696
+ requestId: string;
697
+ prompt: string;
698
+ currentFlow: AiCurrentFlow;
699
+ };
700
+
701
+ export type AiCopilotProposedStep = {
702
+ type: string;
703
+ title: string;
704
+ estimated_duration: number;
705
+ ai_understanding: string;
706
+ };
707
+
708
+ export type AiFlowPartialResp = {
709
+ data: {
710
+ explanation: string;
711
+ proposedSteps: AiCopilotProposedStep[];
712
+ };
713
+ meta: AiMeta;
714
+ };
715
+
716
+ export async function apiAiFlowRegeneratePartial(
717
+ payload: AiFlowPartialReq
718
+ ): Promise<AiFlowPartialResp> {
719
+ const base = getBaseUrl();
720
+ const res = await fetchWithTimeout(
721
+ `${base}/ai/courseware/flow/regenerate-partial`,
722
+ {
723
+ method: "POST",
724
+ headers: { "Content-Type": "application/json" },
725
+ body: JSON.stringify(payload),
726
+ },
727
+ TEACHER_TIMEOUT_MS
728
+ );
729
+ const data = await parseJsonSafe(res);
730
+ if (!res.ok) throw new Error(errMsg(data, "AI flow partial regenerate failed"));
731
+ return data as AiFlowPartialResp;
732
+ }
733
+
734
+ // 2.4 Generate/Polish Lesson Plan Detail
735
+ export type AiPlanDetailReq = {
736
+ requestId: string;
737
+ finalizedSteps: any[];
738
+ };
739
+
740
+ export type AiLessonSection = {
741
+ section_id: string;
742
+ type: string;
743
+ content: string;
744
+ };
745
+
746
+ export type AiPlanDetailResp = {
747
+ data: { sections: AiLessonSection[] };
748
+ meta: AiMeta;
749
+ };
750
+
751
+ export async function apiAiPlanDetailGenerate(
752
+ payload: AiPlanDetailReq
753
+ ): Promise<AiPlanDetailResp> {
754
+ const base = getBaseUrl();
755
+ const res = await fetchWithTimeout(
756
+ `${base}/ai/courseware/plan/detail/generate`,
757
+ {
758
+ method: "POST",
759
+ headers: { "Content-Type": "application/json" },
760
+ body: JSON.stringify(payload),
761
+ },
762
+ TEACHER_TIMEOUT_MS
763
+ );
764
+ const data = await parseJsonSafe(res);
765
+ if (!res.ok) throw new Error(errMsg(data, "AI plan detail generate failed"));
766
+ return data as AiPlanDetailResp;
767
+ }
768
+
769
+ // 2.5 Generate Reflection Report
770
+ export type AiTeachAnnotation = {
771
+ category: string;
772
+ selectedText?: string | null;
773
+ feedback?: string | null;
774
+ };
775
+
776
+ export type AiQuizAggregations = {
777
+ averageScore?: number | null;
778
+ lowestTopic?: string | null;
779
+ };
780
+
781
+ export type AiReflectionReq = {
782
+ requestId: string;
783
+ teachAnnotations: AiTeachAnnotation[];
784
+ quizAggregations?: AiQuizAggregations | null;
785
+ };
786
+
787
+ export type AiReflectionUnderstanding = {
788
+ status: string;
789
+ summary: string;
790
+ bulletPoints?: string[] | null;
791
+ };
792
+
793
+ export type AiReflectionEngagement = {
794
+ status: string;
795
+ summary?: string | null;
796
+ };
797
+
798
+ export type AiReflectionDifficulty = {
799
+ status: string;
800
+ challengingTopics?: any[] | null;
801
+ };
802
+
803
+ export type AiReflectionMisconception = {
804
+ status: string;
805
+ issues?: any[] | null;
806
+ };
807
+
808
+ export type AiNextLessonSuggestion = {
809
+ actionText: string;
810
+ deepLinkType?: string | null;
811
+ };
812
+
813
+ export type AiReflectionResp = {
814
+ data: {
815
+ understanding: AiReflectionUnderstanding;
816
+ engagement: AiReflectionEngagement;
817
+ difficulty: AiReflectionDifficulty;
818
+ misconceptions: AiReflectionMisconception;
819
+ nextLessonSuggestions: AiNextLessonSuggestion[];
820
+ };
821
+ meta: AiMeta;
822
+ };
823
+
824
+ export async function apiAiReflectionGenerate(
825
+ payload: AiReflectionReq
826
+ ): Promise<AiReflectionResp> {
827
+ const base = getBaseUrl();
828
+ const res = await fetchWithTimeout(
829
+ `${base}/ai/courseware/reflection/generate`,
830
+ {
831
+ method: "POST",
832
+ headers: { "Content-Type": "application/json" },
833
+ body: JSON.stringify(payload),
834
+ },
835
+ TEACHER_TIMEOUT_MS
836
+ );
837
+ const data = await parseJsonSafe(res);
838
+ if (!res.ok) throw new Error(errMsg(data, "AI reflection generate failed"));
839
+ return data as AiReflectionResp;
840
+ }
841
+
842
+ // 2.6 Generate Improvement Proposals
843
+ export type AiImprovementReq = {
844
+ requestId: string;
845
+ reflectionReports: any[];
846
+ };
847
+
848
+ export type AiImprovementProposal = {
849
+ title: string;
850
+ priority: string;
851
+ affectedWeeks?: string | null;
852
+ evidence?: string | null;
853
+ rootCause?: string | null;
854
+ proposedSolution?: string | null;
855
+ expectedImpact?: string | null;
856
+ };
857
+
858
+ export type AiImprovementResp = {
859
+ data: { proposals: AiImprovementProposal[] };
860
+ meta: AiMeta;
861
+ };
862
+
863
+ export async function apiAiImprovementGenerate(
864
+ payload: AiImprovementReq
865
+ ): Promise<AiImprovementResp> {
866
+ const base = getBaseUrl();
867
+ const res = await fetchWithTimeout(
868
+ `${base}/ai/courseware/improvement/generate`,
869
+ {
870
+ method: "POST",
871
+ headers: { "Content-Type": "application/json" },
872
+ body: JSON.stringify(payload),
873
+ },
874
+ TEACHER_TIMEOUT_MS
875
+ );
876
+ const data = await parseJsonSafe(res);
877
+ if (!res.ok) throw new Error(errMsg(data, "AI improvement generate failed"));
878
+ return data as AiImprovementResp;
879
+ }