82b9de382a
Task 4-2-01: prompts/variant.py (render-only contract — the LLM never invents params;
slots change scenario, never difficulty) + variants/generator.py — seed =
sha256(template|learner|milestone) (D-029), pure-code seeded slot sampling, LLM render
via the D-020 defense with a deterministic skeleton-render fallback (never blocks on the
provider; the seed IS the provenance — no model column needed), cache-first (second call
= stored variant, zero LLM calls), deterministic task_id derivation.
Tests: distinct learners -> distinct statements; same learner -> cached, calls asserted;
params schema-valid; fallback deterministic + bounded retry (calls==2); unknown template
raises; a-5 fairness envelope BINDING — 10 seeded draws per template produce digests
inside the anchor bands (same bar testable); store roundtrip.
313 tests green; ruff clean.
---ci---
phase: 4
milestone: v0.3
status: execute
requirements: {covered: [REQ-3-005], partial: []}
---/ci---
47 lines
2.0 KiB
Python
47 lines
2.0 KiB
Python
"""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\": \"<rendered statement>\"}.\n"
|
|
)
|
|
|
|
VARIANT_SCHEMA_HINT = '{"statement": "<complete rendered task statement string>"}'
|
|
|
|
|
|
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),
|
|
]
|