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