Skip to main content

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:

  1. 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;
     

  2. 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 defina contact.identify e para message utilizei input.content, mas você pode usar outra variável, e salve o retorno desse script em uma variável, no meu caso chamei de wfmbody (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
    }
    }

     

  3. 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 como wfmresponse. 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 None

    Como 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.

     

  4. 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
  5. 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 API wfmresponse@message como a mensagem enviada pelo usuário, pois ele irá trazer todas as mensagem concatenadas e não poderá mais utilizar input.content ou qualquer outra variável definida em Entrada do usuário.

    Um exemplo visual do antes e depois de um fluxo simples
    O que antes era assim
    Agora ficou assim

    Uma 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.

Olá ​@Anderson Filho tudo bem ? 

Ótimo guia, poderia só colocar uma imagem de como ficou a requisição http dentro do blip? acredito que iria ajudar bastante ou o exemplo do código dela.


Olá ​@Anderson Filho tudo bem ? 

Ótimo guia, poderia só colocar uma imagem de como ficou a requisição http dentro do blip? acredito que iria ajudar bastante ou o exemplo do código dela.

Claro, segue como ficou a Requisição HTTP:

Em CORPO ficou assim {{wfmbody}}, pois eu já havia definido a variável no script anterior (passo 2)
 

Aqui uma boa prática que acabei não fazendo, mas recomendo, é definir uma variável para o status da resposta e caso o endpoint fique fora do ar ou retorne um status diferente do que você definiu para sucesso é só definir outra condição de saída e fazer a tratativa que achar melhor, seja tentar novamente, utilizar um endpoint backup ou receber as mensagens individualmente.


Comente