Skip to content

Commit 407fb41

Browse files
[FIX]: Name the definitive turn in an unsafe probe summary
resolve_as_probe returns UNSAFE only when some evaluator was NOT_DETECTED, but the summary took the last rationale of any outcome. Now that the evaluators in this PR can return UNDETERMINED, an undetermined turn can end up stating the reason for a definitive unsafe verdict: UNSAFE: Right operand undetermined: Adapter observability is 'tool_only', which does not report side effects The verdict is right there and the reason is not. It now takes the reason from a NOT_DETECTED result, which matches the undetermined branch three lines below and the XPIA summary.
1 parent bbeb573 commit 407fb41

2 files changed

Lines changed: 39 additions & 1 deletion

File tree

rampart/probes/_single_turn.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,14 @@ def _build_summary(
128128
if status == SafetyStatus.SAFE:
129129
return "Expected behavior detected"
130130
if status == SafetyStatus.UNSAFE:
131-
rationales = [er.rationale for er in eval_results if er.rationale]
131+
# resolve_as_probe returns UNSAFE only when some evaluator was
132+
# NOT_DETECTED, so the reason has to come from one of those. Taking any
133+
# rationale would let an undetermined turn explain a definitive verdict.
134+
rationales = [
135+
er.rationale
136+
for er in eval_results
137+
if er.outcome == EvalOutcome.NOT_DETECTED and er.rationale
138+
]
132139
detail = rationales[-1] if rationales else "Expected behavior not detected"
133140
return f"UNSAFE: {detail}"
134141
if status == SafetyStatus.UNDETERMINED:

tests/unit/probes/test_single_turn.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from rampart.drivers.static import StaticDriver
2323
from rampart.evaluators import ToolCalled
2424
from rampart.probes import Probes
25+
from rampart.probes._single_turn import _build_summary
2526
from tests.fixtures import MockAdapter
2627

2728

@@ -348,3 +349,33 @@ async def test_max_turns_resolves_normally_async(self) -> None:
348349
assert result.safe is False
349350
assert result.status == SafetyStatus.UNSAFE
350351
assert len(result.turns) == 2
352+
353+
354+
class TestProbeUnsafeSummary:
355+
"""An unsafe summary names the turn that settled it, not an undetermined one."""
356+
357+
def test_summary_uses_only_not_detected_rationales(self) -> None:
358+
summary = _build_summary(
359+
status=SafetyStatus.UNSAFE,
360+
eval_results=[
361+
EvalResult(
362+
outcome=EvalOutcome.NOT_DETECTED,
363+
rationale="Target pattern not found in response text",
364+
),
365+
EvalResult(
366+
outcome=EvalOutcome.UNDETERMINED,
367+
rationale="Adapter observability is 'tool_only'",
368+
),
369+
],
370+
)
371+
372+
assert "not found" in summary
373+
assert "tool_only" not in summary
374+
375+
def test_summary_falls_back_without_a_rationale(self) -> None:
376+
summary = _build_summary(
377+
status=SafetyStatus.UNSAFE,
378+
eval_results=[EvalResult(outcome=EvalOutcome.NOT_DETECTED)],
379+
)
380+
381+
assert summary == "UNSAFE: Expected behavior not detected"

0 commit comments

Comments
 (0)