Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import logging | |
| from typing import Any | |
| from src.agent.runtime_helpers.errors import LkpdValidationError | |
| from src.agent.types import ( | |
| EssayInsertArgs, | |
| GenerateType, | |
| LkpdGeneratedPayload, | |
| MaterialGeneratedPayload, | |
| McqInsertArgs, | |
| SummaryInsertArgs, | |
| ) | |
| def build_mcp_insert_plan( | |
| *, | |
| job_id: str, | |
| user_id: str, | |
| document_id: str, | |
| payload: MaterialGeneratedPayload, | |
| requested_types: list[GenerateType], | |
| ) -> tuple[list[tuple[str, dict[str, Any]]], list[str]]: | |
| plans: list[tuple[str, dict[str, Any]]] = [] | |
| warnings: list[str] = [] | |
| selected = set(requested_types) | |
| if "mcq" in selected: | |
| if payload.mcq_quiz is None: | |
| warnings.append("mcp_insert_skipped:mcq_quiz_missing") | |
| else: | |
| mcq_args = McqInsertArgs( | |
| job_id=job_id, | |
| user_id=user_id, | |
| document_id=document_id, | |
| mcq_quiz=payload.mcq_quiz, | |
| ) | |
| plans.append( | |
| ( | |
| "insert_mcq", | |
| mcq_args.model_dump(mode="json"), | |
| ) | |
| ) | |
| if "essay" in selected: | |
| if payload.essay_quiz is None: | |
| warnings.append("mcp_insert_skipped:essay_quiz_missing") | |
| else: | |
| essay_args = EssayInsertArgs( | |
| job_id=job_id, | |
| user_id=user_id, | |
| document_id=document_id, | |
| essay_quiz=payload.essay_quiz, | |
| ) | |
| plans.append( | |
| ( | |
| "insert_essay", | |
| essay_args.model_dump(mode="json"), | |
| ) | |
| ) | |
| if "summary" in selected: | |
| if payload.summary is None: | |
| warnings.append("mcp_insert_skipped:summary_missing") | |
| else: | |
| summary_args = SummaryInsertArgs( | |
| job_id=job_id, | |
| user_id=user_id, | |
| document_id=document_id, | |
| summary=payload.summary, | |
| ) | |
| plans.append( | |
| ( | |
| "insert_summary", | |
| summary_args.model_dump(mode="json"), | |
| ) | |
| ) | |
| return plans, warnings | |
| def enforce_generation_contract( | |
| payload: MaterialGeneratedPayload, | |
| *, | |
| generate_types: list[GenerateType], | |
| mcq_count: int, | |
| essay_count: int, | |
| summary_max_words: int, | |
| warnings: list[str], | |
| logger: logging.Logger, | |
| ) -> MaterialGeneratedPayload: | |
| selected = set(generate_types) | |
| if "mcq" in selected: | |
| if payload.mcq_quiz is None: | |
| logger.warning("model_output_validation_failed type=mcq reason=missing") | |
| warnings.append("model_output_validation_failed:mcq_missing") | |
| else: | |
| mcq_questions = payload.mcq_quiz.questions | |
| if len(mcq_questions) < mcq_count: | |
| logger.warning( | |
| "model_output_validation_failed type=mcq reason=too_few got=%s expected=%s", | |
| len(mcq_questions), | |
| mcq_count, | |
| ) | |
| warnings.append( | |
| f"model_output_validation_failed:mcq_count_lt_requested:{len(mcq_questions)}<{mcq_count}" | |
| ) | |
| if len(mcq_questions) > mcq_count: | |
| warnings.append( | |
| f"MCQ questions trimmed from {len(mcq_questions)} to {mcq_count}." | |
| ) | |
| mcq_questions = mcq_questions[:mcq_count] | |
| payload.mcq_quiz.questions = mcq_questions | |
| elif payload.mcq_quiz is not None: | |
| warnings.append("Model returned mcq_quiz even though it was not requested.") | |
| payload.mcq_quiz = None | |
| if "essay" in selected: | |
| if payload.essay_quiz is None: | |
| logger.warning("model_output_validation_failed type=essay reason=missing") | |
| warnings.append("model_output_validation_failed:essay_missing") | |
| else: | |
| essay_questions = payload.essay_quiz.questions | |
| if len(essay_questions) < essay_count: | |
| logger.warning( | |
| "model_output_validation_failed type=essay reason=too_few got=%s expected=%s", | |
| len(essay_questions), | |
| essay_count, | |
| ) | |
| warnings.append( | |
| f"model_output_validation_failed:essay_count_lt_requested:{len(essay_questions)}<{essay_count}" | |
| ) | |
| if len(essay_questions) > essay_count: | |
| warnings.append( | |
| f"Essay questions trimmed from {len(essay_questions)} to {essay_count}." | |
| ) | |
| essay_questions = essay_questions[:essay_count] | |
| payload.essay_quiz.questions = essay_questions | |
| elif payload.essay_quiz is not None: | |
| warnings.append("Model returned essay_quiz even though it was not requested.") | |
| payload.essay_quiz = None | |
| if "summary" in selected: | |
| if payload.summary is None: | |
| logger.warning("model_output_validation_failed type=summary reason=missing") | |
| warnings.append("model_output_validation_failed:summary_missing") | |
| else: | |
| overview_words = payload.summary.overview.split() | |
| if len(overview_words) > summary_max_words: | |
| warnings.append( | |
| f"Summary overview trimmed from {len(overview_words)} to {summary_max_words} words." | |
| ) | |
| payload.summary.overview = " ".join( | |
| overview_words[:summary_max_words] | |
| ) | |
| elif payload.summary is not None: | |
| warnings.append("Model returned summary even though it was not requested.") | |
| payload.summary = None | |
| return payload | |
| def enforce_lkpd_contract( | |
| payload: LkpdGeneratedPayload, | |
| *, | |
| activity_count: int, | |
| warnings: list[str], | |
| ) -> LkpdGeneratedPayload: | |
| lkpd = payload.lkpd | |
| if not lkpd.learning_objectives: | |
| raise LkpdValidationError("LKPD must contain learning objectives.") | |
| if not lkpd.instructions: | |
| raise LkpdValidationError("LKPD must contain instructions.") | |
| if not lkpd.assessment_rubric: | |
| raise LkpdValidationError("LKPD must contain assessment rubric.") | |
| activities = lkpd.activities | |
| if len(activities) < activity_count: | |
| raise LkpdValidationError( | |
| f"Model generated too few LKPD activities ({len(activities)} < {activity_count})." | |
| ) | |
| if len(activities) > activity_count: | |
| warnings.append( | |
| f"LKPD activities trimmed from {len(activities)} to {activity_count}." | |
| ) | |
| activities = activities[:activity_count] | |
| for idx, activity in enumerate(activities, start=1): | |
| activity.activity_no = idx | |
| lkpd.activities = activities | |
| return payload | |