mirror of
https://github.com/hoshikawa2/first_contas.git
synced 2026-07-09 18:24:20 +00:00
244 lines
9.1 KiB
Python
244 lines
9.1 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from datetime import (
|
|
datetime,
|
|
timezone,
|
|
)
|
|
from typing import Any
|
|
from agente_contas_tim.constants.ic_tags_enum import VEBTag
|
|
from agente_contas_tim.integrations import agent_framework_bridge
|
|
from agente_contas_tim.observability import get_session_id
|
|
from agente_contas_tim.text_utils import vocalize_digits
|
|
from agente_contas_tim.workflows.actions.common.helpers import (
|
|
_final_msisdn,
|
|
_format_list_pt_br,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _build_vas_lines_from_items(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
grouped: dict[str, dict[str, Any]] = {}
|
|
for item in items:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
msisdn = str(item.get("msisdn", "")).strip()
|
|
item_type = str(item.get("type", "")).strip().lower()
|
|
name = str(item.get("name", "")).strip()
|
|
if not msisdn or not name or item_type not in {"bundle", "estrategico"}:
|
|
continue
|
|
current = grouped.setdefault(
|
|
msisdn,
|
|
{
|
|
"msisdn": msisdn,
|
|
"items": [],
|
|
"bundle_names": [],
|
|
"estrategico_names": [],
|
|
},
|
|
)
|
|
current["items"].append(
|
|
{"type": item_type, "msisdn": msisdn, "name": name}
|
|
)
|
|
if item_type == "bundle":
|
|
current["bundle_names"].append(name)
|
|
else:
|
|
current["estrategico_names"].append(name)
|
|
return list(grouped.values())
|
|
|
|
|
|
def _build_vas_initial_message(
|
|
lines: list[dict[str, Any]],
|
|
) -> str:
|
|
sections: list[str] = []
|
|
estrategicos_mencionados: list[str] = []
|
|
for line in lines:
|
|
if not isinstance(line, dict):
|
|
continue
|
|
msisdn = str(line.get("msisdn", "")).strip()
|
|
final_line = _final_msisdn(msisdn)
|
|
bundle_names = [
|
|
str(name).strip()
|
|
for name in line.get("bundle_names", [])
|
|
if str(name).strip()
|
|
]
|
|
estrategico_names = [
|
|
str(name).strip()
|
|
for name in line.get("estrategico_names", [])
|
|
if str(name).strip()
|
|
]
|
|
if bundle_names:
|
|
services_txt = _format_list_pt_br(bundle_names)
|
|
if len(bundle_names) == 1:
|
|
bundle_txt = (
|
|
f"Na linha final {final_line}, o serviço {services_txt} "
|
|
"está incluído no plano e não gera cobrança adicional, além disso, não pode ser cancelado."
|
|
)
|
|
else:
|
|
bundle_txt = (
|
|
f"Na linha final {final_line}, os serviços {services_txt} "
|
|
"estão incluídos no plano e não geram cobrança adicional, além disso, não podem ser cancelados."
|
|
)
|
|
# Linha só com bundle (sem estratégico): a explicação precisa convidar
|
|
# a resposta do cliente, igual ao estratégico, para o workflow pausar
|
|
# em "sanei sua dúvida?" e NÃO finalizar/registrar protocolo no mesmo
|
|
# turno. Em linha mista, a pergunta já vem na seção estratégica.
|
|
if not estrategico_names:
|
|
bundle_txt += " Com essa explicação, sanei sua dúvida?"
|
|
sections.append(bundle_txt)
|
|
if estrategico_names:
|
|
services_txt = _format_list_pt_br(estrategico_names)
|
|
estrategicos_mencionados.extend(estrategico_names)
|
|
if len(estrategico_names) == 1:
|
|
sections.append(
|
|
f"O {services_txt} só é ativado depois que você confirma a contratação. "
|
|
"No momento da contratação, é enviado um código de verificação para a sua linha, e a "
|
|
"ativação acontece somente depois dessa confirmação. Como essa "
|
|
"validação foi feita com sucesso, a cobrança é considerada válida e, por isso, "
|
|
"não conseguimos retirar ou ressarcir esse valor da fatura. Com essa explicação, sanei sua dúvida?"
|
|
)
|
|
|
|
else:
|
|
sections.append(
|
|
f"Os {services_txt} só são ativados depois que você confirma a contratação. "
|
|
"No momento da contratação, é enviado um código de verificação para a sua linha, e a "
|
|
"ativação acontece somente depois dessa confirmação. Como essa "
|
|
"validação foi feita com sucesso, a cobrança é considerada válida e, por isso, "
|
|
"não conseguimos retirar ou ressarcir esse valor da fatura. Com essa explicação, sanei sua dúvida?"
|
|
)
|
|
|
|
if not sections:
|
|
return (
|
|
"Não encontrei itens válidos para a tratativa de VAS estratégico. "
|
|
"Poderia revisar os dados informados?"
|
|
)
|
|
return " ".join(sections)
|
|
|
|
|
|
def _build_vas_accept_message(_lines: list[dict[str, Any]]) -> str:
|
|
# Após o cliente aceitar manter o serviço, o fluxo apenas registra
|
|
# o atendimento e devolve o controle ao agente roteador sem
|
|
# mensagem adicional ao cliente.
|
|
return ""
|
|
|
|
|
|
def _build_vas_bundle_close_message(lines: list[dict[str, Any]]) -> str:
|
|
"""Mensagem de fechamento do fluxo de bundle, usada DEPOIS que o cliente
|
|
responde ao "sanei sua dúvida?". Reafirma de forma breve que o serviço é
|
|
incluso no plano e não cancelável, sem repetir a explicação completa nem a
|
|
pergunta (que já foram ditas na pausa). O sufixo de protocolo e o
|
|
encerramento são anexados por ``registrar_atendimento_vas_estrategico``."""
|
|
names: list[str] = []
|
|
for line in lines:
|
|
if not isinstance(line, dict):
|
|
continue
|
|
names.extend(
|
|
str(name).strip()
|
|
for name in line.get("bundle_names", [])
|
|
if str(name).strip()
|
|
)
|
|
if not names:
|
|
return ""
|
|
services_txt = _format_list_pt_br(names)
|
|
if len(names) == 1:
|
|
return (
|
|
f"Como expliquei, o serviço {services_txt} é incluso no seu plano e "
|
|
"não há cobrança a remover."
|
|
)
|
|
return (
|
|
f"Como expliquei, os serviços {services_txt} são inclusos no seu plano e "
|
|
"não há cobrança a remover."
|
|
)
|
|
|
|
|
|
def _format_protocolo_suffix_vas(
|
|
protocolos_por_linha: list[dict[str, Any]],
|
|
) -> str:
|
|
"""Formata o sufixo 'Seu número de protocolo é X.' para anexar à mensagem.
|
|
|
|
O número é vocalizado (dígitos como palavra, letras isoladas) para que
|
|
o TTS leia cada caractere individualmente.
|
|
"""
|
|
protocolos = [
|
|
str(p.get("protocolo_id", "")).strip()
|
|
for p in protocolos_por_linha
|
|
if isinstance(p, dict) and str(p.get("protocolo_id", "")).strip()
|
|
]
|
|
if not protocolos:
|
|
return ""
|
|
spoken = [vocalize_digits(p) for p in protocolos]
|
|
if len(spoken) == 1:
|
|
return f"Seu número de protocolo é {spoken[0]}."
|
|
return f"Seus números de protocolo são {_format_list_pt_br(spoken)}."
|
|
|
|
|
|
def _is_vas_cancelamento_info_path(
|
|
state: dict[str, Any],
|
|
*,
|
|
mensagem_base: str,
|
|
) -> bool:
|
|
vars_state = state.get("vars", {}) if isinstance(state, dict) else {}
|
|
if not isinstance(vars_state, dict):
|
|
return False
|
|
explicar_payload = vars_state.get("explicar_cancelamento", {})
|
|
if not isinstance(explicar_payload, dict):
|
|
return False
|
|
return (
|
|
bool(mensagem_base)
|
|
and str(explicar_payload.get("mensagem", "") or "").strip()
|
|
== mensagem_base
|
|
)
|
|
|
|
|
|
def _emit_veb_005_cancelamento_info(
|
|
*,
|
|
input_state: dict[str, Any],
|
|
msisdn: str,
|
|
mensagem_base: str,
|
|
) -> None:
|
|
try:
|
|
metadata: dict[str, Any] = {
|
|
"sessionId": str(get_session_id() or ""),
|
|
"tag": VEBTag.INFO_CANCELAMENTO,
|
|
"eventDate": int(datetime.now(timezone.utc).timestamp() * 1000),
|
|
"uraCallId": str(input_state.get("ura_call_id", "") or "").strip(),
|
|
"gsm": str(msisdn or "").strip(),
|
|
"agentId": "contas",
|
|
"channelId": str(
|
|
input_state.get("channel_id")
|
|
or input_state.get("channelId")
|
|
or "URA"
|
|
).strip()
|
|
or "URA",
|
|
"llmResponse": mensagem_base,
|
|
}
|
|
ani = str(input_state.get("ani", "") or "").strip()
|
|
if ani:
|
|
metadata["ani"] = ani
|
|
message_id = str(
|
|
input_state.get("message_id") or input_state.get("messageId") or ""
|
|
).strip()
|
|
if message_id:
|
|
metadata["messageId"] = message_id
|
|
customer_message = str(
|
|
input_state.get("customer_message")
|
|
or input_state.get("customerMessage")
|
|
or ""
|
|
).strip()
|
|
if customer_message:
|
|
metadata["customerMessage"] = customer_message
|
|
agent_framework_bridge.event(VEBTag.INFO_CANCELAMENTO, metadata=metadata)
|
|
except Exception:
|
|
logger.debug("registrar_atendimento_vas_estrategico: falha ao emitir VEB.005", exc_info=True)
|
|
|
|
|
|
__all__ = [
|
|
'_build_vas_lines_from_items',
|
|
'_build_vas_initial_message',
|
|
'_build_vas_accept_message',
|
|
'_format_protocolo_suffix_vas',
|
|
'_is_vas_cancelamento_info_path',
|
|
'_emit_veb_005_cancelamento_info',
|
|
]
|