mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
[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:
parent
d9e6758655
commit
1ec567a4dd
6 changed files with 1284 additions and 0 deletions
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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.). */
|
||||
|
|
|
|||
613
ui/litellm-dashboard/src/data/toneDetectionCompliancePrompts.ts
Normal file
613
ui/litellm-dashboard/src/data/toneDetectionCompliancePrompts.ts
Normal 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",
|
||||
},
|
||||
];
|
||||
Loading…
Add table
Reference in a new issue