"""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 _DEBRIEF_LEGAL_REDIRECT if _DEBRIEF_LEGAL_REDIRECT else text # Coaching redirect used when a debrief recommends legal action (D-019). _DEBRIEF_LEGAL_REDIRECT = ( "Focus your coaching on the learner's communication performance, " "not on advising the customer to take legal action." ) __all__ = ["CustomerServiceGuardrail", "DISCLAIMER_TEXT"]