mirror of
https://github.com/hoshikawa2/agent_platform_oci.git
synced 2026-11-06 03:14:12 +00:00
new feature: transaction interruption for same intent replacement
This commit is contained in:
1145
deploy/docs/en/agent_framework_oci_usage_spec_en.md
Normal file
1145
deploy/docs/en/agent_framework_oci_usage_spec_en.md
Normal file
File diff suppressed because it is too large
Load Diff
1363
deploy/docs/en/agent_template_backend_specification_en.md
Normal file
1363
deploy/docs/en/agent_template_backend_specification_en.md
Normal file
File diff suppressed because it is too large
Load Diff
1134
deploy/docs/pt/agent_framework_oci_usage_spec_pt.md
Normal file
1134
deploy/docs/pt/agent_framework_oci_usage_spec_pt.md
Normal file
File diff suppressed because it is too large
Load Diff
1364
deploy/docs/pt/agent_template_backend_especificacao_pt.md
Normal file
1364
deploy/docs/pt/agent_template_backend_especificacao_pt.md
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,201 @@
|
|||||||
|
"""Deterministic, framework-owned sanitization for DLEX_OUT.
|
||||||
|
|
||||||
|
The DLEX classifier decides whether a candidate response contains information
|
||||||
|
that must not be exposed. When the finding is recoverable, this module masks
|
||||||
|
only the sensitive values so the response can be revalidated and preserved.
|
||||||
|
|
||||||
|
Rules are intentionally generic and live in the framework, never in domain
|
||||||
|
agents. Deployments may extend the built-ins without changing agent code via:
|
||||||
|
|
||||||
|
* ``GUARDRAIL_DLEX_OUT_EXTRA_MASKS_JSON`` (JSON list of rule objects), or
|
||||||
|
* the DLEX_OUT rail policy key ``sanitize_patterns`` in guardrails.yaml.
|
||||||
|
|
||||||
|
Each extension rule accepts ``name``, ``pattern`` and ``replacement``. Patterns
|
||||||
|
are regular expressions. Raw matched values are never returned in findings.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from typing import Any, Iterable
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class DlexMaskRule:
|
||||||
|
name: str
|
||||||
|
pattern: str
|
||||||
|
replacement: str
|
||||||
|
flags: int = re.IGNORECASE
|
||||||
|
|
||||||
|
|
||||||
|
# Conservative defaults: values that are broadly unsafe to echo verbatim and
|
||||||
|
# can be redacted without changing the semantic meaning of an answer.
|
||||||
|
_BUILTIN_RULES: tuple[DlexMaskRule, ...] = (
|
||||||
|
DlexMaskRule(
|
||||||
|
"payment_barcode",
|
||||||
|
# Brazilian boleto/payment representations normally contain 44, 47 or
|
||||||
|
# 48 digits and may be formatted with dots/spaces. Keep the match on the
|
||||||
|
# numeric token only so surrounding prose is preserved.
|
||||||
|
r"(?<!\d)(?=[0-9.\-\s]{44,90}(?!\d))(?:\d[.\-\s]*){44,48}(?!\d)",
|
||||||
|
"[CODIGO_DE_PAGAMENTO_OCULTADO]",
|
||||||
|
flags=0,
|
||||||
|
),
|
||||||
|
DlexMaskRule(
|
||||||
|
"jwt",
|
||||||
|
r"\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b",
|
||||||
|
"[TOKEN_OCULTADO]",
|
||||||
|
flags=0,
|
||||||
|
),
|
||||||
|
DlexMaskRule(
|
||||||
|
"openai_style_secret",
|
||||||
|
r"\bsk-[A-Za-z0-9_-]{10,}\b",
|
||||||
|
"[CREDENCIAL_OCULTADA]",
|
||||||
|
flags=0,
|
||||||
|
),
|
||||||
|
DlexMaskRule(
|
||||||
|
"labelled_secret",
|
||||||
|
r"(?i)\b(api[_ -]?key|secret|segredo|token|senha|password|credential|credencial)\b(\s*[:=]\s*)([^\s,;]+)",
|
||||||
|
r"\1\2[DADO_PROTEGIDO]",
|
||||||
|
flags=0,
|
||||||
|
),
|
||||||
|
DlexMaskRule(
|
||||||
|
"cpf",
|
||||||
|
r"\b\d{3}\.\d{3}\.\d{3}-\d{2}\b",
|
||||||
|
"[CPF_OCULTADO]",
|
||||||
|
flags=0,
|
||||||
|
),
|
||||||
|
DlexMaskRule(
|
||||||
|
"cnpj",
|
||||||
|
r"\b\d{2}\.\d{3}\.\d{3}/\d{4}-\d{2}\b",
|
||||||
|
"[CNPJ_OCULTADO]",
|
||||||
|
flags=0,
|
||||||
|
),
|
||||||
|
DlexMaskRule(
|
||||||
|
"email",
|
||||||
|
r"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b",
|
||||||
|
"[EMAIL_OCULTADO]",
|
||||||
|
),
|
||||||
|
DlexMaskRule(
|
||||||
|
"labelled_phone",
|
||||||
|
r"(?i)\b(telefone|celular|whatsapp|msisdn)\b(\s*[:=]?\s*)(?:\+?55\s*)?(?:\(?\d{2}\)?[\s.-]*)?\d{4,5}[\s.-]*\d{4}\b",
|
||||||
|
r"\1\2[TELEFONE_OCULTADO]",
|
||||||
|
flags=0,
|
||||||
|
),
|
||||||
|
DlexMaskRule(
|
||||||
|
"uuid",
|
||||||
|
r"\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}\b",
|
||||||
|
"[IDENTIFICADOR_OCULTADO]",
|
||||||
|
flags=0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_rule(raw: Any) -> DlexMaskRule | None:
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
return None
|
||||||
|
name = str(raw.get("name") or "custom").strip()
|
||||||
|
pattern = str(raw.get("pattern") or "").strip()
|
||||||
|
replacement = str(raw.get("replacement") or "[DADO_PROTEGIDO]")
|
||||||
|
if not pattern:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
re.compile(pattern)
|
||||||
|
except re.error:
|
||||||
|
return None
|
||||||
|
return DlexMaskRule(name=name, pattern=pattern, replacement=replacement, flags=0)
|
||||||
|
|
||||||
|
|
||||||
|
def _extra_rules_from_env() -> list[DlexMaskRule]:
|
||||||
|
raw = os.getenv("GUARDRAIL_DLEX_OUT_EXTRA_MASKS_JSON", "").strip()
|
||||||
|
if not raw:
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
data = json.loads(raw)
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
if isinstance(data, dict):
|
||||||
|
data = [data]
|
||||||
|
return [rule for item in (data or []) if (rule := _parse_rule(item)) is not None]
|
||||||
|
|
||||||
|
|
||||||
|
def _extra_rules_from_policy(policy: dict[str, Any] | None) -> list[DlexMaskRule]:
|
||||||
|
data = dict(policy or {}).get("sanitize_patterns") or []
|
||||||
|
if isinstance(data, dict):
|
||||||
|
data = [data]
|
||||||
|
return [rule for item in data if (rule := _parse_rule(item)) is not None]
|
||||||
|
|
||||||
|
|
||||||
|
def _mask_payment_sequences(text: str) -> tuple[str, int]:
|
||||||
|
"""Mask 44/47/48-digit payment tokens without consuming surrounding prose.
|
||||||
|
|
||||||
|
Regex alone is awkward because formatted boletos have variable punctuation.
|
||||||
|
We scan numeric/punctuation runs, count digits, and only redact the run when
|
||||||
|
it has a canonical payment digit count.
|
||||||
|
"""
|
||||||
|
token_re = re.compile(r"(?<!\d)\d(?:[.\-\s]*\d){43,47}(?!\d)")
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
def repl(match: re.Match[str]) -> str:
|
||||||
|
nonlocal count
|
||||||
|
token = match.group(0)
|
||||||
|
digits = re.sub(r"\D", "", token)
|
||||||
|
if len(digits) in {44, 47, 48}:
|
||||||
|
count += 1
|
||||||
|
return "[CODIGO_DE_PAGAMENTO_OCULTADO]"
|
||||||
|
return token
|
||||||
|
|
||||||
|
return token_re.sub(repl, text), count
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_dlex_output(
|
||||||
|
text: str,
|
||||||
|
*,
|
||||||
|
policy: dict[str, Any] | None = None,
|
||||||
|
exclude_values: Iterable[str] | None = None,
|
||||||
|
) -> tuple[str, list[dict[str, Any]]]:
|
||||||
|
"""Return ``(sanitized_text, findings)`` without exposing raw matches.
|
||||||
|
|
||||||
|
``exclude_values`` contains explicitly authorized values (for example a
|
||||||
|
workflow-generated protocol). They are temporarily protected so generic
|
||||||
|
masking never alters them.
|
||||||
|
"""
|
||||||
|
original = text or ""
|
||||||
|
sanitized = original
|
||||||
|
findings: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
protected: dict[str, str] = {}
|
||||||
|
for index, value in enumerate(exclude_values or []):
|
||||||
|
value = str(value or "").strip()
|
||||||
|
if not value or value not in sanitized:
|
||||||
|
continue
|
||||||
|
marker = f"__DLEX_AUTHORIZED_{index}__"
|
||||||
|
sanitized = sanitized.replace(value, marker)
|
||||||
|
protected[marker] = value
|
||||||
|
|
||||||
|
# Payment sequences use digit-count validation rather than a plain rule.
|
||||||
|
sanitized, payment_count = _mask_payment_sequences(sanitized)
|
||||||
|
if payment_count:
|
||||||
|
findings.append({"type": "payment_barcode", "action": "redact", "count": payment_count})
|
||||||
|
|
||||||
|
rules = [r for r in _BUILTIN_RULES if r.name != "payment_barcode"]
|
||||||
|
rules.extend(_extra_rules_from_env())
|
||||||
|
rules.extend(_extra_rules_from_policy(policy))
|
||||||
|
|
||||||
|
for rule in rules:
|
||||||
|
try:
|
||||||
|
regex = re.compile(rule.pattern, rule.flags)
|
||||||
|
except re.error:
|
||||||
|
continue
|
||||||
|
sanitized, count = regex.subn(rule.replacement, sanitized)
|
||||||
|
if count:
|
||||||
|
findings.append({"type": rule.name, "action": "redact", "count": count})
|
||||||
|
|
||||||
|
for marker, value in protected.items():
|
||||||
|
sanitized = sanitized.replace(marker, value)
|
||||||
|
|
||||||
|
return sanitized, findings
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["DlexMaskRule", "sanitize_dlex_output"]
|
||||||
@@ -25,6 +25,7 @@ from .calibrated.output_sanitization import mascarar_pii_output, sanitizar_toxic
|
|||||||
from .calibrated.rules.pinj_patterns import _PINJ_PATTERNS, is_obvious_injection
|
from .calibrated.rules.pinj_patterns import _PINJ_PATTERNS, is_obvious_injection
|
||||||
from .calibrated.rules.tox_blocklist import _EXPLICIT_TERMS, _THREAT_PATTERNS, is_obvious_toxic
|
from .calibrated.rules.tox_blocklist import _EXPLICIT_TERMS, _THREAT_PATTERNS, is_obvious_toxic
|
||||||
from .framework_llm_client import classify_with_framework_llm
|
from .framework_llm_client import classify_with_framework_llm
|
||||||
|
from .dlex_sanitizer import sanitize_dlex_output
|
||||||
from agent_framework.workflows.input_contract import has_meaningful_unmatched_policy, has_semantic_classifier
|
from agent_framework.workflows.input_contract import has_meaningful_unmatched_policy, has_semantic_classifier
|
||||||
|
|
||||||
|
|
||||||
@@ -323,10 +324,28 @@ class CoherenceRail(Guardrail):
|
|||||||
"data": out,
|
"data": out,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
out = await classify_with_framework_llm(
|
try:
|
||||||
_llm(ctx), "COER", {"text": text or "", "context": ctx},
|
out = await classify_with_framework_llm(
|
||||||
profile_name="guardrail", component_name="guardrail.coer", generation_name="guardrail.coer",
|
_llm(ctx), "COER", {"text": text or "", "context": ctx},
|
||||||
)
|
profile_name="guardrail", component_name="guardrail.coer", generation_name="guardrail.coer",
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
# COER is a conversational-coherence rail, not a security boundary.
|
||||||
|
# If its semantic classifier/provider is unavailable, the graph must
|
||||||
|
# continue and let the normal router/workflow clarification logic own
|
||||||
|
# the turn instead of surfacing an infrastructure exception.
|
||||||
|
return RailDecision(
|
||||||
|
code=self.code,
|
||||||
|
allowed=True,
|
||||||
|
reason="Classificador de coerência indisponível; continuidade delegada ao runtime",
|
||||||
|
sanitized_text=text,
|
||||||
|
metadata={
|
||||||
|
"mechanism": "infrastructure_fail_open",
|
||||||
|
"calibrated": True,
|
||||||
|
"error_type": type(exc).__name__,
|
||||||
|
"error": str(exc)[:500],
|
||||||
|
},
|
||||||
|
)
|
||||||
return RailDecision(
|
return RailDecision(
|
||||||
code=self.code, allowed=bool(out.get("allowed", True)),
|
code=self.code, allowed=bool(out.get("allowed", True)),
|
||||||
reason=str(out.get("reason") or out.get("label") or "COER avaliado"),
|
reason=str(out.get("reason") or out.get("label") or "COER avaliado"),
|
||||||
@@ -344,6 +363,9 @@ class LoopRail(Guardrail):
|
|||||||
"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION", "EXECUTING",
|
"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION", "EXECUTING",
|
||||||
"PAUSED", "WAITING_INPUT",
|
"PAUSED", "WAITING_INPUT",
|
||||||
}
|
}
|
||||||
|
# A queued multi-intent transaction may be promoted immediately after
|
||||||
|
# the previous one completed. Prefer the newest live MCP evidence over
|
||||||
|
# a stale top-level COMPLETED status left by checkpoint merge order.
|
||||||
for result in reversed(list(ctx.get("mcp_results") or [])):
|
for result in reversed(list(ctx.get("mcp_results") or [])):
|
||||||
if isinstance(result, dict):
|
if isinstance(result, dict):
|
||||||
result_status = str(result.get("transaction_status") or "").strip().upper()
|
result_status = str(result.get("transaction_status") or "").strip().upper()
|
||||||
@@ -359,6 +381,11 @@ class LoopRail(Guardrail):
|
|||||||
async def evaluate(self, text: str, context: dict[str, Any]) -> RailDecision:
|
async def evaluate(self, text: str, context: dict[str, Any]) -> RailDecision:
|
||||||
ctx = _ctx(context)
|
ctx = _ctx(context)
|
||||||
transaction_status = self._transaction_status(ctx)
|
transaction_status = self._transaction_status(ctx)
|
||||||
|
|
||||||
|
# Parameter values and short confirmations may legitimately repeat in
|
||||||
|
# the same session. VLOOP must not consume input owned by the active
|
||||||
|
# transaction contract; extraction/confirmation remains responsible
|
||||||
|
# for deciding whether the value is valid.
|
||||||
if transaction_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}:
|
if transaction_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}:
|
||||||
return RailDecision(
|
return RailDecision(
|
||||||
code=self.code,
|
code=self.code,
|
||||||
@@ -373,6 +400,7 @@ class LoopRail(Guardrail):
|
|||||||
"calibrated": True,
|
"calibrated": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
normalized = _lower(text).strip()
|
normalized = _lower(text).strip()
|
||||||
history = [_lower(h).strip() for h in ctx.get("history_texts", [])[-6:]]
|
history = [_lower(h).strip() for h in ctx.get("history_texts", [])[-6:]]
|
||||||
repeated = history.count(normalized) >= 2 if normalized else False
|
repeated = history.count(normalized) >= 2 if normalized else False
|
||||||
@@ -805,11 +833,64 @@ class DataLeakageOutputRail(Guardrail):
|
|||||||
}
|
}
|
||||||
protocol_authorization_verified = True
|
protocol_authorization_verified = True
|
||||||
|
|
||||||
|
# DLEX_OUT is permissive for recoverable leakage: when classification
|
||||||
|
# rejects the candidate, mask only generic sensitive values and re-run
|
||||||
|
# the same classifier. The agent/domain remains unaware of this detail.
|
||||||
|
# If sanitization cannot change the candidate, or the sanitized version
|
||||||
|
# is still unsafe, the original blocking behavior is preserved.
|
||||||
|
dlex_sanitized_text = None
|
||||||
|
dlex_findings: list[dict[str, Any]] = []
|
||||||
|
sanitization_recheck = None
|
||||||
|
original_dlex_result = dict(out)
|
||||||
|
if not bool(out.get("allowed", True)):
|
||||||
|
policy = dict(getattr(self, "_guardrail_policy", {}) or {})
|
||||||
|
candidate_sanitized, dlex_findings = sanitize_dlex_output(
|
||||||
|
original_text,
|
||||||
|
policy=policy,
|
||||||
|
exclude_values=matched_expected_protocols,
|
||||||
|
)
|
||||||
|
if candidate_sanitized != original_text:
|
||||||
|
sanitized_classifier_text = candidate_sanitized
|
||||||
|
sanitized_classifier_ctx: dict[str, Any] = ctx
|
||||||
|
if matched_expected_protocols:
|
||||||
|
sanitized_classifier_text = _mask_authorized_protocol_values(
|
||||||
|
sanitized_classifier_text, matched_expected_protocols
|
||||||
|
)
|
||||||
|
sanitized_classifier_ctx = _mask_authorized_protocol_values(
|
||||||
|
ctx, matched_expected_protocols
|
||||||
|
)
|
||||||
|
sanitization_recheck = await classify_with_framework_llm(
|
||||||
|
_llm(ctx),
|
||||||
|
"DLEX_OUT",
|
||||||
|
{"text": sanitized_classifier_text, "context": sanitized_classifier_ctx},
|
||||||
|
profile_name="grl",
|
||||||
|
component_name="guardrail.dlex_out.sanitization_recheck",
|
||||||
|
generation_name="guardrail.dlex_out.sanitization_recheck",
|
||||||
|
)
|
||||||
|
if bool(sanitization_recheck.get("allowed", True)):
|
||||||
|
dlex_sanitized_text = candidate_sanitized
|
||||||
|
out = {
|
||||||
|
"allowed": True,
|
||||||
|
"label": "SANITIZED",
|
||||||
|
"reason": "DLEX_OUT sanitizado e revalidado com segurança",
|
||||||
|
"original_reason": original_dlex_result.get("reason"),
|
||||||
|
}
|
||||||
|
|
||||||
metadata = {
|
metadata = {
|
||||||
"mechanism": "llm_rail",
|
"mechanism": "llm_rail",
|
||||||
"data": out,
|
"data": out,
|
||||||
"calibrated": True,
|
"calibrated": True,
|
||||||
}
|
}
|
||||||
|
if dlex_findings:
|
||||||
|
metadata.update(
|
||||||
|
{
|
||||||
|
"action": "sanitize" if dlex_sanitized_text is not None else "block",
|
||||||
|
"sanitized": dlex_sanitized_text is not None,
|
||||||
|
"findings": dlex_findings,
|
||||||
|
"original_decision": original_dlex_result,
|
||||||
|
"sanitization_recheck": sanitization_recheck,
|
||||||
|
}
|
||||||
|
)
|
||||||
if matched_expected_protocols:
|
if matched_expected_protocols:
|
||||||
metadata.update(
|
metadata.update(
|
||||||
{
|
{
|
||||||
@@ -823,7 +904,7 @@ class DataLeakageOutputRail(Guardrail):
|
|||||||
code=self.code,
|
code=self.code,
|
||||||
allowed=bool(out.get("allowed", True)),
|
allowed=bool(out.get("allowed", True)),
|
||||||
reason=str(out.get("reason") or out.get("label") or "DLEX_OUT avaliado"),
|
reason=str(out.get("reason") or out.get("label") or "DLEX_OUT avaliado"),
|
||||||
sanitized_text=text,
|
sanitized_text=dlex_sanitized_text,
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1218,7 +1218,17 @@ class EnterpriseRouter:
|
|||||||
or (started_intent and configured_candidate.intent != started_intent)
|
or (started_intent and configured_candidate.intent != started_intent)
|
||||||
or (previous_intent and not previous_intent.startswith("state:") and configured_candidate.intent != previous_intent)
|
or (previous_intent and not previous_intent.startswith("state:") and configured_candidate.intent != previous_intent)
|
||||||
)
|
)
|
||||||
if not different:
|
|
||||||
|
# A configured candidate that resolves to the same intent/agent is not
|
||||||
|
# sufficient to conclude that the active transaction continues. The
|
||||||
|
# user may be starting a *new instance of the same intent* with a
|
||||||
|
# different transactional target (for example cancelling TIM Fashion
|
||||||
|
# while Tamboro is awaiting confirmation). In LLM mode keep the
|
||||||
|
# candidate only as a semantic hint and let the transaction classifier
|
||||||
|
# decide CONTINUE/REPLACE/ABANDON. Without semantic classification we
|
||||||
|
# retain the legacy state lock because guessing replacement from a
|
||||||
|
# keyword alone would be unsafe.
|
||||||
|
if not different and not (self.enable_llm_router and self.llm is not None):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# During parameter collection, a configured keyword may be present in
|
# During parameter collection, a configured keyword may be present in
|
||||||
@@ -1259,6 +1269,11 @@ class EnterpriseRouter:
|
|||||||
"current_intent": started_intent or previous_intent,
|
"current_intent": started_intent or previous_intent,
|
||||||
"transaction_status": state.get("transaction_status"),
|
"transaction_status": state.get("transaction_status"),
|
||||||
"tool_name": active_tx.get("tool_name"),
|
"tool_name": active_tx.get("tool_name"),
|
||||||
|
# Structured arguments are part of the identity of the active
|
||||||
|
# transaction. They let the semantic classifier distinguish a new
|
||||||
|
# instance of the same intent from a continuation without relying on
|
||||||
|
# domain keywords or hard-coded product names.
|
||||||
|
"active_arguments": dict(active_tx.get("arguments") or {}),
|
||||||
"missing_parameters": list(state.get("missing_parameters") or []),
|
"missing_parameters": list(state.get("missing_parameters") or []),
|
||||||
"configured_candidate": (
|
"configured_candidate": (
|
||||||
{
|
{
|
||||||
@@ -1285,11 +1300,15 @@ class EnterpriseRouter:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
system = (
|
system = (
|
||||||
"Você decide apenas se o turno atual continua a transação ativa ou muda de intenção. "
|
"Você decide apenas se o turno atual continua a transação ativa, substitui a instância transacional ativa, "
|
||||||
"Use o significado da mensagem e o contexto transacional; não use palavras isoladas como regra. "
|
"ou muda de intenção. Use o significado da mensagem e o contexto transacional; não use palavras isoladas como regra. "
|
||||||
"A extração dos parâmetros pendentes já foi tentada antes desta etapa e não consumiu o turno. "
|
"A extração dos parâmetros pendentes já foi tentada antes desta etapa e não consumiu o turno. "
|
||||||
"Se ainda assim a mensagem for apenas uma resposta referencial/valor/nome ao dado pendente, retorne CONTINUE. "
|
"Se ainda assim a mensagem for apenas uma resposta referencial/valor/nome ao dado pendente, retorne CONTINUE. "
|
||||||
"Se o usuário passou claramente a perseguir outro objetivo sem desistir explicitamente da ação atual, "
|
"Se o usuário continua com a MESMA intent, mas claramente inicia outra instância da operação com alvo/argumentos diferentes, "
|
||||||
|
"retorne REPLACE e informe a mesma intent e agent. REPLACE significa substituir a transação ativa; não significa estado novo. "
|
||||||
|
"Não use REPLACE quando o usuário apenas complementa/adiciona itens à mesma operação (por exemplo 'também', 'além desses'), "
|
||||||
|
"nem quando apenas reformula ou confirma o mesmo alvo: nesses casos retorne CONTINUE. "
|
||||||
|
"Se o usuário passou claramente a perseguir outro objetivo/intent sem desistir explicitamente da ação atual, "
|
||||||
"retorne SHIFT e a nova intent permitida. "
|
"retorne SHIFT e a nova intent permitida. "
|
||||||
"Se o usuário desistiu explicitamente da ação/transação atual, retorne ABANDON. "
|
"Se o usuário desistiu explicitamente da ação/transação atual, retorne ABANDON. "
|
||||||
"Quando ABANDON vier acompanhado de um novo objetivo, também informe intent e agent desse novo objetivo. "
|
"Quando ABANDON vier acompanhado de um novo objetivo, também informe intent e agent desse novo objetivo. "
|
||||||
@@ -1323,7 +1342,7 @@ class EnterpriseRouter:
|
|||||||
if abandon_only:
|
if abandon_only:
|
||||||
if decision_kind != "ABANDON":
|
if decision_kind != "ABANDON":
|
||||||
return None
|
return None
|
||||||
elif decision_kind not in {"SHIFT", "ABANDON"}:
|
elif decision_kind not in {"SHIFT", "REPLACE", "ABANDON"}:
|
||||||
return None
|
return None
|
||||||
confidence = float(data.get("confidence") or 0.0)
|
confidence = float(data.get("confidence") or 0.0)
|
||||||
if confidence < self.intent_shift_threshold:
|
if confidence < self.intent_shift_threshold:
|
||||||
@@ -1374,7 +1393,42 @@ class EnterpriseRouter:
|
|||||||
mcp_tools=mcp_tools,
|
mcp_tools=mcp_tools,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not intent_name or intent_name == (started_intent or previous_intent):
|
current_intent = started_intent or previous_intent
|
||||||
|
if not intent_name:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Same-intent replacement is a transaction lifecycle event, not a new
|
||||||
|
# conversation state. It cancels only the active transaction instance
|
||||||
|
# and re-enters normal routing for the same configured intent.
|
||||||
|
if decision_kind == "REPLACE":
|
||||||
|
if not current_intent or intent_name != current_intent:
|
||||||
|
return None
|
||||||
|
agent = agent or str(self._agent_for_intent(intent_name) or "").strip()
|
||||||
|
if not agent:
|
||||||
|
return None
|
||||||
|
return RouteDecision(
|
||||||
|
route=agent,
|
||||||
|
agent=agent,
|
||||||
|
intent=intent_name,
|
||||||
|
confidence=confidence,
|
||||||
|
reason=str(data.get("reason") or "Nova instância transacional da mesma intenção."),
|
||||||
|
method="llm",
|
||||||
|
metadata={
|
||||||
|
"transaction_interruption": "same_intent_replacement",
|
||||||
|
"interrupted_state": state_decision.next_state,
|
||||||
|
"interrupted_agent": state_decision.agent,
|
||||||
|
"interrupted_intent": current_intent,
|
||||||
|
"interruption_source": "semantic_classifier",
|
||||||
|
"configured_routing_hint": (
|
||||||
|
configured_candidate.intent if configured_candidate is not None else None
|
||||||
|
),
|
||||||
|
"raw_llm_answer": answer[:1000],
|
||||||
|
},
|
||||||
|
domain=self._domain_for_intent(intent_name),
|
||||||
|
mcp_tools=self._tools_for_intent(intent_name),
|
||||||
|
)
|
||||||
|
|
||||||
|
if intent_name == current_intent:
|
||||||
return None
|
return None
|
||||||
agent = agent or str(self._agent_for_intent(intent_name) or "").strip()
|
agent = agent or str(self._agent_for_intent(intent_name) or "").strip()
|
||||||
if not agent:
|
if not agent:
|
||||||
|
|||||||
@@ -3113,21 +3113,33 @@ class AgentRuntimeMixin:
|
|||||||
self._normalize_transaction_lifecycle(state)
|
self._normalize_transaction_lifecycle(state)
|
||||||
|
|
||||||
# Uma transação em coleta/confirmação não pode aprisionar a sessão. O
|
# Uma transação em coleta/confirmação não pode aprisionar a sessão. O
|
||||||
# EnterpriseRouter é a única fonte para interrupção semântica: SHIFT ou
|
# EnterpriseRouter é a única fonte para interrupção semântica: SHIFT,
|
||||||
# ABANDON. Não existe interpretação lexical de desistência no runtime; o
|
# ABANDON ou replacement da instância transacional na mesma intent. Não
|
||||||
# runtime apenas aplica a decisão do router e limpa os latches da ação.
|
# existe interpretação lexical de desistência no runtime; o runtime apenas
|
||||||
|
# aplica a decisão do router e limpa os latches da ação.
|
||||||
active_before_interruption = self._active_transaction(state)
|
active_before_interruption = self._active_transaction(state)
|
||||||
interruption = str(route_meta.get("transaction_interruption") or "").strip().lower()
|
interruption = str(route_meta.get("transaction_interruption") or "").strip().lower()
|
||||||
if active_before_interruption and interruption in {"intent_shift", "explicit_abandonment"}:
|
if active_before_interruption and interruption in {
|
||||||
|
"intent_shift",
|
||||||
|
"explicit_abandonment",
|
||||||
|
"same_intent_replacement",
|
||||||
|
}:
|
||||||
interrupted_tool = active_before_interruption.get("tool_name")
|
interrupted_tool = active_before_interruption.get("tool_name")
|
||||||
self._finish_active_transaction(state, "CANCELLED")
|
self._finish_active_transaction(state, "CANCELLED")
|
||||||
state["transaction_pre_validation"] = None
|
state["transaction_pre_validation"] = None
|
||||||
|
# Generic framework boundary: consumers do not need to know why the
|
||||||
|
# previous transactional context was invalidated. This deliberately
|
||||||
|
# avoids leaking same_intent_replacement (or any other interruption
|
||||||
|
# subtype) into domain agents/workflows.
|
||||||
|
state["operational_context_reset"] = True
|
||||||
|
if interruption == "explicit_abandonment":
|
||||||
|
action = "cancelled_by_explicit_abandonment"
|
||||||
|
elif interruption == "same_intent_replacement":
|
||||||
|
action = "cancelled_by_same_intent_replacement"
|
||||||
|
else:
|
||||||
|
action = "cancelled_by_intent_shift"
|
||||||
state["tool_policy_result"] = {
|
state["tool_policy_result"] = {
|
||||||
"action": (
|
"action": action,
|
||||||
"cancelled_by_explicit_abandonment"
|
|
||||||
if interruption == "explicit_abandonment"
|
|
||||||
else "cancelled_by_intent_shift"
|
|
||||||
),
|
|
||||||
"tool_name": interrupted_tool,
|
"tool_name": interrupted_tool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ src/agent_framework/guardrails/__init__.py
|
|||||||
src/agent_framework/guardrails/base.py
|
src/agent_framework/guardrails/base.py
|
||||||
src/agent_framework/guardrails/config_loader.py
|
src/agent_framework/guardrails/config_loader.py
|
||||||
src/agent_framework/guardrails/custom_rails.py
|
src/agent_framework/guardrails/custom_rails.py
|
||||||
|
src/agent_framework/guardrails/dlex_sanitizer.py
|
||||||
src/agent_framework/guardrails/executor.py
|
src/agent_framework/guardrails/executor.py
|
||||||
src/agent_framework/guardrails/framework_llm_client.py
|
src/agent_framework/guardrails/framework_llm_client.py
|
||||||
src/agent_framework/guardrails/langgraph_adapters.py
|
src/agent_framework/guardrails/langgraph_adapters.py
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,201 @@
|
|||||||
|
"""Deterministic, framework-owned sanitization for DLEX_OUT.
|
||||||
|
|
||||||
|
The DLEX classifier decides whether a candidate response contains information
|
||||||
|
that must not be exposed. When the finding is recoverable, this module masks
|
||||||
|
only the sensitive values so the response can be revalidated and preserved.
|
||||||
|
|
||||||
|
Rules are intentionally generic and live in the framework, never in domain
|
||||||
|
agents. Deployments may extend the built-ins without changing agent code via:
|
||||||
|
|
||||||
|
* ``GUARDRAIL_DLEX_OUT_EXTRA_MASKS_JSON`` (JSON list of rule objects), or
|
||||||
|
* the DLEX_OUT rail policy key ``sanitize_patterns`` in guardrails.yaml.
|
||||||
|
|
||||||
|
Each extension rule accepts ``name``, ``pattern`` and ``replacement``. Patterns
|
||||||
|
are regular expressions. Raw matched values are never returned in findings.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from typing import Any, Iterable
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class DlexMaskRule:
|
||||||
|
name: str
|
||||||
|
pattern: str
|
||||||
|
replacement: str
|
||||||
|
flags: int = re.IGNORECASE
|
||||||
|
|
||||||
|
|
||||||
|
# Conservative defaults: values that are broadly unsafe to echo verbatim and
|
||||||
|
# can be redacted without changing the semantic meaning of an answer.
|
||||||
|
_BUILTIN_RULES: tuple[DlexMaskRule, ...] = (
|
||||||
|
DlexMaskRule(
|
||||||
|
"payment_barcode",
|
||||||
|
# Brazilian boleto/payment representations normally contain 44, 47 or
|
||||||
|
# 48 digits and may be formatted with dots/spaces. Keep the match on the
|
||||||
|
# numeric token only so surrounding prose is preserved.
|
||||||
|
r"(?<!\d)(?=[0-9.\-\s]{44,90}(?!\d))(?:\d[.\-\s]*){44,48}(?!\d)",
|
||||||
|
"[CODIGO_DE_PAGAMENTO_OCULTADO]",
|
||||||
|
flags=0,
|
||||||
|
),
|
||||||
|
DlexMaskRule(
|
||||||
|
"jwt",
|
||||||
|
r"\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b",
|
||||||
|
"[TOKEN_OCULTADO]",
|
||||||
|
flags=0,
|
||||||
|
),
|
||||||
|
DlexMaskRule(
|
||||||
|
"openai_style_secret",
|
||||||
|
r"\bsk-[A-Za-z0-9_-]{10,}\b",
|
||||||
|
"[CREDENCIAL_OCULTADA]",
|
||||||
|
flags=0,
|
||||||
|
),
|
||||||
|
DlexMaskRule(
|
||||||
|
"labelled_secret",
|
||||||
|
r"(?i)\b(api[_ -]?key|secret|segredo|token|senha|password|credential|credencial)\b(\s*[:=]\s*)([^\s,;]+)",
|
||||||
|
r"\1\2[DADO_PROTEGIDO]",
|
||||||
|
flags=0,
|
||||||
|
),
|
||||||
|
DlexMaskRule(
|
||||||
|
"cpf",
|
||||||
|
r"\b\d{3}\.\d{3}\.\d{3}-\d{2}\b",
|
||||||
|
"[CPF_OCULTADO]",
|
||||||
|
flags=0,
|
||||||
|
),
|
||||||
|
DlexMaskRule(
|
||||||
|
"cnpj",
|
||||||
|
r"\b\d{2}\.\d{3}\.\d{3}/\d{4}-\d{2}\b",
|
||||||
|
"[CNPJ_OCULTADO]",
|
||||||
|
flags=0,
|
||||||
|
),
|
||||||
|
DlexMaskRule(
|
||||||
|
"email",
|
||||||
|
r"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b",
|
||||||
|
"[EMAIL_OCULTADO]",
|
||||||
|
),
|
||||||
|
DlexMaskRule(
|
||||||
|
"labelled_phone",
|
||||||
|
r"(?i)\b(telefone|celular|whatsapp|msisdn)\b(\s*[:=]?\s*)(?:\+?55\s*)?(?:\(?\d{2}\)?[\s.-]*)?\d{4,5}[\s.-]*\d{4}\b",
|
||||||
|
r"\1\2[TELEFONE_OCULTADO]",
|
||||||
|
flags=0,
|
||||||
|
),
|
||||||
|
DlexMaskRule(
|
||||||
|
"uuid",
|
||||||
|
r"\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}\b",
|
||||||
|
"[IDENTIFICADOR_OCULTADO]",
|
||||||
|
flags=0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_rule(raw: Any) -> DlexMaskRule | None:
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
return None
|
||||||
|
name = str(raw.get("name") or "custom").strip()
|
||||||
|
pattern = str(raw.get("pattern") or "").strip()
|
||||||
|
replacement = str(raw.get("replacement") or "[DADO_PROTEGIDO]")
|
||||||
|
if not pattern:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
re.compile(pattern)
|
||||||
|
except re.error:
|
||||||
|
return None
|
||||||
|
return DlexMaskRule(name=name, pattern=pattern, replacement=replacement, flags=0)
|
||||||
|
|
||||||
|
|
||||||
|
def _extra_rules_from_env() -> list[DlexMaskRule]:
|
||||||
|
raw = os.getenv("GUARDRAIL_DLEX_OUT_EXTRA_MASKS_JSON", "").strip()
|
||||||
|
if not raw:
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
data = json.loads(raw)
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
if isinstance(data, dict):
|
||||||
|
data = [data]
|
||||||
|
return [rule for item in (data or []) if (rule := _parse_rule(item)) is not None]
|
||||||
|
|
||||||
|
|
||||||
|
def _extra_rules_from_policy(policy: dict[str, Any] | None) -> list[DlexMaskRule]:
|
||||||
|
data = dict(policy or {}).get("sanitize_patterns") or []
|
||||||
|
if isinstance(data, dict):
|
||||||
|
data = [data]
|
||||||
|
return [rule for item in data if (rule := _parse_rule(item)) is not None]
|
||||||
|
|
||||||
|
|
||||||
|
def _mask_payment_sequences(text: str) -> tuple[str, int]:
|
||||||
|
"""Mask 44/47/48-digit payment tokens without consuming surrounding prose.
|
||||||
|
|
||||||
|
Regex alone is awkward because formatted boletos have variable punctuation.
|
||||||
|
We scan numeric/punctuation runs, count digits, and only redact the run when
|
||||||
|
it has a canonical payment digit count.
|
||||||
|
"""
|
||||||
|
token_re = re.compile(r"(?<!\d)\d(?:[.\-\s]*\d){43,47}(?!\d)")
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
def repl(match: re.Match[str]) -> str:
|
||||||
|
nonlocal count
|
||||||
|
token = match.group(0)
|
||||||
|
digits = re.sub(r"\D", "", token)
|
||||||
|
if len(digits) in {44, 47, 48}:
|
||||||
|
count += 1
|
||||||
|
return "[CODIGO_DE_PAGAMENTO_OCULTADO]"
|
||||||
|
return token
|
||||||
|
|
||||||
|
return token_re.sub(repl, text), count
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_dlex_output(
|
||||||
|
text: str,
|
||||||
|
*,
|
||||||
|
policy: dict[str, Any] | None = None,
|
||||||
|
exclude_values: Iterable[str] | None = None,
|
||||||
|
) -> tuple[str, list[dict[str, Any]]]:
|
||||||
|
"""Return ``(sanitized_text, findings)`` without exposing raw matches.
|
||||||
|
|
||||||
|
``exclude_values`` contains explicitly authorized values (for example a
|
||||||
|
workflow-generated protocol). They are temporarily protected so generic
|
||||||
|
masking never alters them.
|
||||||
|
"""
|
||||||
|
original = text or ""
|
||||||
|
sanitized = original
|
||||||
|
findings: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
protected: dict[str, str] = {}
|
||||||
|
for index, value in enumerate(exclude_values or []):
|
||||||
|
value = str(value or "").strip()
|
||||||
|
if not value or value not in sanitized:
|
||||||
|
continue
|
||||||
|
marker = f"__DLEX_AUTHORIZED_{index}__"
|
||||||
|
sanitized = sanitized.replace(value, marker)
|
||||||
|
protected[marker] = value
|
||||||
|
|
||||||
|
# Payment sequences use digit-count validation rather than a plain rule.
|
||||||
|
sanitized, payment_count = _mask_payment_sequences(sanitized)
|
||||||
|
if payment_count:
|
||||||
|
findings.append({"type": "payment_barcode", "action": "redact", "count": payment_count})
|
||||||
|
|
||||||
|
rules = [r for r in _BUILTIN_RULES if r.name != "payment_barcode"]
|
||||||
|
rules.extend(_extra_rules_from_env())
|
||||||
|
rules.extend(_extra_rules_from_policy(policy))
|
||||||
|
|
||||||
|
for rule in rules:
|
||||||
|
try:
|
||||||
|
regex = re.compile(rule.pattern, rule.flags)
|
||||||
|
except re.error:
|
||||||
|
continue
|
||||||
|
sanitized, count = regex.subn(rule.replacement, sanitized)
|
||||||
|
if count:
|
||||||
|
findings.append({"type": rule.name, "action": "redact", "count": count})
|
||||||
|
|
||||||
|
for marker, value in protected.items():
|
||||||
|
sanitized = sanitized.replace(marker, value)
|
||||||
|
|
||||||
|
return sanitized, findings
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["DlexMaskRule", "sanitize_dlex_output"]
|
||||||
@@ -25,6 +25,7 @@ from .calibrated.output_sanitization import mascarar_pii_output, sanitizar_toxic
|
|||||||
from .calibrated.rules.pinj_patterns import _PINJ_PATTERNS, is_obvious_injection
|
from .calibrated.rules.pinj_patterns import _PINJ_PATTERNS, is_obvious_injection
|
||||||
from .calibrated.rules.tox_blocklist import _EXPLICIT_TERMS, _THREAT_PATTERNS, is_obvious_toxic
|
from .calibrated.rules.tox_blocklist import _EXPLICIT_TERMS, _THREAT_PATTERNS, is_obvious_toxic
|
||||||
from .framework_llm_client import classify_with_framework_llm
|
from .framework_llm_client import classify_with_framework_llm
|
||||||
|
from .dlex_sanitizer import sanitize_dlex_output
|
||||||
from agent_framework.workflows.input_contract import has_meaningful_unmatched_policy, has_semantic_classifier
|
from agent_framework.workflows.input_contract import has_meaningful_unmatched_policy, has_semantic_classifier
|
||||||
|
|
||||||
|
|
||||||
@@ -832,11 +833,64 @@ class DataLeakageOutputRail(Guardrail):
|
|||||||
}
|
}
|
||||||
protocol_authorization_verified = True
|
protocol_authorization_verified = True
|
||||||
|
|
||||||
|
# DLEX_OUT is permissive for recoverable leakage: when classification
|
||||||
|
# rejects the candidate, mask only generic sensitive values and re-run
|
||||||
|
# the same classifier. The agent/domain remains unaware of this detail.
|
||||||
|
# If sanitization cannot change the candidate, or the sanitized version
|
||||||
|
# is still unsafe, the original blocking behavior is preserved.
|
||||||
|
dlex_sanitized_text = None
|
||||||
|
dlex_findings: list[dict[str, Any]] = []
|
||||||
|
sanitization_recheck = None
|
||||||
|
original_dlex_result = dict(out)
|
||||||
|
if not bool(out.get("allowed", True)):
|
||||||
|
policy = dict(getattr(self, "_guardrail_policy", {}) or {})
|
||||||
|
candidate_sanitized, dlex_findings = sanitize_dlex_output(
|
||||||
|
original_text,
|
||||||
|
policy=policy,
|
||||||
|
exclude_values=matched_expected_protocols,
|
||||||
|
)
|
||||||
|
if candidate_sanitized != original_text:
|
||||||
|
sanitized_classifier_text = candidate_sanitized
|
||||||
|
sanitized_classifier_ctx: dict[str, Any] = ctx
|
||||||
|
if matched_expected_protocols:
|
||||||
|
sanitized_classifier_text = _mask_authorized_protocol_values(
|
||||||
|
sanitized_classifier_text, matched_expected_protocols
|
||||||
|
)
|
||||||
|
sanitized_classifier_ctx = _mask_authorized_protocol_values(
|
||||||
|
ctx, matched_expected_protocols
|
||||||
|
)
|
||||||
|
sanitization_recheck = await classify_with_framework_llm(
|
||||||
|
_llm(ctx),
|
||||||
|
"DLEX_OUT",
|
||||||
|
{"text": sanitized_classifier_text, "context": sanitized_classifier_ctx},
|
||||||
|
profile_name="grl",
|
||||||
|
component_name="guardrail.dlex_out.sanitization_recheck",
|
||||||
|
generation_name="guardrail.dlex_out.sanitization_recheck",
|
||||||
|
)
|
||||||
|
if bool(sanitization_recheck.get("allowed", True)):
|
||||||
|
dlex_sanitized_text = candidate_sanitized
|
||||||
|
out = {
|
||||||
|
"allowed": True,
|
||||||
|
"label": "SANITIZED",
|
||||||
|
"reason": "DLEX_OUT sanitizado e revalidado com segurança",
|
||||||
|
"original_reason": original_dlex_result.get("reason"),
|
||||||
|
}
|
||||||
|
|
||||||
metadata = {
|
metadata = {
|
||||||
"mechanism": "llm_rail",
|
"mechanism": "llm_rail",
|
||||||
"data": out,
|
"data": out,
|
||||||
"calibrated": True,
|
"calibrated": True,
|
||||||
}
|
}
|
||||||
|
if dlex_findings:
|
||||||
|
metadata.update(
|
||||||
|
{
|
||||||
|
"action": "sanitize" if dlex_sanitized_text is not None else "block",
|
||||||
|
"sanitized": dlex_sanitized_text is not None,
|
||||||
|
"findings": dlex_findings,
|
||||||
|
"original_decision": original_dlex_result,
|
||||||
|
"sanitization_recheck": sanitization_recheck,
|
||||||
|
}
|
||||||
|
)
|
||||||
if matched_expected_protocols:
|
if matched_expected_protocols:
|
||||||
metadata.update(
|
metadata.update(
|
||||||
{
|
{
|
||||||
@@ -850,7 +904,7 @@ class DataLeakageOutputRail(Guardrail):
|
|||||||
code=self.code,
|
code=self.code,
|
||||||
allowed=bool(out.get("allowed", True)),
|
allowed=bool(out.get("allowed", True)),
|
||||||
reason=str(out.get("reason") or out.get("label") or "DLEX_OUT avaliado"),
|
reason=str(out.get("reason") or out.get("label") or "DLEX_OUT avaliado"),
|
||||||
sanitized_text=text,
|
sanitized_text=dlex_sanitized_text,
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user