Spaces:
Sleeping
Sleeping
fix(graders): harden against keyword stuffing attacks
Browse files- Keywords must appear in sentences with ≥6 words (not bare mentions)
- Add keyword density penalty (>15% density triggers deduction)
- Add degenerate content check (low unique word ratio)
- Extract helper functions: _split_sentences, _keyword_in_context, _keyword_density_penalty
- domains/aerospace/graders.py +48 -3
domains/aerospace/graders.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""Aerospace domain graders with deterministic scoring."""
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
@@ -12,9 +12,40 @@ from rag_master.rewards import _SCORE_MAX, _SCORE_MIN, clamp_score
|
|
| 12 |
|
| 13 |
logger = get_logger(__name__)
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
class KeywordCoverageGrader(BaseGrader):
|
| 17 |
-
"""Grades based on keyword coverage
|
| 18 |
|
| 19 |
def __init__(
|
| 20 |
self,
|
|
@@ -31,14 +62,28 @@ class KeywordCoverageGrader(BaseGrader):
|
|
| 31 |
return _SCORE_MIN
|
| 32 |
|
| 33 |
total_score = 0.0
|
|
|
|
| 34 |
for category, keywords in self._required_keywords.items():
|
| 35 |
weight = self._rubric_weights.get(category, 0.0)
|
| 36 |
if not keywords:
|
| 37 |
continue
|
| 38 |
-
|
|
|
|
|
|
|
| 39 |
coverage = hits / len(keywords)
|
| 40 |
total_score += weight * coverage
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
# Process quality bonus
|
| 43 |
process_bonus = self._evaluate_process(trajectory)
|
| 44 |
total_score = total_score * 0.8 + process_bonus * 0.2
|
|
|
|
| 1 |
+
"""Aerospace domain graders with deterministic scoring and anti-hack measures."""
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
| 12 |
|
| 13 |
logger = get_logger(__name__)
|
| 14 |
|
| 15 |
+
# Minimum words in a sentence for a keyword match to count
|
| 16 |
+
_MIN_SENTENCE_WORDS = 6
|
| 17 |
+
# Maximum keyword density before penalty kicks in
|
| 18 |
+
_MAX_KEYWORD_DENSITY = 0.15
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _split_sentences(text: str) -> List[str]:
|
| 22 |
+
"""Split text into sentences."""
|
| 23 |
+
return [s.strip() for s in re.split(r'[.!?]+', text) if s.strip()]
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _keyword_in_context(keyword: str, text: str) -> bool:
|
| 27 |
+
"""Check if keyword appears in a sentence with at least _MIN_SENTENCE_WORDS words."""
|
| 28 |
+
sentences = _split_sentences(text)
|
| 29 |
+
kw_lower = keyword.lower()
|
| 30 |
+
for sentence in sentences:
|
| 31 |
+
if kw_lower in sentence.lower() and len(sentence.split()) >= _MIN_SENTENCE_WORDS:
|
| 32 |
+
return True
|
| 33 |
+
return False
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def _keyword_density_penalty(answer: str, keywords_found: int) -> float:
|
| 37 |
+
"""Penalize if keyword density is suspiciously high (keyword stuffing)."""
|
| 38 |
+
word_count = len(answer.split())
|
| 39 |
+
if word_count == 0:
|
| 40 |
+
return 0.0
|
| 41 |
+
density = keywords_found / word_count
|
| 42 |
+
if density > _MAX_KEYWORD_DENSITY:
|
| 43 |
+
return min((density - _MAX_KEYWORD_DENSITY) * 5.0, 0.5)
|
| 44 |
+
return 0.0
|
| 45 |
+
|
| 46 |
|
| 47 |
class KeywordCoverageGrader(BaseGrader):
|
| 48 |
+
"""Grades based on keyword coverage with anti-stuffing measures."""
|
| 49 |
|
| 50 |
def __init__(
|
| 51 |
self,
|
|
|
|
| 62 |
return _SCORE_MIN
|
| 63 |
|
| 64 |
total_score = 0.0
|
| 65 |
+
total_keywords_found = 0
|
| 66 |
for category, keywords in self._required_keywords.items():
|
| 67 |
weight = self._rubric_weights.get(category, 0.0)
|
| 68 |
if not keywords:
|
| 69 |
continue
|
| 70 |
+
# Keywords must appear in sentences with sufficient context
|
| 71 |
+
hits = sum(1 for kw in keywords if _keyword_in_context(kw, answer))
|
| 72 |
+
total_keywords_found += hits
|
| 73 |
coverage = hits / len(keywords)
|
| 74 |
total_score += weight * coverage
|
| 75 |
|
| 76 |
+
# Apply keyword density penalty (anti-stuffing)
|
| 77 |
+
density_penalty = _keyword_density_penalty(answer, total_keywords_found)
|
| 78 |
+
total_score = max(0.0, total_score - density_penalty)
|
| 79 |
+
|
| 80 |
+
# Degenerate content check: low unique word ratio
|
| 81 |
+
words = answer.split()
|
| 82 |
+
if len(words) > 10:
|
| 83 |
+
unique_ratio = len(set(words)) / len(words)
|
| 84 |
+
if unique_ratio < 0.3:
|
| 85 |
+
total_score *= 0.3
|
| 86 |
+
|
| 87 |
# Process quality bonus
|
| 88 |
process_bonus = self._evaluate_process(trajectory)
|
| 89 |
total_score = total_score * 0.8 + process_bonus * 0.2
|