From 53d57e4fa33e406bcf63de6efc5f336a1593e316 Mon Sep 17 00:00:00 2001 From: Joao Monezi Date: Sat, 21 Jun 2025 20:01:29 +0000 Subject: [PATCH] Grafos implementado --- app/__pycache__/main.cpython-312.pyc | Bin 749 -> 1658 bytes app/dialog_flow/graph_definition.py | 141 +++++++ app/main.py | 2 +- .../webhook_service.cpython-312.pyc | Bin 27287 -> 31866 bytes app/services/webhook_service.py | 377 +++++++++++++++--- 5 files changed, 459 insertions(+), 61 deletions(-) create mode 100644 app/dialog_flow/graph_definition.py diff --git a/app/__pycache__/main.cpython-312.pyc b/app/__pycache__/main.cpython-312.pyc index 7e36a39d8897b5962f31316f711fe904d98a42dc..ccd07a115c743c3a0029c37245e72ce909bce5c1 100644 GIT binary patch literal 1658 zcmb_c&u<$=6rR~1-fe8J+q8}2$d#5N0lwG;AtCW2B8^n3Ds>T+K=LJ8b_RRR?#^mv zH&JqGDH7!bT<95bKqzqJFUXNg+NMghv=Rsoa6zsD63K}-yI#ixIKi`K_M3g*JkR^y z+xfOyok3u8zrH#6MnmXFxhPB@gZ&al=nf)?U>^mzhq1(puLNpO&9LfgJq_Mzz8;i% zKvxjOL?jp3;x_|cQ9!`Y)%fi~ekrC|iS&gOV}CMOC8nRy>=j$OSn zup8679Fy{dU72E6|Ak$hu&pV!wXU{mzsP_soEn!L5ih-dS;95?_JFZ>yMRv!cR)$v zQ@$~`%tDd)aX{_222L!NqR55~$*T|mo_6GL$M(XQ_PMk5#n$J{c3EJvBl5D(5+b+c zRL7nKY;Win+85f-w$xOQz1XK|nNZB~exbnD#DFAN4*q3@=0coMA zm?$2;j=omca7*o`MnHvd`ZRe4DDtn#^noS`Q4w!_W^+cIkgzAdM7$!y!a#>lbp|Y; zogs5rz(Ts=btcE|I8oFI94~A~>#4>Wi+K&U$kPZp{fz#&fxfSt-L5ZfmoIK>7Y_vV zBzjLbKx7d+JWis&BqTd4OUMV1Clmw9Z79<{X2OogeTC3a#y)06A7K<5g^S>#8!%dj zUnZ5(OZZZd+F$nB9?%D(4RNGc})k9;c=lBr#!Y;eV5p|BrIM1bO^jP8zzD^Gr`>Ly!Aj z-w7apSp1`yT=o%{!tgV4l6^TPqE+NQR#9FR=U|8yOu352$z5aqo-q$9?wSjC&4nEk z6a^ixJTPAa9nHo8p96NQmKyKC9pFSBmwlM35=n(qBU$xa#=~@8P!5oa_V|ie8NbL2 z4?CjN@Z|3~o$v5^Yr++gvL%E@jEQGaPGU-cz596nv7+K@nY%H{;Ap0B5AWlKbYIQf J3pvc{@h4M9dJ6ym delta 281 zcmeyx^OlwGG%qg~0}xcL^~~U4n#d=?G>u`RhKvCdLn>nma|%le>l(Jz%pesE3{fnp zELq$THZqyYoW(P-L)M2Siyv7S!cJjd%?M?2v@ooODv08!jMDdDmG96 diff --git a/app/dialog_flow/graph_definition.py b/app/dialog_flow/graph_definition.py new file mode 100644 index 0000000..d1ba053 --- /dev/null +++ b/app/dialog_flow/graph_definition.py @@ -0,0 +1,141 @@ +# Consultme/app/dialog_flow/graph_definition.py + +from typing import Dict, Any, List + +# --- DEFINIÇÃO DOS ESTADOS DO SEU CHATBOT (O GRAFO) --- +DIALOG_GRAPH: Dict[str, Dict[str, Any]] = { + "INICIO": { + "message": "", + "expected_input_type": "text", + "transitions": { + "default": "MENU_PRINCIPAL" + } + + }, + + "MENU_PRINCIPAL": { + "message": "*Olá você inicou o Consultme!:*", + "action_to_perform": "send_main_menu", # Ação para enviar um menu interativo (botões) + "expected_input_type": "button_click", + "transitions": { + "OPTION_AGENDAR": "MENU_PRINCIPAL_STORE", + "OPTION_CADASTRO_FLOW": "MENU_PRINCIPAL_STORE", + "OPTION_STATUS": "MENU_PRINCIPAL_STORE", + "OPTION_FALAR_ATENDENTE": "MENU_PRINCIPAL_STORE", + "default": "MENU_PRINCIPAL_STORE" # Volta para o menu se a opção não for reconhecida + } + }, + + "MENU_PRINCIPAL_STORE": { + "action_to_perform": "send_main_store", # Ação para enviar um menu interativo (botões) + "expected_input_type": "button_click", + "transitions": { + "OPTION_AGENDAR": "AGENDAMENTO_INICIO", + "OPTION_CADASTRO_FLOW": "INICIAR_FLOW_CADASTRO", + "OPTION_STATUS": "PEDIR_NUMERO_PEDIDO", + "OPTION_FALAR_ATENDENTE": "ENCAMINHAR_ATENDENTE", + "default": "MENU_PRINCIPAL" # Volta para o menu se a opção não for reconhecida + } + }, + + "RESPOSTA_NAO_ENTENDIDA": { + "message": "Desculpe, não entendi sua última mensagem. Por favor, digite 'menu' para ver as opções ou 'ajuda'.", + "expected_input_type": "text", + "transitions": { + "menu": "MENU_PRINCIPAL", + "ajuda": "MENU_PRINCIPAL", + "default": "RESPOSTA_NAO_ENTENDIDA" # Continua não entendendo + } + }, + # --- FLUXO DE CADASTRO (com Flow) --- + "INICIAR_FLOW_CADASTRO": { + "action_to_perform": "send_flow_cadastro", # Ação para enviar o Flow + "flow_id": "COLOQUE_AQUI_O_FLOW_ID_DO_SEU_CADASTRO_PUBLICADO", # ID do seu Flow publicado + "flow_cta": "Abrir Cadastro", + "expected_input_type": "flow_nfm_reply", # Espera a resposta do Flow + "transitions": { + "success": "CADASTRO_CONCLUIDO", # Se o Flow for concluído com sucesso + "failure": "CADASTRO_FALHA" # Se houver um problema no Flow (ou o usuário não preencher) + } + }, + "CADASTRO_CONCLUIDO": { + "message": "Obrigado por se cadastrar, ${nome_completo}! Seu e-mail é: ${email}. Já pode explorar nossos serviços!", + "action_to_perform": "process_cadastro_data", # Ação para salvar no BD/CRM + "expected_input_type": "any", # Qualquer coisa leva ao menu + "transitions": { + "default": "MENU_PRINCIPAL" + } + }, + "CADASTRO_FALHA": { + "message": "Não foi possível completar seu cadastro. Por favor, tente novamente ou digite 'ajuda'.", + "expected_input_type": "text", + "transitions": { + "default": "MENU_PRINCIPAL" + } + }, + # --- FLUXO DE AGENDAMENTO (Exemplo Básico) --- + "AGENDAMENTO_INICIO": { + "message": "Certo! Para agendar, qual serviço você precisa?", + "expected_input_type": "text", + "transitions": { + "default": "AGENDAMENTO_CONFIRMAR_SERVICO" + } + }, + "AGENDAMENTO_CONFIRMAR_SERVICO": { + "message": "Você precisa de *${servico_agendado}*. Qual data e horário você prefere?", + "expected_input_type": "text", + "transitions": { + "default": "AGENDAMENTO_FINALIZAR" + }, + "action_to_perform": "save_temp_service" # Ação para guardar o serviço temporariamente + }, + "AGENDAMENTO_FINALIZAR": { + "message": "Seu agendamento para *${servico_agendado}* em *${data_horario_agendado}* foi confirmado! Te vejo lá!", + "action_to_perform": "confirm_appointment", # Ação para agendar no sistema real + "expected_input_type": "any", + "transitions": { + "default": "MENU_PRINCIPAL" + } + }, + # --- FLUXO DE STATUS DO PEDIDO --- + "PEDIR_NUMERO_PEDIDO": { + "message": "Por favor, digite o número do seu pedido para consultar o status:", + "expected_input_type": "text", + "transitions": { + "default": "CONSULTAR_STATUS_API" + } + }, + "CONSULTAR_STATUS_API": { + "message": "Consultando o status do pedido *${numero_pedido}*...", + "action_to_perform": "call_external_status_api", # Ação para chamar um sistema externo (API futura) + "expected_input_type": "api_response", # A resposta vem de uma API, não do usuário + "transitions": { + "success": "STATUS_EXIBIR", + "failure": "STATUS_NAO_ENCONTRADO" + } + }, + "STATUS_EXIBIR": { + "message": "O status do pedido *${numero_pedido}* é: *${status_retornado}*.", + "expected_input_type": "any", + "transitions": { + "default": "MENU_PRINCIPAL" + } + }, + "STATUS_NAO_ENCONTRADO": { + "message": "Não consegui encontrar o pedido *${numero_pedido}*. Verifique e digite novamente, ou 'menu'.", + "expected_input_type": "text", + "transitions": { + "menu": "MENU_PRINCIPAL", + "default": "PEDIR_NUMERO_PEDIDO" + } + }, + # --- FLUXO DE ATENDENTE --- + "ENCAMINHAR_ATENDENTE": { + "message": "Encaminhando você para um de nossos atendentes. Por favor, aguarde.", + "terminal": True # Indica que a conversa termina aqui (até o atendente assumir) + }, + # ... Adicione mais estados conforme a complexidade da sua conversa ... +} + +# --- ESTADO INICIAL --- +INITIAL_STATE_ID = "INICIO" \ No newline at end of file diff --git a/app/main.py b/app/main.py index 57923c8..2089847 100644 --- a/app/main.py +++ b/app/main.py @@ -35,4 +35,4 @@ async def shutdown_event(): # Exemplo de uso de Uvicorn para rodar o FastAPI if __name__ == "__main__": - uvicorn.run(app, host="0.0.0.0", port=5000) \ No newline at end of file + uvicorn.run(app, host="0.0.0.0", port=5000) diff --git a/app/services/__pycache__/webhook_service.cpython-312.pyc b/app/services/__pycache__/webhook_service.cpython-312.pyc index 5364451b3ad6803c292e1ac52131b636c6d30f10..19bc54919534bc1116939e195ed608f4741a1679 100644 GIT binary patch delta 12149 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 AO#lD@ delta 8077 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 I%|ZTu0aRke-~a#s diff --git a/app/services/webhook_service.py b/app/services/webhook_service.py index 0c5ad2c..3de0858 100644 --- a/app/services/webhook_service.py +++ b/app/services/webhook_service.py @@ -36,6 +36,10 @@ from cryptography.hazmat.backends import default_backend from config import WHATSAPP_ACCESS_TOKEN, WHATSAPP_PHONE_NUMBER_ID, VERIFY_TOKEN, FLOW_PRIVATE_KEY_PASSWORD # <-- ADICIONADO # Importe Message para type hinting. Certifique-se que webhook_model.py está correto primeiro! +# Certifique-se de importar o grafo e o estado inicial +from dialog_flow.graph_definition import DIALOG_GRAPH, INITIAL_STATE_ID +# Certifique-se de que FlowStatusChangeEvent também está importado se ainda não estiver + from models.webhook_model import Message, TextMessage, ButtonMessage, InteractiveMessage, ListReply # Adicione os modelos necessários FLOW_PRIVATE_KEY_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../private.pem') @@ -254,88 +258,254 @@ def start_or_update_session(sender_id: str): +#Fluxo de mensagens: + +# --- NOVA FUNÇÃO: O MOTOR DO CHATBOT BASEADO EM GRAFO --- +async def process_user_input_with_graph(sender_id: str, message_type: str, message_content: Any): + """ + Processa a entrada do usuário e avança o estado da conversa no grafo. + """ + session_data = get_session_state(sender_id) + if not session_data: + # Se a sessão não existe (timeout ou novo usuário), inicie do estado INICIO + current_state_id = INITIAL_STATE_ID + start_or_update_session(sender_id) # Garante que a sessão existe + session_data = get_session_state(sender_id) # Recarrega para ter certeza + else: + current_state_id = session_data.get("current_state", INITIAL_STATE_ID) # Pega o estado atual + + current_state = DIALOG_GRAPH.get(current_state_id) + if not current_state: + print(f"❌ ERRO GRAVE: Estado '{current_state_id}' não encontrado no DIALOG_GRAPH. Retornando ao INICIO.") + current_state_id = INITIAL_STATE_ID + current_state = DIALOG_GRAPH[INITIAL_STATE_ID] + session_data["current_state"] = INITIAL_STATE_ID # Reseta o estado na sessão + + print(f"DEBUG_GRAPH: Usuário {sender_id} no estado '{current_state_id}'. Tipo de entrada: {message_type}. Conteúdo: {message_content}") + + next_state_id = None + user_input_processed = str(message_content).lower().strip() # Padroniza a entrada para transições + + # --- Lógica para determinar o próximo estado (transição) --- + if message_type == "text": + # Verifica se alguma palavra-chave da transição corresponde ao texto + for keyword, target_state in current_state.get("transitions", {}).items(): + if keyword != "default" and keyword in user_input_processed: + next_state_id = target_state + break + if not next_state_id: # Se nenhuma palavra-chave específica, usa o default + next_state_id = current_state.get("transitions", {}).get("default", INITIAL_STATE_ID) + + session_data["last_text_input"] = message_content # Salva a última entrada de texto para uso futuro + + elif message_type == "button_click": + # O message_content já é o payload do botão + next_state_id = current_state.get("transitions", {}).get(message_content, INITIAL_STATE_ID) + + elif message_type == "flow_nfm_reply": + # Quando uma resposta de Flow chega, o next_state depende do sucesso/falha do Flow + # Assumimos que handle_flow_response já processou o Flow e determinou sucesso/falha + # A lógica para a transição do Flow precisa ser mais sofisticada. + # Por enquanto, vamos para o estado de conclusão do Flow. + next_state_id = current_state.get("transitions", {}).get("success", INITIAL_STATE_ID) # Por padrão, vai para o sucesso + session_data["flow_data"] = message_content # Salva os dados do Flow para processamento posterior + + # ... (adicionar outros tipos de entrada, como "api_response" para transições internas, se necessário) ... + + # Atualiza o estado da sessão + session_data["current_state"] = next_state_id + start_or_update_session(sender_id) # Atualiza o timestamp da sessão + + print(f"DEBUG_GRAPH: Usuário {sender_id} transicionou para o estado: '{next_state_id}'") + await execute_state_action_and_respond(sender_id, next_state_id, session_data) + + +async def execute_state_action_and_respond(sender_id: str, state_id: str, session_data: Dict[str, Any]): + """ + Executa a ação de um estado e envia a resposta correspondente. + """ + state_definition = DIALOG_GRAPH.get(state_id) + if not state_definition: + print(f"❌ ERRO GRAVE: Definição para o estado '{state_id}' não encontrada. Não é possível responder.") + await send_text_message(sender_id, "Ops! Ocorreu um erro interno. Por favor, tente novamente.") + session_data["current_state"] = INITIAL_STATE_ID # Reseta o estado + start_or_update_session(sender_id) + return + + # --- Ações a serem realizadas pelo bot no novo estado --- + if "action_to_perform" in state_definition: + action = state_definition["action_to_perform"] + print(f"DEBUG_GRAPH: Executando ação para estado '{state_id}': {action}") + + if action == "send_main_menu": + await send_time_menu(sender_id) + elif action == "send_main_store": + await send_store_menu(sender_id) + elif action == "send_flow_cadastro": + flow_id = state_definition.get("flow_id") + flow_cta = state_definition.get("flow_cta") + if flow_id: + # O Flow ID real virá da DIALOG_GRAPH, não do SendFlowRequest + await send_whatsapp_flow(sender_id, flow_id, flow_cta) + else: + await send_text_message(sender_id, "Ops! O Flow de cadastro não está configurado. Tente novamente mais tarde.") + session_data["current_state"] = INITIAL_STATE_ID + start_or_update_session(sender_id) + + elif action == "process_cadastro_data": + # Aqui você processaria os dados do Flow que estão em session_data["flow_data"] + flow_data = json.loads(session_data.get("flow_data", "{}")) # Carregar os dados salvos + nome_completo = flow_data.get("nome_completo") + email = flow_data.get("email") + + if nome_completo and email: + print(f"DEBUG_GRAPH: Processando cadastro para {nome_completo}, {email}") + # AQUI você salvaria no BD/CRM + # salvar_usuario_no_banco_de_dados(sender_id, nome_completo, email) + # enviar_para_crm(nome_completo, email) + response_message = state_definition["message"].replace("${nome_completo}", nome_completo).replace("${email}", email) + await send_text_message(sender_id, response_message) + else: + await send_text_message(sender_id, "Falha ao processar cadastro. Dados incompletos. Por favor, tente novamente.") + session_data["current_state"] = DIALOG_GRAPH.get("CADASTRO_FALHA", {}).get("transitions",{}).get("default", INITIAL_STATE_ID) # Ir para estado de falha + start_or_update_session(sender_id) + + + elif action == "save_temp_service": + # Exemplo: guardar o serviço agendado + # service = session_data.get("last_text_input") # Pega o último texto que o usuário enviou + # session_data["servico_agendado"] = service + # start_or_update_session(sender_id) # Atualiza a sessão + pass # Ação interna, não envia mensagem aqui + + elif action == "confirm_appointment": + # Exemplo: confirmar agendamento no sistema externo + # (Chamada de API para sistema de agendamento) + pass # Ação interna, a mensagem já vem do state_definition + + + elif action == "call_external_status_api": + # AQUI é onde você faria a chamada para o seu sistema externo + # Por enquanto, apenas um placeholder + # numero_pedido = session_data.get("last_text_input") + # print(f"DEBUG_GRAPH: Chamando API externa para pedido {numero_pedido}") + # try: + # status_api = await external_api_call(numero_pedido) + # session_data["status_retornado"] = status_api # Salva na sessão + # # Transicionar internamente para STATUS_EXIBIR ou STATUS_NAO_ENCONTRADO + # await execute_state_action_and_respond(sender_id, "STATUS_EXIBIR", session_data) # Transição interna + # return + # except Exception: + # await execute_state_action_and_respond(sender_id, "STATUS_NAO_ENCONTRADO", session_data) # Transição interna + # return + pass # Implementação futura, a mensagem vem do estado. + + # --- Enviar a mensagem do novo estado (se houver) --- + if "message" in state_definition: + # Substitui placeholders na mensagem + final_message = state_definition["message"] + if "${nome_completo}" in final_message and session_data.get("flow_data"): + flow_data = json.loads(session_data["flow_data"]) + final_message = final_message.replace("${nome_completo}", flow_data.get("nome_completo", "usuário")) + if "${email}" in final_message and session_data.get("flow_data"): + flow_data = json.loads(session_data["flow_data"]) + final_message = final_message.replace("${email}", flow_data.get("email", "não informado")) + + # Para outros placeholders como ${servico_agendado}, ${data_horario_agendado}, ${numero_pedido}, ${status_retornado} + # você faria substituições semelhantes baseadas em session_data + + await send_text_message(sender_id, final_message) + + # Lógica para limpar a sessão se o estado for terminal + if state_definition.get("terminal"): + await clean_session_and_notify(sender_id, send_timeout_message=False) # Não envia msg de timeout + print(f"DEBUG_GRAPH: Sessão para {sender_id} encerrada no estado terminal '{state_id}'.") + + + + + + + + + + + + + + + # --- Lógicas de Tratamento de Mensagens Recebidas (Funções Auxiliares) --- # Estas funções contêm a lógica de como seu bot irá interagir. -# Esta função precisa ser definida no arquivo! +# --- handle_message_type: Agora é o ponto de entrada principal que chama o motor do grafo --- +#--- FUNÇÃO handle_message_type: A VERSÃO CORRETA PARA O TIMEOUT E GRAFO --- async def handle_message_type(message: Message): sender_id = message.from_ - # 1. Iniciar ou atualizar a sessão para o remetente atual - # Isso vai automaticamente agendar/reagendar a limpeza. - start_or_update_session(sender_id) # Esta função agora agenda a limpeza! + # 1. Limpar sessões inativas + # Esta função limpa as sessões que expiraram e envia a mensagem de timeout via scheduler. + start_or_update_session(sender_id) # Esta é a que precisa ser assíncrona - # 2. Recuperar o estado da sessão (se houver, para uso futuro com grafos) + # 2. INICIAR OU ATUALIZAR A SESSÃO PARA O REMETENTE ATUAL. + # ISSO É ESSENCIAL PARA O TIMEOUT. Se o usuário mandar mensagem, a sessão dele é atualizada + # e o agendamento de timeout é resetado. + start_or_update_session(sender_id) + + # 3. Recuperar o estado da sessão (já atualizado) session_data = get_session_state(sender_id) if session_data: print(f"DEBUG_SESSION: Estado atual da sessão para {sender_id}: {session_data['current_state']}") else: # Isso não deveria acontecer se start_or_update_session funcionou - print(f"DEBUG_SESSION: Sessão para {sender_id} não encontrada após start_or_update_session (possível erro).") + print(f"DEBUG_SESSION: ERRO: Sessão para {sender_id} não encontrada após atualização. Isso é inesperado.") + # Se por algum motivo não tiver sessão, podemos resetar para o estado inicial + start_or_update_session(sender_id) + session_data = get_session_state(sender_id) # Tenta novamente + + message_content = None + message_type = None + + # --- IDENTIFICAÇÃO DO TIPO DE MENSAGEM --- + # Esta parte permanece como estava, identificando o tipo e conteúdo. if message.type == 'text' and message.text: - await handle_text_message(sender_id, message.text.body) # Passe sender_id - elif message.type == 'button' and message.button: - await handle_button_response(sender_id, message.button.payload) # Passe sender_id + message_type = "text" + message_content = message.text.body + elif message.type == 'button' and message.button: # Resposta de botão de resposta rápida + message_type = "button_click" + message_content = message.button.payload elif message.type == 'interactive' and message.interactive: - if message.interactive.type == 'list_reply' and message.interactive.list_reply: - await handle_list_response(sender_id, message.interactive.list_reply.id, message.interactive.list_reply.title) # Passe sender_id - elif message.interactive.type == 'button_reply' and message.interactive.button_reply: - await handle_button_response(sender_id, message.interactive.button_reply.payload) # Passe sender_id + if message.interactive.type == 'list_reply' and message.interactive.list_reply: # Resposta de lista + message_type = "list_reply" + message_content = message.interactive.list_reply.id + elif message.interactive.type == 'button_reply' and message.interactive.button_reply: # Resposta de botão interativo + message_type = "button_click" # Tratar como clique de botão + message_content = message.interactive.button_reply.id + elif message.interactive.type == 'nfm_reply' and message.interactive.nfm_reply: # Resposta de Flow + message_type = "flow_nfm_reply" + message_content = message.interactive.nfm_reply.response_json else: print(f" Tipo interativo desconhecido recebido: {message.interactive.type}") await send_text_message(sender_id, "Desculpe, não entendi essa interação interativa.") - elif message.type == 'image': - print(' Recebi uma imagem!') - await send_text_message(sender_id, "Que legal! Recebi sua imagem. No momento, só consigo processar texto e interações.") - else: - print(f" Tipo de mensagem não suportado: {message.type}") + return # Sai, não há transição no grafo para isso + elif message.type == 'image': # Mensagens que não são processadas pelo grafo + message_type = "image" + message_content = "imagem_recebida" # Placeholder + await send_text_message(sender_id, "Recebi sua imagem. No momento, só consigo processar texto e interações.") + return # Sai, não há transição no grafo para isso + else: # Tipo de mensagem não suportado ou desconhecido + message_type = "unsupported" + message_content = "tipo_desconhecido" await send_text_message(sender_id, "Desculpe, não entendi o tipo de mensagem que você enviou.") + return # Sai + # 4. O motor do grafo processa a entrada e gerencia o estado da sessão. + # A `session_data` já foi atualizada no passo 2. + await process_user_input_with_graph(sender_id, message_type, message_content) -async def handle_text_message(sender_id: str, text: str): - lower_text = text.lower() - if lower_text != "" : - await send_text_message(sender_id, "Olá! Você iniciou o Consultme. Escolha uma das opções de consulta no menu abaixo:") - await send_interactive_menu(sender_id) # Chama a função para enviar um menu interativo - elif 'menu' in lower_text: - await send_interactive_menu(sender_id) - elif 'cadastro' in lower_text: - await send_text_message(sender_id, "Para iniciar seu cadastro, por favor, me diga seu nome completo:") - # Implemente lógica para salvar o estado do usuário aqui (ex: em um DB) - elif 'ajuda' in lower_text: - await send_text_message(sender_id, "Posso te guiar com as funcionalidades principais. Escolha uma opção do menu ou digite uma pergunta.") - await send_interactive_menu(sender_id) - else: - await send_text_message(sender_id, f"Recebi sua mensagem: \"{text}\". Parece que não entendi bem. Você pode digitar 'menu' para ver as opções disponíveis ou 'ajuda'.") - -async def handle_button_response(sender_id: str, payload: str): - response_text = '' - if payload == 'OPTION_AGENDAR': - response_text = "Certo! Para agendar um serviço, qual serviço você precisa e a data/hora preferida?" - elif payload == 'OPTION_STATUS': - response_text = "Para verificar o status de seu pedido, informe o número do seu pedido." - elif payload == 'OPTION_FALAR_ATENDENTE': - response_text = "Encaminhando você para um de nossos atendentes. Aguarde, por favor." - else: - response_text = "Não entendi a opção de botão selecionada. Tente novamente ou digite 'menu'." - await send_text_message(sender_id, response_text) - -async def handle_list_response(sender_id: str, list_id: str, list_title: str): - response_text = '' - if list_id == 'item_reparo_geral': - response_text = f"Você selecionou \"{list_title}\". Qual o problema específico que você precisa de reparo?" - elif list_id == 'item_instalacao': - response_text = f"Você selecionou \"{list_title}\". Qual tipo de instalação você precisa?" - elif list_id == 'item_duvidas': - response_text = f"Você selecionou \"{list_title}\". Por favor, digite sua pergunta." - elif list_id == 'item_reclamacoes': - response_text = f"Você selecionou \"{list_title}\". Por favor, descreva o problema em detalhes." - else: - response_text = "Opção de lista não reconhecida. Tente novamente ou digite 'menu'." - await send_text_message(sender_id, response_text) # --- Funções de Envio de Mensagens para o WhatsApp --- # Estas funções fazem as chamadas à API da Meta para enviar mensagens. @@ -449,8 +619,85 @@ async def mark_message_as_read(message_id: str): except httpx.RequestError as e: print(f"❌ Erro de rede ao marcar mensagem {message_id} como lida: {e}") + + # --- Função de Exemplo para Enviar um Menu Interativo --- -async def send_interactive_menu(to: str): +# --- NOVA FUNÇÃO: send_stores_list_menu (Para menu de LISTA de lojas) --- +async def send_time_menu(to: str): # <-- FUNÇÃO QUE VOCÊ PEDIU + """ + Envia uma mensagem interativa de LISTA para o usuário com opções de lojas. + """ + header_text = "Escolha o Período que você quer visualizar o indicador" + body_text = "Veja as opções a baixo" + button_title = "Clique aqui" # Texto do botão que o usuário clica para ABRIR a lista + + sections = [ + { + "title": "Acumulados", # Título da primeira seção + "rows": [ + {"id": 'OPTION_ANO', "title": 'Acumulado do Ano', "description": "Do do ano até ontem."}, + {"id": 'OPTION_MES', "title": 'Acumulado do Mês', "description": "Do primeira do mês até ontem."}, + ] + }, + { + "title": "Resultados", # Título da segunda seção + "rows": [ + {"id": 'OPTION_ONTEM', "title": 'Resultado de Ontem', "description": "Ver o realizado de ontem."}, + {"id": 'OPTION_HOJE', "title": 'Resultado de Hoje', "description": "Resultado Parcial de Hoje"} + ] + }, + { + "title": "Sair", # Título da segunda seção + "rows": [ + {"id": 'OPTION_SAIR', "title": 'Sair', "description": "Encerre a conversa"}, + ] + } + ] + # Relembrando: até 10 seções e 10 itens por seção (total 100 itens). + + # CHAMA send_interactive_list DIRETAMENTE AQUI + await send_interactive_list(to, header_text, body_text, button_title, sections) + + +async def send_store_menu(to: str): # <-- FUNÇÃO QUE VOCÊ PEDIU + """ + Envia uma mensagem interativa de LISTA para o usuário com opções de lojas. + """ + header_text = "Escolha o Período que você quer visualizar o indicador" + body_text = "Veja as opções a baixo" + button_title = "Clique aqui" # Texto do botão que o usuário clica para ABRIR a lista + + sections = [ + { + "title": "Acumulados", # Título da primeira seção + "rows": [ + {"id": 'OPTION_ANO', "title": 'Total do CP', "description": "Visualize o resultado Total do CP"}, + {"id": 'OPTION_MES', "title": 'Total do Estado', "description": "Visualize o resultado Total das suas Lojas"}, + ] + }, + { + "title": "Por Loja", # Título da segunda seção + "rows": [ + {"id": 'OPTION_ONTEM', "title": 'Total das suas Lojas', "description": "Visualize o total das suas Lojas"}, + {"id": 'OPTION_HOJE', "title": 'Total de uma Loja', "description": "Visualize o total de uma loja."} + ] + }, + { + "title": "Sair", # Título da segunda seção + "rows": [ + {"id": 'OPTION_SAIR', "title": 'Sair', "description": "Encerre a conversa"}, + ] + } + ] + # Relembrando: até 10 seções e 10 itens por seção (total 100 itens). + + # CHAMA send_interactive_list DIRETAMENTE AQUI + await send_interactive_list(to, header_text, body_text, button_title, sections) + +# Exemplo de como usar o MENU, só aceita 3 opções mas já aparece direto pro usuário as opções. + +''' +async def send_stores_menu(to: str): buttons = [ {"id": 'OPTION_AGENDAR', "title": 'Total do CP'}, {"id": 'OPTION_STATUS', "title": 'Total das suas Lojas'}, @@ -458,7 +705,17 @@ async def send_interactive_menu(to: str): ] await send_interactive_buttons(to, "Menu Principal", "Escolha a Dimensão das Lojas que você quer visulizar o indicador:", buttons) +async def send_time_menu(to: str): + buttons = [ + {"id": 'OPTION_AGENDAR', "title": 'Acumulado do Ano'}, + {"id": 'OPTION_STATUS', "title": 'Acumulado do Mês'}, + {"id": 'OPTION_FALAR_ATENDENTE', "title": 'Resultado de Ontem'} + #{"id": 'OPTION_FALAR_ATENDENTE', "title": 'Resultado Parcial de Hoje'}, + #{"id": 'OPTION_FALAR_ATENDENTE', "title": 'Sair'} + ] + await send_interactive_buttons(to, "Escolha o Período que você quer visualizar o indicador", "Veja as opções abaixo:", buttons) +''' async def send_whatsapp_flow(to: str, flow_id: str, flow_cta: str, screen: str = "welcome_screen") -> Dict[str, Any]: url = f"https://graph.facebook.com/v18.0/{WHATSAPP_PHONE_NUMBER_ID}/messages" headers = {