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', ]