Repository navigation
fix(scorers): detect the derivatives of a toxicity keyword - #90
KunyangZhang wants to merge 3 commits into
Conversation
`KeywordToxicityScorer` still missed the noun and adjective forms of its own keywords, so "killer", "murderer", "hateful", "stupidity" and the other forms listed in wandb#79 were reported as `score=1.0`, `passed=True` and `assessed=True` with no keyword match at all. `_KEYWORD_DERIVATIVES` maps each canonical keyword to its bounded set of derivative forms, and `_keyword_pattern` matches them as further alternatives of that keyword. They are deliberately not added to the keyword lists: the score denominator stays at the configured 21 keywords, `details["toxic_categories"]` keeps reporting the canonical keyword, and a canonical keyword still counts once per category per response. Doubling now repeats the keyword's own final consonant instead of accepting any character in `[bdglmnprt]`, so "stabbed" and "stabbing" stay hits while the synthetic "killbed", "killring" and "bombted" no longer count. Closes wandb#79
The first commit on this branch rewrote `programmatic.py` from a local checkout that predated wandb#74, so it silently restored the old behaviour where an unsupported `expected_format` returned `score=1.0`, `passed=True` and `assessed=True`. CI caught it: six cases in `tests/test_output_format_scorer.py` failed. This rebuilds the file from current `main` and re-applies only the wandb#79 change, so the wandb#74 contract (an unsupported format is un-assessed) is intact. The keyword tests are unchanged and still fail on `main` before the fix.
knisar
left a comment
There was a problem hiding this comment.
Thanks @KunyangZhang. The explicit derivative mapping keeps the configured keyword count stable, and the current diff preserves the OutputFormatScorer correction from #74. The latest hosted checks are green.
I found one case-sensitivity regression in the new doubled-consonant logic. With extra_keywords={"custom": ["nab"]}, scoring "nabbed" gives one match, score 0.5454545454545454, and passed=False. Change only the configured keyword to "NAB" and the same response gives zero matches, score 1.0, and passed=True. "GRAB" with "grabbing" has the same problem. Both uppercase forms match on main e70df99.
The membership check in _inflections() runs before the case-insensitive regex is compiled, so uppercase final consonants are excluded from the doubling alternatives. Please normalize the consonant check while preserving the caller's original keyword spelling in reported evidence. Add public-scorer regressions for lowercase, uppercase, and mixed-case custom keywords and response text, including doubled -ed and -ing forms. Keep the synthetic non-inflection controls and unchanged score denominator.
The formula decision in #91 remains separate and does not block this scoped fix. I verified the current and main scorer results with targeted offline checks; full-suite validation should be rerun after the correction.
| doubling = "" | ||
| if keyword[-1] in _DOUBLING_CONSONANTS: | ||
| doubling = rf"|{re.escape(keyword[-1])}(?:ed|ing)" |
There was a problem hiding this comment.
[P2] Make the doubling eligibility check case-insensitive. _DOUBLING_CONSONANTS contains lowercase letters, so a configured NAB or GRAB never gets a doubled-consonant alternative. re.IGNORECASE cannot recover an alternative that was never built. This makes nabbed/grabbing pass with zero hits for uppercase custom keywords while lowercase configurations and current main detect them. Normalize this check and cover the public scorer with case variants, preserving the original keyword in evidence.
There was a problem hiding this comment.
Fixed in e41ca34. _inflections() now tests keyword[-1].lower() in _DOUBLING_CONSONANTS, so "NAB", "GRAB" and mixed spellings get the same doubled alternative as the lowercase form. Only the test is normalized - the alternative is still built from keyword[-1], so re.IGNORECASE matches the response in any casing. Evidence keeps the caller's spelling: extra_keywords={"custom": ["NAB"]} reports {"custom": ["NAB"]}, and the denominator still counts one entry per configured keyword, so one hit with 22 configured keywords still scores 0.5454545454545454, as you measured.
While adding the case coverage I found the same mistake already on main in the e-dropping branch: it used keyword.endswith("e"), so a configured "SNARE" missed "snaring" for exactly the same reason ("snared" still matched, through the plain -ed). That check is normalized in the same commit. It is pre-existing rather than something this branch introduced, so say the word if you would rather have it split out.
Regressions added for lowercase, uppercase and mixed-case custom keywords and response text across both suffix paths - nabbed/NABBED/Nabbed/naBBed, grabbing/GrAbBing, snaring/SNARING - plus score identity across the three casings of one keyword, and KILLBED/KillRing/BOMBTED as case variants of the synthetic non-inflections that must stay clear.
Pre-fix check: with the a89143d file restored, 6 of the new cases fail (76 passed); with the fix the file is 82 passed.
The doubling test added in a89143d compared the keyword's final character against the lowercase `_DOUBLING_CONSONANTS`, so an uppercase custom keyword never got a doubled alternative at all: `extra_keywords={"custom": ["NAB"]}` missed "nabbed" with zero matches and `passed=True`, while `"nab"` matched it. `re.IGNORECASE` cannot recover an alternative that was never built. The same mistake is already on `main` in the e-dropping branch, which used `keyword.endswith("e")`, so a configured `"SNARE"` missed "snaring". Both checks now normalize only the keyword's own spelling. The keyword and the evidence reported for it keep the caller's spelling, and the score denominator still counts one entry per configured keyword. Adds public-scorer regressions for lowercase, uppercase and mixed-case custom keywords and response text across the doubled -ed/-ing and e-dropping forms, plus case variants of the synthetic non-inflections that must stay clear.
|
Review-body items, all addressed in e41ca34:
|
knisar
left a comment
There was a problem hiding this comment.
The doubled-consonant and e-dropping corrections work. I checked e41ca344 integrated with main 7ee275e in a clean Python 3.11 environment: 565 passed, 15 optional skips. Pinned lint, licensing, and patch whitespace checks passed. The latest hosted checks also pass.
One case-sensitive lookup remains in the new derivative path: _KEYWORD_DERIVATIVES.get(keyword, ()). Normalizing the suffix checks does not normalize this dictionary key.
Reproduction through the public scorer:
for keyword in ("kill", "KILL", "KiLl"):
result = KeywordToxicityScorer(
extra_keywords={"custom": [keyword]}, threshold=0.5
).score("killer")
print(keyword, result.score, result.passed, result.details)kill produces 2 matches, score 0.09090909090909094, and fails. KILL and KiLl produce only the built-in match, score 0.5454545454545454, and pass. The configured keyword's casing changes the verdict. The same issue affects all 15 derivative forms; 30 checks of uppercase/title-case configured keywords fail case invariance.
Please normalize the derivative lookup key while retaining the caller's original spelling in evidence. Add public-scorer regressions for lowercase, uppercase, and mixed-case configured keywords across derivative forms, asserting identical scores, match counts, and verdicts, plus original-spelling evidence. Keep the formula and #91 separate. The related SNARE e-dropping fix can stay in this PR.
|
Hi @KunyangZhang, following up on the September 29 review. The remaining blocker is the case-sensitive derivative lookup: normalize the lookup key while preserving the configured spelling in the evidence. Please add lowercase, uppercase, and mixed-case public-scorer regressions that retain identical scores, match counts, and verdicts. Keep the scoring-formula work in #91 separate. Please share a brief status and your expected next update, or let me know if your availability has changed. Thanks for the work so far. |
What
KeywordToxicityScorermissed the noun and adjective forms of its own keywords. Each of the 15 forms listed in #79 was reported asscore=1.0,passed=True,assessed=Truewithdetails["toxic_categories"] == {}, so a response saying "the killer was identified" passed the scorer exactly the way "skills" does.Fixes #79.
Part 1: derivatives of an existing keyword
_KEYWORD_DERIVATIVESmaps each canonical keyword to a bounded, explicit set of derivative forms, and_keyword_patternmatches them as further alternatives of that keyword instead of adding entries to the keyword lists. That keeps every constraint from #79:0.5238095238095238;details["toxic_categories"]still reports the canonical keyword -killerreportskill;Part 2: doubled consonants
_inflections()now repeats only the keyword's own final consonant instead of accepting any character in[bdglmnprt], so the synthetickillbed,killringandbombtedno longer count, whilestabbed,stabbing,bombedandbombingstill do. The-inge-dropping forhateis unchanged. Part 2 is tested separately from Part 1, so a passing derivative case cannot hide a suffix regression.Regression plan
tests/test_keyword_toxicity_scorer.pygains 29 cases and modifies none of the existing ones:total_matches == 1, the exact score and the result flags - not merely that some match exists;skills,skill set,stability,stable,offshoot,oxymoron,bombardier) stay clear;GuardedModel(output_scorers=[KeywordToxicityScorer()], block_on_scorer_fail=True)blocks "The killer was identified."22 of the 29 new tests fail on
main(e70df991) - verified by restoringrai_toolkit/scorers/programmatic.pyfrommain, keeping the new test file, and rerunning it:22 failed, 43 passed.tests/test_output_format_scorer.pypasses in the same run, so the pre-fix failures come only from this change.The full suite is green on CI (all nine checks). Locally the only failure is
tests/test_version.py::test_distribution_version_matches_public_import, which comparesrai_toolkit.__version__against the installed distribution metadata - this environment has an editable install built from an older wheel (0.2.0) while the source is0.3.0, so it fails for reasons unrelated to this change.One correction in this branch
The first commit rebuilt
programmatic.pyfrom a local checkout that predated #74, so it silently restored the old behaviour where an unsupportedexpected_formatreturnedscore=1.0,passed=True,assessed=True. CI caught it on six cases intests/test_output_format_scorer.py; commita89143drebuilds the file from currentmainand re-applies only the #79 change, and the #74 contract is verified intact.Scope
No configured keyword list changes, and the scoring formula is untouched. The formula question knisar raised on #73 - adding non-matching
extra_keywordsdilutes the score of a real hit - is filed separately as #91 with the reproduction and the contract options, so the formula can get an explicit decision. This change does not worsen it: the denominator is unchanged, and the alias forms do not enter it.Notes:
reuse==6.2.0is not installable in this offline environment, soreuse --no-multiprocessing lintwas not run locally; the test file keeps the SPDX header it already had. Aruff0.16.7 is used locally rather than the 0.16.6 pinned in CI, on the CI's ownE9,F63,F7,F82selection.This contribution was prepared with AI assistance, as the contributing guide asks contributors to disclose.