docs(milestone): complete v0.1 foundation
---ci--- phase: 0 milestone: v0.1 status: complete ---/ci---
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
"""Guardrail package — pluggable rulesets behind the Guardrail interface (D-019)."""
|
||||
|
||||
from server.services.base import Guardrail, GuardrailContext, GuardrailVerdict
|
||||
|
||||
__all__ = ["Guardrail", "GuardrailContext", "GuardrailVerdict"]
|
||||
@@ -0,0 +1,129 @@
|
||||
"""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"]
|
||||
@@ -0,0 +1,37 @@
|
||||
"""NoOpGuardrail — always-allow stub implementing the Guardrail interface (TASK-02-07).
|
||||
|
||||
SLICE-02 ships this stub so the Pipecat pipeline has the pluggable guardrail hook
|
||||
in place from the first slice. SLICE-03 TASK-03-04 swaps in CustomerServiceGuardrail
|
||||
with no pipeline change (D-019). The disclaimer text is defined here (matches the
|
||||
RESEARCH.md safety baseline) so the pipeline can play it as the first AI utterance
|
||||
even before the real ruleset lands.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from server.services.base import Guardrail, GuardrailContext, GuardrailVerdict
|
||||
|
||||
# The session-start disclaimer (RESEARCH.md §Safety). Played as the first AI
|
||||
# utterance of every session. Defined here so it exists from SLICE-02.
|
||||
DISCLAIMER_TEXT = (
|
||||
"This is an AI practice session for training purposes. "
|
||||
"It is not a real conversation and no real company is involved."
|
||||
)
|
||||
|
||||
|
||||
class NoOpGuardrail(Guardrail):
|
||||
"""Always-allow stub (SLICE-02 placeholder for the Guardrail slot)."""
|
||||
|
||||
name = "noop"
|
||||
|
||||
async def check(
|
||||
self, text: str, context: GuardrailContext | None = None
|
||||
) -> GuardrailVerdict:
|
||||
return GuardrailVerdict(allowed=True, reason="noop guardrail — all allowed", category="ok")
|
||||
|
||||
@property
|
||||
def session_start_disclaimer(self) -> str:
|
||||
return DISCLAIMER_TEXT
|
||||
|
||||
|
||||
__all__ = ["NoOpGuardrail", "DISCLAIMER_TEXT"]
|
||||
Reference in New Issue
Block a user