nbintang
initialize projects
62516b8
Raw
History Blame Contribute Delete
6.83 kB
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