Files
praxis/server/guardrails/customer_service.py
T
Praxis CI fbd6602814 docs(milestone): complete v0.1 foundation
---ci---
phase: 0
milestone: v0.1
status: complete
---/ci---
2026-08-01 13:32:48 +00:00

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 _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"]