mirror of
https://github.com/hoshikawa2/first_contas.git
synced 2026-07-10 02:34:20 +00:00
first commit
This commit is contained in:
408
legacy_reference/workflows/actions/vas_estrategico/actions.py
Normal file
408
legacy_reference/workflows/actions/vas_estrategico/actions.py
Normal file
@@ -0,0 +1,408 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
from typing import Any
|
||||
from agente_contas_tim.integrations.rct_policy import RCTOperation
|
||||
from agente_contas_tim.observability import get_session_id
|
||||
from agente_contas_tim.protocol_triplets import resolve_protocol_triplet
|
||||
from agente_contas_tim.workflows.actions.registry import (
|
||||
WorkflowRuntimeContext,
|
||||
workflow_action,
|
||||
)
|
||||
from agente_contas_tim.workflows.runtime_types import ActionResult
|
||||
from agente_contas_tim.workflows.actions.common.helpers import (
|
||||
_final_msisdn,
|
||||
_first_text_from_params_or_state,
|
||||
_result_failed_or_missing_data,
|
||||
_runtime_llm_callbacks,
|
||||
_runtime_llm_metadata,
|
||||
_to_dict,
|
||||
)
|
||||
from agente_contas_tim.workflows.actions.vas_estrategico.helpers import (
|
||||
_build_vas_accept_message,
|
||||
_build_vas_bundle_close_message,
|
||||
_build_vas_initial_message,
|
||||
_build_vas_lines_from_items,
|
||||
_emit_veb_005_cancelamento_info,
|
||||
_format_protocolo_suffix_vas,
|
||||
_is_vas_cancelamento_info_path,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("agente_contas_tim.workflows.actions.tim_actions")
|
||||
|
||||
|
||||
@workflow_action("montar_resposta_texto")
|
||||
def montar_resposta_text(
|
||||
state: dict[str, Any],
|
||||
params: dict[str, Any],
|
||||
runtime: WorkflowRuntimeContext,
|
||||
) -> ActionResult:
|
||||
"""Monta resposta de texto para o cliente (sem SMS)."""
|
||||
dados = params.get("dados", {})
|
||||
texto = str(dados.get("texto_usuario", ""))
|
||||
return ActionResult.ok({
|
||||
"success": True,
|
||||
"mensagem": texto,
|
||||
})
|
||||
|
||||
|
||||
@workflow_action("preparar_vas_estrategico")
|
||||
def preparar_vas_estrategico(
|
||||
state: dict[str, Any],
|
||||
params: dict[str, Any],
|
||||
runtime: WorkflowRuntimeContext,
|
||||
) -> ActionResult:
|
||||
lines = params.get("linhas", [])
|
||||
if not isinstance(lines, list) or not lines:
|
||||
items = params.get("items", [])
|
||||
if not isinstance(items, list):
|
||||
items = []
|
||||
lines = _build_vas_lines_from_items(
|
||||
[item for item in items if isinstance(item, dict)]
|
||||
)
|
||||
|
||||
normalized_lines = []
|
||||
for line in lines:
|
||||
if not isinstance(line, dict):
|
||||
continue
|
||||
msisdn = str(line.get("msisdn", "")).strip()
|
||||
if not msisdn:
|
||||
continue
|
||||
raw_items = line.get("items", [])
|
||||
line_items = (
|
||||
[item for item in raw_items if isinstance(item, dict)]
|
||||
if isinstance(raw_items, list)
|
||||
else []
|
||||
)
|
||||
if not line_items:
|
||||
line_items = []
|
||||
for bundle_name in line.get("bundle_names", []):
|
||||
name = str(bundle_name).strip()
|
||||
if name:
|
||||
line_items.append(
|
||||
{"type": "bundle", "msisdn": msisdn, "name": name}
|
||||
)
|
||||
for estrategico_name in line.get("estrategico_names", []):
|
||||
name = str(estrategico_name).strip()
|
||||
if name:
|
||||
line_items.append(
|
||||
{"type": "estrategico", "msisdn": msisdn, "name": name}
|
||||
)
|
||||
rebuilt = _build_vas_lines_from_items(line_items)
|
||||
if rebuilt:
|
||||
normalized_lines.extend(rebuilt)
|
||||
|
||||
if not normalized_lines:
|
||||
return ActionResult.fail(
|
||||
"Nenhum item valido para o fluxo de VAS estrategico."
|
||||
)
|
||||
|
||||
vars_state = state.get("vars", {}) if isinstance(state, dict) else {}
|
||||
source_node = str(params.get("source_node", "resolve")).strip() or "resolve"
|
||||
payload = vars_state.get(source_node, {})
|
||||
if not isinstance(payload, dict):
|
||||
payload = {}
|
||||
instrucoes = str(payload.get("content", "")).strip()
|
||||
|
||||
has_estrategico_items = any(
|
||||
bool(
|
||||
isinstance(line, dict)
|
||||
and isinstance(line.get("estrategico_names", []), list)
|
||||
and any(str(name).strip() for name in line.get("estrategico_names", []))
|
||||
)
|
||||
for line in normalized_lines
|
||||
)
|
||||
has_bundle_items = any(
|
||||
bool(
|
||||
isinstance(line, dict)
|
||||
and isinstance(line.get("bundle_names", []), list)
|
||||
and any(str(name).strip() for name in line.get("bundle_names", []))
|
||||
)
|
||||
for line in normalized_lines
|
||||
)
|
||||
|
||||
return ActionResult.ok(
|
||||
{
|
||||
"linhas": normalized_lines,
|
||||
"instrucoes": instrucoes,
|
||||
"mensagem": _build_vas_initial_message(normalized_lines),
|
||||
"mensagem_pos_aceite": _build_vas_accept_message(normalized_lines),
|
||||
"mensagem_bundle_fechamento": _build_vas_bundle_close_message(
|
||||
normalized_lines
|
||||
),
|
||||
# Bundle também pausa em "sanei sua dúvida?": o agente não cancela,
|
||||
# só explica que é incluso. A finalização ocorre DEPOIS da resposta
|
||||
# do cliente, não no turno da explicação. (incidente CY0010)
|
||||
"await_user_input": has_estrategico_items or has_bundle_items,
|
||||
"has_estrategico_items": has_estrategico_items,
|
||||
"has_bundle_items": has_bundle_items,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@workflow_action("montar_explicacao_cancelamento_vas_estrategico")
|
||||
def montar_explicacao_cancelamento_vas_estrategico(
|
||||
state: dict[str, Any],
|
||||
params: dict[str, Any],
|
||||
runtime: WorkflowRuntimeContext,
|
||||
) -> ActionResult:
|
||||
lines = params.get("linhas", [])
|
||||
orientacoes = params.get("orientacoes_cancelamento", [])
|
||||
orientacoes_map: dict[tuple[str, str], str] = {}
|
||||
if isinstance(orientacoes, list):
|
||||
for item in orientacoes:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
msisdn = str(item.get("msisdn", "")).strip()
|
||||
name = str(item.get("name", "")).strip().lower()
|
||||
orientacao = str(item.get("orientacao", "")).strip()
|
||||
if msisdn and name and orientacao:
|
||||
orientacoes_map[(msisdn, name)] = orientacao
|
||||
|
||||
context_lines: list[str] = []
|
||||
fallback_sections: list[str] = []
|
||||
if isinstance(lines, list):
|
||||
for line in lines:
|
||||
if not isinstance(line, dict):
|
||||
continue
|
||||
msisdn = str(line.get("msisdn", "")).strip()
|
||||
if not msisdn:
|
||||
continue
|
||||
final_line = _final_msisdn(msisdn)
|
||||
estrategico_names = [
|
||||
str(name).strip()
|
||||
for name in line.get("estrategico_names", [])
|
||||
if str(name).strip()
|
||||
]
|
||||
for service_name in estrategico_names:
|
||||
orientacao = orientacoes_map.get(
|
||||
(msisdn, service_name.lower()),
|
||||
(
|
||||
"O cancelamento deve ser feito no app ou site oficial "
|
||||
"do parceiro que fornece o serviço."
|
||||
),
|
||||
)
|
||||
raw = str(orientacao).replace("\\n", "\n")
|
||||
parts = [
|
||||
re.sub(r"\s+", " ", p).strip()
|
||||
for p in raw.split("\n---\n")
|
||||
]
|
||||
cleaned_orientation = " || ".join(p for p in parts if p)
|
||||
context_lines.append(
|
||||
f"Serviço: {service_name} | Linha final: {final_line} | "
|
||||
f"Orientação bruta: {cleaned_orientation}"
|
||||
)
|
||||
fallback_sections.append(
|
||||
f"Para cancelar {service_name} na linha final {final_line}: "
|
||||
"acesse o app ou site oficial do parceiro e solicite "
|
||||
"o cancelamento da assinatura."
|
||||
)
|
||||
|
||||
if not context_lines:
|
||||
fallback_sections.append(
|
||||
"Não há serviços estratégicos com procedimento adicional de "
|
||||
"cancelamento para este atendimento."
|
||||
)
|
||||
|
||||
input_state = state.get("input", {}) if isinstance(state, dict) else {}
|
||||
next_subject = str(
|
||||
(input_state.get("next_subject") if isinstance(input_state, dict) else "")
|
||||
or params.get("next_subject", "")
|
||||
or ""
|
||||
).strip()
|
||||
|
||||
if runtime.llm_gateway is not None and context_lines:
|
||||
try:
|
||||
cancelamento_context = "\n".join(context_lines)
|
||||
llm_result = runtime.llm_gateway.execute(
|
||||
capability_id="fluxo_vas_estrategico_cancelamento_resumo",
|
||||
variables={
|
||||
"cancelamento_context": cancelamento_context,
|
||||
"next_subject": next_subject,
|
||||
},
|
||||
user_text=cancelamento_context,
|
||||
callbacks=_runtime_llm_callbacks(runtime),
|
||||
tags=["workflow_action"],
|
||||
metadata=_runtime_llm_metadata(runtime),
|
||||
)
|
||||
llm_message = str(llm_result.content or "").strip()
|
||||
if llm_message:
|
||||
return ActionResult.ok({"mensagem": llm_message})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
fallback_msg = "\n".join(fallback_sections[:4])
|
||||
if next_subject:
|
||||
transition = f" Podemos seguir agora com o tratamento de {next_subject}?"
|
||||
if transition.strip().lower() not in fallback_msg.lower():
|
||||
fallback_msg = f"{fallback_msg.rstrip()}{transition}"
|
||||
return ActionResult.ok({"mensagem": fallback_msg})
|
||||
|
||||
|
||||
@workflow_action("registrar_atendimento_vas_estrategico")
|
||||
def registrar_atendimento_vas_estrategico(
|
||||
state: dict[str, Any],
|
||||
params: dict[str, Any],
|
||||
runtime: WorkflowRuntimeContext,
|
||||
) -> ActionResult:
|
||||
"""Registra protocolo por linha para atendimento de VAS estratégico."""
|
||||
lines = params.get("linhas", [])
|
||||
if not isinstance(lines, list):
|
||||
lines = []
|
||||
mensagem_base = str(params.get("mensagem_base", "")).strip()
|
||||
input_state = state.get("input", {}) if isinstance(state, dict) else {}
|
||||
social_sec_no = re.sub(
|
||||
r"\D",
|
||||
"",
|
||||
_first_text_from_params_or_state(
|
||||
state,
|
||||
params,
|
||||
"social_sec_no",
|
||||
"socialSecNo",
|
||||
"cpf",
|
||||
"customer_document",
|
||||
"customerDocument",
|
||||
"document",
|
||||
),
|
||||
)
|
||||
open_triplet = resolve_protocol_triplet("vas_estrategico", stage="open")
|
||||
request_status = str(params.get("request_status", "Fechado")).strip() or "Fechado"
|
||||
status = str(params.get("status", "CLOSED")).strip() or "CLOSED"
|
||||
message_id = _first_text_from_params_or_state(
|
||||
state,
|
||||
params,
|
||||
"message_id",
|
||||
"messageId",
|
||||
)
|
||||
should_emit_veb_005 = _is_vas_cancelamento_info_path(
|
||||
state,
|
||||
mensagem_base=mensagem_base,
|
||||
)
|
||||
protocolos_por_linha: list[dict[str, Any]] = []
|
||||
erros: list[dict[str, Any]] = []
|
||||
_ic_emitted = False
|
||||
_veb_005_emitted = False
|
||||
|
||||
for line in lines:
|
||||
if not isinstance(line, dict):
|
||||
continue
|
||||
msisdn = str(line.get("msisdn", "")).strip()
|
||||
if not msisdn:
|
||||
continue
|
||||
bundle_names = line.get("bundle_names", [])
|
||||
estrategico_names = line.get("estrategico_names", [])
|
||||
if not isinstance(bundle_names, list):
|
||||
bundle_names = []
|
||||
if not isinstance(estrategico_names, list):
|
||||
estrategico_names = []
|
||||
reason3 = open_triplet.reason3 or "Valor"
|
||||
_ic_base: dict[str, Any] = {
|
||||
"sessionId": str(get_session_id() or ""),
|
||||
"gsm": str(msisdn),
|
||||
"ani": str((input_state.get("ani") if isinstance(input_state, dict) else "") or "").strip(),
|
||||
"uraCallId": str((input_state.get("ura_call_id") if isinstance(input_state, dict) else "") or "").strip(),
|
||||
"agentId": "contas",
|
||||
"channelId": str((input_state.get("channel_id") if isinstance(input_state, dict) else "URA") or "URA").strip(),
|
||||
}
|
||||
if should_emit_veb_005 and not _veb_005_emitted:
|
||||
_emit_veb_005_cancelamento_info(
|
||||
input_state=input_state if isinstance(input_state, dict) else {},
|
||||
msisdn=msisdn,
|
||||
mensagem_base=mensagem_base,
|
||||
)
|
||||
_veb_005_emitted = True
|
||||
register_result = runtime.factory.create_protocol_v2(
|
||||
msisdn=msisdn,
|
||||
interaction_protocol="",
|
||||
social_sec_no=social_sec_no,
|
||||
source="CHAT",
|
||||
reason1=open_triplet.reason1 or "Informação",
|
||||
reason2=open_triplet.reason2 or "Conta",
|
||||
reason3=reason3,
|
||||
direction_contact="FROM-CLIENT",
|
||||
type="CLIENTE",
|
||||
request_flag=True,
|
||||
request_status=request_status,
|
||||
status=status,
|
||||
service_request_notes="Solicitação via chat",
|
||||
client_id="AIAGENTCR",
|
||||
ic_context=_ic_base,
|
||||
rct_operation=RCTOperation.REG_ATEND_VAS_ESTRAT,
|
||||
message_id=message_id,
|
||||
).execute()
|
||||
if register_result.success:
|
||||
_ic_emitted = True
|
||||
if _result_failed_or_missing_data(register_result, state=state):
|
||||
erros.append(
|
||||
{
|
||||
"msisdn": msisdn,
|
||||
"stage": "register_protocol",
|
||||
"erro": register_result.error or "Falha ao registrar protocolo",
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
payload = _to_dict(register_result.data)
|
||||
response = payload.get("response", {}) if isinstance(payload, dict) else {}
|
||||
protocolo_id = ""
|
||||
if isinstance(response, dict):
|
||||
protocolo_id = str(
|
||||
response.get("interactionProtocol")
|
||||
or response.get("protocolId")
|
||||
or response.get("protocolo_id")
|
||||
or ""
|
||||
).strip()
|
||||
if not protocolo_id:
|
||||
erros.append(
|
||||
{
|
||||
"msisdn": msisdn,
|
||||
"stage": "register_protocol",
|
||||
"erro": "Protocolo não retornado pelo serviço",
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
protocolos_por_linha.append(
|
||||
{
|
||||
"msisdn": msisdn,
|
||||
"protocolo_id": protocolo_id,
|
||||
}
|
||||
)
|
||||
|
||||
suffix = _format_protocolo_suffix_vas(protocolos_por_linha)
|
||||
if not mensagem_base:
|
||||
mensagem_final = "Manteremos o serviço."
|
||||
if suffix:
|
||||
mensagem_final = f"{mensagem_final} {suffix}"
|
||||
else:
|
||||
mensagem_final = (
|
||||
f"{mensagem_base} {suffix}".strip() if suffix else mensagem_base
|
||||
)
|
||||
|
||||
protocolos_raw = [
|
||||
str(p.get("protocolo_id", "")).strip()
|
||||
for p in protocolos_por_linha
|
||||
if isinstance(p, dict) and str(p.get("protocolo_id", "")).strip()
|
||||
]
|
||||
output: dict[str, Any] = {
|
||||
"success": len(protocolos_por_linha) > 0,
|
||||
"mensagem": mensagem_final,
|
||||
"protocolos_por_linha": protocolos_por_linha,
|
||||
"erros": erros,
|
||||
"protocol_closed": len(protocolos_por_linha) > 0,
|
||||
"mocked": False,
|
||||
}
|
||||
if protocolos_raw:
|
||||
output["requires_protocol_in_response"] = True
|
||||
output["protocols_for_response"] = protocolos_raw
|
||||
return ActionResult.ok(output)
|
||||
|
||||
|
||||
__all__ = [
|
||||
'montar_resposta_text',
|
||||
'preparar_vas_estrategico',
|
||||
'montar_explicacao_cancelamento_vas_estrategico',
|
||||
'registrar_atendimento_vas_estrategico',
|
||||
]
|
||||
243
legacy_reference/workflows/actions/vas_estrategico/helpers.py
Normal file
243
legacy_reference/workflows/actions/vas_estrategico/helpers.py
Normal file
@@ -0,0 +1,243 @@
|
||||
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',
|
||||
]
|
||||
Reference in New Issue
Block a user