Projeto do Agent Contas ORACLE
This commit is contained in:
361
tests/migration/test_adversarial_parity.py
Normal file
361
tests/migration/test_adversarial_parity.py
Normal file
@@ -0,0 +1,361 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
from agent_framework.channels.interruption import evaluate_interruption
|
||||
from agent_framework.guardrails.calibrated.contracts import GuardRailContext
|
||||
from agent_framework.guardrails.calibrated.rails.anatel import AnatelRail
|
||||
|
||||
from app.domain.contas.vas_variation import VariedCharge, varied_current_charges
|
||||
from app.domain.contas.workflow_actions import build_contas_workflow_actions
|
||||
|
||||
|
||||
PAST = "2026-03-10T00:00:00.000Z"
|
||||
CURRENT = "2026-04-07T00:00:00.000Z"
|
||||
|
||||
|
||||
def _group(*items: dict, type_: str = "servicos_contratados_de_parceiros") -> dict:
|
||||
return {"type": type_, "desc": "Serviços de valor adicionado", "items": list(items)}
|
||||
|
||||
|
||||
def test_vas_variation_preserva_multiset_e_direcao_atual():
|
||||
payload = {"invoiceVariation": [
|
||||
_group(
|
||||
{"desc": "Qualifica Mensal", "value": "19.99", "invoice": PAST},
|
||||
{"desc": "Qualifica Mensal", "value": "19.99", "invoice": CURRENT},
|
||||
{"desc": "Qualifica Mensal", "value": "19.99", "invoice": CURRENT},
|
||||
)
|
||||
]}
|
||||
result = varied_current_charges(payload)
|
||||
assert result == (VariedCharge(desc="Qualifica Mensal", value=result[0].value),)
|
||||
assert str(result[0].value) == "19.99"
|
||||
|
||||
|
||||
def test_vas_variation_streaming_nao_entra_no_recorte_avulso_padrao():
|
||||
payload = {"invoiceVariation": [
|
||||
_group(
|
||||
{"desc": "Netflix Padrão", "value": "20.90", "invoice": CURRENT},
|
||||
type_="streaming",
|
||||
)
|
||||
]}
|
||||
assert varied_current_charges(payload) == ()
|
||||
|
||||
|
||||
def test_post_finalize_replay_e_short_circuit_framework():
|
||||
decision = evaluate_interruption(
|
||||
payload={},
|
||||
message_text="alô, ainda está aí?",
|
||||
session_metadata={
|
||||
"conversation_closed": True,
|
||||
"terminal_status": "nao_resolvido",
|
||||
"last_assistant_text": "Aguarde um instante na linha. Protocolo: 2026492272073",
|
||||
"last_assistant_is_interruptible": True,
|
||||
},
|
||||
)
|
||||
assert decision.action == "replay"
|
||||
assert decision.reason == "post_finalize"
|
||||
assert "2026492272073" in decision.replay_text
|
||||
assert decision.is_interruptible is False
|
||||
|
||||
|
||||
def test_anatel_rail_e_framework_native_sem_dependencia_legado():
|
||||
rail = AnatelRail()
|
||||
blocked = rail.evaluate(
|
||||
GuardRailContext(
|
||||
session_id="s1",
|
||||
user_text="O ajuste foi concluído.",
|
||||
agent_metadata={
|
||||
"tipo_fluxo": "ajuste",
|
||||
"requer_protocolo": True,
|
||||
"expected_protocols": ["2026492272073"],
|
||||
},
|
||||
)
|
||||
)
|
||||
assert blocked.allowed is False
|
||||
assert blocked.sanitized_text
|
||||
assert "protocolo" in blocked.sanitized_text.lower()
|
||||
|
||||
approved = rail.evaluate(
|
||||
GuardRailContext(
|
||||
session_id="s1",
|
||||
user_text="Ajuste concluído. Protocolo: 2026492272073.",
|
||||
agent_metadata={"tipo_fluxo": "ajuste"},
|
||||
)
|
||||
)
|
||||
assert approved.allowed is True
|
||||
|
||||
|
||||
class _FakeClient:
|
||||
def __init__(self):
|
||||
self.contestar_calls = 0
|
||||
|
||||
def billing_analysis(self, msisdn: str):
|
||||
return {
|
||||
"Servicos de Valor Adicionado": [
|
||||
{"desc": "Pacote Musica", "valor": "10,00"}
|
||||
]
|
||||
}
|
||||
|
||||
def contestar(self, payload):
|
||||
self.contestar_calls += 1
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
class _FakeService:
|
||||
def __init__(self):
|
||||
self.client = _FakeClient()
|
||||
|
||||
|
||||
def test_cval_bloqueia_contestacao_antes_do_side_effect():
|
||||
service = _FakeService()
|
||||
reg = build_contas_workflow_actions(service) # type: ignore[arg-type]
|
||||
action = reg.get("abrir_contestacao_cliente")
|
||||
result = action(
|
||||
{
|
||||
"msisdn": "11999999999",
|
||||
"items": [
|
||||
{
|
||||
"itemName": "Pacote nao cobrado",
|
||||
"claimedAmount": "10",
|
||||
"validatedAmount": "10",
|
||||
}
|
||||
],
|
||||
},
|
||||
{"input": {}},
|
||||
)
|
||||
assert result["success"] is False
|
||||
assert result["blocked"] is True
|
||||
assert result["guardrail_code"] == "CVAL"
|
||||
assert service.client.contestar_calls == 0
|
||||
|
||||
from app.domain.contas.item_matcher import SimilarityItemMatcher
|
||||
|
||||
|
||||
def test_similarity_matcher_recupera_erros_de_transcricao_comuns():
|
||||
matcher = SimilarityItemMatcher()
|
||||
candidates = [
|
||||
"Tamboro Mensal",
|
||||
"Netflix Padrão",
|
||||
"Truecaller Semanal",
|
||||
"MasterChef Mensal",
|
||||
"Paramount+ Mensal",
|
||||
]
|
||||
assert matcher.match("mestershef", candidates)[0] == "MasterChef Mensal"
|
||||
assert matcher.match("trucaller", candidates)[0] == "Truecaller Semanal"
|
||||
assert matcher.match("paramaunt", candidates)[0] == "Paramount+ Mensal"
|
||||
|
||||
|
||||
def test_tool_clarification_aceita_opcao_ordinal():
|
||||
from agent_framework.runtime.agent_runtime import AgentRuntimeMixin
|
||||
|
||||
options = [
|
||||
{"label": "Tamboro Mensal — linha final 1111", "value": "Tamboro Mensal"},
|
||||
{"label": "Tamboro Mensal — linha final 2222", "value": "Tamboro Mensal"},
|
||||
]
|
||||
selected = AgentRuntimeMixin._choose_tool_clarification_option("2", options)
|
||||
assert selected == options[1]
|
||||
selected_by_name = AgentRuntimeMixin._choose_tool_clarification_option(
|
||||
"Tamboro Mensal — linha final 1111", options
|
||||
)
|
||||
assert selected_by_name == options[0]
|
||||
|
||||
|
||||
def test_output_guardrail_context_extrai_protocolos_das_tools():
|
||||
from app.workflows.agent_graph import AgentWorkflow
|
||||
|
||||
ctx = AgentWorkflow._output_guardrail_context(
|
||||
{
|
||||
"context": {},
|
||||
"mcp_results": [
|
||||
{
|
||||
"ok": True,
|
||||
"result": {
|
||||
"result": {
|
||||
"protocolo_id": "2026492272073",
|
||||
"nested": {"protocolNumber": "2026492272074"},
|
||||
}
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
assert ctx["requer_protocolo"] is True
|
||||
assert ctx["tipo_fluxo"] == "ajuste"
|
||||
assert ctx["expected_protocols"] == ["2026492272073", "2026492272074"]
|
||||
assert ctx["tool_executed"] is True
|
||||
|
||||
|
||||
def test_fix_whole_utterance_transcription_preserva_fronteira_de_frase():
|
||||
from agent_framework.channels.transcription import fix_whole_utterance_transcription
|
||||
|
||||
assert fix_whole_utterance_transcription("Fim") == "Sim"
|
||||
assert fix_whole_utterance_transcription("Mim?") == "Sim"
|
||||
assert fix_whole_utterance_transcription("chegou ao fim") == "chegou ao fim"
|
||||
assert fix_whole_utterance_transcription("isso é pra mim") == "isso é pra mim"
|
||||
|
||||
|
||||
def test_post_finalize_replay_preserva_terminal_status_e_fallback_sem_historico():
|
||||
decision = evaluate_interruption(
|
||||
payload={},
|
||||
message_text="oi",
|
||||
session_metadata={
|
||||
"conversation_closed": True,
|
||||
"terminal_status": "nao_resolvido",
|
||||
"last_assistant_text": "",
|
||||
"terminal_replay_text": "",
|
||||
},
|
||||
terminal_fallback_text=(
|
||||
"Por aqui finalizamos o tratamento da sua solicitação. "
|
||||
"Aguarde um instante na linha."
|
||||
),
|
||||
)
|
||||
assert decision.action == "replay"
|
||||
assert decision.reason == "post_finalize"
|
||||
assert decision.terminal_status == "nao_resolvido"
|
||||
assert decision.replay_text.startswith("Por aqui finalizamos")
|
||||
assert decision.is_interruptible is False
|
||||
|
||||
|
||||
def test_post_finalize_replay_status_invalido_usa_fallback_seguro():
|
||||
decision = evaluate_interruption(
|
||||
payload={},
|
||||
message_text="oi",
|
||||
session_metadata={"conversation_closed": True},
|
||||
terminal_fallback_text="Atendimento já encerrado.",
|
||||
)
|
||||
assert decision.action == "replay"
|
||||
assert decision.terminal_status == "erro_falha_sistema"
|
||||
assert decision.replay_text == "Atendimento já encerrado."
|
||||
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entrada", "esperado"),
|
||||
[
|
||||
("Fim", "Sim"),
|
||||
("fim", "Sim"),
|
||||
("FIM", "Sim"),
|
||||
("Fim.", "Sim"),
|
||||
(" fim ", "Sim"),
|
||||
("fim!", "Sim"),
|
||||
("Mim", "Sim"),
|
||||
("mim", "Sim"),
|
||||
("Mim?", "Sim"),
|
||||
("chegou ao fim", "chegou ao fim"),
|
||||
("isso é pra mim", "isso é pra mim"),
|
||||
("fim de semana", "fim de semana"),
|
||||
("Fim, pode encerrar", "Fim, pode encerrar"),
|
||||
("mim mesmo", "mim mesmo"),
|
||||
("Sim", "Sim"),
|
||||
("não", "não"),
|
||||
("", ""),
|
||||
(" ", " "),
|
||||
("cancela o Tamboro", "cancela o Tamboro"),
|
||||
],
|
||||
)
|
||||
def test_transcription_fix_paridade_original(entrada: str, esperado: str):
|
||||
from agent_framework.channels.transcription import fix_whole_utterance_transcription
|
||||
|
||||
assert fix_whole_utterance_transcription(entrada) == esperado
|
||||
|
||||
|
||||
class _ClassifierResponse:
|
||||
def __init__(self, content: str):
|
||||
self.content = content
|
||||
|
||||
|
||||
class _ClassifierLLM:
|
||||
def __init__(self, content: str = "0", *, fail: bool = False):
|
||||
self.content = content
|
||||
self.fail = fail
|
||||
self.calls = []
|
||||
|
||||
async def ainvoke(self, messages, **kwargs):
|
||||
self.calls.append((messages, kwargs))
|
||||
if self.fail:
|
||||
raise RuntimeError("classifier unavailable")
|
||||
return _ClassifierResponse(self.content)
|
||||
|
||||
|
||||
def test_processing_interruption_interrompivel_pede_classificacao():
|
||||
decision = evaluate_interruption(
|
||||
payload={"processing_interruption": {"heard_text": "cancelar também"}},
|
||||
message_text="cancelar também",
|
||||
session_metadata={
|
||||
"last_assistant_text": "Vou verificar sua fatura.",
|
||||
"last_assistant_is_interruptible": True,
|
||||
},
|
||||
)
|
||||
assert decision.action == "classify"
|
||||
assert decision.replay_text == "Vou verificar sua fatura."
|
||||
assert decision.heard_text == "cancelar também"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_processing_interruption_classifier_resultado_1_reprocessa():
|
||||
from agent_framework.channels.interruption import classify_processing_interruption
|
||||
|
||||
llm = _ClassifierLLM("1")
|
||||
result = await classify_processing_interruption(
|
||||
llm,
|
||||
original_agent="Vou orientar sobre o cancelamento.",
|
||||
original_client="Quero cancelar VOD e o",
|
||||
supplement_client="Aluguel de Filme 1.",
|
||||
)
|
||||
assert result is True
|
||||
assert llm.calls
|
||||
_, kwargs = llm.calls[0]
|
||||
assert kwargs["profile_name"] == "processing_interruption_classifier"
|
||||
assert kwargs["component_name"] == "processing_interruption_classifier"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_processing_interruption_classifier_falha_faz_replay_seguro():
|
||||
from agent_framework.channels.interruption import classify_processing_interruption
|
||||
|
||||
llm = _ClassifierLLM(fail=True)
|
||||
result = await classify_processing_interruption(
|
||||
llm,
|
||||
original_agent="Aqui vai a resposta.",
|
||||
original_client="quanto custa?",
|
||||
supplement_client="oi",
|
||||
)
|
||||
assert result is False
|
||||
|
||||
|
||||
|
||||
def test_finalizacao_informacional_infer_tipo_pela_fatura():
|
||||
from app.domain.contas.workflow_actions import _infer_informational_vas_types
|
||||
|
||||
invoice_detail = {
|
||||
"11999999999": {
|
||||
"Serviços Bundle Inclusos": [{"desc": "EXA Cloud", "value": 0}],
|
||||
"Serviços Contratados de Terceiros": [{"desc": "Netflix Premium", "value": 59.9}],
|
||||
"SVA Detalhe Total": [{"desc": "Aluguel de Filme 2", "value": 19.9}],
|
||||
}
|
||||
}
|
||||
assert _infer_informational_vas_types(
|
||||
["EXA Cloud", "Netflix Premium", "Aluguel de Filme 2"], invoice_detail
|
||||
) == {"bundle", "estrategico", "avulso"}
|
||||
assert _infer_informational_vas_types(["exa clod"], invoice_detail) == {"bundle"}
|
||||
|
||||
|
||||
def test_finalizacao_informacional_infer_tipo_de_billing_analysis_json():
|
||||
import json
|
||||
from app.domain.contas.workflow_actions import _infer_informational_vas_types
|
||||
payload = {
|
||||
"currentInvoice": [
|
||||
{"desc": "Serviços de valor adicionado", "items": [
|
||||
{"contestable": True, "desc": "Aluguel de Filme 3", "type": "servicos_contratados_de_parceiros"},
|
||||
{"contestable": False, "desc": "VOD + Canais Abertos", "type": "servicos_contratados_de_parceiros"},
|
||||
]},
|
||||
{"desc": "Streamings", "items": [
|
||||
{"contestable": False, "desc": "YouTube Premium Mensal", "type": "streaming"}
|
||||
]},
|
||||
]
|
||||
}
|
||||
assert _infer_informational_vas_types(
|
||||
["YouTube Premium Mensal", "Aluguel de Filme 3"], json.dumps(payload, ensure_ascii=False)
|
||||
) == {"estrategico", "avulso"}
|
||||
Reference in New Issue
Block a user