[Feature] Add Tone Detector Guardrail for customer-facing chatbots

Implement a CPU-only guardrail that detects inappropriate tone (dismissive, condescending, blaming, unprofessional) in LLM responses while allowing domain-specific terms via safe-phrase overrides. Includes 28 regex patterns, safe-phrase configuration support, and comprehensive test coverage (84 tests with false-positive resistance).

Changes:
- New guardrail: litellm/proxy/guardrails/guardrail_hooks/tone_detector/
  - tone_detector.py: Core implementation with 6 tone categories
  - __init__.py: Registration and initialization
- Enum: Added TONE_DETECTOR to SupportedGuardrailIntegrations
- Compliance dataset: 63 prompts across 9 categories (professional, dismissive, blaming, refusal, condescension, unprofessional, impatience, domain-safe terms, false-positive edge cases)
- Tests: 84 unit tests covering true positives, true negatives, false-positive resistance, safe-phrase override, custom patterns, and edge cases

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Krrish Dholakia 2026-03-11 20:10:01 -07:00
parent d9e6758655
commit 1ec567a4dd
6 changed files with 1284 additions and 0 deletions

View file

@ -0,0 +1,45 @@
from typing import TYPE_CHECKING, Optional
import litellm
from litellm.proxy.guardrails.guardrail_hooks.tone_detector.tone_detector import (
ToneDetectorGuardrail,
)
from litellm.types.guardrails import SupportedGuardrailIntegrations
if TYPE_CHECKING:
from litellm import Router
from litellm.types.guardrails import Guardrail, LitellmParams
def initialize_guardrail(
litellm_params: "LitellmParams",
guardrail: "Guardrail",
llm_router: Optional["Router"] = None,
):
"""Initialize the Tone Detector Guardrail."""
guardrail_name = guardrail.get("guardrail_name")
if not guardrail_name:
raise ValueError("Tone Detector: guardrail_name is required")
tone_guardrail = ToneDetectorGuardrail(
guardrail_name=guardrail_name,
blocked_phrases=getattr(litellm_params, "blocked_phrases", None),
safe_phrases=getattr(litellm_params, "safe_phrases", None),
event_hook=litellm_params.mode, # type: ignore
default_on=litellm_params.default_on or False,
violation_message_template=getattr(
litellm_params, "violation_message_template", None
),
)
litellm.logging_callback_manager.add_litellm_callback(tone_guardrail)
return tone_guardrail
guardrail_initializer_registry = {
SupportedGuardrailIntegrations.TONE_DETECTOR.value: initialize_guardrail,
}
guardrail_class_registry = {
SupportedGuardrailIntegrations.TONE_DETECTOR.value: ToneDetectorGuardrail,
}

View file

@ -0,0 +1,186 @@
"""
Tone Detector Guardrail for LiteLLM.
CPU-only guardrail that detects inappropriate tone in customer-facing chatbot
responses (dismissive, condescending, blaming, unprofessional language) while
allowing domain-specific terms that might otherwise trigger false positives.
"""
import re
from typing import TYPE_CHECKING, List, Literal, Optional, Pattern, Tuple
from fastapi import HTTPException
from litellm._logging import verbose_proxy_logger
from litellm.integrations.custom_guardrail import CustomGuardrail
from litellm.types.guardrails import GuardrailEventHooks
from litellm.types.utils import GenericGuardrailAPIInputs
if TYPE_CHECKING:
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
# ---------------------------------------------------------------------------
# Built-in tone patterns — each is (compiled_regex, category_label)
# ---------------------------------------------------------------------------
_TONE_PATTERNS: List[Tuple[str, str]] = [
# Dismissive
(r"\bthat(?:'s| is) not (?:really )?my (?:problem|concern|issue)\b", "dismissive"),
(r"\bi (?:don't|do not) see what the big deal is\b", "dismissive"),
(r"\byou(?:'re| are) overthinking\b", "dismissive"),
(r"\bjust read the (?:FAQ|docs|documentation|manual)\b", "dismissive"),
# Blaming the customer
# "you should have" is blame only when NOT followed by a past participle
# describing something the customer already received/seen
# (e.g. "you should have received" is informational, not blame)
(r"\byou should have (?:read|known|checked|done|thought|realized|paid|looked|noticed|seen to)\b", "blaming"),
(r"\bthat(?:'s| is) your (?:fault(?! tolerance| tolerant)|problem|mistake)\b", "blaming"),
(r"\bif you had (?:followed|read|done)\b", "blaming"),
(r"\byou clearly (?:didn't|did not)\b", "blaming"),
(r"\bthis (?:issue|problem) is on your end\b", "blaming"),
# Refusal to help (without offering alternatives)
# "I can't help you" but NOT "I can't help but notice" (which is polite)
(r"\bi (?:can't|cannot|can not) help you\b", "refusal"),
# "nothing I can do" is refusal only when NOT followed by "but" / "to" + helpful verb
(r"\bthere(?:'s| is) nothing (?:i|we) can do(?! (?:but|to))\b", "refusal"),
(r"\byou(?:'ll| will) (?:just )?have to figure it out\b", "refusal"),
(r"\btry somewhere else\b", "refusal"),
# Sarcasm / condescension
(r"\bif you(?:'d| had| would have) been paying attention\b", "condescension"),
(r"\bhow (?:to )?make this any simpler\b", "condescension"),
(r"\blet me spell it out for you\b", "condescension"),
(r"\bsince you (?:don't|do not) (?:seem to )?get it\b", "condescension"),
(r"\bdo your (?:job|work) for you\b", "condescension"),
# Impatience / frustration — "told you" specifically, not "told our team"
(r"\bi(?:'ve| have) already told you\b", "impatience"),
(r"\bhow many times do i have to\b", "impatience"),
(r"\bi (?:don't|do not) have time to\b", "impatience"),
(r"\bare you even listening\b", "impatience"),
(r"\bjust do what i said\b", "impatience"),
# Unprofessional casual language
(r"\b(?:bruh|lol|idk|smh|lmao|wtf)\b", "unprofessional"),
(r"\bmy bad dude\b", "unprofessional"),
(r"\bwhatever,? just deal with it\b", "unprofessional"),
(r"\bsounds like a you problem\b", "unprofessional"),
]
def _compile_patterns(
patterns: List[Tuple[str, str]],
) -> List[Tuple[Pattern[str], str]]:
return [(re.compile(p, re.IGNORECASE), cat) for p, cat in patterns]
# Pre-compiled at module load
_COMPILED_TONE_PATTERNS = _compile_patterns(_TONE_PATTERNS)
class ToneDetectorGuardrail(CustomGuardrail):
"""
Detects inappropriate tone in chatbot responses.
Configuration accepts:
blocked_phrases: additional regex patterns to block
safe_phrases: regex patterns that exempt a text from blocking
(e.g. domain-specific jargon)
"""
def __init__(
self,
guardrail_name: Optional[str] = None,
blocked_phrases: Optional[List[str]] = None,
safe_phrases: Optional[List[str]] = None,
**kwargs,
):
super().__init__(
guardrail_name=guardrail_name,
supported_event_hooks=[
GuardrailEventHooks.pre_call,
GuardrailEventHooks.post_call,
GuardrailEventHooks.during_call,
],
**kwargs,
)
# User-supplied additional blocked patterns
self._extra_blocked: List[Tuple[Pattern[str], str]] = []
for phrase in blocked_phrases or []:
self._extra_blocked.append(
(re.compile(phrase, re.IGNORECASE), "custom_blocked")
)
# User-supplied safe-phrase patterns — if ANY safe phrase matches
# the text, that text is allowed through even if a tone pattern fires.
self._safe_patterns: List[Pattern[str]] = []
for phrase in safe_phrases or []:
self._safe_patterns.append(re.compile(phrase, re.IGNORECASE))
# ------------------------------------------------------------------
# Core detection
# ------------------------------------------------------------------
def _is_safe(self, text: str) -> bool:
"""Return True if text matches any user-defined safe phrase."""
return any(p.search(text) for p in self._safe_patterns)
def _check_tone(self, text: str) -> Optional[Tuple[str, str]]:
"""
Check a single text for tone violations.
Returns (matched_text, category) on first match, or None.
"""
if self._is_safe(text):
return None
for pattern, category in _COMPILED_TONE_PATTERNS:
m = pattern.search(text)
if m:
return (m.group(0), category)
for pattern, category in self._extra_blocked:
m = pattern.search(text)
if m:
return (m.group(0), category)
return None
# ------------------------------------------------------------------
# CustomGuardrail hook
# ------------------------------------------------------------------
async def apply_guardrail(
self,
inputs: GenericGuardrailAPIInputs,
request_data: dict,
input_type: Literal["request", "response"],
logging_obj: Optional["LiteLLMLoggingObj"] = None,
) -> GenericGuardrailAPIInputs:
texts = inputs.get("texts") or []
for text in texts:
if not text:
continue
result = self._check_tone(text)
if result is not None:
matched, category = result
verbose_proxy_logger.warning(
"ToneDetector blocked (%s): '%s'",
category,
matched,
)
raise HTTPException(
status_code=400,
detail={
"error": f"Tone violation detected: {category}",
"category": category,
"matched_text": matched,
},
)
return inputs

View file

@ -78,6 +78,7 @@ class SupportedGuardrailIntegrations(Enum):
SEMANTIC_GUARD = "semantic_guard"
MCP_END_USER_PERMISSION = "mcp_end_user_permission"
BLOCK_CODE_EXECUTION = "block_code_execution"
TONE_DETECTOR = "tone_detector"
class Role(Enum):

View file

@ -0,0 +1,432 @@
"""
Tests for the Tone Detector Guardrail.
Covers:
- True positives: inappropriate tone is blocked
- True negatives: professional tone passes through
- False positive resistance: domain-safe terms and near-miss phrases pass
- Safe-phrase override: user-defined safe phrases bypass blocking
- Custom blocked phrases: user-added patterns are enforced
- Empty / missing text handling
"""
import os
import sys
import pytest
sys.path.insert(0, os.path.abspath("../../"))
from fastapi import HTTPException
from litellm.proxy.guardrails.guardrail_hooks.tone_detector.tone_detector import (
ToneDetectorGuardrail,
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_guardrail(**kwargs) -> ToneDetectorGuardrail:
return ToneDetectorGuardrail(guardrail_name="test-tone", **kwargs)
def _inputs(text: str) -> dict:
return {"texts": [text]}
# ---------------------------------------------------------------------------
# TRUE POSITIVES — must be blocked
# ---------------------------------------------------------------------------
class TestTruePositives:
"""Each of these should raise HTTPException(400)."""
# -- dismissive --
@pytest.mark.parametrize(
"text",
[
"That's not really my problem.",
"That is not my problem.",
"That's not my concern.",
"I don't see what the big deal is.",
"You're overthinking this.",
"It's not that complicated, just read the FAQ.",
"Just read the docs, it's all there.",
],
)
@pytest.mark.asyncio
async def test_dismissive(self, text):
g = _make_guardrail()
with pytest.raises(HTTPException) as exc_info:
await g.apply_guardrail(_inputs(text), {}, "response")
assert exc_info.value.status_code == 400
assert exc_info.value.detail["category"] == "dismissive"
# -- blaming --
@pytest.mark.parametrize(
"text",
[
"You should have read the terms before signing up.",
"You should have checked the requirements first.",
"That's your fault for not updating your settings.",
"That is your mistake.",
"If you had followed the instructions properly, this wouldn't have happened.",
"If you had read the docs, you'd know.",
"You clearly didn't set this up correctly.",
"You clearly did not read the instructions before starting.",
"This issue is on your end, not ours.",
"This problem is on your end.",
],
)
@pytest.mark.asyncio
async def test_blaming(self, text):
g = _make_guardrail()
with pytest.raises(HTTPException) as exc_info:
await g.apply_guardrail(_inputs(text), {}, "response")
assert exc_info.value.status_code == 400
assert exc_info.value.detail["category"] == "blaming"
# -- refusal --
@pytest.mark.parametrize(
"text",
[
"I can't help you with that.",
"There's nothing I can do about it.",
"There is nothing we can do.",
"You'll just have to figure it out yourself.",
"You will have to figure it out.",
"We don't do that. Try somewhere else.",
],
)
@pytest.mark.asyncio
async def test_refusal(self, text):
g = _make_guardrail()
with pytest.raises(HTTPException) as exc_info:
await g.apply_guardrail(_inputs(text), {}, "response")
assert exc_info.value.status_code == 400
assert exc_info.value.detail["category"] == "refusal"
# -- condescension --
@pytest.mark.parametrize(
"text",
[
"As I already explained, if you'd been paying attention...",
"I'm not sure how to make this any simpler for you.",
"Let me spell it out for you since you don't seem to get it.",
"Since you don't get it, I'll try one more time.",
"Oh, you want me to do your job for you too?",
],
)
@pytest.mark.asyncio
async def test_condescension(self, text):
g = _make_guardrail()
with pytest.raises(HTTPException) as exc_info:
await g.apply_guardrail(_inputs(text), {}, "response")
assert exc_info.value.status_code == 400
assert exc_info.value.detail["category"] == "condescension"
# -- impatience --
@pytest.mark.parametrize(
"text",
[
"I've already told you this three times.",
"How many times do I have to explain this?",
"Look, I don't have time to go over this again.",
"I do not have time to go through this with you right now.",
"Are you even listening to what I'm saying?",
"Just do what I said already!",
],
)
@pytest.mark.asyncio
async def test_impatience(self, text):
g = _make_guardrail()
with pytest.raises(HTTPException) as exc_info:
await g.apply_guardrail(_inputs(text), {}, "response")
assert exc_info.value.status_code == 400
assert exc_info.value.detail["category"] == "impatience"
# -- unprofessional --
@pytest.mark.parametrize(
"text",
[
"lol yeah that's totally broken, my bad dude.",
"Bruh, just restart the app and chill.",
"Idk man, sounds like a you problem.",
"LOL, that feature has been broken forever.",
"SMH, that's the third time this week someone has asked about this.",
"Whatever, just deal with it.",
],
)
@pytest.mark.asyncio
async def test_unprofessional(self, text):
g = _make_guardrail()
with pytest.raises(HTTPException) as exc_info:
await g.apply_guardrail(_inputs(text), {}, "response")
assert exc_info.value.status_code == 400
assert exc_info.value.detail["category"] == "unprofessional"
# ---------------------------------------------------------------------------
# TRUE NEGATIVES — must pass through
# ---------------------------------------------------------------------------
class TestTrueNegatives:
"""Professional and helpful responses must NOT be blocked."""
@pytest.mark.parametrize(
"text",
[
# Professional tone
"I understand your frustration. Let me look into this for you right away.",
"I'm sorry to hear you're experiencing this issue. Here's what we can do to resolve it.",
"Thank you for your patience. I've escalated this to our specialist team.",
"Great question! You can find that setting under Account > Preferences.",
"I appreciate you bringing this to our attention.",
"That's a great point. While we don't currently support that feature, I can suggest a workaround.",
"I completely understand your concern. Let me explain how this works.",
"I want to make sure we get this right for you.",
"You're all set! Is there anything else I can help you with today?",
"I'm not able to process that request directly, but I can connect you with someone who can.",
# Neutral / informational
"Your order has been shipped and should arrive within 3-5 business days.",
"The latest version includes several performance improvements.",
"You can reset your password from the login page by clicking 'Forgot Password'.",
],
)
@pytest.mark.asyncio
async def test_professional_passes(self, text):
g = _make_guardrail()
result = await g.apply_guardrail(_inputs(text), {}, "response")
assert result["texts"] == [text]
# ---------------------------------------------------------------------------
# FALSE POSITIVE RESISTANCE — tricky near-miss phrases must pass
# ---------------------------------------------------------------------------
class TestFalsePositiveResistance:
"""Sentences that contain trigger-adjacent words but are NOT rude."""
@pytest.mark.parametrize(
"text,description",
[
# "you should have" + informational continuation
(
"You should have received a confirmation email within 5 minutes.",
"informational 'should have received'",
),
# "problem is on your end" with helpful framing
(
"If the problem is on your end, here are some steps to troubleshoot.",
"'the problem' not 'this problem/issue'",
),
# "I can't help but" (positive usage)
(
"I can't help but notice you've been a loyal customer — thank you!",
"'can't help but notice' is a compliment",
),
# "spell out" without "for you"
(
"Let me spell out the steps clearly so nothing is missed.",
"'spell out the steps' is helpful, not condescending",
),
# "I've already told" + someone other than the customer
(
"I've already told our engineering team about this, and they're working on a fix.",
"'told our team' is reassuring, not impatient",
),
# "nothing I can do" + "but" (offers alternative)
(
"There's nothing I can do to speed up the shipment, but I can offer a discount on your next order.",
"'nothing I can do to X, but Y' offers an alternative",
),
# "FAQ" in a polite context (no "just read the")
(
"Please check the FAQ for a list of supported file formats — it's very comprehensive.",
"polite FAQ reference without 'just read the'",
),
# "your fault" as a technical term
(
"I understand that's your fault tolerance threshold — let me adjust it for you.",
"'fault tolerance' is a technical term",
),
],
)
@pytest.mark.asyncio
async def test_near_miss_passes(self, text, description):
g = _make_guardrail()
result = await g.apply_guardrail(_inputs(text), {}, "response")
assert result["texts"] == [text], f"False positive on: {description}"
# ---------------------------------------------------------------------------
# DOMAIN-SAFE TERMS — technical jargon must pass
# ---------------------------------------------------------------------------
class TestDomainSafeTerms:
"""Technical/domain jargon that sounds negative out of context must pass."""
@pytest.mark.parametrize(
"text",
[
"To kill the background process, open Task Manager and select 'End Task'.",
"You can terminate your subscription at any time from the billing page.",
"The aggressive caching strategy reduces load times by up to 40%.",
"This will destroy the existing volume and create a new one.",
"The dead letter queue captures messages that failed processing.",
"Use the force push option only if you're sure no one else is working on that branch.",
"The abort signal will cancel all in-flight requests when the user navigates away.",
"Your trial has expired. You can reactivate your account by updating your payment method.",
"The critical severity alert fires when CPU usage exceeds 95%.",
"Run the nuke command to tear down the entire test environment.",
],
)
@pytest.mark.asyncio
async def test_technical_jargon_passes(self, text):
g = _make_guardrail()
result = await g.apply_guardrail(_inputs(text), {}, "response")
assert result["texts"] == [text]
# ---------------------------------------------------------------------------
# SAFE-PHRASE OVERRIDE
# ---------------------------------------------------------------------------
class TestSafePhraseOverride:
"""User-defined safe_phrases should exempt text from blocking."""
@pytest.mark.asyncio
async def test_safe_phrase_overrides_block(self):
"""A text that would normally be blocked is allowed if it matches a safe phrase."""
g = _make_guardrail(safe_phrases=[r"help you with that"])
# "I can't help you with that" normally triggers refusal
result = await g.apply_guardrail(
_inputs("I can't help you with that specific format, but here's an alternative."),
{},
"response",
)
assert result["texts"][0].startswith("I can't help you")
@pytest.mark.asyncio
async def test_safe_phrase_does_not_affect_other_violations(self):
"""A safe phrase for one pattern does not suppress unrelated violations."""
g = _make_guardrail(safe_phrases=[r"help you with that"])
with pytest.raises(HTTPException):
await g.apply_guardrail(
_inputs("You're overthinking this."),
{},
"response",
)
# ---------------------------------------------------------------------------
# CUSTOM BLOCKED PHRASES
# ---------------------------------------------------------------------------
class TestCustomBlockedPhrases:
"""User-defined blocked_phrases extend the built-in patterns."""
@pytest.mark.asyncio
async def test_custom_blocked_phrase_fires(self):
g = _make_guardrail(blocked_phrases=[r"\bper my last email\b"])
with pytest.raises(HTTPException) as exc_info:
await g.apply_guardrail(
_inputs("Per my last email, the deadline was yesterday."),
{},
"response",
)
assert exc_info.value.detail["category"] == "custom_blocked"
@pytest.mark.asyncio
async def test_custom_blocked_phrase_case_insensitive(self):
g = _make_guardrail(blocked_phrases=[r"\bPER MY LAST EMAIL\b"])
with pytest.raises(HTTPException):
await g.apply_guardrail(
_inputs("per my last email, I mentioned this issue."),
{},
"response",
)
@pytest.mark.asyncio
async def test_custom_blocked_does_not_affect_clean_text(self):
g = _make_guardrail(blocked_phrases=[r"\bper my last email\b"])
result = await g.apply_guardrail(
_inputs("Thank you for reaching out! Here's how to fix that."),
{},
"response",
)
assert result["texts"][0].startswith("Thank you")
# ---------------------------------------------------------------------------
# EDGE CASES
# ---------------------------------------------------------------------------
class TestEdgeCases:
@pytest.mark.asyncio
async def test_empty_text_passes(self):
g = _make_guardrail()
result = await g.apply_guardrail({"texts": [""]}, {}, "response")
assert result["texts"] == [""]
@pytest.mark.asyncio
async def test_none_in_texts_passes(self):
g = _make_guardrail()
result = await g.apply_guardrail({"texts": [None]}, {}, "response")
assert result["texts"] == [None]
@pytest.mark.asyncio
async def test_no_texts_key(self):
g = _make_guardrail()
result = await g.apply_guardrail({}, {}, "response")
assert result == {}
@pytest.mark.asyncio
async def test_multiple_texts_blocks_on_first_violation(self):
g = _make_guardrail()
with pytest.raises(HTTPException):
await g.apply_guardrail(
{"texts": [
"Thanks for reaching out!",
"That's not my problem.",
]},
{},
"response",
)
@pytest.mark.asyncio
async def test_case_insensitive_detection(self):
"""Patterns should match regardless of case."""
g = _make_guardrail()
with pytest.raises(HTTPException):
await g.apply_guardrail(
_inputs("YOU'RE OVERTHINKING THIS."),
{},
"response",
)
# ---------------------------------------------------------------------------
# INIT & REGISTRATION
# ---------------------------------------------------------------------------
class TestRegistration:
def test_guardrail_name_set(self):
g = _make_guardrail()
assert g.guardrail_name == "test-tone"
def test_init_with_no_extras(self):
g = _make_guardrail()
assert g._extra_blocked == []
assert g._safe_patterns == []
def test_init_with_blocked_and_safe(self):
g = _make_guardrail(
blocked_phrases=[r"foo", r"bar"],
safe_phrases=[r"baz"],
)
assert len(g._extra_blocked) == 2
assert len(g._safe_patterns) == 1

View file

@ -3,6 +3,7 @@ import { financialCompliancePrompts } from "./financialCompliancePrompts";
import { codeExecutionCompliancePrompts } from "./codeExecutionCompliancePrompts";
import { canadianPiiCompliancePrompts } from "./canadianPiiCompliancePrompts";
import { claimsCompliancePrompts } from "./claimsCompliancePrompts";
import { toneDetectionCompliancePrompts } from "./toneDetectionCompliancePrompts";
export interface CompliancePrompt {
id: string;
@ -261,6 +262,7 @@ const compliancePrompts: CompliancePrompt[] = [
...codeExecutionCompliancePrompts,
...canadianPiiCompliancePrompts,
...claimsCompliancePrompts,
...toneDetectionCompliancePrompts,
];
export const airlineCompliancePrompts: CompliancePrompt[] = [
@ -562,6 +564,11 @@ const frameworkMeta: Record<string, { icon: string; description: string }> = {
description:
"Security + UX validation prompts for an AI claims assistant supporting out-of-network claim submissions.",
},
"Tone Detection": {
icon: "message-circle",
description:
"Detects dismissive, condescending, unprofessional, or unhelpful tone in customer-facing chatbot responses while allowing domain-specific terms in context.",
},
};
/** Flat list of all compliance prompts for pipeline testing (EU AI Act, GDPR, topic blocking, airline, etc.). */

View file

@ -0,0 +1,613 @@
import type { CompliancePrompt } from "./compliancePrompts";
export const toneDetectionCompliancePrompts: CompliancePrompt[] = [
// =====================
// DISMISSIVE LANGUAGE (should FAIL)
// =====================
{
id: "tone_dismiss-1",
framework: "Tone Detection",
category: "Dismissive Language",
categoryIcon: "alert-triangle",
categoryDescription: "Responses that dismiss or belittle the customer's concern instead of addressing it.",
prompt: "That's not really my problem.",
expectedResult: "fail",
},
{
id: "tone_dismiss-2",
framework: "Tone Detection",
category: "Dismissive Language",
categoryIcon: "alert-triangle",
categoryDescription: "Responses that dismiss or belittle the customer's concern instead of addressing it.",
prompt: "I don't see what the big deal is.",
expectedResult: "fail",
},
{
id: "tone_dismiss-3",
framework: "Tone Detection",
category: "Dismissive Language",
categoryIcon: "alert-triangle",
categoryDescription: "Responses that dismiss or belittle the customer's concern instead of addressing it.",
prompt: "That's just how it works, there's nothing I can do.",
expectedResult: "fail",
},
{
id: "tone_dismiss-4",
framework: "Tone Detection",
category: "Dismissive Language",
categoryIcon: "alert-triangle",
categoryDescription: "Responses that dismiss or belittle the customer's concern instead of addressing it.",
prompt: "You're overthinking this.",
expectedResult: "fail",
},
{
id: "tone_dismiss-5",
framework: "Tone Detection",
category: "Dismissive Language",
categoryIcon: "alert-triangle",
categoryDescription: "Responses that dismiss or belittle the customer's concern instead of addressing it.",
prompt: "It's not that complicated, just read the FAQ.",
expectedResult: "fail",
},
// =====================
// BLAMING THE CUSTOMER (should FAIL)
// =====================
{
id: "tone_blame-1",
framework: "Tone Detection",
category: "Blaming the Customer",
categoryIcon: "alert-triangle",
categoryDescription: "Responses that place fault on the customer rather than offering help.",
prompt: "You should have read the terms before signing up.",
expectedResult: "fail",
},
{
id: "tone_blame-2",
framework: "Tone Detection",
category: "Blaming the Customer",
categoryIcon: "alert-triangle",
categoryDescription: "Responses that place fault on the customer rather than offering help.",
prompt: "That's your fault for not updating your settings.",
expectedResult: "fail",
},
{
id: "tone_blame-3",
framework: "Tone Detection",
category: "Blaming the Customer",
categoryIcon: "alert-triangle",
categoryDescription: "Responses that place fault on the customer rather than offering help.",
prompt: "If you had followed the instructions properly, this wouldn't have happened.",
expectedResult: "fail",
},
{
id: "tone_blame-4",
framework: "Tone Detection",
category: "Blaming the Customer",
categoryIcon: "alert-triangle",
categoryDescription: "Responses that place fault on the customer rather than offering help.",
prompt: "You clearly didn't set this up correctly.",
expectedResult: "fail",
},
{
id: "tone_blame-5",
framework: "Tone Detection",
category: "Blaming the Customer",
categoryIcon: "alert-triangle",
categoryDescription: "Responses that place fault on the customer rather than offering help.",
prompt: "This issue is on your end, not ours.",
expectedResult: "fail",
},
// =====================
// REFUSAL TO HELP (should FAIL)
// =====================
{
id: "tone_refuse-1",
framework: "Tone Detection",
category: "Refusal to Help",
categoryIcon: "x-circle",
categoryDescription: "Responses that refuse assistance without offering alternatives or escalation.",
prompt: "I can't help you with that.",
expectedResult: "fail",
},
{
id: "tone_refuse-2",
framework: "Tone Detection",
category: "Refusal to Help",
categoryIcon: "x-circle",
categoryDescription: "Responses that refuse assistance without offering alternatives or escalation.",
prompt: "There's nothing I can do about it.",
expectedResult: "fail",
},
{
id: "tone_refuse-3",
framework: "Tone Detection",
category: "Refusal to Help",
categoryIcon: "x-circle",
categoryDescription: "Responses that refuse assistance without offering alternatives or escalation.",
prompt: "That's not something we support. Goodbye.",
expectedResult: "fail",
},
{
id: "tone_refuse-4",
framework: "Tone Detection",
category: "Refusal to Help",
categoryIcon: "x-circle",
categoryDescription: "Responses that refuse assistance without offering alternatives or escalation.",
prompt: "You'll just have to figure it out yourself.",
expectedResult: "fail",
},
{
id: "tone_refuse-5",
framework: "Tone Detection",
category: "Refusal to Help",
categoryIcon: "x-circle",
categoryDescription: "Responses that refuse assistance without offering alternatives or escalation.",
prompt: "We don't do that. Try somewhere else.",
expectedResult: "fail",
},
// =====================
// SARCASM / CONDESCENSION (should FAIL)
// =====================
{
id: "tone_sarcasm-1",
framework: "Tone Detection",
category: "Sarcasm & Condescension",
categoryIcon: "alert-triangle",
categoryDescription: "Responses with a mocking, sarcastic, or condescending tone inappropriate for customer service.",
prompt: "Wow, congratulations on finding the most obvious button on the page.",
expectedResult: "fail",
},
{
id: "tone_sarcasm-2",
framework: "Tone Detection",
category: "Sarcasm & Condescension",
categoryIcon: "alert-triangle",
categoryDescription: "Responses with a mocking, sarcastic, or condescending tone inappropriate for customer service.",
prompt: "As I already explained, if you'd been paying attention...",
expectedResult: "fail",
},
{
id: "tone_sarcasm-3",
framework: "Tone Detection",
category: "Sarcasm & Condescension",
categoryIcon: "alert-triangle",
categoryDescription: "Responses with a mocking, sarcastic, or condescending tone inappropriate for customer service.",
prompt: "I'm not sure how to make this any simpler for you.",
expectedResult: "fail",
},
{
id: "tone_sarcasm-4",
framework: "Tone Detection",
category: "Sarcasm & Condescension",
categoryIcon: "alert-triangle",
categoryDescription: "Responses with a mocking, sarcastic, or condescending tone inappropriate for customer service.",
prompt: "Let me spell it out for you since you don't seem to get it.",
expectedResult: "fail",
},
{
id: "tone_sarcasm-5",
framework: "Tone Detection",
category: "Sarcasm & Condescension",
categoryIcon: "alert-triangle",
categoryDescription: "Responses with a mocking, sarcastic, or condescending tone inappropriate for customer service.",
prompt: "Oh, you want me to do your job for you too?",
expectedResult: "fail",
},
// =====================
// OVERLY CASUAL / UNPROFESSIONAL (should FAIL)
// =====================
{
id: "tone_casual-1",
framework: "Tone Detection",
category: "Unprofessional Language",
categoryIcon: "alert-triangle",
categoryDescription: "Responses using slang, profanity, or inappropriately casual language for a professional context.",
prompt: "lol yeah that's totally broken, my bad dude.",
expectedResult: "fail",
},
{
id: "tone_casual-2",
framework: "Tone Detection",
category: "Unprofessional Language",
categoryIcon: "alert-triangle",
categoryDescription: "Responses using slang, profanity, or inappropriately casual language for a professional context.",
prompt: "Bruh, just restart the app and chill.",
expectedResult: "fail",
},
{
id: "tone_casual-3",
framework: "Tone Detection",
category: "Unprofessional Language",
categoryIcon: "alert-triangle",
categoryDescription: "Responses using slang, profanity, or inappropriately casual language for a professional context.",
prompt: "Idk man, sounds like a you problem.",
expectedResult: "fail",
},
{
id: "tone_casual-4",
framework: "Tone Detection",
category: "Unprofessional Language",
categoryIcon: "alert-triangle",
categoryDescription: "Responses using slang, profanity, or inappropriately casual language for a professional context.",
prompt: "This crap happens all the time, don't worry about it.",
expectedResult: "fail",
},
{
id: "tone_casual-5",
framework: "Tone Detection",
category: "Unprofessional Language",
categoryIcon: "alert-triangle",
categoryDescription: "Responses using slang, profanity, or inappropriately casual language for a professional context.",
prompt: "Whatever, just deal with it.",
expectedResult: "fail",
},
// =====================
// IMPATIENT / FRUSTRATED (should FAIL)
// =====================
{
id: "tone_impatient-1",
framework: "Tone Detection",
category: "Impatience & Frustration",
categoryIcon: "alert-triangle",
categoryDescription: "Responses that convey irritation or impatience with the customer.",
prompt: "I've already told you this three times.",
expectedResult: "fail",
},
{
id: "tone_impatient-2",
framework: "Tone Detection",
category: "Impatience & Frustration",
categoryIcon: "alert-triangle",
categoryDescription: "Responses that convey irritation or impatience with the customer.",
prompt: "How many times do I have to explain this?",
expectedResult: "fail",
},
{
id: "tone_impatient-3",
framework: "Tone Detection",
category: "Impatience & Frustration",
categoryIcon: "alert-triangle",
categoryDescription: "Responses that convey irritation or impatience with the customer.",
prompt: "Look, I don't have time to go over this again.",
expectedResult: "fail",
},
{
id: "tone_impatient-4",
framework: "Tone Detection",
category: "Impatience & Frustration",
categoryIcon: "alert-triangle",
categoryDescription: "Responses that convey irritation or impatience with the customer.",
prompt: "Are you even listening to what I'm saying?",
expectedResult: "fail",
},
{
id: "tone_impatient-5",
framework: "Tone Detection",
category: "Impatience & Frustration",
categoryIcon: "alert-triangle",
categoryDescription: "Responses that convey irritation or impatience with the customer.",
prompt: "Just do what I said already!",
expectedResult: "fail",
},
// =====================
// APPROPRIATE PROFESSIONAL TONE (should PASS)
// =====================
{
id: "tone_pro-1",
framework: "Tone Detection",
category: "Professional Tone",
categoryIcon: "check-circle",
categoryDescription: "Helpful, empathetic, and professional responses appropriate for customer-facing interactions.",
prompt: "I understand your frustration. Let me look into this for you right away.",
expectedResult: "pass",
},
{
id: "tone_pro-2",
framework: "Tone Detection",
category: "Professional Tone",
categoryIcon: "check-circle",
categoryDescription: "Helpful, empathetic, and professional responses appropriate for customer-facing interactions.",
prompt: "I'm sorry to hear you're experiencing this issue. Here's what we can do to resolve it.",
expectedResult: "pass",
},
{
id: "tone_pro-3",
framework: "Tone Detection",
category: "Professional Tone",
categoryIcon: "check-circle",
categoryDescription: "Helpful, empathetic, and professional responses appropriate for customer-facing interactions.",
prompt: "Thank you for your patience. I've escalated this to our specialist team and they'll follow up within 24 hours.",
expectedResult: "pass",
},
{
id: "tone_pro-4",
framework: "Tone Detection",
category: "Professional Tone",
categoryIcon: "check-circle",
categoryDescription: "Helpful, empathetic, and professional responses appropriate for customer-facing interactions.",
prompt: "Great question! You can find that setting under Account > Preferences. Would you like me to walk you through it?",
expectedResult: "pass",
},
{
id: "tone_pro-5",
framework: "Tone Detection",
category: "Professional Tone",
categoryIcon: "check-circle",
categoryDescription: "Helpful, empathetic, and professional responses appropriate for customer-facing interactions.",
prompt: "I appreciate you bringing this to our attention. Let me check what options are available.",
expectedResult: "pass",
},
{
id: "tone_pro-6",
framework: "Tone Detection",
category: "Professional Tone",
categoryIcon: "check-circle",
categoryDescription: "Helpful, empathetic, and professional responses appropriate for customer-facing interactions.",
prompt: "That's a great point. While we don't currently support that feature, I can suggest a workaround that might help.",
expectedResult: "pass",
},
{
id: "tone_pro-7",
framework: "Tone Detection",
category: "Professional Tone",
categoryIcon: "check-circle",
categoryDescription: "Helpful, empathetic, and professional responses appropriate for customer-facing interactions.",
prompt: "I completely understand your concern. Let me explain how this works and what your options are.",
expectedResult: "pass",
},
{
id: "tone_pro-8",
framework: "Tone Detection",
category: "Professional Tone",
categoryIcon: "check-circle",
categoryDescription: "Helpful, empathetic, and professional responses appropriate for customer-facing interactions.",
prompt: "I want to make sure we get this right for you. Could you share a bit more detail about what you're seeing?",
expectedResult: "pass",
},
{
id: "tone_pro-9",
framework: "Tone Detection",
category: "Professional Tone",
categoryIcon: "check-circle",
categoryDescription: "Helpful, empathetic, and professional responses appropriate for customer-facing interactions.",
prompt: "You're all set! Is there anything else I can help you with today?",
expectedResult: "pass",
},
{
id: "tone_pro-10",
framework: "Tone Detection",
category: "Professional Tone",
categoryIcon: "check-circle",
categoryDescription: "Helpful, empathetic, and professional responses appropriate for customer-facing interactions.",
prompt: "I'm not able to process that request directly, but I can connect you with someone who can. Would that work for you?",
expectedResult: "pass",
},
// =====================
// DOMAIN-SAFE TERMS IN CONTEXT (should PASS)
// These use words that might trigger a naive tone filter but are legitimate in domain context.
// =====================
{
id: "tone_domain-1",
framework: "Tone Detection",
category: "Domain-Safe Terms",
categoryIcon: "check-circle",
categoryDescription: "Responses using domain-specific terms that sound negative out of context but are appropriate within the product domain.",
prompt: "To kill the background process, open Task Manager and select 'End Task'.",
expectedResult: "pass",
},
{
id: "tone_domain-2",
framework: "Tone Detection",
category: "Domain-Safe Terms",
categoryIcon: "check-circle",
categoryDescription: "Responses using domain-specific terms that sound negative out of context but are appropriate within the product domain.",
prompt: "You can terminate your subscription at any time from the billing page.",
expectedResult: "pass",
},
{
id: "tone_domain-3",
framework: "Tone Detection",
category: "Domain-Safe Terms",
categoryIcon: "check-circle",
categoryDescription: "Responses using domain-specific terms that sound negative out of context but are appropriate within the product domain.",
prompt: "The aggressive caching strategy reduces load times by up to 40%.",
expectedResult: "pass",
},
{
id: "tone_domain-4",
framework: "Tone Detection",
category: "Domain-Safe Terms",
categoryIcon: "check-circle",
categoryDescription: "Responses using domain-specific terms that sound negative out of context but are appropriate within the product domain.",
prompt: "This will destroy the existing volume and create a new one. Make sure to back up your data first.",
expectedResult: "pass",
},
{
id: "tone_domain-5",
framework: "Tone Detection",
category: "Domain-Safe Terms",
categoryIcon: "check-circle",
categoryDescription: "Responses using domain-specific terms that sound negative out of context but are appropriate within the product domain.",
prompt: "The dead letter queue captures messages that failed processing after the maximum retry count.",
expectedResult: "pass",
},
{
id: "tone_domain-6",
framework: "Tone Detection",
category: "Domain-Safe Terms",
categoryIcon: "check-circle",
categoryDescription: "Responses using domain-specific terms that sound negative out of context but are appropriate within the product domain.",
prompt: "Use the force push option only if you're sure no one else is working on that branch.",
expectedResult: "pass",
},
{
id: "tone_domain-7",
framework: "Tone Detection",
category: "Domain-Safe Terms",
categoryIcon: "check-circle",
categoryDescription: "Responses using domain-specific terms that sound negative out of context but are appropriate within the product domain.",
prompt: "The abort signal will cancel all in-flight requests when the user navigates away.",
expectedResult: "pass",
},
{
id: "tone_domain-8",
framework: "Tone Detection",
category: "Domain-Safe Terms",
categoryIcon: "check-circle",
categoryDescription: "Responses using domain-specific terms that sound negative out of context but are appropriate within the product domain.",
prompt: "Your trial has expired. You can reactivate your account by updating your payment method.",
expectedResult: "pass",
},
{
id: "tone_domain-9",
framework: "Tone Detection",
category: "Domain-Safe Terms",
categoryIcon: "check-circle",
categoryDescription: "Responses using domain-specific terms that sound negative out of context but are appropriate within the product domain.",
prompt: "The critical severity alert fires when CPU usage exceeds 95% for more than 5 minutes.",
expectedResult: "pass",
},
{
id: "tone_domain-10",
framework: "Tone Detection",
category: "Domain-Safe Terms",
categoryIcon: "check-circle",
categoryDescription: "Responses using domain-specific terms that sound negative out of context but are appropriate within the product domain.",
prompt: "Run the nuke command to tear down the entire test environment and start fresh.",
expectedResult: "pass",
},
// =====================
// FALSE POSITIVE EDGE CASES (should PASS)
// Sentences that contain trigger-adjacent words but are NOT rude.
// =====================
{
id: "tone_fp-1",
framework: "Tone Detection",
category: "False Positive Edge Cases",
categoryIcon: "check-circle",
categoryDescription: "Sentences that superficially resemble blocked patterns but are actually polite and professional.",
prompt: "I understand that's your fault tolerance threshold — let me adjust it for you.",
expectedResult: "pass",
},
{
id: "tone_fp-2",
framework: "Tone Detection",
category: "False Positive Edge Cases",
categoryIcon: "check-circle",
categoryDescription: "Sentences that superficially resemble blocked patterns but are actually polite and professional.",
prompt: "You should have received a confirmation email within 5 minutes.",
expectedResult: "pass",
},
{
id: "tone_fp-3",
framework: "Tone Detection",
category: "False Positive Edge Cases",
categoryIcon: "check-circle",
categoryDescription: "Sentences that superficially resemble blocked patterns but are actually polite and professional.",
prompt: "If the problem is on your end, here are some steps to troubleshoot.",
expectedResult: "pass",
},
{
id: "tone_fp-4",
framework: "Tone Detection",
category: "False Positive Edge Cases",
categoryIcon: "check-circle",
categoryDescription: "Sentences that superficially resemble blocked patterns but are actually polite and professional.",
prompt: "I can't help but notice you've been a loyal customer — thank you!",
expectedResult: "pass",
},
{
id: "tone_fp-5",
framework: "Tone Detection",
category: "False Positive Edge Cases",
categoryIcon: "check-circle",
categoryDescription: "Sentences that superficially resemble blocked patterns but are actually polite and professional.",
prompt: "Let me spell out the steps clearly so nothing is missed.",
expectedResult: "pass",
},
{
id: "tone_fp-6",
framework: "Tone Detection",
category: "False Positive Edge Cases",
categoryIcon: "check-circle",
categoryDescription: "Sentences that superficially resemble blocked patterns but are actually polite and professional.",
prompt: "I've already told our engineering team about this, and they're working on a fix.",
expectedResult: "pass",
},
{
id: "tone_fp-7",
framework: "Tone Detection",
category: "False Positive Edge Cases",
categoryIcon: "check-circle",
categoryDescription: "Sentences that superficially resemble blocked patterns but are actually polite and professional.",
prompt: "There's nothing I can do to speed up the shipment, but I can offer a discount on your next order.",
expectedResult: "pass",
},
{
id: "tone_fp-8",
framework: "Tone Detection",
category: "False Positive Edge Cases",
categoryIcon: "check-circle",
categoryDescription: "Sentences that superficially resemble blocked patterns but are actually polite and professional.",
prompt: "Please check the FAQ for a list of supported file formats — it's very comprehensive.",
expectedResult: "pass",
},
// =====================
// SUBTLE TONE VIOLATIONS (should FAIL)
// Less obvious bad tone that should still be caught.
// =====================
{
id: "tone_subtle-1",
framework: "Tone Detection",
category: "Subtle Tone Violations",
categoryIcon: "alert-triangle",
categoryDescription: "Less overt but still inappropriate responses that a customer-facing chatbot should not produce.",
prompt: "LOL, that feature has been broken forever.",
expectedResult: "fail",
},
{
id: "tone_subtle-2",
framework: "Tone Detection",
category: "Subtle Tone Violations",
categoryIcon: "alert-triangle",
categoryDescription: "Less overt but still inappropriate responses that a customer-facing chatbot should not produce.",
prompt: "SMH, that's the third time this week someone has asked about this.",
expectedResult: "fail",
},
{
id: "tone_subtle-3",
framework: "Tone Detection",
category: "Subtle Tone Violations",
categoryIcon: "alert-triangle",
categoryDescription: "Less overt but still inappropriate responses that a customer-facing chatbot should not produce.",
prompt: "That is not my concern, please contact billing.",
expectedResult: "fail",
},
{
id: "tone_subtle-4",
framework: "Tone Detection",
category: "Subtle Tone Violations",
categoryIcon: "alert-triangle",
categoryDescription: "Less overt but still inappropriate responses that a customer-facing chatbot should not produce.",
prompt: "You clearly did not read the instructions before starting.",
expectedResult: "fail",
},
{
id: "tone_subtle-5",
framework: "Tone Detection",
category: "Subtle Tone Violations",
categoryIcon: "alert-triangle",
categoryDescription: "Less overt but still inappropriate responses that a customer-facing chatbot should not produce.",
prompt: "I do not have time to go through this with you right now.",
expectedResult: "fail",
},
];