"""Variant instantiation prompt (D-029, REQ-3-005). The model's ONLY job is to render already-sampled slot values into a task statement — it never invents parameters (the seeded sampler is pure code) and never changes difficulty. Prompt-injection surface is bounded: the variable inputs are the skeleton text, the seeded slot values, and the template title — nothing from the learner's environment. """ from __future__ import annotations from typing import TYPE_CHECKING from ..llm.types import Message if TYPE_CHECKING: # pragma: no cover - keeps this module pure text from ..variants.templates import TaskTemplate VARIANT_SYSTEM_PROMPT = ( "You instantiate per-learner task variants for a competency-based AI school. " "You receive a task statement skeleton and ALREADY-SAMPLED slot values. " "Render the slot values into the skeleton, producing a complete, unambiguous " "task statement a learner can build against. Rules:\n" "- Use EXACTLY the given slot values; do not invent, rename, or add parameters.\n" "- Keep the engineering depth IDENTICAL across draws: slot values change the " "scenario, never the difficulty or scope.\n" "- Keep the statement in the same language and register as the skeleton.\n" "- Output STRICT JSON only: {\"statement\": \"\"}.\n" ) VARIANT_SCHEMA_HINT = '{"statement": ""}' def render_variant_prompt(template: TaskTemplate, params: dict[str, str | int]) -> list[Message]: """Messages for one seeded instantiation (D-020 defense drives the call).""" slot_lines = "\n".join(f" {{{slot.name}}} = {params[slot.name]!r}" for slot in template.slots) user = ( f"Template: {template.title} (id={template.id})\n" f"Statement skeleton:\n{template.statement_skeleton}\n\n" f"Seeded slot values (use EXACTLY these):\n{slot_lines}\n\n" "Render the complete task statement now." ) return [ Message(role="system", content=VARIANT_SYSTEM_PROMPT), Message(role="user", content=user), ]