feat(major2): prior_feedback is the third composable block of the hypothesis prompt [skip-docs]
Ordre 20260904T173146Z-8102814273-from-portfolio-optimiser, steg 2 av 10. _build_messages faar prior_feedback (keyword-only, None => byte-identisk base-prompt, samme kontrakt prior_rejection og approach alt oppgir). Ekspertens ORD, ordrett - aldri forrige forslags JSON, av samme grunn prior_rejection kun baerer grunnen. Blokken beskriver noe annet enn en avvisning: en kandidat validatoren AKSEPTERTE og et menneske likevel ba om aa endre. Aa slaa dem sammen ville fortalt modellen at maskinen protesterte da en person gjorde det. Rekkefoelgen er fast: base -> approach-hode -> avvisning -> tilbakemelding. En prompt kan lovlig baere BEGGE - det er forsoeket etter en revise hvis kjoepte forsoek validatoren saa avviste: menneskets instruks STAAR til mennesket svarer neste gang, mens maskinens grunn er per forsoek (kun den nyeste, som i dag). RODT foer impl paa tre armer (TypeError: uventet keyword). Kontrollen (None => byte-identisk) er halvdelen som holder hver eksisterende kjoering, golden og nav-fixtur uroert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
ce4c15e6cb
commit
d4c8691326
2 changed files with 92 additions and 3 deletions
|
|
@ -279,6 +279,7 @@ def _build_messages(
|
||||||
prior_rejection: Rejection | None = None,
|
prior_rejection: Rejection | None = None,
|
||||||
*,
|
*,
|
||||||
approach: Approach | None = None,
|
approach: Approach | None = None,
|
||||||
|
prior_feedback: str | None = None,
|
||||||
) -> list[Message]:
|
) -> list[Message]:
|
||||||
"""Build the hypothesis prompt. When ``prior_rejection`` is set (Step 5, målbilde §5/§7),
|
"""Build the hypothesis prompt. When ``prior_rejection`` is set (Step 5, målbilde §5/§7),
|
||||||
append a revision block carrying ONLY the falsification *reason* verbatim — never the prior
|
append a revision block carrying ONLY the falsification *reason* verbatim — never the prior
|
||||||
|
|
@ -294,8 +295,18 @@ def _build_messages(
|
||||||
exactly the part the model cannot infer from the cost data. ``None`` -> the byte-identical
|
exactly the part the model cannot infer from the cost data. ``None`` -> the byte-identical
|
||||||
base prompt, so an un-commissioned run is untouched (mirrors ``prior_rejection``).
|
base prompt, so an un-commissioned run is untouched (mirrors ``prior_rejection``).
|
||||||
|
|
||||||
The two are composable: a commissioned approach that the validator rejects is refined through
|
When ``prior_feedback`` is set (MAJOR-2) a THIRD block carries a domain expert's own words
|
||||||
the SAME informed-refinement block, still bound to that approach.
|
VERBATIM — and only the words, never the previous proposal JSON, for exactly the reason
|
||||||
|
``prior_rejection`` carries only the reason. This block describes a candidate the validator
|
||||||
|
ACCEPTED and a human nonetheless asked to change, which is a different instruction from
|
||||||
|
"your numbers were refuted"; conflating the two would tell the model the machine objected
|
||||||
|
when a person did. ``None`` -> the byte-identical base prompt, like the other two.
|
||||||
|
|
||||||
|
All three are composable, and the ORDER is fixed: base -> approach head -> rejection ->
|
||||||
|
feedback. A prompt can legitimately carry a rejection AND a feedback at once — that is the
|
||||||
|
attempt after a revise whose bought attempt the validator then rejected: the human's
|
||||||
|
instruction STANDS until the human next answers, while the machine's reason is per-attempt
|
||||||
|
(only the most recent, as today).
|
||||||
"""
|
"""
|
||||||
if approach is None:
|
if approach is None:
|
||||||
head = "Propose ONE concrete cost-saving measure for this project.\n"
|
head = "Propose ONE concrete cost-saving measure for this project.\n"
|
||||||
|
|
@ -323,6 +334,13 @@ def _build_messages(
|
||||||
f"Reason: {prior_rejection.reason}\n"
|
f"Reason: {prior_rejection.reason}\n"
|
||||||
"Produce a REVISED SavingsProposal that resolves this."
|
"Produce a REVISED SavingsProposal that resolves this."
|
||||||
)
|
)
|
||||||
|
if prior_feedback is not None:
|
||||||
|
prompt += (
|
||||||
|
"\n\nA domain expert reviewed your previous proposal, which the deterministic "
|
||||||
|
"validator had accepted, and asked for a revision.\n"
|
||||||
|
f"Expert feedback: {prior_feedback}\n"
|
||||||
|
"Produce a REVISED SavingsProposal that follows this feedback."
|
||||||
|
)
|
||||||
return [Message(role="user", contents=[prompt])]
|
return [Message(role="user", contents=[prompt])]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,13 @@ import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from spikes._harness import message_texts
|
||||||
|
|
||||||
from portfolio_optimiser import proposal_review as pr
|
from portfolio_optimiser import proposal_review as pr
|
||||||
|
from portfolio_optimiser.generate import _build_messages
|
||||||
from portfolio_optimiser.ir import AffectedItem, SavingsProposal
|
from portfolio_optimiser.ir import AffectedItem, SavingsProposal
|
||||||
from portfolio_optimiser.validator import ValidatedProposal
|
from portfolio_optimiser.reference_domain import load_reference_projects
|
||||||
|
from portfolio_optimiser.validator import Rejection, ValidatedProposal
|
||||||
|
|
||||||
_REPO = Path(__file__).resolve().parents[1]
|
_REPO = Path(__file__).resolve().parents[1]
|
||||||
_BUNDLE_DIR = _REPO / "shared" / "examples" / "bygg-energi-mikro"
|
_BUNDLE_DIR = _REPO / "shared" / "examples" / "bygg-energi-mikro"
|
||||||
|
|
@ -203,3 +206,71 @@ def test_the_payload_carries_every_field_and_takes_its_key_from_the_injected_fun
|
||||||
assert seen == [reviews[0].proposal.proposal, reviews[1].proposal.proposal]
|
assert seen == [reviews[0].proposal.proposal, reviews[1].proposal.proposal]
|
||||||
# The payload is a PLAIN mapping: ``outbox`` stays MAF-free and byte-deterministic.
|
# The payload is a PLAIN mapping: ``outbox`` stays MAF-free and byte-deterministic.
|
||||||
assert json.loads(json.dumps(payload)) == payload
|
assert json.loads(json.dumps(payload)) == payload
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------------------------
|
||||||
|
# Group A1 (Step 2) — ``prior_feedback``: the third composable block of the hypothesis prompt.
|
||||||
|
# ---------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def project():
|
||||||
|
return load_reference_projects()[0] # FV42-GSV-E1
|
||||||
|
|
||||||
|
|
||||||
|
def test_prior_feedback_none_keeps_the_base_prompt_byte_identical(project) -> None:
|
||||||
|
"""CONTROL: the new block is INERT when nobody reviewed. A test that can only go green
|
||||||
|
proves nothing, and this is the half that keeps every existing run, golden and nav-fixture
|
||||||
|
untouched — the contract ``prior_rejection`` and ``approach`` already state."""
|
||||||
|
base = message_texts(_build_messages(project, "ctx"))[0]
|
||||||
|
assert base == message_texts(_build_messages(project, "ctx", prior_feedback=None))[0]
|
||||||
|
assert _FEEDBACK_SENTINEL not in base
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_experts_words_reach_the_next_prompt_verbatim(project) -> None:
|
||||||
|
"""Detach point: dropping the ``prior_feedback`` block (M3 — print-and-discard).
|
||||||
|
|
||||||
|
VERBATIM and only the text: never the previous proposal JSON. That is the ``prior_rejection``
|
||||||
|
rule, and it is the same reason — the model must address what the human said, not parrot the
|
||||||
|
candidate they were unhappy with."""
|
||||||
|
text = message_texts(
|
||||||
|
_build_messages(
|
||||||
|
project, "ctx", prior_feedback=f"Use 150000, not 200000. {_FEEDBACK_SENTINEL}"
|
||||||
|
)
|
||||||
|
)[0]
|
||||||
|
assert _FEEDBACK_SENTINEL in text
|
||||||
|
assert "asked for a revision" in text
|
||||||
|
assert "Use 150000, not 200000." in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_rejection_and_a_standing_feedback_compose_in_order(project) -> None:
|
||||||
|
"""The two blocks compose, and the ORDER is fixed: base -> approach head -> rejection ->
|
||||||
|
feedback.
|
||||||
|
|
||||||
|
This is the shape T6 measures end to end: a revise whose bought attempt the validator then
|
||||||
|
rejects leaves the attempt after it carrying BOTH — the human's instruction stands until the
|
||||||
|
human next answers, the machine's reason is per-attempt (only the most recent, as today)."""
|
||||||
|
rejection = Rejection(proposal=_proposal(), reason="claimed saving 270000 exceeds P90 121057")
|
||||||
|
text = message_texts(
|
||||||
|
_build_messages(
|
||||||
|
project, "ctx", prior_rejection=rejection, prior_feedback=_FEEDBACK_SENTINEL
|
||||||
|
)
|
||||||
|
)[0]
|
||||||
|
assert text.count(rejection.reason) == 1
|
||||||
|
assert text.count(_FEEDBACK_SENTINEL) == 1
|
||||||
|
assert text.index("REJECTED by the deterministic validator") < text.index(_FEEDBACK_SENTINEL)
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_feedback_sentinel_cannot_arrive_from_the_knowledge_base() -> None:
|
||||||
|
"""The known-positive control on every sentinel arm in this file: the marker exists NOWHERE
|
||||||
|
in the fixture bundle, so a prompt that carries it can only have got it through the expert's
|
||||||
|
feedback block. Paired with an assertion that the scan actually reads the files — a scan that
|
||||||
|
silently finds nothing makes a gate that can only go green."""
|
||||||
|
texts = [
|
||||||
|
path.read_text(encoding="utf-8")
|
||||||
|
for path in sorted(_BUNDLE_DIR.rglob("*"))
|
||||||
|
if path.is_file()
|
||||||
|
]
|
||||||
|
assert texts, "the fixture bundle must exist for this control to mean anything"
|
||||||
|
assert any("ENERGI-TOTAL-EL" in text for text in texts) # the scan CAN find a known string
|
||||||
|
assert not any(_FEEDBACK_SENTINEL in text for text in texts)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue