feat(phase2): few-shot prompts + bounded repair loop (P5.2)
This commit is contained in:
@@ -13,12 +13,14 @@ from pf1e_simulator.nl_policy import get_args as _get_args
|
||||
from pf1e_simulator.prompt import PromptPack
|
||||
from pf1e_simulator.prompt_builder import (
|
||||
PromptBuilder,
|
||||
_render_examples,
|
||||
_supported_kinds,
|
||||
action_schema,
|
||||
render_candidates,
|
||||
)
|
||||
|
||||
_FIXTURE_PACK = Path(__file__).resolve().parent / "fixtures" / "prompt_pack"
|
||||
_FIXTURE_PACK_FS = Path(__file__).resolve().parent / "fixtures" / "prompt_pack_fs"
|
||||
_FIXTURE_VERSION = "0.1.0-test"
|
||||
_FIXTURE_PROMPT_NAME = "nl_default"
|
||||
|
||||
@@ -221,3 +223,75 @@ def test_action_schema_cross_pin_with_nl_policy_kindvalue() -> None:
|
||||
"""The kind enum in the schema must match nl_policy.KindValue exactly."""
|
||||
schema = action_schema()
|
||||
assert schema["properties"]["kind"]["enum"] == list(_get_args(KindValue))
|
||||
|
||||
|
||||
# ---- few-shot examples (P5.2) ----------------------------------------------
|
||||
|
||||
_FS_PACK = PromptPack.load(_FIXTURE_PACK_FS)
|
||||
|
||||
|
||||
def _fs_builder() -> PromptBuilder:
|
||||
return PromptBuilder(pack=_FS_PACK)
|
||||
|
||||
|
||||
def test_render_examples_block_format() -> None:
|
||||
"""Few-shot pairs render as numbered observation-to-response blocks."""
|
||||
rendered = _render_examples((_FS_PACK.examples(_FIXTURE_PROMPT_NAME)[0],))
|
||||
assert rendered == (
|
||||
"## few-shot examples\n"
|
||||
"### example 1\n"
|
||||
"observation:\n"
|
||||
'{"combatant": "goblin", "distance_ft": 10, "enemy": "orc", "round": 1}\n'
|
||||
"\n"
|
||||
"response:\n"
|
||||
'[{"kind": "attack", "target_id": "orc"}]'
|
||||
)
|
||||
|
||||
|
||||
def test_build_without_examples_unchanged() -> None:
|
||||
"""Packs without examples keep the pre-few-shot bit-for-bit section order."""
|
||||
plain = _builder().build(_obs())
|
||||
assert "## few-shot examples" not in plain
|
||||
# Template, then observation, then candidates, then schema — in order.
|
||||
sections = [
|
||||
"## observation\n",
|
||||
"## plausible actions\n",
|
||||
"## action schema\n",
|
||||
]
|
||||
positions = [plain.index(section) for section in sections]
|
||||
assert positions == sorted(positions)
|
||||
# The template comes first, unmodified.
|
||||
assert plain.startswith("You are the tactical director")
|
||||
|
||||
|
||||
def test_build_with_examples_inserts_between_template_and_observation() -> None:
|
||||
"""The few-shot section sits between the template and the observation."""
|
||||
prompt = _fs_builder().build(_obs())
|
||||
assert "## few-shot examples\n" in prompt
|
||||
assert prompt.index("## few-shot examples\n") < prompt.index("## observation\n")
|
||||
# Template still leads the prompt.
|
||||
assert prompt.startswith("You are the tactical director")
|
||||
# Observation/candidates/schema keep their relative order.
|
||||
assert prompt.index("## observation\n") < prompt.index("## plausible actions\n")
|
||||
assert prompt.index("## plausible actions\n") < prompt.index("## action schema\n")
|
||||
|
||||
|
||||
def test_build_with_examples_includes_example_content() -> None:
|
||||
"""The rendered example content is embedded verbatim (sorted keys)."""
|
||||
prompt = _fs_builder().build(_obs())
|
||||
assert '{"combatant": "goblin", "distance_ft": 10, "enemy": "orc", "round": 1}' in prompt
|
||||
assert '[{"kind": "attack", "target_id": "orc"}]' in prompt
|
||||
assert "### example 1" in prompt
|
||||
|
||||
|
||||
def test_empty_examples_array_no_section(tmp_path: Path) -> None:
|
||||
"""An empty examples array yields no few-shot section (not an error)."""
|
||||
root = tmp_path / "emptyfs"
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
(root / "version.txt").write_text("1.0.0", encoding="utf-8")
|
||||
(root / "nl_default.md").write_text("T", encoding="utf-8")
|
||||
(root / "nl_default.examples.json").write_text("[]", encoding="utf-8")
|
||||
pack = PromptPack.load(root)
|
||||
assert pack.examples("nl_default") == ()
|
||||
prompt = PromptBuilder(pack=pack).build(_obs())
|
||||
assert "## few-shot examples" not in prompt
|
||||
|
||||
Reference in New Issue
Block a user