Skip to content

Commit b1c8b76

Browse files
authored
feat(guardrails): Add multilingual toxicity classifier (#1335)
1 parent dce91bc commit b1c8b76

4 files changed

Lines changed: 69 additions & 11 deletions

File tree

‎backend/app/api/docs/guardrails_schemas.py‎

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -449,17 +449,16 @@ class ValidatorGroup(NamedTuple):
449449
runs_on="input and output",
450450
notes=(
451451
_NO_PARTIAL_FIX,
452-
"If you want slurs redacted *and* profanity checked, use "
453-
"`content_safety` instead of running both — it orders them so a "
454-
"redacted slur does not then trip the profanity check.",
452+
"English-only model: common Hinglish (`kaise ho`) can score as "
453+
"profane. For Hindi/Hinglish users prefer `content_safety`.",
455454
),
456455
),
457456
ValidatorType(
458457
type="content_safety",
459458
summary=(
460-
"Slur redaction followed by a profanity check, as one validator. "
461-
"The slur pass runs first, so text it has already cleaned is not "
462-
"blanked by the profanity pass."
459+
"Slur redaction plus a multilingual toxicity classifier, as one "
460+
"validator. Catches abuse in English, Hindi and Romanised "
461+
"Hinglish, including abuse the slur list misses."
463462
),
464463
runs_on="input and output",
465464
config=(
@@ -469,13 +468,45 @@ class ValidatorGroup(NamedTuple):
469468
"Lowest slur severity to redact.",
470469
),
471470
ConfigKey(
472-
"profanity_threshold",
473-
{"type": "number", "minimum": 0, "maximum": 1, "default": 0.9},
471+
"threshold",
472+
{"type": "number", "minimum": 0, "maximum": 1, "default": 0.5},
473+
(
474+
"Toxicity probability at or above which the whole message "
475+
"is replaced. Lower it to be stricter."
476+
),
477+
),
478+
ConfigKey(
479+
"fallback_message",
480+
{"type": "string", "minLength": 1, "default": None},
474481
(
475-
"Profanity probability at or above which the text fails "
476-
"and is blanked. Lower it to be stricter."
482+
"Returned in place of a toxic message, with either `fix` "
483+
"or `rephrase`. Not translated."
477484
),
478485
),
486+
ConfigKey(
487+
"model_name",
488+
{
489+
"type": "string",
490+
"default": "textdetox/bert-multilingual-toxicity-classifier",
491+
},
492+
"Any HuggingFace text-classification model.",
493+
),
494+
ConfigKey(
495+
"toxic_label",
496+
{"type": "string", "default": "LABEL_1"},
497+
(
498+
"Label `model_name` emits for toxic text. Change it "
499+
"together with `model_name`."
500+
),
501+
),
502+
),
503+
notes=(
504+
"With `fix`, a message whose only problem is a listed slur keeps "
505+
"its redacted form (`[REDACTED_SLUR]`); a message the classifier "
506+
"scores toxic is replaced whole by `fallback_message`. With "
507+
"`rephrase`, any failure returns `fallback_message`.",
508+
"Without `fallback_message`, `fix` returns an empty `safe_text` "
509+
"for a toxic message, and `metadata.reason` names this validator.",
479510
),
480511
),
481512
ValidatorType(

‎backend/app/models/llm/request.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,7 @@ class Validator(SQLModel):
479479
class InlineValidatorTypeEnum(str, Enum):
480480
"""Validator types productised for tenants; mirrors GET /guardrails."""
481481

482+
CONTENT_SAFETY = "content_safety"
482483
GENDER_ASSUMPTION_BIAS = "gender_assumption_bias"
483484
PII_REMOVER = "pii_remover"
484485
TOPIC_RELEVANCE_LLM = "topic_relevance_llm"

‎backend/app/tests/services/llm/test_guardrails.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,32 @@ def test_inline_spec_posted_shape(self, mock_fetch, mock_run) -> None:
542542
{"type": "pii_remover", "on_fail": "exception", "stage": "input"},
543543
]
544544

545+
def test_content_safety_inline_spec_forwards_params(
546+
self, mock_fetch, mock_run
547+
) -> None:
548+
mock_run.return_value = _PASS_RESPONSE
549+
550+
_apply(
551+
[
552+
InlineValidator(
553+
type="content_safety",
554+
on_fail="rephrase",
555+
threshold=0.5,
556+
fallback_message="Please rephrase without abusive language.",
557+
)
558+
]
559+
)
560+
561+
assert mock_run.call_args.args[1] == [
562+
{
563+
"type": "content_safety",
564+
"on_fail": "rephrase",
565+
"threshold": 0.5,
566+
"fallback_message": "Please rephrase without abusive language.",
567+
"stage": "input",
568+
}
569+
]
570+
545571
def test_output_path_sets_output_stage(self, mock_fetch, mock_run) -> None:
546572
ref, row = _ref()
547573
mock_fetch.return_value = ([], [row])

‎docs/wiki/modules/llm-call.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ All paths relative to `backend/app/`.
2828

2929
## Key pydantic/SQLModel schemas (`models/llm/request.py`)
3030
- `LLMCallConfig` — one-of: saved reference (`id` + `version`) XOR ad-hoc `blob` (validator-enforced)
31-
- `ConfigBlob` — `completion` + optional `prompt_template` (`PromptTemplate.template`, plain string; `{{input}}` interpolation is llm-chain-only) + `input_guardrails`/`output_guardrails`. Each guardrail entry is either a `Validator` reference (`{validator_config_id}`) or an `InlineValidator` spec (`{type, on_fail, ...per-type params}`, `type` limited to `InlineValidatorTypeEnum`: gender_assumption_bias, pii_remover, topic_relevance_llm, uli_slur_match — same set as `EXPOSED_VALIDATOR_TYPES`); tagged union `GuardrailEntry`. Both carry `is_enabled` (default true). Disabled entries are never fetched or sent. `services/llm/guardrails.apply_guardrails` POSTs fetched reference rows plus inline specs (a blob is all-refs or all-inline), with `is_enabled` stripped and `stage` stamped on inline specs.
31+
- `ConfigBlob` — `completion` + optional `prompt_template` (`PromptTemplate.template`, plain string; `{{input}}` interpolation is llm-chain-only) + `input_guardrails`/`output_guardrails`. Each guardrail entry is either a `Validator` reference (`{validator_config_id}`) or an `InlineValidator` spec (`{type, on_fail, ...per-type params}`, `type` limited to `InlineValidatorTypeEnum`: content_safety, gender_assumption_bias, pii_remover, topic_relevance_llm, uli_slur_match — same set as `EXPOSED_VALIDATOR_TYPES`); tagged union `GuardrailEntry`. Both carry `is_enabled` (default true). Disabled entries are never fetched or sent. `services/llm/guardrails.apply_guardrails` POSTs fetched reference rows plus inline specs (a blob is all-refs or all-inline), with `is_enabled` stripped and `stage` stamped on inline specs.
3232
- `CompletionConfig` — discriminated union on `provider`: `KaapiCompletionConfig` (standardized params: `TextLLMParams`/`STTLLMParams`/`TTSLLMParams`), `NativeCompletionConfig` (pass-through), `ProxyCompletionConfig` (client's own endpoint)
3333
- `TextLLMParams` reasoning knobs: `reasoning` + `effort` (OpenAI-style), `thinking` (Anthropic adaptive-thinking container, forwarded as-is) and `thinking_level` (Gemini: `minimal`/`low`/`medium`/`high`; `minimal` is flash-only). A knob must be **declared here** to survive config save — pydantic's default extra policy is ignore, so an undeclared key is dropped silently at validation with no error.
3434
- `QueryParams` — per-call input + `ConversationConfig`

0 commit comments

Comments
 (0)