-
Notifications
You must be signed in to change notification settings - Fork 19
answers: give the driver the answer, not the worksheet (#52) #67
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,9 +14,10 @@ | |
| `cannot_sense` and into `facts`, and the car earns the right to talk | ||
| about them. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import re | ||
|
|
||
| # Default identity: the GLE. Overridden per car via build_system_prompt's | ||
| # identity arg (fed from the config's `car` block) so the same code serves | ||
| # @gle in Berlin and @eclass in Helsinki without edits (the Helsinki move). | ||
|
|
@@ -38,6 +39,51 @@ | |
| Answer in the LANGUAGE the question was asked in: Finnish gets Finnish, English gets English. Voice transcripts may be imperfect Finnish; answer the likely intent in Finnish rather than declaring the message unparseable.""" | ||
|
|
||
|
|
||
| # The headings this module writes INTO the prompt. A model that echoes its | ||
| # worksheet starts its reply with one of them, so they are also the markers | ||
| # for stripping it back out (#52: an /ask answer opened with the verbatim | ||
| # "- KNOWN FACTS: OBD is running but cable NOT U..."). Defined once so the | ||
| # prompt and the stripper can never drift apart. | ||
| FACTS_HEADING = "KNOWN FACTS" | ||
| CANNOT_HEADING = "YOU CANNOT SENSE" | ||
| RULES_HEADING = "STRICT GROUNDING RULES" | ||
| _SCAFFOLD_MARKERS = (FACTS_HEADING, CANNOT_HEADING, RULES_HEADING) | ||
|
|
||
| _THINK_BLOCK = re.compile(r"<(think|thinking|reasoning)>.*?</\1>", | ||
| re.IGNORECASE | re.DOTALL) | ||
| _BULLET = re.compile(r"^\s*(?:[-*\u2022]|\d+[.)])\s+") | ||
|
|
||
|
|
||
| def strip_scaffold(text: str) -> str: | ||
| """Give the driver the answer, not the worksheet. | ||
|
|
||
| Removes any reasoning block, then drops LEADING lines that are the | ||
| prompt's own scaffold plus the fact bullets trailing them. Conservative | ||
| on purpose: it only strips from the START, it matches the headings in | ||
| their prompt casing so ordinary prose cannot trip it, and if stripping | ||
| would leave nothing it returns the original. A scaffolded answer is bad; | ||
| an empty one is worse. | ||
| """ | ||
| if not text: | ||
| return text | ||
| cleaned = _THINK_BLOCK.sub("", text).strip() | ||
| lines = cleaned.splitlines() | ||
| i, dropping = 0, False | ||
| while i < len(lines): | ||
| bare = _BULLET.sub("", lines[i]).strip().lstrip("#").strip() | ||
| if any(bare.startswith(m) or bare[:40].find(m) >= 0 | ||
| for m in _SCAFFOLD_MARKERS): | ||
| dropping = True | ||
| i += 1 | ||
| continue | ||
| if dropping and (not bare or _BULLET.match(lines[i])): | ||
| i += 1 # the fact bullets that follow a heading | ||
|
Comment on lines
+79
to
+80
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If the model echoes the complete facts section produced by Useful? React with 👍 / 👎. |
||
| continue | ||
| break | ||
| out = "\n".join(lines[i:]).strip() | ||
| return out or cleaned or text | ||
|
|
||
|
|
||
| def build_system_prompt( | ||
| facts: dict[str, str] | None = None, | ||
| cannot_sense: list[str] | None = None, | ||
|
|
@@ -52,7 +98,8 @@ def build_system_prompt( | |
| "a Raspberry Pi 5 named Vadelma running a language model fully offline, no internet") | ||
|
|
||
| rules = RULES.format(identity=identity or DEFAULT_IDENTITY) | ||
| lines = [rules, "", "KNOWN FACTS (the only current state you may assert):"] | ||
| lines = [rules, "", | ||
| f"{FACTS_HEADING} (the only current state you may assert):"] | ||
| # Car state first, the Pi's own vitals last: models lead with whatever | ||
| # is listed first, and a status answer that opens with CPU fans instead | ||
| # of fuel and tyres flattens the whole point of a car that talks | ||
|
|
@@ -70,7 +117,7 @@ def _is_computer(key): | |
| lines.append(f"- {k}: {v}") | ||
|
|
||
| if cannot_sense: | ||
| lines += ["", "YOU CANNOT SENSE THESE AT ALL RIGHT NOW (say so if asked):"] | ||
| lines += ["", f"{CANNOT_HEADING} THESE AT ALL RIGHT NOW (say so if asked):"] | ||
| lines += [f"- {item}" for item in cannot_sense] | ||
|
|
||
| if manual_excerpts: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| """#52: /ask answers began with the prompt's own worksheet. | ||
|
|
||
| Observed on VTA 14 Sep: the reply opened with the verbatim | ||
| "- KNOWN FACTS: OBD is running but cable NOT U..." before the actual answer. | ||
| The driver should get the answer, not the scaffold. | ||
| """ | ||
| import os | ||
| import sys | ||
| import unittest | ||
|
|
||
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | ||
|
|
||
| from carwatch.grounding import ( # noqa: E402 | ||
| CANNOT_HEADING, FACTS_HEADING, build_system_prompt, strip_scaffold, | ||
| ) | ||
|
|
||
|
|
||
| class ScaffoldLeak(unittest.TestCase): | ||
|
|
||
| def test_the_reported_leak_is_removed(self): | ||
| out = strip_scaffold( | ||
| "- KNOWN FACTS: OBD is running but cable NOT UP\n" | ||
| "- fuel: 58%\n\nYour tyres look fine, Petrus.") | ||
| self.assertEqual(out, "Your tyres look fine, Petrus.") | ||
|
|
||
| def test_a_real_echoed_heading_and_its_bullets_go(self): | ||
| # Build the scaffold from the prompt itself, so this test tracks the | ||
| # real wording rather than a copy that can drift. | ||
| p = build_system_prompt(facts={"fuel": "58%", "tyres": "2.4 bar"}) | ||
| lines = p.splitlines() | ||
| start = next(i for i, l in enumerate(lines) if l.startswith(FACTS_HEADING)) | ||
| echoed = "\n".join(lines[start:start + 4]) + "\n\nHalf a tank, Petrus." | ||
| self.assertEqual(strip_scaffold(echoed), "Half a tank, Petrus.") | ||
|
|
||
| def test_reasoning_blocks_are_removed(self): | ||
| self.assertEqual( | ||
| strip_scaffold("<think>weighing it up</think>Your tyres look fine."), | ||
| "Your tyres look fine.") | ||
|
|
||
| def test_ordinary_prose_is_untouched(self): | ||
| for s in ("I cannot sense that yet.", | ||
| "I only state what appears in KNOWN FACTS, and that is half a tank.", | ||
| "Your coolant is 90 degrees."): | ||
| self.assertEqual(strip_scaffold(s), s) | ||
|
|
||
| def test_never_returns_empty(self): | ||
| # Over-stripping must not silence the car. A scaffolded answer is bad; | ||
| # no answer at all is worse. | ||
| only = "- " + FACTS_HEADING + ": only this" | ||
| self.assertTrue(strip_scaffold(only)) | ||
| self.assertEqual(strip_scaffold(""), "") | ||
|
|
||
| def test_prompt_and_stripper_share_the_headings(self): | ||
| # If someone reworded the prompt, the stripper must move with it. | ||
| p = build_system_prompt(facts={"fuel": "58%"}, | ||
| cannot_sense=["outside temperature"]) | ||
| self.assertIn(FACTS_HEADING, p) | ||
| self.assertIn(CANNOT_HEADING, p) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a multiline answer begins with ordinary prose such as
According to my KNOWN FACTS, fuel is at 58%., the substring search within the first 40 characters treats that sentence as scaffold and deletes it, leaving only the subsequent lines. The single-line prose test passes only because the empty-output fallback restores the original; require the marker to occupy the heading position rather than appearing anywhere near the start.Useful? React with 👍 / 👎.