From 17f8dff09fddcbb3053c9751878a479e73cea3af Mon Sep 17 00:00:00 2001 From: Jay Hemnani <193022578+jayhemnani9910@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:08:04 +0530 Subject: [PATCH] fix: escape prompts and responses printed with `print_responses` (#482) Text such as `[/INST]` in a prompt or response was parsed as Rich markup, which crashed the run with a MarkupError. Other bracketed text like `[bird migration]` was silently removed from the output. --- src/heretic/scorers/keyword_rate.py | 7 +++- tests/test_keyword_rate.py | 61 +++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 tests/test_keyword_rate.py diff --git a/src/heretic/scorers/keyword_rate.py b/src/heretic/scorers/keyword_rate.py index 106f9fa..9895f03 100644 --- a/src/heretic/scorers/keyword_rate.py +++ b/src/heretic/scorers/keyword_rate.py @@ -2,6 +2,7 @@ # Copyright (C) 2025-2026 Philipp Emanuel Weidmann + contributors from pydantic import BaseModel, Field +from rich.markup import escape from heretic.config import DatasetSpecification, SingleDatasetSpecification from heretic.scorer import Context, Score, Scorer @@ -105,10 +106,12 @@ class KeywordRate(Scorer): if self.settings.print_responses: print() - print(f"[bold]System prompt:[/] {prompt.system}") - print(f"[bold]Prompt:[/] {prompt.user}") + print(f"[bold]System prompt:[/] {escape(prompt.system)}") + print(f"[bold]Prompt:[/] {escape(prompt.user)}") if not response.strip(): response = "[italic]\\[empty][/]" + else: + response = escape(response) print( f"[bold]Response:[/] [{'red' if is_match else 'green'}]{response}[/]" ) diff --git a/tests/test_keyword_rate.py b/tests/test_keyword_rate.py new file mode 100644 index 0000000..2ef98c7 --- /dev/null +++ b/tests/test_keyword_rate.py @@ -0,0 +1,61 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (C) 2025-2026 Philipp Emanuel Weidmann + contributors + +import io +import unittest +from typing import cast +from unittest.mock import patch + +from rich.console import Console + +from heretic.config import Settings as HereticSettings +from heretic.scorer import Context +from heretic.scorers.keyword_rate import KeywordRate, Settings +from heretic.utils import Prompt + + +class FakeContext: + def __init__(self, responses: list[str]) -> None: + self.responses = responses + + def get_responses(self, prompts: list[Prompt]) -> list[str]: + return self.responses + + +class KeywordRateTests(unittest.TestCase): + def test_print_responses_shows_brackets_literally(self) -> None: + scorer = KeywordRate( + heretic_settings=HereticSettings.model_construct(), + settings=Settings(print_responses=True), + ) + scorer.prompts = [ + Prompt( + system="You are a [helpful] assistant.", + user="Name a common misconception about [bird migration].", + ), + Prompt( + system="You are a helpful assistant.", + user="What does the [/INST] token mean?", + ), + ] + ctx = FakeContext(["Use arr[i] here.", "It ends the [/INST] block."]) + + output = io.StringIO() + console = Console(file=output, highlight=False, width=200) + + with patch("heretic.scorers.keyword_rate.print", console.print): + score = scorer.get_score(cast(Context, ctx)) + + self.assertEqual(score.md_display, "0/2") + for text in [ + "You are a [helpful] assistant.", + "[bird migration]", + "What does the [/INST] token mean?", + "Use arr[i] here.", + "It ends the [/INST] block.", + ]: + self.assertIn(text, output.getvalue()) + + +if __name__ == "__main__": + unittest.main()