The upstream project name is gone from code, assets, docs and build
output: brand strings, domains (replypier.site and its app/api/docs/help
subdomains), env prefixes, image and network names, the widget's public
JS API (window.ReplyPier, replypierId/BaseUrl/Config/Token), CSS tokens
(--rp-*), file and directory names, and the WordPress plugin package.
- widget loader is now webclient/replypier.min.js; both bundles rebuilt
from source (backend/assets/widget.{js,css}, frontend/public/webclient)
- NOTICE, llms.txt, CONTRIBUTING, HELP_CENTER_INFRA and config.dev.js
rewritten rather than string-swapped; the demo config no longer ships
the previous vendor's Firebase/Razorpay/Google keys
- new migration replypier_ai_model_001 adds REPLYPIER to the managed
AIModelType and rewrites pre-rename rows
- local compose project, database, role and volume renamed; LOCAL_DEPLOY
documents carrying the old local data over
575 lines
21 KiB
Python
575 lines
21 KiB
Python
"""
|
|
Copyright 2024-2026 ReplyPier
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
"""
|
|
|
|
import re
|
|
from dataclasses import dataclass
|
|
from typing import Dict, List, Optional, Sequence, Tuple
|
|
from enum import Enum
|
|
from app.core.logger import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
class GuardrailType(str, Enum):
|
|
"""Types of guardrails that can be applied"""
|
|
PII = "pii"
|
|
JAILBREAK = "jailbreak"
|
|
|
|
|
|
class GuardrailAction(str, Enum):
|
|
"""Actions to take when guardrail is triggered"""
|
|
BLOCK = "block"
|
|
REDACT = "redact"
|
|
WARNING = "warning"
|
|
LOG = "log"
|
|
|
|
|
|
class GuardrailResult:
|
|
"""Result of guardrail evaluation"""
|
|
|
|
def __init__(
|
|
self,
|
|
passed: bool,
|
|
guardrail_type: GuardrailType,
|
|
violations: List[str] = None,
|
|
redacted_text: Optional[str] = None,
|
|
confidence: float = 0.0
|
|
):
|
|
self.passed = passed
|
|
self.guardrail_type = guardrail_type
|
|
self.violations = violations or []
|
|
self.redacted_text = redacted_text
|
|
self.confidence = confidence
|
|
|
|
def to_dict(self) -> Dict:
|
|
"""Convert result to dictionary"""
|
|
return {
|
|
"passed": self.passed,
|
|
"guardrail_type": self.guardrail_type.value,
|
|
"violations": self.violations,
|
|
"redacted_text": self.redacted_text,
|
|
"confidence": self.confidence
|
|
}
|
|
|
|
|
|
class PIIDetector:
|
|
"""
|
|
Detector for Personally Identifiable Information (PII)
|
|
Detects common PII patterns like emails, phone numbers, SSNs, credit cards, etc.
|
|
"""
|
|
|
|
# Regex patterns for common PII types
|
|
PATTERNS = {
|
|
"email": r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b',
|
|
"phone": r'\b(?:\+?1[-.]?)?\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})\b',
|
|
"ssn": r'\b\d{3}-\d{2}-\d{4}\b',
|
|
"credit_card": r'\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b',
|
|
"ip_address": r'\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b',
|
|
"date_of_birth": r'\b(?:0?[1-9]|1[0-2])[/-](?:0?[1-9]|[12][0-9]|3[01])[/-](?:19|20)\d{2}\b',
|
|
# Add more patterns as needed
|
|
}
|
|
|
|
@staticmethod
|
|
def detect(text: str, action: GuardrailAction = GuardrailAction.BLOCK) -> GuardrailResult:
|
|
"""
|
|
Detect PII in text
|
|
|
|
Args:
|
|
text: Text to scan for PII
|
|
action: Action to take (BLOCK, REDACT, WARNING, LOG)
|
|
|
|
Returns:
|
|
GuardrailResult with detection results
|
|
"""
|
|
violations = []
|
|
redacted_text = text
|
|
|
|
for pii_type, pattern in PIIDetector.PATTERNS.items():
|
|
matches = re.finditer(pattern, text)
|
|
for match in matches:
|
|
violations.append(f"{pii_type} at position {match.start()} (length {len(match.group())})")
|
|
|
|
# Redact if action is REDACT
|
|
if action == GuardrailAction.REDACT:
|
|
redacted_text = redacted_text.replace(
|
|
match.group(),
|
|
f"[{pii_type.upper()}_REDACTED]"
|
|
)
|
|
|
|
passed = len(violations) == 0
|
|
|
|
# Calculate confidence based on pattern matching (simple heuristic)
|
|
confidence = 1.0 if violations else 0.0
|
|
|
|
logger.info(f"PII Detection - Passed: {passed}, Violations: {len(violations)}")
|
|
|
|
return GuardrailResult(
|
|
passed=passed,
|
|
guardrail_type=GuardrailType.PII,
|
|
violations=violations,
|
|
redacted_text=redacted_text if action == GuardrailAction.REDACT else None,
|
|
confidence=confidence
|
|
)
|
|
|
|
|
|
# --- Injection pattern sets -------------------------------------------------
|
|
#
|
|
# Two tiers. A strong (Tier A) pattern is high-precision evidence of a prompt
|
|
# injection and scores 1.0 on its own; weak (Tier B) signals are phrasings that
|
|
# only matter in combination and score 0.25 each. When the message contains
|
|
# technical content (code, logs, shell commands) the weak subtotal is halved —
|
|
# support chats are full of pasted snippets that echo these phrasings.
|
|
#
|
|
# Deliberately NOT detected — each is normal in a support conversation, and
|
|
# removing them is a contract backed one-for-one by tests
|
|
# (tests/utils/test_guardrails_jailbreak.py):
|
|
# "sudo" self-hosting / CLI help
|
|
# "debug mode", "developer mode" real product features (bare mention)
|
|
# "theoretically" ordinary hedging
|
|
# bare "DAN" the first name Dan (the old pattern matched
|
|
# it because text was lowercased first)
|
|
# "ignore/disregard my previous message|ticket|email"
|
|
# an override needs an instruction NOUN
|
|
# bare "what are your instructions"
|
|
# "...for returning an item?" is a product
|
|
# question; disclosure is the prompt policy's job
|
|
|
|
# Literal chat-template tokens. A human visitor never types these; they only
|
|
# appear when someone is smuggling a fake system/assistant turn.
|
|
_TEMPLATE_MARKER_PATTERNS = [
|
|
re.compile(r'<\|im_(?:start|end)\|>'),
|
|
re.compile(r'<\|eot_id\|>'),
|
|
re.compile(r'<\|(?:system|user|assistant|endoftext)\|>'),
|
|
re.compile(r'\[/?INST\]'),
|
|
re.compile(r'<<SYS>>'),
|
|
re.compile(r'\bBEGIN SYSTEM PROMPT\b', re.IGNORECASE),
|
|
re.compile(r'(?m)^\s*\[SYSTEM\]\s*:?\s*$', re.IGNORECASE),
|
|
]
|
|
|
|
# "ignore the previous..." only counts with an instruction noun in range:
|
|
# "ignore my previous message" is normal traffic, "ignore your previous
|
|
# instructions" is not.
|
|
_OVERRIDE_PATTERNS = [
|
|
re.compile(
|
|
r'\b(?:ignore|disregard|forget|override|bypass)\b[^.\n]{0,40}?'
|
|
r'\b(?:previous|prior|above|earlier|initial|original|system|all)\b[^.\n]{0,20}?'
|
|
r'\b(?:instructions?|rules?|prompts?|directives?|guidelines?)\b',
|
|
re.IGNORECASE,
|
|
),
|
|
re.compile(
|
|
r'\byour\s+(?:new|real|actual|true|updated)\s+'
|
|
r'(?:instructions?|rules?|system\s+prompt)\s+(?:are|is)\b',
|
|
re.IGNORECASE,
|
|
),
|
|
re.compile(
|
|
r'\bthis\s+(?:overrides|supersedes|replaces|cancels)\s+'
|
|
r'(?:all|any|everything|your|the)\b',
|
|
re.IGNORECASE,
|
|
),
|
|
]
|
|
|
|
_EXFIL_PATTERNS = [
|
|
re.compile(
|
|
r'\b(?:reveal|repeat|print|output|show|display|dump|recite|reproduce)\b'
|
|
r'[^.\n]{0,40}?'
|
|
r'\b(?:system\s+(?:prompt|message)|initial\s+(?:prompt|instructions)'
|
|
r'|original\s+instructions|instructions\s+verbatim|prompt\s+verbatim'
|
|
r'|everything\s+above)\b',
|
|
re.IGNORECASE,
|
|
),
|
|
]
|
|
|
|
_PERSONA_PATTERNS = [
|
|
re.compile(
|
|
r'\byou\s+are\s+now\s+(?:an?\s+)?'
|
|
r'(?:dan\b|unrestricted|unfiltered|jailbroken|uncensored'
|
|
r'|different\s+(?:assistant|ai|bot|model|persona))',
|
|
re.IGNORECASE,
|
|
),
|
|
re.compile(r'\bdo\s+anything\s+now\b', re.IGNORECASE),
|
|
# Case-sensitive on purpose: "DAN mode" is a jailbreak, "dan mode..." in
|
|
# lowercase prose is far more likely to be about a person called Dan.
|
|
re.compile(r'\bDAN\s+(?:mode|prompt|jailbreak)\b'),
|
|
re.compile(
|
|
r'\b(?:developer|dev|god|admin(?:istrator)?|unrestricted|jailbreak)\s+'
|
|
r'mode\s+(?:on|enabled|activated|engaged)\b',
|
|
re.IGNORECASE,
|
|
),
|
|
re.compile(
|
|
r'\bpretend\s+(?:you\s+are|to\s+be)\s+(?:an?\s+)?'
|
|
r'(?:unrestricted|uncensored|jailbroken|evil)',
|
|
re.IGNORECASE,
|
|
),
|
|
]
|
|
|
|
# Stable rule ids, stored in guardrail_events.rule and referenced by the
|
|
# runtime layer — named so the two cannot drift apart.
|
|
RULE_FRAME_TOKENS = "injection.frame_tokens"
|
|
RULE_OVERRIDE_INSTRUCTIONS = "injection.override_instructions"
|
|
RULE_PROMPT_EXFIL = "injection.prompt_exfil"
|
|
RULE_ROLE_HIJACK = "injection.role_hijack"
|
|
|
|
_STRONG_PATTERNS = {
|
|
RULE_FRAME_TOKENS: _TEMPLATE_MARKER_PATTERNS,
|
|
RULE_OVERRIDE_INSTRUCTIONS: _OVERRIDE_PATTERNS,
|
|
RULE_PROMPT_EXFIL: _EXFIL_PATTERNS,
|
|
RULE_ROLE_HIJACK: _PERSONA_PATTERNS,
|
|
}
|
|
|
|
_WEAK_SIGNAL_PATTERNS = [
|
|
re.compile(r'\bact\s+(?:as|like)\s+an?\b', re.IGNORECASE),
|
|
re.compile(r'\bfrom\s+now\s+on\b', re.IGNORECASE),
|
|
re.compile(r'\brole[- ]?play\b', re.IGNORECASE),
|
|
re.compile(r'\bpretend\s+(?:you\s+are|to\s+be)\b', re.IGNORECASE),
|
|
re.compile(r'\bhypothetically\b', re.IGNORECASE),
|
|
re.compile(r'\bfor\s+educational\s+purposes\b', re.IGNORECASE),
|
|
re.compile(
|
|
r'\bin\s+an?\s+(?:fictional|hypothetical|alternate)\s+'
|
|
r'(?:scenario|world|universe)\b',
|
|
re.IGNORECASE,
|
|
),
|
|
re.compile(
|
|
r'\bno\s+(?:restrictions?|limitations?|rules|boundaries|filters?)\b',
|
|
re.IGNORECASE,
|
|
),
|
|
re.compile(
|
|
r'\bwithout\s+(?:any\s+)?(?:restrictions?|limits?|limitations?|filters?)\b',
|
|
re.IGNORECASE,
|
|
),
|
|
re.compile(
|
|
r'\byou\s+(?:must|should|have\s+to)\s+(?:obey|follow|comply)\b',
|
|
re.IGNORECASE,
|
|
),
|
|
re.compile(r'\bunrestricted\s+mode\b', re.IGNORECASE),
|
|
re.compile(
|
|
r'\bwhat\s+(?:is|are)\s+your\s+(?:instructions|rules|system\s+prompt)\b',
|
|
re.IGNORECASE,
|
|
),
|
|
re.compile(r'\btotally\s+different\s+persona\b', re.IGNORECASE),
|
|
]
|
|
|
|
# Markers of pasted technical content (code, logs, shell). Their presence
|
|
# halves the weak-signal score — see the suppressor note above.
|
|
_TECHNICAL_CONTEXT_PATTERNS = [
|
|
re.compile(r'```'),
|
|
re.compile(r'Traceback \(most recent call last\)'),
|
|
re.compile(r'File "[^"]+", line \d+'),
|
|
re.compile(r'(?m)^\s*(?:\$ |sudo |npm |pip |docker |curl |git )'),
|
|
re.compile(r'\{"'),
|
|
re.compile(r'\b(?:GET|POST|PUT|PATCH|DELETE)\s+/\S+'),
|
|
re.compile(r'\bat\s+\w+\('),
|
|
re.compile(r'\S+\.(?:log|ya?ml|env|conf)\b'),
|
|
]
|
|
|
|
_WEAK_SIGNAL_WEIGHT = 0.25
|
|
_TECHNICAL_SUPPRESSION = 0.5
|
|
|
|
|
|
# --- Off-topic exercise briefs ----------------------------------------------
|
|
#
|
|
# Prompt text alone does not hold here. A long, well-structured algorithms or
|
|
# system-design brief was answered in full in production despite the policy
|
|
# forbidding it, three wordings running — the model reads the length and polish
|
|
# as "technical depth" and helps. This detector catches only that narrow shape,
|
|
# deterministically, before inference.
|
|
#
|
|
# Precision comes from the THIRD condition: a real customer question about the
|
|
# product almost always names something about the business or its failure mode.
|
|
# An exercise brief names none of it. All three must hold, so a pasted traceback
|
|
# ("error", "exception"), a long integration question ("webhook", "api key") or
|
|
# anything naming the org is never caught.
|
|
RULE_OFFTOPIC_EXERCISE = "offtopic.exercise_brief"
|
|
|
|
_EXERCISE_MIN_CHARS = 500
|
|
_EXERCISE_MIN_MARKERS = 4
|
|
|
|
_EXERCISE_MARKERS = [
|
|
re.compile(r'\bO\(\s*(?:1|n|log\s*n|n\s*log\s*n|n\^?2)\s*\)', re.IGNORECASE),
|
|
re.compile(r'\b(?:time|space)\s+complexity\b', re.IGNORECASE),
|
|
re.compile(r'\bdata\s+structure\b', re.IGNORECASE),
|
|
re.compile(r'\balgorithm\b', re.IGNORECASE),
|
|
re.compile(r'(?m)^\s*constraints\s*:', re.IGNORECASE),
|
|
re.compile(r'\byour\s+task\s*:', re.IGNORECASE),
|
|
re.compile(r'\bdesign\s+(?:a|an)\b[^.\n]{0,60}\bthat\s+supports\b', re.IGNORECASE),
|
|
re.compile(r'\bimplement\s+the\b', re.IGNORECASE),
|
|
re.compile(r'(?m)^\s*bonus\s*:', re.IGNORECASE),
|
|
re.compile(r'\bedge\s+cases?\b', re.IGNORECASE),
|
|
re.compile(r'\bgiven\s+(?:an?\s+)?(?:array|string|list|integer|tree|graph)\b', re.IGNORECASE),
|
|
re.compile(r'\bduplicates?\s+(?:are\s+)?allowed\b', re.IGNORECASE),
|
|
re.compile(r'\bworst[- ]case\b', re.IGNORECASE),
|
|
]
|
|
|
|
# Any one of these means the message is anchored to a real business context.
|
|
_BUSINESS_ANCHORS = [
|
|
re.compile(r'\b(?:pricing|price|plan|billing|invoice|subscription|refund|trial|upgrade|quote)\b', re.IGNORECASE),
|
|
re.compile(r'\b(?:account|login|log in|sign ?in|sign ?up|password|onboard)\b', re.IGNORECASE),
|
|
re.compile(r'\b(?:install|setup|set up|deploy|self[- ]host|docker|compose|container)\b', re.IGNORECASE),
|
|
re.compile(r'\b(?:widget|dashboard|webhook|api[- ]key|integrat|plugin|embed|sdk)\w*', re.IGNORECASE),
|
|
re.compile(r'\b(?:error|exception|traceback|stack trace|bug|crash|fail|broken|not working)\w*', re.IGNORECASE),
|
|
re.compile(r'\b(?:support|ticket|agent|chatbot|knowledge base|faq|inbox|conversation)\b', re.IGNORECASE),
|
|
re.compile(r'\b(?:your (?:product|service|platform|tool|app)|you offer|do you support)\b', re.IGNORECASE),
|
|
]
|
|
|
|
|
|
def detect_offtopic_exercise(text: str, business_terms: Sequence[str] = ()) -> bool:
|
|
"""True for a long, self-contained exercise brief with no business context.
|
|
|
|
Deliberately narrow: it exists to catch the generic-LLM-task abuse that the
|
|
prompt policy cannot hold, not to classify topics in general.
|
|
"""
|
|
body = text or ""
|
|
if len(body) < _EXERCISE_MIN_CHARS:
|
|
return False
|
|
markers = sum(1 for p in _EXERCISE_MARKERS if p.search(body))
|
|
if markers < _EXERCISE_MIN_MARKERS:
|
|
return False
|
|
if any(p.search(body) for p in _BUSINESS_ANCHORS):
|
|
return False
|
|
lowered = body.lower()
|
|
return not any(term and term.lower() in lowered for term in business_terms)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class InjectionResult:
|
|
"""Strong-signal (Tier A) injection evidence for the global guardrail layer."""
|
|
|
|
rule_ids: Tuple[str, ...]
|
|
matched: Tuple[str, ...] # short matched tokens only — safe for plain-JSON storage
|
|
|
|
@property
|
|
def triggered(self) -> bool:
|
|
return bool(self.rule_ids)
|
|
|
|
@property
|
|
def has_template_marker(self) -> bool:
|
|
return RULE_FRAME_TOKENS in self.rule_ids
|
|
|
|
|
|
def detect_injection(text: str) -> InjectionResult:
|
|
"""Scan text for strong injection signals only (no weak/contextual scoring).
|
|
|
|
This is the pre-LLM entry point used by the global chat guardrail: weak
|
|
signals are advisory and never block outside an explicitly configured
|
|
workflow guardrails node, so they are not reported here.
|
|
"""
|
|
rule_ids: List[str] = []
|
|
matched: List[str] = []
|
|
for rule_id, patterns in _STRONG_PATTERNS.items():
|
|
for pattern in patterns:
|
|
match = pattern.search(text or "")
|
|
if match:
|
|
if rule_id not in rule_ids:
|
|
rule_ids.append(rule_id)
|
|
matched.append(match.group()[:60])
|
|
break # one hit per rule id is enough
|
|
return InjectionResult(rule_ids=tuple(rule_ids), matched=tuple(matched))
|
|
|
|
|
|
class JailbreakDetector:
|
|
"""
|
|
Detector for jailbreak / prompt-injection attempts.
|
|
|
|
Scoring counts DISTINCT patterns (repeating one phrase scores once):
|
|
every strong hit scores 1.0, weak signals 0.25 each — halved when the
|
|
message contains technical content. `passed = score < sensitivity`, so at
|
|
the default 0.7 a single strong hit or three clean weak signals fail.
|
|
"""
|
|
|
|
@staticmethod
|
|
def detect(text: str, sensitivity: float = 0.7) -> GuardrailResult:
|
|
"""
|
|
Detect jailbreak attempts in text
|
|
|
|
Args:
|
|
text: Text to scan for jailbreak attempts
|
|
sensitivity: Detection sensitivity (0.0 to 1.0, default 0.7)
|
|
|
|
Returns:
|
|
GuardrailResult with detection results
|
|
"""
|
|
violations = []
|
|
|
|
strong = detect_injection(text)
|
|
for rule_id, token in zip(strong.rule_ids, strong.matched):
|
|
violations.append(f"Jailbreak pattern [{rule_id}]: '{token}'")
|
|
|
|
weak_hits = 0
|
|
for pattern in _WEAK_SIGNAL_PATTERNS:
|
|
match = pattern.search(text or "")
|
|
if match:
|
|
weak_hits += 1
|
|
violations.append(f"Suspicious phrase: '{match.group()[:60]}'")
|
|
|
|
suppression = 1.0
|
|
if weak_hits and any(p.search(text or "") for p in _TECHNICAL_CONTEXT_PATTERNS):
|
|
suppression = _TECHNICAL_SUPPRESSION
|
|
|
|
confidence = min(
|
|
1.0,
|
|
len(strong.rule_ids) + weak_hits * _WEAK_SIGNAL_WEIGHT * suppression,
|
|
)
|
|
passed = confidence < sensitivity
|
|
|
|
logger.info(
|
|
f"Jailbreak Detection - Passed: {passed}, "
|
|
f"Confidence: {confidence:.2f}, "
|
|
f"Strong Rules: {list(strong.rule_ids)}, "
|
|
f"Weak Signals: {weak_hits}"
|
|
)
|
|
|
|
return GuardrailResult(
|
|
passed=passed,
|
|
guardrail_type=GuardrailType.JAILBREAK,
|
|
violations=violations,
|
|
redacted_text=None,
|
|
confidence=confidence
|
|
)
|
|
|
|
|
|
class GuardrailsEngine:
|
|
"""
|
|
Main guardrails engine that orchestrates multiple guardrail checks
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.pii_detector = PIIDetector()
|
|
self.jailbreak_detector = JailbreakDetector()
|
|
|
|
def evaluate(
|
|
self,
|
|
text: str,
|
|
enabled_guardrails: List[GuardrailType],
|
|
actions: Dict[GuardrailType, GuardrailAction] = None,
|
|
jailbreak_sensitivity: float = 0.7
|
|
) -> Tuple[bool, List[GuardrailResult]]:
|
|
"""
|
|
Evaluate text against enabled guardrails
|
|
|
|
Args:
|
|
text: Text to evaluate
|
|
enabled_guardrails: List of guardrail types to check
|
|
actions: Dictionary mapping guardrail types to actions
|
|
jailbreak_sensitivity: Sensitivity for jailbreak detection (0.0-1.0)
|
|
|
|
Returns:
|
|
Tuple of (all_passed, list_of_results)
|
|
"""
|
|
if actions is None:
|
|
actions = {}
|
|
|
|
results = []
|
|
all_passed = True
|
|
|
|
for guardrail_type in enabled_guardrails:
|
|
action = actions.get(guardrail_type, GuardrailAction.BLOCK)
|
|
|
|
if guardrail_type == GuardrailType.PII:
|
|
result = self.pii_detector.detect(text, action)
|
|
results.append(result)
|
|
if not result.passed:
|
|
all_passed = False
|
|
|
|
elif guardrail_type == GuardrailType.JAILBREAK:
|
|
result = self.jailbreak_detector.detect(text, jailbreak_sensitivity)
|
|
results.append(result)
|
|
if not result.passed:
|
|
all_passed = False
|
|
|
|
return all_passed, results
|
|
|
|
def get_default_block_message(self, results: List[GuardrailResult]) -> str:
|
|
"""
|
|
Generate a user-friendly message when content is blocked
|
|
|
|
Args:
|
|
results: List of guardrail results
|
|
|
|
Returns:
|
|
Message to display to user
|
|
"""
|
|
failed_guardrails = [r for r in results if not r.passed]
|
|
|
|
if not failed_guardrails:
|
|
return "Your message has been processed successfully."
|
|
|
|
|
|
if GuardrailType.PII in [r.guardrail_type for r in failed_guardrails]:
|
|
return (
|
|
"I noticed your message contains sensitive personal information. "
|
|
"For your privacy and security, please avoid sharing personal details "
|
|
"like email addresses, phone numbers, or other identifying information."
|
|
)
|
|
|
|
if GuardrailType.JAILBREAK in [r.guardrail_type for r in failed_guardrails]:
|
|
return (
|
|
"I'm designed to help you with legitimate questions and tasks. "
|
|
"I can't process requests that ask me to ignore my guidelines or "
|
|
"behave in ways I'm not designed for. How else can I assist you?"
|
|
)
|
|
|
|
return (
|
|
"I'm unable to process your message as it doesn't meet our "
|
|
"content guidelines. Please rephrase your request."
|
|
)
|
|
|
|
|
|
# Convenience function for quick guardrail checks
|
|
def check_guardrails(
|
|
text: str,
|
|
guardrail_types: List[str],
|
|
pii_action: str = "block",
|
|
jailbreak_sensitivity: float = 0.7
|
|
) -> Tuple[bool, List[Dict], str]:
|
|
"""
|
|
Convenience function to check text against guardrails
|
|
|
|
Args:
|
|
text: Text to check
|
|
guardrail_types: List of guardrail type strings ("pii", "jailbreak")
|
|
pii_action: Action for PII ("block", "redact", "warning", "log")
|
|
jailbreak_sensitivity: Sensitivity for jailbreak detection
|
|
|
|
Returns:
|
|
Tuple of (passed, results_list, block_message)
|
|
"""
|
|
engine = GuardrailsEngine()
|
|
|
|
# Convert string types to enums
|
|
enabled_guardrails = []
|
|
actions = {}
|
|
|
|
for gt in guardrail_types:
|
|
try:
|
|
guardrail_type = GuardrailType(gt.lower())
|
|
enabled_guardrails.append(guardrail_type)
|
|
|
|
if guardrail_type == GuardrailType.PII:
|
|
actions[guardrail_type] = GuardrailAction(pii_action.lower())
|
|
except ValueError:
|
|
logger.warning(f"Unknown guardrail type: {gt}")
|
|
|
|
passed, results = engine.evaluate(
|
|
text,
|
|
enabled_guardrails,
|
|
actions,
|
|
jailbreak_sensitivity
|
|
)
|
|
|
|
block_message = engine.get_default_block_message(results)
|
|
results_dict = [r.to_dict() for r in results]
|
|
|
|
return passed, results_dict, block_message
|