Spaces:
Sleeping
Sleeping
Commit ·
cfa2cde
1
Parent(s): 98148dc
Enforce proposed_draft keys for syllabus partial
Browse files- api/routes_courseware_ai.py +33 -13
- docs/AI_COURSEWARE_API_USAGE.md +5 -1
- web/src/lib/api.ts +9 -4
api/routes_courseware_ai.py
CHANGED
|
@@ -210,7 +210,9 @@ class SyllabusLockedSections(BaseModel):
|
|
| 210 |
|
| 211 |
|
| 212 |
class SyllabusUnlockedSections(BaseModel):
|
| 213 |
-
|
|
|
|
|
|
|
| 214 |
|
| 215 |
|
| 216 |
class CurrentSyllabus(BaseModel):
|
|
@@ -226,7 +228,9 @@ class SyllabusPartialRequest(BaseModel):
|
|
| 226 |
|
| 227 |
|
| 228 |
class SyllabusPartialDraft(BaseModel):
|
| 229 |
-
|
|
|
|
|
|
|
| 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 重写未锁定部分(
|
| 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
|
|
|
|
| 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 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 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(
|
| 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
|
|
|
|
|
|
|
| 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 |
-
|
| 677 |
-
|
| 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 |
};
|