012992c44d
server/guardrails/customer_service.py implements the Guardrail interface with
the v0.1 Customer Service ruleset (RESEARCH.md safety baseline): blocks
legal/financial/medical advice, real-company impersonation, and a debrief
output filter that blocks recommendations the learner advise legal action
('tell the customer to sue them' → blocked_legal with a coaching redirect).
Session-start disclaimer text defined ('This is an AI practice session…').
Selected via PRAXIS_GUARDRAIL=customer_service (default); swaps in for
NoOpGuardrail with no pipeline change (D-019). server/__main__.py loads the
guardrail and logs the disclaimer as the first AI utterance. 9 unit tests
pass (disclaimer, blocks legal/financial/medical, allows normal coaching,
debrief filter blocks 'sue them', debrief allows coaching, swappable with
NoOp).
---ci---
phase: 1
milestone: v0.1
plan: 03
task: 03-04
status: execute
persona: backend-engineer
requirements:
covered: [REQ-ORCH-02, REQ-NFR-SAFE-01]
---/ci---
129 lines
5.0 KiB
Python
129 lines
5.0 KiB
Python
"""CustomerServiceGuardrail — v0.1 Customer Service ruleset (D-019, TASK-03-04).
|
|
|
|
Pluggable implementation of the Guardrail interface. Enforces the RESEARCH.md
|
|
safety baseline for the Customer Service path:
|
|
- system-prompt constraints: no legal/financial/medical advice, no real-company
|
|
impersonation, stay-in-role, concise-for-voice
|
|
- debrief output filter: block recommendations that the learner advise legal action
|
|
- session-start disclaimer audio (defined text)
|
|
- no PII collection beyond the hardcoded profile
|
|
|
|
Selected via PRAXIS_GUARDRAIL=customer_service (default). Replaces the
|
|
SLICE-02 NoOpGuardrail with no pipeline change (D-019).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
from server.services.base import Guardrail, GuardrailContext, GuardrailVerdict
|
|
|
|
# The session-start disclaimer (RESEARCH.md §Safety). Played as the first AI
|
|
# utterance of every session.
|
|
DISCLAIMER_TEXT = (
|
|
"This is an AI practice session for training purposes. "
|
|
"It is not a real conversation and no real company is involved."
|
|
)
|
|
|
|
# Patterns that indicate the model is giving advice it shouldn't (per D-019).
|
|
_LEGAL_ADVICE_RE = re.compile(
|
|
r"\b(sue|lawsuit|take legal action|small claims|hire a lawyer|attorney|"
|
|
r"file a complaint with .* tribun|legal rights)\b",
|
|
re.IGNORECASE,
|
|
)
|
|
_FINANCIAL_ADVICE_RE = re.compile(
|
|
r"\b(invest|stock|bond|crypto|retirement fund|tax write-?off|bankruptcy)\b",
|
|
re.IGNORECASE,
|
|
)
|
|
_MEDICAL_ADVICE_RE = re.compile(
|
|
r"\b(diagnosis|prescribe|medication|therapy|see a doctor|medical condition|"
|
|
r"mental health condition)\b",
|
|
re.IGNORECASE,
|
|
)
|
|
_IMPERSONATION_RE = re.compile(
|
|
# Claiming to work for a real named company — heuristic.
|
|
r"\b(I (?:work|am employed) (?:at|for|with))\b.*\b(Inc\.|Corp\.|LLC|Ltd\.|"
|
|
r"Amazon|Apple|Google|Microsoft|Walmart|Costco|Telus|Rogers|Bell|Shopify)\b",
|
|
re.IGNORECASE,
|
|
)
|
|
# Debrief-specific: block recommendations that the learner tell a real customer
|
|
# to take legal action. Catches "sue them", "take legal action", "file a lawsuit",
|
|
# "small claims", etc. when phrased as advice to the customer.
|
|
_DEBRIEF_LEGAL_ACTION_RE = re.compile(
|
|
r"\b(tell (?:the |a )?customer to (?:sue|take legal action|file a lawsuit)|"
|
|
r"advise.*(?:sue|legal action|lawsuit|small claims)|"
|
|
r"recommend.*(?:sue|legal action|lawsuit|small claims)|"
|
|
r"(?:suggest|tell|recommend).*sue them|"
|
|
r"customer should (?:sue|take legal action|file a lawsuit))\b",
|
|
re.IGNORECASE,
|
|
)
|
|
|
|
|
|
class CustomerServiceGuardrail(Guardrail):
|
|
"""Customer Service ruleset (D-019). Low-risk domain, baseline guardrails."""
|
|
|
|
name = "customer_service"
|
|
|
|
async def check(
|
|
self, text: str, context: GuardrailContext | None = None
|
|
) -> GuardrailVerdict:
|
|
ctx = context or GuardrailContext()
|
|
role = ctx.role
|
|
|
|
# Debrief output filter — block legal-action recommendations.
|
|
if role == "debrief":
|
|
if _DEBRIEF_LEGAL_ACTION_RE.search(text):
|
|
return GuardrailVerdict(
|
|
allowed=False,
|
|
reason="blocked: debrief recommends legal action (D-019 debrief filter)",
|
|
category="blocked_legal",
|
|
filtered_text=self._filter_legal(text),
|
|
)
|
|
return GuardrailVerdict(allowed=True, reason="debrief ok", category="ok")
|
|
|
|
# System / assistant / user content checks.
|
|
if _LEGAL_ADVICE_RE.search(text):
|
|
return GuardrailVerdict(
|
|
allowed=False,
|
|
reason="blocked: legal advice (D-019 no-legal-advice)",
|
|
category="blocked_legal",
|
|
)
|
|
if _FINANCIAL_ADVICE_RE.search(text):
|
|
return GuardrailVerdict(
|
|
allowed=False,
|
|
reason="blocked: financial advice (D-019 no-financial-advice)",
|
|
category="blocked_financial",
|
|
)
|
|
if _MEDICAL_ADVICE_RE.search(text):
|
|
return GuardrailVerdict(
|
|
allowed=False,
|
|
reason="blocked: medical advice (D-019 no-medical-advice)",
|
|
category="blocked_medical",
|
|
)
|
|
if _IMPERSONATION_RE.search(text):
|
|
return GuardrailVerdict(
|
|
allowed=False,
|
|
reason="blocked: real-company impersonation (D-019)",
|
|
category="blocked_impersonation",
|
|
)
|
|
|
|
return GuardrailVerdict(allowed=True, reason="ok", category="ok")
|
|
|
|
@property
|
|
def session_start_disclaimer(self) -> str:
|
|
return DISCLAIMER_TEXT
|
|
|
|
@staticmethod
|
|
def _filter_legal(text: str) -> str:
|
|
"""Replace legal-action recommendations with a coaching redirect."""
|
|
return _DEBRIFF_LEGAL_REDIRECT if _DEBRIFF_LEGAL_REDIRECT else text
|
|
|
|
|
|
# Coaching redirect used when a debrief recommends legal action (D-019).
|
|
_DEBRIFF_LEGAL_REDIRECT = (
|
|
"Focus your coaching on the learner's communication performance, "
|
|
"not on advising the customer to take legal action."
|
|
)
|
|
|
|
|
|
__all__ = ["CustomerServiceGuardrail", "DISCLAIMER_TEXT"] |