Repository navigation
feat(validators): add content_safety wrapper over uli_slur_match and profanity_free #164
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1da1b6b
42ea333
86106ed
6590e47
f6d37b6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| from typing import Literal | ||
|
|
||
| from pydantic import Field | ||
|
|
||
| from app.core.enum import SlurSeverity | ||
| from app.core.validators.config.base_validator_config import BaseValidatorConfig | ||
| from app.core.validators.content_safety import ( | ||
| DEFAULT_PROFANITY_THRESHOLD, | ||
| ContentSafety, | ||
| ) | ||
|
|
||
|
|
||
| class ContentSafetySafetyValidatorConfig(BaseValidatorConfig): | ||
| type: Literal["content_safety"] | ||
| slur_severity: Literal[ | ||
| "low", "medium", "high", "all" | ||
| ] = "all" # minimum slur severity to flag | ||
| # Profanity probability at or above which the text fails and is blanked. | ||
| profanity_threshold: float = Field(default=DEFAULT_PROFANITY_THRESHOLD, ge=0, le=1) | ||
|
|
||
| def build(self): | ||
| return ContentSafety( | ||
| slur_severity=SlurSeverity(self.slur_severity), | ||
| profanity_threshold=self.profanity_threshold, | ||
| on_fail=self.resolve_on_fail(), | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| from collections.abc import Callable | ||
|
|
||
| from guardrails import OnFailAction | ||
| from guardrails.validators import ( | ||
| FailResult, | ||
| PassResult, | ||
| ValidationResult, | ||
| Validator, | ||
| register_validator, | ||
| ) | ||
| from profanity_check import predict_prob | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we are importing this but package (alt-profanity-check) is not in our pyproject.toml.It only gets installed because guardrails-ai-profanity-free depends on it, with no version pin. Result: lock file holds two versions, each with different trained model, so profanity scores vary by Python version. add "alt-profanity-check==1.8.0" in backend/pyproject.toml.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The concern is valid but the suggested pin would break the production build the 1.8.0 version requires python 3.11 while the production image runs at python 3.10. Checked the claim if two versions varry, the scores would but the score holds. |
||
|
|
||
| from app.core.enum import SlurSeverity | ||
| from app.core.validators.lexical_slur import LexicalSlur | ||
|
|
||
| PROFANITY_ERROR_MESSAGE = "Text contains profanity" | ||
| # ProfanityFree uses the model's fixed 0.5 cutoff, which fails common Hinglish: | ||
| # the English model weights "ho" (as in "kaise ho") at 0.883 and "tu" at 0.535. | ||
| DEFAULT_PROFANITY_THRESHOLD = 0.9 | ||
|
|
||
|
|
||
| @register_validator(name="content-safety", data_type="string") | ||
| class ContentSafety(Validator): | ||
| """ | ||
| Runs lexical slur redaction followed by profanity detection as one validator. | ||
|
|
||
| The slur check runs first so a redacted slur doesn't trip the profanity check, | ||
| which has no partial fix and would otherwise blank the whole text. Profanity is | ||
| scored with alt-profanity-check (the model behind ProfanityFree) against a | ||
| configurable threshold instead of ProfanityFree's fixed 0.5. | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| slur_severity: SlurSeverity = SlurSeverity.All, | ||
| profanity_threshold: float = DEFAULT_PROFANITY_THRESHOLD, | ||
| on_fail: Callable | OnFailAction | None = OnFailAction.FIX, | ||
| ): | ||
| # Inner on_fail is never invoked; only this wrapper's on_fail applies. | ||
| self._slur = LexicalSlur(severity=slur_severity, on_fail=OnFailAction.NOOP) | ||
| self.profanity_threshold = profanity_threshold | ||
| super().__init__( | ||
| on_fail=on_fail, | ||
| slur_severity=slur_severity, | ||
| profanity_threshold=profanity_threshold, | ||
| ) | ||
|
|
||
| def _validate(self, value: str, metadata: dict | None = None) -> ValidationResult: | ||
| metadata = metadata or {} | ||
| errors = [] | ||
| current = value | ||
|
|
||
| slur_result = self._slur.validate(current, metadata) | ||
| if isinstance(slur_result, FailResult): | ||
| errors.append(slur_result.error_message) | ||
| current = slur_result.fix_value | ||
|
|
||
| # Fixed message: never echo user text into logs or rephrase responses. | ||
| if predict_prob([current])[0] >= self.profanity_threshold: | ||
| errors.append(PROFANITY_ERROR_MESSAGE) | ||
| current = "" | ||
|
|
||
| if not errors: | ||
| return PassResult() | ||
|
|
||
| return FailResult(error_message="; ".join(errors), fix_value=current) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should add this new validator in
backend/app/core/validators/validators.jsonso it shows all 11 validators. and update readme as well if not alreadyThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added