claudqunwang commited on
Commit
cfa2cde
·
1 Parent(s): 98148dc

Enforce proposed_draft keys for syllabus partial

Browse files
api/routes_courseware_ai.py CHANGED
@@ -210,7 +210,9 @@ class SyllabusLockedSections(BaseModel):
210
 
211
 
212
  class SyllabusUnlockedSections(BaseModel):
213
- syllabus: List[Dict[str, Any]] = Field(default_factory=list)
 
 
214
 
215
 
216
  class CurrentSyllabus(BaseModel):
@@ -226,7 +228,9 @@ class SyllabusPartialRequest(BaseModel):
226
 
227
 
228
  class SyllabusPartialDraft(BaseModel):
229
- syllabus: List[WeekSyllabus]
 
 
230
 
231
 
232
  class SyllabusPartialData(BaseModel):
@@ -241,17 +245,19 @@ class SyllabusPartialResponse(BaseModel):
241
 
242
  @router.post("/syllabus/regenerate-partial", response_model=SyllabusPartialResponse)
243
  def regenerate_partial_syllabus(req: SyllabusPartialRequest):
244
- """Phase 1 (Plan):在锁定部分大纲后,根据 Prompt 重写未锁定部分(主要是 syllabus 数组)。"""
245
  locked_sections = req.current_syllabus.locked_sections.model_dump()
246
  unlocked_sections = req.current_syllabus.unlocked_sections.model_dump()
247
  ctx = req.context
248
 
 
249
  sys_prompt = (
250
  "You are an AI copilot for course syllabus editing. "
251
  "Given lockedSections (must NOT be changed), unlockedSections, a user prompt, "
252
  "and the overall course context, propose a revised draft ONLY for unlockedSections. "
253
  "Output ONLY JSON with keys 'explanation' and 'proposed_draft'. "
254
- "proposed_draft MUST have the same top-level keys as unlockedSections (e.g., 'syllabus')."
 
255
  )
256
  user_prompt = f"""
257
  User prompt:
@@ -274,14 +280,12 @@ Return JSON:
274
  {{
275
  "explanation": "short natural language explanation for why you changed the unlocked sections",
276
  "proposed_draft": {{
277
- "syllabus": [
278
- {{
279
- "week_number": 1,
280
- "title": "string",
281
- "learning_objectives": ["string", "..."],
282
- "topics": ["string", "..."]
283
- }}
284
- ]
285
  }}
286
  }}
287
  """
@@ -294,11 +298,27 @@ Return JSON:
294
  status_code=422,
295
  detail="INVALID_GENERATION: 'explanation' or 'proposed_draft' missing in JSON.",
296
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
 
298
  return SyllabusPartialResponse(
299
  data=SyllabusPartialData(
300
  explanation=data_dict["explanation"],
301
- proposed_draft=SyllabusPartialDraft(**data_dict["proposed_draft"]),
302
  ),
303
  meta=meta,
304
  )
 
210
 
211
 
212
  class SyllabusUnlockedSections(BaseModel):
213
+ """允许前端提交多个待重构顶层区块(动态键)。"""
214
+
215
+ model_config = {"extra": "allow"}
216
 
217
 
218
  class CurrentSyllabus(BaseModel):
 
228
 
229
 
230
  class SyllabusPartialDraft(BaseModel):
231
+ """proposed_draft 顶层键必须与 unlocked_sections 完全一致。"""
232
+
233
+ model_config = {"extra": "allow"}
234
 
235
 
236
  class SyllabusPartialData(BaseModel):
 
245
 
246
  @router.post("/syllabus/regenerate-partial", response_model=SyllabusPartialResponse)
247
  def regenerate_partial_syllabus(req: SyllabusPartialRequest):
248
+ """Phase 1 (Plan):在锁定部分大纲后,根据 Prompt 重写未锁定部分(支持多个顶层区块)。"""
249
  locked_sections = req.current_syllabus.locked_sections.model_dump()
250
  unlocked_sections = req.current_syllabus.unlocked_sections.model_dump()
251
  ctx = req.context
252
 
253
+ expected_keys = set(unlocked_sections.keys())
254
  sys_prompt = (
255
  "You are an AI copilot for course syllabus editing. "
256
  "Given lockedSections (must NOT be changed), unlockedSections, a user prompt, "
257
  "and the overall course context, propose a revised draft ONLY for unlockedSections. "
258
  "Output ONLY JSON with keys 'explanation' and 'proposed_draft'. "
259
+ "proposed_draft MUST have exactly the same top-level key set as unlockedSections "
260
+ "(same names, same level, snake_case). Do not add extra keys and do not omit any key."
261
  )
262
  user_prompt = f"""
263
  User prompt:
 
280
  {{
281
  "explanation": "short natural language explanation for why you changed the unlocked sections",
282
  "proposed_draft": {{
283
+ "syllabus": [{{"week_number": 1, "title": "string", "learning_objectives": ["..."], "topics": ["..."]}}],
284
+ "assessment": "string",
285
+ "course_overview": "string",
286
+ "teaching_approach": "string",
287
+ "workload_expectation": "string",
288
+ "academic_integrity_policy": "string"
 
 
289
  }}
290
  }}
291
  """
 
298
  status_code=422,
299
  detail="INVALID_GENERATION: 'explanation' or 'proposed_draft' missing in JSON.",
300
  )
301
+ if not isinstance(data_dict["proposed_draft"], dict):
302
+ raise HTTPException(
303
+ status_code=422,
304
+ detail="INVALID_GENERATION: 'proposed_draft' must be a JSON object.",
305
+ )
306
+
307
+ actual_keys = set(data_dict["proposed_draft"].keys())
308
+ if actual_keys != expected_keys:
309
+ raise HTTPException(
310
+ status_code=422,
311
+ detail=(
312
+ "INVALID_GENERATION: 'proposed_draft' top-level keys must exactly match "
313
+ f"'current_syllabus.unlocked_sections'. expected={sorted(expected_keys)}, "
314
+ f"actual={sorted(actual_keys)}"
315
+ ),
316
+ )
317
 
318
  return SyllabusPartialResponse(
319
  data=SyllabusPartialData(
320
  explanation=data_dict["explanation"],
321
+ proposed_draft=SyllabusPartialDraft.model_validate(data_dict["proposed_draft"]),
322
  ),
323
  meta=meta,
324
  )
docs/AI_COURSEWARE_API_USAGE.md CHANGED
@@ -135,6 +135,8 @@ const req: AiSyllabusPartialReq = {
135
  topics: ["History of AI"],
136
  },
137
  ],
 
 
138
  },
139
  },
140
  context: {
@@ -148,7 +150,9 @@ const req: AiSyllabusPartialReq = {
148
 
149
  const resp: AiSyllabusPartialResp = await apiAiSyllabusRegeneratePartial(req);
150
  // resp.data.explanation -> Copilot 气泡说明文案
151
- // resp.data.proposed_draft.syllabus -> 用于替换未锁定 syllabus 的新大纲
 
 
152
  ```
153
 
154
  #### 3.3 生成单个模块的教学流程 `apiAiFlowGenerate`(snake_case)
 
135
  topics: ["History of AI"],
136
  },
137
  ],
138
+ assessment: "Weekly assignments + project",
139
+ course_overview: "Intro to GenAI course overview",
140
  },
141
  },
142
  context: {
 
150
 
151
  const resp: AiSyllabusPartialResp = await apiAiSyllabusRegeneratePartial(req);
152
  // resp.data.explanation -> Copilot 气泡说明文案
153
+ // 强约束:resp.data.proposed_draft 的顶层键集合必须与 req.current_syllabus.unlocked_sections 完全一致
154
+ // 例如 unlocked_sections 传了 syllabus/assessment/course_overview,
155
+ // proposed_draft 也必须同时返回这 3 个键(已改写键返回新值,未改写键返回原值,且不能多键/少键)
156
  ```
157
 
158
  #### 3.3 生成单个模块的教学流程 `apiAiFlowGenerate`(snake_case)
web/src/lib/api.ts CHANGED
@@ -655,7 +655,13 @@ export type AiSyllabusLockedSections = {
655
  };
656
 
657
  export type AiSyllabusUnlockedSections = {
658
- syllabus: Partial<AiWeekSyllabus>[];
 
 
 
 
 
 
659
  };
660
 
661
  export type AiCurrentSyllabus = {
@@ -673,9 +679,8 @@ export type AiSyllabusPartialReq = {
673
  export type AiSyllabusPartialResp = {
674
  data: {
675
  explanation: string;
676
- proposed_draft: {
677
- syllabus: AiWeekSyllabus[];
678
- };
679
  };
680
  meta: AiMeta;
681
  };
 
655
  };
656
 
657
  export type AiSyllabusUnlockedSections = {
658
+ syllabus?: Partial<AiWeekSyllabus>[];
659
+ assessment?: string | null;
660
+ course_overview?: string | null;
661
+ teaching_approach?: string | null;
662
+ workload_expectation?: string | null;
663
+ academic_integrity_policy?: string | null;
664
+ [key: string]: any;
665
  };
666
 
667
  export type AiCurrentSyllabus = {
 
679
  export type AiSyllabusPartialResp = {
680
  data: {
681
  explanation: string;
682
+ // Must have exactly the same top-level keys as current_syllabus.unlocked_sections.
683
+ proposed_draft: Record<string, any>;
 
684
  };
685
  meta: AiMeta;
686
  };