Oi pessoal, imagino que muitos aqui que utilizam chatbot, já tiveram a necessidade de receber várias mensagens e dar um uníca resposta como provalvemente um atendente real faria, infelizmente alguns plataforma, como a Blip, não possuem uma solução pronta para esse caso de uso, pois as mensagens são processadas no fluxo de forma síncrona, já utilizamos outra plataforma com essa mesma limitação que só foi solucionada com uma engenharia manual.
ANTES | DEPOIS
|
O problema
No fluxo do chatbot as mensagens enviadas pelo usuário são executadas de forma síncrona, ou seja, o fluxo segue para o próximo passo sem saber se houve novas mensagens e caso tenha surgido novas mensagens elas serão enviadas para o fluxo sequencialmente, porém o recebimento das mensagens pela Blip é asíncrono.
A solução
Sabendo do problema acima, chegamos no seguinte algoritmo: recebemos um mensagem do usuário message
, aguardamos wait_in_sec
segundos (nesse momento o usuário pode enviar outras mensagens), em seguida consultamos as mensagens na API da Blip, que são recebidas asincronamente, e caso a message
não for a última mensagem recebida, significa que a sessão dela deve ser descartada pois uma nova sessão foi iniciada pelo envio de outra mensagem dentro dos wait_in_sec
segundos e fazemos isso até que a mensagem da sessão seja a mesma que a última mensagem recebida pela Blip então retornamos todas as mensagens enviadas nesse período concatenadas em uma string. Essa solução também permite que o usuário envie apenas uma mensagem mas de toda forma wait_in_sec
segundos serão aguardados.
Sem mais delongas vamos para o passo a passo da solução, desenvolvida por mim e meu colega Kaique Fernando:
-
Nos nodes que é esperado uma Entrada do usuário sempre crie um node exclusivo para ela, neste exemplo criei um chamado Wait 15s for messages;
-
Agora em Ações de Saída faremos uma chamada de API, então adicione Executar script para preparar o payload da requisição, e não esqueça de definir os parâmetros em
user
definacontact.identify
e paramessage
utilizeiinput.content
, mas você pode usar outra variável, e salve o retorno desse script em uma variável, no meu caso chamei dewfmbody
(wfm = wait for messages);function run(user, message) {
return {
"user": user, // identificador do usuário
"message": message, // mensagem atual
"wait_in_sec": 15 // tempo em seg que iremos aguardar
}
} -
Adicione uma Requisição HTTP em Ações de Saída, nesse momento você precisará criar um endpoint exclusivo para que essa solução funcione, para fins de testes utilizamos a ferramenta ngrok para criar o endpoint. Defini o método como POST, coloquei em URL o endereço temporário que o ngrok gerou e passei o
wfmbody
no corpo da requisição, e a Variável para o corpo da resposta defini comowfmresponse
. Abaixo o código-fonte do endpoint desenvolvido em Python:from fastapi import FastAPI
from pydantic import BaseModel
import asyncio
from datetime import datetime
import requests
import json
import uuid
app = FastAPI()
url = "https://msging.net/commands"
# A chave de acesso pode ser criada nas configurações do seu Roteador ou Chatbot principal
blip_token = "CHAVE_DE_ACESSO"
class MessageData(BaseModel):
user: str
message: str
wait_in_sec: float = 15
async def get_messages_blip(data: str) -> list:
payload = json.dumps({
"id": str(uuid.uuid4()),
"method": "get",
"uri": f"/threads/{data}?refreshExpiredMedia=true",
})
headers = {
"Authorization": f"Key {blip_token}",
"Content-Type": "application/json",
}
response = requests.request("POST", url, headers=headers, data=payload)
items = response.json().get("resource").get("items")
received = r]
for item in items:
if item.get("direction") == "sent" and len(received) > 0:
return received
if item.get("direction") == "received":
received.append(item.get("content"))
return received
@app.post("/wait")
async def wait(data: MessageData):
await asyncio.sleep(data.wait_in_sec)
messages = await get_messages_blip(data.user)
if not messages:
return None
last_message = messagesw0]
# * Essa condição pode e deve ser melhorada
if data.message == last_message:
if len(messages) == 1:
return {"message": data.message}
else:
messages.reverse()
message_content = "\n".join(messages)
# O Blip Chat utiliza <br> para quebra de linha
# enquanto que o WhatsApp utiliza \n
# * Essa condição pode e deve ser melhorada
if "wa.gw.msging.net" not in data.user:
message_content = message_content.replace("\n", "<br>")
return {"message": message_content}
else:
return NoneComo queríamos resolver esse problema logo deixamos de lado algumas boas práticas como tratar os outros formatos de mensagens (audio, imagem, vídeo, documento, arquivo) e otimizações no código que podem evitar que exceções ocorram, como utilizar a data de criação da mensagem ao invés do conteúdo na comparação, então fica a seu critério utilizar outra linguagem e/ou fazer estes e outros ajustes no código.
- Agora em Condições de saída, inclua a condição abaixo, dessa forma quando houve novas mensagens todas que não forem a última irão retornar para esse mesmo node
- Apenas a última mensagem irá seguir para as demais condições de saída que
você definir, sendo assim você sempre precisará utilizar o retorno da APIwfmresponse@message
como a mensagem enviada pelo usuário, pois ele irá trazer todas as mensagem concatenadas e não poderá mais utilizarinput.content
ou qualquer outra variável definida em Entrada do usuário.
Um exemplo visual do antes e depois de um fluxo simplesUma observação: como no node de Início já existe uma Entrada do usuário fixa então optei por criar um node na sequência sem nenhum Conteúdo, que chamei de Wait 15s for messages on start, poderia ter feito os passos 2, 3 e 4 no próprio node de Início porém no nosso caso utilizamos as Ações de saída do node de Início para executar outras ações que não fariam sentido serem executadas múltiplas vezes como por exemplo um tracking de eventos.
Pode parecer uma solução trabalhosa de início, mas o retorno que isso trará em relação a experiência dos usuários com o chatbot vale a pena. E para quem já tem um chatbot em produção, essa solução pode ir sendo implementada aos poucos substituindo cada referência de uma Entrada do usuário para essa nova abordagem.
Espero que isso possa atender essa necessidade de tratar múltiplas mensagens por um tempo, enquanto a Blip trabalha em uma feature definitiva, pensei que talvez esse mesmo algoritmo, só que melhorado, poderia estar incorporado, por trás dos panos, a Entrada do usuário e nela haver uma configuração para aguardar X tempo por mensagens e se X for 0 continua funcionando como é hoje onde o chatbot responde a cada mensagem.