Files
first_contas/legacy_reference/workflows/actions/vas_estrategico/actions.py
2026-06-16 20:54:49 -03:00

409 lines
15 KiB
Python

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