new feature: Integration with kbdb Autonomous
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.
@@ -17,7 +17,6 @@
|
||||
"formatar": {
|
||||
"output": {
|
||||
"success": true,
|
||||
"await_user_input": true,
|
||||
"mensagem": "A fatura aumentou por causa de juros. Sanei sua dúvida?"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
"formatar": {
|
||||
"output": {
|
||||
"success": true,
|
||||
"await_user_input": true,
|
||||
"mensagem": "A fatura aumentou por causa de juros. Sanei sua dúvida?"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
"formatar": {
|
||||
"output": {
|
||||
"success": true,
|
||||
"await_user_input": true,
|
||||
"mensagem": "A fatura aumentou por causa de juros. Sanei sua dúvida?"
|
||||
}
|
||||
},
|
||||
|
||||
172
tests/migration/test_requested_tools_parity_pente_fino.py
Normal file
172
tests/migration/test_requested_tools_parity_pente_fino.py
Normal file
@@ -0,0 +1,172 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from app.domain.contas.service import ContasDomainService
|
||||
from app.domain.contas.workflow_actions import build_contas_workflow_actions
|
||||
from contas_mcp.servers.contas_mcp_server import main as mcp
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
REQUESTED = {
|
||||
"consultar_faturas", "consultar_plano", "invoice_explanation", "buscar_informacao",
|
||||
"consultar_vas", "consultar_historico_vas", "cancelar_vas_avulso",
|
||||
"tratar_vas_estrategico", "validar_contestacao", "contestar_cobranca",
|
||||
"finalizar_atendimento", "consultar_status_solicitacao", "enviar_sms",
|
||||
"recuperar_fatura_pdf", "pro_rata", "termino_desconto", "valor_divergente",
|
||||
"retomar_workflow",
|
||||
}
|
||||
|
||||
|
||||
def test_todas_as_capabilities_solicitadas_estao_expostas_no_mcp():
|
||||
assert REQUESTED <= set(mcp.TOOLS)
|
||||
|
||||
|
||||
def test_todas_as_capabilities_solicitadas_estao_habilitadas_no_registry():
|
||||
cfg = yaml.safe_load((ROOT / "config/tools.yaml").read_text(encoding="utf-8"))["tools"]
|
||||
assert REQUESTED <= set(cfg)
|
||||
assert all(cfg[name].get("enabled", True) for name in REQUESTED)
|
||||
|
||||
|
||||
def test_buscar_informacao_preserva_tool_original_mas_delega_rag_ao_framework():
|
||||
out = ContasDomainService().buscar_informacao(
|
||||
queries=["Aya Audiobooks Premium o que é", "Food Balance o que é"]
|
||||
)
|
||||
assert out["requires_rag"] is True
|
||||
assert out["source"] == "agent_framework.rag"
|
||||
assert out["rag_queries"] == ["Aya Audiobooks Premium o que é", "Food Balance o que é"]
|
||||
|
||||
|
||||
def test_consultar_plano_extrai_planos_da_evidencia_sem_llm():
|
||||
billing = json.loads((ROOT / "app/domain/contas/fixtures/divergencia.json").read_text(encoding="utf-8"))
|
||||
out = ContasDomainService().consultar_plano(msisdn="119", billing_analysis=billing)
|
||||
assert out["count"] == 2
|
||||
assert {p["name"] for p in out["plans"]} == {"TIM Black A 8.0", "TIM CTRL Redes Sociais 8.0"}
|
||||
|
||||
|
||||
def test_invoice_explanation_preserva_detail_e_resumo_semantico_para_composicao():
|
||||
svc = ContasDomainService()
|
||||
detail = {"parsed_content": {"total_geral": 197.43}}
|
||||
out = svc.invoice_explanation(
|
||||
msisdn="119",
|
||||
complete_invoices_payload={"paymentItems": []},
|
||||
billing_analysis={"invoiceExplanation": "Base"},
|
||||
invoice_detail=detail,
|
||||
invoice_amount="197.43",
|
||||
invoice_period="14/10 a 13/11",
|
||||
)
|
||||
assert out["invoice_detail"] == detail
|
||||
assert out["invoice_amount"] == "197.43"
|
||||
assert out["invoice_period"] == "14/10 a 13/11"
|
||||
|
||||
|
||||
def test_invoice_explanation_restaura_composicao_llm_no_framework():
|
||||
reg = build_contas_workflow_actions(ContasDomainService())
|
||||
out = reg.get("formatar_invoice_explanation")(
|
||||
{"explicacao_base": "TIM Fashion R$ 10,00", "invoice_detail": {}}, {"input": {}}
|
||||
)
|
||||
assert out["await_user_input"] is True
|
||||
assert out["requires_llm_composition"] is True
|
||||
assert "não invente" in out["response_instruction"]
|
||||
assert "Com essa explicação, sanei sua dúvida?" in out["mensagem"]
|
||||
|
||||
|
||||
def test_pro_rata_deriva_exatamente_dois_planos_do_pdf_sem_llm():
|
||||
detail = json.loads((ROOT / "app/domain/contas/fixtures/invoice_pdf_include_danfe_true.json").read_text(encoding="utf-8"))
|
||||
args = {"msisdn": "11999999999", "invoice_detail": detail}
|
||||
plans = mcp._derive_pro_rata_plans(args)
|
||||
assert len(plans) == 2
|
||||
assert {p["desc"] for p in plans} == {"TIM Black A 8.0", "TIM CTRL Redes Sociais 8.0"}
|
||||
assert mcp._prepare_pro_rata(args) is None
|
||||
assert args["has_plano_controle"] is True
|
||||
|
||||
|
||||
def test_pro_rata_falha_fechado_sem_exatamente_dois_planos():
|
||||
args = {"msisdn": "119", "planos": [{"desc": "Plano único"}]}
|
||||
out = mcp._prepare_pro_rata(args)
|
||||
assert out["success"] is False
|
||||
assert out["reason"] == "requires_exactly_two_plans"
|
||||
assert out["plans_found"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validar_contestacao_usa_mesma_cval_e_rejeita_valor_acima_da_fatura(monkeypatch):
|
||||
billing = json.loads((ROOT / "app/domain/contas/fixtures/divergencia.json").read_text(encoding="utf-8"))
|
||||
|
||||
async def no_enrich(name, args):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(mcp, "_enrich_invoice_context", no_enrich)
|
||||
out = await mcp._validate_contestation({
|
||||
"msisdn": "11999999999",
|
||||
"subject": "TIM Fashion Mensal",
|
||||
"valor": 150,
|
||||
"motivo": "quero contestar TIM Fashion Mensal",
|
||||
"billing_analysis": billing,
|
||||
})
|
||||
assert out["eligible"] is False
|
||||
assert out["reason"] == "CVAL"
|
||||
assert out["validation_log"][0]["erro"] == "valor_ajuste_maior_que_item"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validar_contestacao_aprova_quando_item_e_valor_sao_comprovados(monkeypatch):
|
||||
billing = json.loads((ROOT / "app/domain/contas/fixtures/divergencia.json").read_text(encoding="utf-8"))
|
||||
|
||||
async def no_enrich(name, args):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(mcp, "_enrich_invoice_context", no_enrich)
|
||||
out = await mcp._validate_contestation({
|
||||
"msisdn": "11999999999",
|
||||
"subject": "TIM Fashion Mensal",
|
||||
"valor": 50,
|
||||
"motivo": "quero contestar TIM Fashion Mensal",
|
||||
"billing_analysis": billing,
|
||||
})
|
||||
assert out["eligible"] is True
|
||||
assert out["status"] == "ELIGIBLE"
|
||||
|
||||
|
||||
def test_termino_desconto_e_valor_divergente_preservam_semantica_do_original():
|
||||
reg = build_contas_workflow_actions(ContasDomainService())
|
||||
action = reg.get("formatar_capability_resposta")
|
||||
end = action({"tipo": "termino_desconto", "nome_plano": "TIM Black", "msisdn": "11999999999"}, {"input": {}})
|
||||
div = action({"tipo": "valor_divergente", "msisdn": "11999999999"}, {"input": {}})
|
||||
assert "desconto de fidelidade do plano TIM Black" in end["mensagem"]
|
||||
assert "período promocional contratado expirou" in end["mensagem"]
|
||||
assert "alteração no valor do plano" in div["mensagem"]
|
||||
assert "final 99" in div["mensagem"]
|
||||
|
||||
|
||||
def test_status_solicitacao_nao_confunde_message_id_com_protocolo():
|
||||
mapping = yaml.safe_load((ROOT / "config/mcp_parameter_mapping.yaml").read_text(encoding="utf-8"))["mcp_parameter_mapping"]["tools"]["consultar_status_solicitacao"]
|
||||
assert "interaction_key" not in mapping.get("map", {})
|
||||
assert "protocol" in mapping.get("extract", {})
|
||||
|
||||
|
||||
def test_finalizacao_preserva_status_explicito_do_contrato_original():
|
||||
cfg = yaml.safe_load((ROOT / "config/tools.yaml").read_text(encoding="utf-8"))["tools"]["finalizar_atendimento"]
|
||||
assert "status" in cfg.get("requires", [])
|
||||
mapping = yaml.safe_load((ROOT / "config/mcp_parameter_mapping.yaml").read_text(encoding="utf-8"))["mcp_parameter_mapping"]["tools"]["finalizar_atendimento"]
|
||||
assert "status" in mapping.get("extract", {})
|
||||
|
||||
|
||||
class _DirectClient:
|
||||
def consultar_vas(self, msisdn): return {"products": [{"name": "VAS"}]}
|
||||
def historico_vas(self, msisdn): return {"services": [{"name": "OLD"}]}
|
||||
def status_sr(self, payload): return {"protocolNumber": payload.get("protocolNumber"), "status": "Fechado"}
|
||||
def sms(self, msisdn, message): return {"sent": True, "msisdn": msisdn, "message": message}
|
||||
def secure_pdf(self, msisdn, invoice_id, customer_id=""): return {"invoiceId": invoice_id, "url": "secure"}
|
||||
|
||||
|
||||
def test_tools_diretas_vas_historico_status_sms_pdf_mantem_contratos_de_dominio():
|
||||
svc = ContasDomainService(client=_DirectClient()) # type: ignore[arg-type]
|
||||
assert svc.consultar_vas(msisdn="119")["products"][0]["name"] == "VAS"
|
||||
assert svc.consultar_historico_vas(msisdn="119")["services"][0]["name"] == "OLD"
|
||||
assert svc.consultar_status_solicitacao(msisdn="119", protocol="P1")["protocolNumber"] == "P1"
|
||||
assert svc.enviar_sms(msisdn="119", message="ok")["sent"] is True
|
||||
assert svc.recuperar_fatura_pdf(msisdn="119", invoice_id="I1")["invoiceId"] == "I1"
|
||||
92
tests/migration/test_tim_aoferta_transaction_continuation.py
Normal file
92
tests/migration/test_tim_aoferta_transaction_continuation.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from app.extensions.tim_guardrails import TimProactiveOfferRail
|
||||
|
||||
|
||||
class _FailIfCalledLLM:
|
||||
async def ainvoke(self, *args, **kwargs):
|
||||
raise AssertionError('TIM_AOFERTA LLM must not run for transaction continuation')
|
||||
|
||||
|
||||
class _BlockingLLM:
|
||||
def __init__(self):
|
||||
self.calls = 0
|
||||
|
||||
async def ainvoke(self, *args, **kwargs):
|
||||
self.calls += 1
|
||||
return '{"allowed": false, "reason": "oferta proativa"}'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('status', ['COLLECTING_PARAMETERS', 'AWAITING_CONFIRMATION'])
|
||||
async def test_tim_aoferta_bypasses_native_transaction_continuation_states(status):
|
||||
decision = await TimProactiveOfferRail().evaluate(
|
||||
'Você confirma o cancelamento do serviço TIM Fashion?',
|
||||
{
|
||||
'transaction_status': status,
|
||||
'guardrail_llm': _FailIfCalledLLM(),
|
||||
},
|
||||
)
|
||||
|
||||
assert decision.allowed is True
|
||||
assert decision.code == 'TIM_AOFERTA'
|
||||
assert decision.reason == f'continuidade_transacional:{status}'
|
||||
assert decision.metadata['mechanism'] == 'deterministic_transaction_bypass'
|
||||
assert decision.metadata['transaction_status'] == status
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tim_aoferta_detects_awaiting_confirmation_from_mcp_results():
|
||||
decision = await TimProactiveOfferRail().evaluate(
|
||||
'Você confirma o cancelamento do serviço TIM Fashion?',
|
||||
{
|
||||
'mcp_results': [
|
||||
{
|
||||
'tool_name': 'cancelar_vas_avulso',
|
||||
'awaiting_confirmation': True,
|
||||
'transaction_status': 'AWAITING_CONFIRMATION',
|
||||
}
|
||||
],
|
||||
'guardrail_llm': _FailIfCalledLLM(),
|
||||
},
|
||||
)
|
||||
|
||||
assert decision.allowed is True
|
||||
assert decision.metadata['transaction_status'] == 'AWAITING_CONFIRMATION'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tim_aoferta_detects_parameter_collection_from_tool_result():
|
||||
decision = await TimProactiveOfferRail().evaluate(
|
||||
'Para prosseguir, informe valor.',
|
||||
{
|
||||
'tool_result': [
|
||||
{
|
||||
'tool_name': 'contestar_cobranca',
|
||||
'transaction_status': 'COLLECTING_PARAMETERS',
|
||||
}
|
||||
],
|
||||
'guardrail_llm': _FailIfCalledLLM(),
|
||||
},
|
||||
)
|
||||
|
||||
assert decision.allowed is True
|
||||
assert decision.metadata['transaction_status'] == 'COLLECTING_PARAMETERS'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tim_aoferta_still_uses_llm_outside_transaction_continuation():
|
||||
llm = _BlockingLLM()
|
||||
decision = await TimProactiveOfferRail().evaluate(
|
||||
'Caso queira, posso cancelar outro serviço.',
|
||||
{
|
||||
'transaction_status': 'COMPLETED',
|
||||
'guardrail_llm': llm,
|
||||
},
|
||||
)
|
||||
|
||||
assert llm.calls == 1
|
||||
assert decision.allowed is False
|
||||
assert decision.reason == 'oferta proativa'
|
||||
@@ -35,7 +35,7 @@ def test_workflow_completo_e_registrado_sem_pending():
|
||||
state = {}
|
||||
runtime._capture_pending_domain_workflow(state, _envelope("COMPLETED", "pro_rata"))
|
||||
assert state["business_workflows_executed"] == ["pro_rata"]
|
||||
assert "pending_domain_workflow" not in state
|
||||
assert state["pending_domain_workflow"] is None
|
||||
|
||||
|
||||
def test_latch_nao_duplica_workflow():
|
||||
|
||||
Reference in New Issue
Block a user