Ajustes no agente de contas
This commit is contained in:
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.
107
tests/migration/test_contas_conversation_policy_residuals.py
Normal file
107
tests/migration/test_contas_conversation_policy_residuals.py
Normal file
@@ -0,0 +1,107 @@
|
||||
from app.domain.contas.conversation_policy import evaluate, is_regulatory_threat
|
||||
|
||||
|
||||
def test_no_match_three_consecutive_then_terminal(monkeypatch):
|
||||
monkeypatch.setenv("CONTAS_NO_MATCH_MAX_CONSECUTIVE", "3")
|
||||
base = {"user_text": "???", "intent": "contas_no_match", "route": "suporte_contas_agent", "route_decision": {"method": "llm", "intent": "contas_no_match"}}
|
||||
d1 = evaluate(base)
|
||||
assert d1 and d1.patch["no_match_count"] == 1 and d1.route == "conversation_policy_response"
|
||||
d2 = evaluate({**base, "no_match_count": 1})
|
||||
assert d2 and d2.patch["no_match_count"] == 2 and not d2.patch.get("session_ended")
|
||||
d3 = evaluate({**base, "no_match_count": 2})
|
||||
assert d3 and d3.patch["no_match_count"] == 3
|
||||
assert d3.route == "end_session"
|
||||
assert d3.patch["terminal_status"] == "erro_no_match"
|
||||
|
||||
|
||||
def test_understood_turn_resets_no_match_counter():
|
||||
state = {"user_text": "quero minha fatura", "intent": "contas_invoice_query", "route": "faturas_agent", "route_decision": {"method": "keyword", "intent": "contas_invoice_query"}, "no_match_count": 2}
|
||||
d = evaluate(state)
|
||||
assert d and d.patch == {"no_match_count": 0}
|
||||
|
||||
|
||||
def test_regulatory_threat_without_focus_does_not_invent_transaction():
|
||||
state = {"user_text": "vou procurar a Anatel e o Procon", "intent": "fallback", "route": "faturas_agent", "route_decision": {"method": "llm", "intent": "fallback"}}
|
||||
d = evaluate(state)
|
||||
assert d and d.route == "suporte_contas_agent"
|
||||
assert d.intent == "contas_regulatory_complaint"
|
||||
assert d.patch["mcp_tools"] == []
|
||||
|
||||
|
||||
def test_regulatory_threat_with_focused_subject_preserves_action_context():
|
||||
state = {
|
||||
"user_text": "se não resolver vou para a Anatel",
|
||||
"intent": "fallback", "route": "faturas_agent", "route_decision": {"method": "llm", "intent": "fallback"},
|
||||
"last_transaction": {"arguments": {"subject": "HBO Max Standard"}},
|
||||
}
|
||||
d = evaluate(state)
|
||||
assert d and d.route == "contestacao_agent"
|
||||
assert d.intent == "contas_vas_cancel"
|
||||
assert d.patch["regulatory_context"]["focused_items"] == ["HBO Max Standard"]
|
||||
|
||||
|
||||
def test_regulatory_detector_terms():
|
||||
assert is_regulatory_threat("vou procurar meus direitos no Procon")
|
||||
assert is_regulatory_threat("vou à justiça")
|
||||
|
||||
|
||||
def test_retention_offer_and_two_step_acceptance(monkeypatch):
|
||||
import app.domain.contas.conversation_policy as policy
|
||||
items = [{"name": "VAS A", "msisdn": "11999999999"}, {"name": "VAS B", "msisdn": "11999999999"}]
|
||||
monkeypatch.setattr(policy, "_retention_items", lambda state: items)
|
||||
first = policy.evaluate({"user_text": "quero um atendente", "route": "human_handoff", "intent": "human_handoff", "route_decision": {"method": "continuity", "intent": "human_handoff"}})
|
||||
assert first and first.route == "conversation_policy_response"
|
||||
assert first.patch["contas_retention"]["pending_stage"] == "explain"
|
||||
|
||||
second = policy.evaluate({"user_text": "não, quero atendente", "route": "human_handoff", "intent": "human_handoff", "route_decision": {"method": "continuity", "intent": "human_handoff"}, "contas_retention": first.patch["contas_retention"]})
|
||||
assert second and second.patch["contas_retention"]["pending_stage"] == "continue"
|
||||
|
||||
third = policy.evaluate({"user_text": "sim", "route": "human_handoff", "intent": "human_handoff", "route_decision": {"method": "continuity", "intent": "human_handoff"}, "contas_retention": second.patch["contas_retention"]})
|
||||
assert third and third.route == "contestacao_agent"
|
||||
assert third.patch["context"]["focused_items"] == ["VAS A", "VAS B"]
|
||||
assert third.patch["_route_metadata"]["original_input"] == "cancelar VAS A e VAS B"
|
||||
|
||||
def test_retention_second_decline_handoffs(monkeypatch):
|
||||
import app.domain.contas.conversation_policy as policy
|
||||
state = {"user_text": "não, atendente", "route": "human_handoff", "intent": "human_handoff", "route_decision": {"method": "continuity", "intent": "human_handoff"}, "contas_retention": {"pending_stage": "continue", "stages_offered": ["explain", "continue"], "items": [{"name": "VAS A", "msisdn": "11999999999"}]}}
|
||||
d = policy.evaluate(state)
|
||||
assert d and d.route == "human_handoff" and d.reason == "human_retention_declined"
|
||||
|
||||
|
||||
def test_generic_router_fallback_is_not_counted_as_incomprehensible():
|
||||
d = evaluate({"user_text": "quero falar sobre outra empresa", "intent": "fallback", "route": "faturas_agent", "route_decision": {"method": "fallback", "intent": "fallback"}, "no_match_count": 1})
|
||||
assert d and d.patch == {"no_match_count": 0}
|
||||
|
||||
|
||||
def test_explicit_gratitude_closes_only_without_live_transaction():
|
||||
history = [{"role": "assistant", "content": "Com essa explicação, sanei sua dúvida?", "metadata": {"intent": "contas_invoice_explanation"}}]
|
||||
d = evaluate({"user_text": "entendi, obrigado, era só isso", "intent": "fallback", "route": "suporte_contas_agent", "history": history})
|
||||
assert d and d.route == "end_session"
|
||||
assert d.patch["terminal_status"] == "resolvido"
|
||||
|
||||
active = evaluate({
|
||||
"user_text": "entendi, obrigado, era só isso",
|
||||
"intent": "contas_vas_cancel",
|
||||
"route": "contestacao_agent",
|
||||
"history": history,
|
||||
"transaction_status": "AWAITING_CONFIRMATION",
|
||||
"active_transaction": {"tool_name": "cancelar_vas_avulso", "status": "AWAITING_CONFIRMATION"},
|
||||
})
|
||||
assert active is None or active.route != "end_session"
|
||||
|
||||
|
||||
def test_plural_confirmation_after_invoice_explanation_stays_in_explanation():
|
||||
history = [{
|
||||
"role": "assistant",
|
||||
"content": "Essas duas cobranças de VOD + Canais abertos são as que você não reconhece.",
|
||||
"metadata": {"intent": "contas_invoice_explanation"},
|
||||
}]
|
||||
d = evaluate({
|
||||
"user_text": "as duas mesmo, pode seguir",
|
||||
"intent": "fallback",
|
||||
"route": "suporte_contas_agent",
|
||||
"history": history,
|
||||
})
|
||||
assert d and d.route == "faturas_agent"
|
||||
assert d.intent == "contas_invoice_explanation"
|
||||
assert d.patch["mcp_tools"] == ["invoice_explanation"]
|
||||
25
tests/migration/test_multiple_vas_subject_resolution.py
Normal file
25
tests/migration/test_multiple_vas_subject_resolution.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import importlib
|
||||
|
||||
mcp = importlib.import_module("contas_mcp.servers.contas_mcp_server.main")
|
||||
|
||||
|
||||
def test_multiple_named_subjects_use_unique_catalog_resolution(monkeypatch):
|
||||
catalog = [{"name": "Netflix Mensal"}, {"name": "HBO Max Standard"}, {"name": "TIM Fashion Mensal"}]
|
||||
monkeypatch.setattr(mcp, "_vas_domain_policy_from_invoice_detail", lambda name, args: ("cancelar_vas_avulso", "avulso"))
|
||||
monkeypatch.setattr(mcp, "_invoice_detail_msisdn", lambda detail, name: "11999999999")
|
||||
items = mcp._resolve_multiple_vas_subjects("cancela Netflix e HBO", catalog, {"msisdn": "11999999999"})
|
||||
assert [x["name"] for x in items] == ["Netflix Mensal", "HBO Max Standard"]
|
||||
|
||||
|
||||
def test_generic_todos_does_not_expand_mass_cancellation(monkeypatch):
|
||||
catalog = [{"name": "Netflix Mensal"}, {"name": "TIM Fashion Mensal"}]
|
||||
monkeypatch.setattr(mcp, "_vas_domain_policy_from_invoice_detail", lambda name, args: ("cancelar_vas_avulso", "avulso"))
|
||||
assert mcp._resolve_multiple_vas_subjects("cancela todos", catalog, {}) == []
|
||||
|
||||
|
||||
def test_explicit_all_avulsos_filters_non_avulso(monkeypatch):
|
||||
catalog = [{"name": "VAS A"}, {"name": "Netflix"}]
|
||||
monkeypatch.setattr(mcp, "_vas_domain_policy_from_invoice_detail", lambda name, args: ("cancelar_vas_avulso", "avulso") if name == "VAS A" else ("vas_estrategico", "estrategico"))
|
||||
monkeypatch.setattr(mcp, "_invoice_detail_msisdn", lambda detail, name: "11999999999")
|
||||
items = mcp._resolve_multiple_vas_subjects("cancele todos os VAS avulsos", catalog, {"msisdn": "11999999999"})
|
||||
assert [x["name"] for x in items] == ["VAS A"]
|
||||
@@ -234,3 +234,46 @@ async def test_vas_validator_real_invoice_detail_redirects_youtube_without_resol
|
||||
assert decision['target_tool'] == 'tratar_vas_estrategico'
|
||||
assert decision['action_changed'] is True
|
||||
assert decision['requires_reconfirmation'] is True
|
||||
|
||||
|
||||
def test_multi_item_execution_preflight_does_not_reresolve_joined_subject():
|
||||
args = {
|
||||
'msisdn': '11999999999',
|
||||
'subject': 'Tamboro Mensal, Paramount+',
|
||||
'items': [
|
||||
{'name': 'Tamboro Mensal', 'msisdn': '11999999999'},
|
||||
{'name': 'Paramount+', 'msisdn': '11999999999'},
|
||||
],
|
||||
}
|
||||
assert main._preflight_subject('cancelar_vas_avulso', args) is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_contestation_wrong_amount_is_recoverable_with_invoice_amount(monkeypatch):
|
||||
async def no_enrich(name, args):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(main, '_enrich_invoice_context', no_enrich)
|
||||
monkeypatch.setattr(main, '_preflight_subject', lambda name, args: None)
|
||||
monkeypatch.setattr(main, 'validate_contestation_items', lambda items, payload: (
|
||||
items,
|
||||
[{
|
||||
'erro': 'valor_ajuste_maior_que_item',
|
||||
'valor_item_fatura': '14.99',
|
||||
'item_fatura_resolvido': 'Tamboro Mensal',
|
||||
}],
|
||||
"Valor de ajuste excede o valor cobrado",
|
||||
))
|
||||
result = await main._validate_contestation({
|
||||
'msisdn': '11999999999',
|
||||
'subject': 'Tamboro Mensal',
|
||||
'valor': '29.98',
|
||||
'billing_analysis': {},
|
||||
'target_tool': 'contestar_cobranca',
|
||||
})
|
||||
assert result['eligible'] is False
|
||||
assert result['status'] == 'NEEDS_PARAMETER'
|
||||
assert result['parameter'] == 'valor'
|
||||
assert result['resolved_value'] == '14.99'
|
||||
assert 'R$ 14,99' in result['parameter_message']
|
||||
assert 'R$ 29,98' in result['parameter_message']
|
||||
|
||||
Reference in New Issue
Block a user