diff --git a/src/flask/brew_advice.py b/src/flask/brew_advice.py index 31ce02b..0dce4b2 100644 --- a/src/flask/brew_advice.py +++ b/src/flask/brew_advice.py @@ -25,6 +25,7 @@ from __future__ import annotations import json import logging import os +import re import urllib.error import urllib.request @@ -271,12 +272,38 @@ def _strip_reasoning(text: str) -> str: working did leak in, the brewer sees a slightly rambling reply rather than no advice at all. """ - index = text.find(_ADVICE_MARKER) - if index == -1: + index = _find_marker(text) + if index is None: return text.strip() - advice = text[index + len(_ADVICE_MARKER):].strip() - return advice or "" + advice = text[index:].strip() + + # The model may wrap the marker in markdown or a code block. Strip + # the leftover formatting so the advice reads as prose. + advice = re.sub(r"^\*\*|\*\*$", "", advice).strip() + if advice.startswith("```"): + advice = advice[3:] + if advice.endswith("```"): + advice = advice[:-3] + if advice.startswith("##"): + advice = advice.lstrip("#").strip() + + return advice.strip() or "" + + +def _find_marker(text: str) -> int | None: + """Where the advice starts, or ``None`` if there is no marker. + + The marker is matched case-insensitively and with the markdown + decoration the model might put around it - ``**ADVICE:**``, + ``## ADVICE:`` - because the instruction asks for "ADVICE:" but the + model formats it its own way often enough to matter. + """ + match = re.search(r"^[\s#>\*]*advice\s*:[\s\*>]*", text, + re.IGNORECASE | re.MULTILINE) + if match: + return match.end() + return None def generate_advice(run: dict, reading: dict, history: list[dict], *, diff --git a/tests/test_brew_advice.py b/tests/test_brew_advice.py index 2a8e52e..12dbf57 100644 --- a/tests/test_brew_advice.py +++ b/tests/test_brew_advice.py @@ -274,6 +274,28 @@ def test_a_reply_with_the_marker_is_kept_whole(): assert brew_advice._strip_reasoning(reply) == advice +def test_the_marker_is_found_however_the_model_formats_it(): + """The model formats the marker its own way often enough to matter. + + The instruction asks for "ADVICE:", but the model writes + "**ADVICE:**", "## ADVICE:" or puts it in a code block. Each of + those has to extract the advice cleanly, with no leftover markdown. + """ + advice = "Warm it to 20C and wait." + + for reply in ( + f"ADVICE:\n{advice}", + f"**ADVICE:**\n{advice}", + f"## ADVICE:\n{advice}", + f"```\nADVICE:\n{advice}\n```", + f"**ADVICE:**\n```\n{advice}\n```", + f"advice:\n{advice}", + ): + assert brew_advice._strip_reasoning(reply) == advice, ( + f"the marker was not extracted cleanly from {reply!r}" + ) + + def test_a_reply_without_the_marker_is_used_as_it_stands(): """The safe direction when the model does not use the marker.