Melhorias no guardrails de FRASEOLOGY. Melhorias no Transaction Workflow

This commit is contained in:
2026-08-19 16:08:03 -03:00
parent 560e79d21b
commit f9c66b4792
3 changed files with 87 additions and 212 deletions

View File

@@ -328,6 +328,47 @@ class ProactiveOfferRail(Guardrail):
)
def _sanitize_low_risk_phraseology(text: str, reason: str) -> str | None:
"""Remove apenas fechamentos/redirecionamentos de baixo risco.
FRASEOLOGIA continua fail-closed para conteúdo material. Para B4 e ofertas
genéricas de continuação, porém, bloquear toda uma resposta grounded piora a
UX; nesses casos removemos somente a sentença ofensora.
"""
normalized_reason = (reason or "").casefold()
low_risk = any(token in normalized_reason for token in (
"viola b4", "outro canal", "atendimento especializado",
"realizar alguma ação", "oferta de ação", "orienta o cliente",
))
if not low_risk:
return None
forbidden = (
"entre em contato", "fale com um atendente", "procure uma loja",
"acesse o app", "acesse o site", "atendimento especializado",
"área de planos", "area de planos", "é só me avisar",
"e so me avisar", "realizar alguma ação", "realizar alguma acao",
"gerenciar esses serviços", "gerenciar esses servicos",
)
sentences = re.split(r"(?<=[.!?])\s+", (text or "").strip())
kept: list[str] = []
removed = False
for sentence in sentences:
normalized = sentence.casefold()
proactive = (
("se quiser" in normalized or "caso queira" in normalized or "se desejar" in normalized)
and any(token in normalized for token in ("realizar", "gerenciar", "cancelar", "contratar", "alterar", "ação", "acao"))
)
if proactive or any(token in normalized for token in forbidden):
removed = True
continue
kept.append(sentence.strip())
sanitized = " ".join(x for x in kept if x).strip()
if removed and sanitized:
return sanitized
return None
class PhraseologyRail(Guardrail):
"""FRASEOLOGIA calibrado: bloqueia fraseados proibidos do agente."""
code = "FRASEOLOGIA"
@@ -339,9 +380,26 @@ class PhraseologyRail(Guardrail):
_llm(ctx), "FRASEOLOGIA", {"text": text or "", "context": ctx},
profile_name="grl", component_name="guardrail.fraseologia", generation_name="guardrail.fraseologia",
)
allowed = bool(out.get("allowed", True))
reason = str(out.get("reason") or out.get("label") or "FRASEOLOGIA avaliado")
if not allowed:
sanitized = _sanitize_low_risk_phraseology(text or "", reason)
if sanitized:
return RailDecision(
code=self.code,
allowed=True,
reason=f"FRASEOLOGIA sanitizada: {reason}",
sanitized_text=sanitized,
metadata={
"mechanism": "llm_rail+deterministic_sanitize",
"data": out,
"calibrated": True,
"original_allowed": False,
},
)
return RailDecision(
code=self.code, allowed=bool(out.get("allowed", True)),
reason=str(out.get("reason") or out.get("label") or "FRASEOLOGIA avaliado"),
code=self.code, allowed=allowed,
reason=reason,
sanitized_text=text, metadata={"mechanism": "llm_rail", "data": out, "calibrated": True},
)

View File

@@ -1167,7 +1167,25 @@ class AgentRuntimeMixin:
return None
def _select_transactional_tool(self, tools: list[str], text: str) -> str | None:
return self._transactional_action_match(text, tools)
"""Seleciona a ação transacional da intent atual.
O match por ``selection_keywords`` continua tendo precedência. Porém, depois
que o EnterpriseRouter já restringiu ``tools`` às capabilities da intent,
uma única tool transacional é uma escolha determinística e segura. Isso
evita perder frases naturais como ``quero cancelar TIM Fashion Mensal`` ou
``não contratei esse serviço`` só porque elas não repetem literalmente uma
keyword de ``tools.yaml``.
"""
matched = self._transactional_action_match(text, tools)
if matched:
return matched
transactional = [
tool
for tool in tools
if self._resolve_tool_execution_policy(tool).get("operation_type") == "transactional"
]
return transactional[0] if len(transactional) == 1 else None
@staticmethod
def _agent_state_prefix(agent_name: str | None) -> str:
@@ -1521,6 +1539,11 @@ class AgentRuntimeMixin:
**previous_args,
**{k: v for k, v in new_args.items() if v not in (None, "", [], {})},
}
# Execute parameter extraction before deciding whether the workflow
# must enter COLLECTING_PARAMETERS. Otherwise parameters declared
# with strategy=llm in mcp_parameter_mapping.yaml are invisible to
# the deterministic transaction state machine.
arguments = await self._extract_mcp_parameters(tool_name, arguments, state)
policy = self._resolve_tool_execution_policy(tool_name, arguments)
missing = self._missing_required_arguments(policy, arguments)
if missing:
@@ -1653,6 +1676,9 @@ class AgentRuntimeMixin:
aliases=aliases,
extra_args=self._extract_action_arguments(text),
)
# Extract parameters (including LLM-declared extraction rules) before
# validating required fields and before persisting the pending call.
action_args = await self._extract_mcp_parameters(selected_action, action_args, state)
policy = self._resolve_tool_execution_policy(selected_action, action_args)
selected = {"tool_name": selected_action, "arguments": action_args}
state["selected_tool_call"] = selected