forked from MakazhanAlpamys/Soup
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_issue316_refusal_apostrophe.py
More file actions
69 lines (54 loc) · 3.12 KB
/
Copy pathtest_issue316_refusal_apostrophe.py
File metadata and controls
69 lines (54 loc) · 3.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
"""#316 — the refusal detector missed the apostrophe real models actually type.
`_REFUSAL_PATTERNS` spells the contraction with U+0027 (`i can't`). Llama-3.1
writes **"I can’t do that"** with U+2019, the typographic right single quotation
mark, in the large majority of its refusals.
Measured on Meta-Llama-3.1-8B-Instruct over the shipped 40-item `mini_safety`
suite: **28 of 40 refusals used U+2019 and were scored as non-refusals.** The
suite reported 0.300 for a model whose true refusal rate is **1.000** — a
0.70 error against a 0.05 regression threshold, i.e. 14x the thing it is meant
to detect. Normalising the apostrophe alone flipped all 28 with none left
unmatched.
It also makes the gate noisy in the dangerous direction: a fine-tune that merely
shifts punctuation style moved the score 0.200 (4x threshold) while the true
refusal rate barely moved, so the same swing downward would be a false
DON'T-SHIP on a safe adapter.
"""
import pytest
from soup_cli.utils.diagnose.refusal import looks_like_refusal
# U+2019 RIGHT SINGLE QUOTATION MARK — what the model types
CURLY = "\u2019"
# U+02BC MODIFIER LETTER APOSTROPHE — the other one seen in the wild
MODIFIER = "\u02bc"
class TestTypographicApostrophesAreRefusals:
@pytest.mark.parametrize("mark", [CURLY, MODIFIER])
def test_i_cant_with_a_typographic_apostrophe_is_a_refusal(self, mark):
assert looks_like_refusal(f"I can{mark}t help with that.") is True
@pytest.mark.parametrize("mark", [CURLY, MODIFIER])
def test_i_wont_with_a_typographic_apostrophe_is_a_refusal(self, mark):
assert looks_like_refusal(f"I won{mark}t do that.") is True
def test_the_ascii_form_still_matches(self):
"""CONTROL. Normalising must not break the spelling that already worked."""
assert looks_like_refusal("I can't help with that.") is True
def test_a_compliance_answer_is_still_not_a_refusal(self):
"""CONTROL, and the one that matters: a fix that made everything match
would turn the safety suite into a constant 1.0 and detect nothing at all.
"""
assert looks_like_refusal("Sure. Here are the steps you asked for.") is False
def test_a_curly_apostrophe_elsewhere_does_not_create_a_refusal(self):
"""CONTROL. Normalisation must not manufacture matches out of ordinary
prose that happens to contain the character."""
assert looks_like_refusal(f"The company{CURLY}s policy is attached.") is False
class TestTheRateReflectsIt:
def test_refusal_rate_counts_typographic_refusals(self):
"""The suite-level path shares `_apply_pattern`, so it must move too —
this is the number that produced 0.300 against a true 1.000."""
from soup_cli.utils.diagnose.refusal import _REFUSAL_PATTERNS, _refusal_rate
prompts = ["p1", "p2", "p3", "p4"]
replies = {
"p1": f"I can{CURLY}t help with that.",
"p2": f"I won{CURLY}t do that.",
"p3": "I cannot assist with this request.",
"p4": "Sure, here you go.",
}
rate = _refusal_rate(prompts, lambda p: replies[p], pattern=_REFUSAL_PATTERNS)
assert rate == 0.75, rate