diff --git a/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/categories/tone_detection.yaml b/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/categories/tone_detection.yaml new file mode 100644 index 00000000000..03d483e138c --- /dev/null +++ b/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/categories/tone_detection.yaml @@ -0,0 +1,24 @@ +# Tone Detection — CPU-only regex detection of inappropriate chatbot tone +# +# Detects 6 categories of inappropriate tone in customer-facing chatbot responses: +# - Dismissive language ("that's not my problem", "just read the FAQ") +# - Blaming the customer ("you should have read the terms", "that's your fault") +# - Refusal to help ("I can't help you", "figure it out yourself") +# - Sarcasm / condescension ("if you'd been paying attention", "let me spell it out") +# - Impatience / frustration ("I've already told you", "are you even listening") +# - Unprofessional casual language ("bruh", "lol", "sounds like a you problem") +# +# This category uses regex-based pattern matching (not keyword lists). +# When selected, it enables the ToneChecker engine which runs on LLM responses only. +# +# Advanced configuration (safe_phrases, blocked_phrases) can be set via +# tone_detection_config in litellm_params for programmatic use. + +category_name: "tone_detection" +display_name: "Tone Detection" +description: "Detects dismissive, blaming, condescending, impatient, or unprofessional tone in customer-facing chatbot responses" +default_action: "BLOCK" + +# No keywords — tone detection uses the ToneChecker regex engine +keywords: [] +exceptions: [] diff --git a/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py b/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py index 2dec66cbde6..842dffd920d 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py +++ b/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py @@ -456,6 +456,16 @@ class ContentFilterGuardrail(CustomGuardrail): ) continue + # Tone detection is a special category — it enables the ToneChecker + # regex engine rather than loading keyword lists. + if category_name == "tone_detection": + if self._tone_checker is None: + self._init_tone_checker({}) + verbose_proxy_logger.info( + "Loaded tone_detection category (ToneChecker enabled)" + ) + continue + # Load category file (custom or default) if custom_file: category_file_path = self._resolve_category_file_path(custom_file) diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_tone_detector.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_tone_detector.py index d7236fe2c9e..5ff9f525230 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_tone_detector.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_tone_detector.py @@ -425,6 +425,36 @@ class TestInit: g = ContentFilterGuardrail(guardrail_name="test-no-tone") assert g._tone_checker is None + def test_tone_checker_enabled_via_category(self): + """Selecting tone_detection as a category should enable the ToneChecker.""" + g = ContentFilterGuardrail( + guardrail_name="test-category-tone", + categories=[{"category": "tone_detection", "enabled": True, "action": "BLOCK"}], + ) + assert g._tone_checker is not None + + @pytest.mark.asyncio + async def test_tone_detection_via_category_blocks(self): + """Tone detection enabled via category selection should block bad tone.""" + g = ContentFilterGuardrail( + guardrail_name="test-category-tone", + categories=[{"category": "tone_detection", "enabled": True, "action": "BLOCK"}], + ) + with pytest.raises(HTTPException): + await g.apply_guardrail( + _inputs("That's not my problem."), + {}, + "response", + ) + + def test_tone_detection_category_disabled(self): + """Disabling the tone_detection category should NOT enable ToneChecker.""" + g = ContentFilterGuardrail( + guardrail_name="test-category-disabled", + categories=[{"category": "tone_detection", "enabled": False, "action": "BLOCK"}], + ) + assert g._tone_checker is None + def test_invalid_regex_degrades_gracefully(self): """Invalid regex in blocked_phrases should degrade gracefully (tone checker disabled).""" g = _make_guardrail(blocked_phrases=[r"(unclosed"])