Construindo um chatbot pro Messenger

Diário de desenvolvimento

Marcus Beckenkamp
Beck Blog

--

Introdução

Este é um diário de desenvolvimento de um projeto pessoal que comecei cerca de 30 dias atrás. A cada dia em que eu tive tempo livre para parar e desenvolver algo eu adicionei uma entrada no diário.

Se você não está com paciência e quer ver logo como ficou o projeto final, vá direto para a última sessão dessa publicação. Lá tem um vídeo mostrando como ficou a versão "final".

Se tiver tempo e interesse, em cada uma das entradas do diário eu trouxe as dúvidas, os problemas e as soluções que encontrei. Tentei abordar tudo que aconteceu, não só de forma técnica como também o que eu pensei em cada situação.

Dia 1

A ideia e a escolha do chat

A primeira coisa que precisei para começar meu primeiro chatbot para o Facebook Messenger foi uma ideia.

Já fazia bastante tempo que eu queria experimentar a criação de um chatbot. Como já falei em outro texto, eu só aprendo algo novo quando realmente preciso usar em algum projeto. A ideia era algo essencial para eu dar o pontapé inicial.

Costumo guardar minhas informações de orçamento doméstico em uma planilha comum no Google Drive. Mas resolvi que, sendo um desenvolvedor, eu poderia automatizar o processo de input dos meus dados de gastos diários. Ficar preenchendo planilha é um trabalho sacal e me parece uma perda de tempo.

Só que eu não queria criar mais um sistema web comum para guardar meus dados de orçamento doméstico, até porque já existem centenas deles por aí.

Já faz um tempo que venho lendo sobre Conversation UI e acredito que um aplicativo de mensagens pode, em alguns casos, substituir os formulários chatos dos sistemas web.

Somando tudo isso, a ideia de usar um chatbot para atingir esse objetivo me pareceu muito boa.

O Facebook Messenger é um app que já tenho no meu celular e achei um que seria um bom local para facilitar esses inputs, já que eles devem ser feitos várias vezes ao dia. Afinal, estamos falando de um orçamento de controle de gastos, certo?

Eu poderia facilmente usar o Telegram para esse objetivo, mas, como já experimentei desenvolver um chatbot para esse app, quis tentar algo novo.

Começando o desenvolvimento

Comecei o meu chatbot me baseando em um template simples que encontrei no github que usa o Flask, um microframework web em Python que já conheço e domino bem.

Segui o README do projeto para configurar e fiz o primeiro teste no meu novo chatbot servindo localmente usando o ngrok. Foi um sucesso.

Tudo bem que ele apenas responde “Hello World” para qualquer mensagem enviada. Mas já é um primeiro passo!

Depois foi a hora de subir para o github e fazer um deploy no Heroku para deixá-lo no ar, independente do meu computador estar ligado ou não.

Dia 2

Guardando dados do usuário

Agora que o bot já está rodando de forma básica, chegou a hora de começar a personalizar suas ações e adicionar as funcionalidades.

A primeira coisa que fiz foi criar uma forma de guardar os dados do usuário. Adicionei um banco de dados simples que guarda as informações que o Facebook disponibiliza através de sua API.

Para conseguir esses dados, basta executar um GET em https://graph.facebook.com/v2.6/<USER_ID>?access_token=PAGE_ACCESS_TOKEN. A resposta é algo como:

{
"first_name": "Peter",
"last_name": "Chang",
"profile_pic": "https://fbcdn-profile-a.akamaihd.net/...",
"locale": "en_US",
"timezone": -7,
"gender": "male"
}

Para o banco de dados eu usei um SQLite, usando o flask_sqlalchemy como ORM.

Com os dados do usuário em mãos, agora o bot não responde apenas "Hello World" para cada mensagem enviada. Ele responde "Hello, {nome do usuário}".

Carregando…

Para dar uma naturalidade e uma sensação de conversa entre o usuário e o bot, resolvi adicionar um bônus ao fluxo. Uma espécie de "Carregando…" de chat. O "typing on".

Basta efetuar um request com o parâmetro "sender_action": "typing_on" no mesmo endpoint de mensagens que já utilizamos e ele adiciona uma animação com três pontinhos antes de enviar a mensagem.

Isso é importante para quando alguma funcionalidade exigir um tempo maior de processamento. Com esse sinal, o usuário vai ter a sensação de que a conversa não "morreu" enquanto o processamento é feito.

Dia 3

Dados

Do dia dois até esse começo de volta ao trabalho no bot, eu fiquei pensando muito em como organizar os dados orçamentários de cada usuário.

Pesquisei bastante, mas acabei resolvendo manter tudo muito simples nesse primeiro momento de desenvolvimento. Então criei models muito simples para guardar as informações de orçamento.

Criei um model de categorias e um de itens do orçamento. No caso os dois vão virar tabelas no banco de dados e serão ligados por chaves estrangeiras (Foreign Keys) comuns. Ambos também terão relacionamento com a tabela de usuários por chave estrangeira.

Logo depois disso, acabei percebendo que esse não era o meu maior desafio nessa fase inicial do desenvolvimento do bot e sim a própria conversação entre o bot e o usuário.

Respostas do bot

Nesse momento resolvi criar um arquivo com dicionários de respostas padrão, classificadas pelo seu tipo, e um método que escolhe aleatoriamente uma dos itens da lista de cada tipo de resposta.

chat_responses['no_answer'] = [
'Estou sem resposta para você...',
'Desculpe, não entendi.',
'Ooops, não captei vossa mensagem...',
]

A ideia de escolher aleatoriamente um item de um dicionário é para que o bot não fique sempre respondendo a mesma coisa em situações parecidas. Então descrevo vários tipos de respostas e ele seleciona uma deles aleatoriamente.

Esses tipos nada mais são do que chaves de um dicionário que classificam uma lista de textos de resposta. Por exemplo:

chat_responses['greetings'] = ['Olá, {name}', 'Oi, {name}', 'Yo!']

Essa lista pode ser usada para responder qualquer tipo de cumprimento inicial, como um 'Olá' ou um 'Oi, bot', por exemplo.

Para saber qual o tipo de resposta, também criei um dicionário do mesmo estilo com as palavras chaves, como no exemplo abaixo.

chat_keywords['greetings'] = ['olá', 'oi']

Eu verifico a palavra chave através de um método dedicado à isso e ele me retorna qual o tipo de resposta que o bot deve retornar.

Isso tudo ainda é um teste. Não tenho certeza se essa é a melhor forma de trabalhar com as respostas que não sejam "ações" do bot.

Quando o usuário executar ações, eu pretendo trabalhar com os botões padrão de respostas que o Facebook Messenger disponibiliza. Mas mesmo assim, vou precisar achar um jeito de entender o que o usuário está querendo fazer no momento que envia a mensagem.

Dia 4

Modelo de interação

Foi divertido a experiência do dia três, mas percebi que estava perdendo o foco do que realmente importava no desenvolvimento.

Graças aos botões que o Facebook Messenger oferece para os desenvolvedores de bot, o mais importante é desenhar o fluxo de entrada e saída de dados.

Para isso, procurei um serviço online que me possibilitasse desenhar um mockup com o fluxo do bot e encontrei uma boa opção chamada Botsociety.io.

Com ele consegui criar um mockup experimental do que será meu chatbot para orçamento doméstico. Algo rápido, apenas para ter uma noção de como vai ser o funcionamento real da ferramenta.

Primeiro adicionando categoria e uma saída de valor, com confirmação…
Adicionando categoria e uma entrada no orçamento, com uma correção após confirmação

Com isso tenho um fluxo básico do que será necessário desenvolver a partir de agora. Certamente fica muito mais fácil pensar nos passos a seguir a partir de agora.

Dia 5

Organizando tudo

Hoje foi o dia para organizar como será o comportamento do bot de acordo com os tipos de ações que o usuário quiser tomar. Consegui melhorar a distribuição dos métodos que tomam conta de cada parte do processo.

Aproveitei as mensagens criadas no Dia 3 para aplicar os textos de respostas para cada ação. Com isso, os textos ficam separados do código para que seja fácil adicionar ou trocar qualquer um deles.

Também organizei uma maneira de separar o tipo de resposta que será enviada para o usuário. Como o Messenger permite que utilizemos diferentes tipos de botões no chat, comecei a criar métodos que tem a função de responder e interpretar os dados vindos de acordo com cada tipo de resposta.

O primeiro método de criação de respostas foi o dos Quick Replies, pequenos botões que servem para respostas rápidas. Com eles eu criei uma espécie de menu de ação para o usuário interagir com o bot. Por enquanto são apenas quatro opções, mas já fazem o bastante.

[{
'content_type': 'text',
'title': 'Adicionar saída',
'payload': 'withdrawal'
},
{
'content_type': 'text',
'title': 'Adicionar entrada',
'payload': 'deposit'
},
{
'content_type': 'text',
'title': 'Ver categorias',
'payload': 'list_categories'
},
{
'content_type': 'text',
'title': 'Adicionar categoria',
'payload': 'add_category'
}]

Esse é o padrão que o Messenger espera que seja o envio de uma mensagem com Quick Replies, então já deixei um método pronto para montar essa resposta.

O "menu" utilizando os Quick Replies do Facebook Messenger

Também criei métodos que interpretam as respostas de acordo com o tipo. No caso dos Quick Replies, eu recebo o payload que pode-se ver no dicionário acima. Então o método sabe o que o usuário quer fazer e toma uma ação a partir dessa informação.

Mantendo o contexto da conversa

Uma das minhas maiores dúvidas sobre desenvolver um chatbot é em relação ao contexto da conversa, ou o "estado" da conversa.

Eu não sabia, e ainda não sei, como os desenvolvedores de chatbots conseguem saber onde a conversa está no momento em que recebem uma mensagem. Se o usuário está no meio de uma inserção de dados ou se o mesmo já finalizou o que estava fazendo e é hora de enviar o "menu" dos Quick Replies novamente.

A minha solução foi simples. Eu criei uma tabela no banco de dados que guarda o status da conversa. Então se o usuário começou a adicionar uma categoria, por exemplo, se ele tomou uma ação em relação a isso, eu guardo um status begin_add_category na minha tabela.

Toda a vez que eu recebo alguma mensagem do usuário, eu verifico em que estado está a conversa, para saber que atitude tomar.

Quando é a primeira vez que o usuário está interagindo o status é init e quando não estamos em nenhuma transação guardo como waiting o estado da conversa.

Não tenho certeza se essa é a melhor opção, mas por enquanto parece funcionar bem para o meu propósito.

Gerenciando categorias

Finalmente, depois de tanto experimento e tantas tentativas e erros, consegui concluir a primeira funcionalidade do Fin, o nome que dei para o meu chatbot de organização de orçamento doméstico.

É algo muito pequeno para tudo que eu quero que ele esteja fazendo ao final desse processo, mas já dá um certo orgulho ver algo funcionando de verdade.

A primeira feature em funcionamento!

Para terminar a primeira fase do projeto, eu preciso conseguir fazer com que o bot salve os valores de entradas e saídas de dinheiro do orçamento. Isso vai exigir mais dele, pois terei que interpretar dados que não são apenas textos simples.

Serão valores monetários e datas para serem reconhecidas e interpretadas de um texto corrido. Esse desafio fica para o próximo dia.

Dia 6

Adicionando saídas ao orçamento

Hora de iniciar uma das principais funcionalidade do bot: adicionar dados ao orçamento.

O primeiro passo foi definir o fluxo de status de conversação dessa entrada dados no orçamento. O fluxo ficou assim: waiting > begin_add_withdrawal > draft_add_withdrawal > confirm_add_withdrawal > waiting.

No status waiting o usuário escolhe a opção Adicionar Saída. Ao identificar essa ação o sistema coloca o status da conversa em begin_add_withdrawal. O próximo passo é exibir as categorias, para que o usuário escolha em qual irá adicionar a saída no orçamento.

Quando é escolhida a categoria, o sistema passa o status para draf_add_withdrawal e já adiciona um item no banco de dados com o identificador do usuário e de qual categoria ele escolheu. O status do item é draft.

O bot pede para que se diga qual a descrição, o valor e a data. Quando o usuário faz isso, o sistema muda o status para confirm_add_withdrawal e adiciona o que foi escrito no mesmo item e muda o status do banco de dados para revision.

O próximo passo é enviar para o usuário um resumo do que ele está querendo adicionar, para que seja confirmado se está correto ou não. Se estiver ele muda o status do banco de dados para done e o status da conversa volta para waiting.

Essa última parte ainda não concluí no dia de hoje. Cheguei até a parte onde exibo o resumo da nova entrada com um template default de botões do Messenger.

Para montar esse template eu usei o mesmo método que utilizei para os Quick Replies no dia cinco.

O retorno postback

Quando consegui montar o template de botões, percebi que eu não estava recebendo os dados e isso fazia com que a resposta do bot não fosse enviada para o chat.

Verifiquei que era preciso ativar o messages_postback nas configurações de webhooks no ambiente do desenvolvedor do Facebook. A partir desse momento, comecei a receber os dados novamente.

Esse tipo de template, o que a documentaçao do Facebook chama de Modelo de Botão é enviado para o usuário de uma forma diferente. Ele é um attachment e não apenas uma mensagem, como o envio de testes e de Quick Replies que eu já havia utilizado anteriormente.

O modelo de botões é enviado com o seguinte formato:

"recipient":{
"id":"USER_ID"
},
"message":{
"attachment":{
"type":"template",
"payload":{
"template_type":"button",
"text":"What do you want to do next?",
"buttons":[
{
"type":"web_url",
"url":"https://petersapparel.parseapp.com",
"title":"Show Website"
},
{
"type":"postback",
"title":"Start Chatting",
"payload":"USER_DEFINED_PAYLOAD"
}
]
}
}
}

Este exemplo foi retirado da Documentação do Messenger. É de lá que tenho tirado tudo que preciso para montar o bot. Ela é bem completa.

Com esse modelo, a resposta de confirmação da entrada ficou assim:

Por enquanto parei por aí. Agora é finalizar esse fluxo e repetí-lo com a entrada de valores nos próximos dias.

Dia 7

Finalizando a entrada de dados

Hoje foi o dia de finalizar o básico do funcionamento do Fin. Era hora dele finalizar a inserção de dados com a confirmação do usuário.

Não foi complicado, pois o fluxo já estava quase todo pronto. Bastou adicionar o passo final que era mudar o status do registro do banco de dados para done e pronto!

Aproveitei para generalizar mais os status da conversação, já que adicionar entradas e saídas no orçamento seguem o mesmo processo. Em vez de usar status e métodos específicos para saque e depósito, resolvi generalizar e mudar o fluxo da conversação de waiting > begin_add_withdrawal > draft_add_withdrawal > confirm_add_withdrawal > waiting para waiting > begin_add_data > draft_add_data > confirm_add_data > waiting.

Antes eu teria dois fluxos, um para entrada (deposit) e outro para saque (withdraw). Mas isso não fez sentido nenhum na prática, então alterei tudo para a forma genérica.

Neste momento o bot está fazendo tudo que eu planejei para sua versão incial, uma espécie de MVP (Minimum Viable Product). Ele insere e lista categorias e insere dados de orçamento, tanto de entrada como de saída.

Então está pronto?

TODO List

Ainda não está pronto. Existem alguns ajustes muito importantes na entrada de dados de orçamento. No momento atual, o bot não está conseguindo identificar direito o valor passado pelo usuário e nem a data, que por enquanto está usando sempre a do dia atual.

No valor existe o problema da vírgula. Como estou usando esse sinal para separar o conteúdo do que vem da mensagem, eu preciso tratar a interpretação do texto para quando o usuário escrever um valor com centavos.

Já na interpretação da data eu ainda nem pensei em como fazer, pois existem diversas maneiras para uma pessoa escrever esse tipo de dado.

Preciso trabalhar nessas duas partes para deixar o MVP do Fin redondo e fazer deploy da versão final, para que eu e alguns betatesters (leia-se: amigos e parentes) possam começar a usar de verdade.

Riscando um item da TODO List

Resolvi retomar o desenvolvimento mais tarde, ainda nesse mesmo dia.

Voltei à prancheta e acabei achando uma solução para um dos dois problemas que ainda tenho.

Consegui retirar o valor do item do orçamento, mesmo que com vírgulas, da mensagem que o usuário me manda antes mesmo de executar o split que separa cada item pela vírgula. Como eu previa, a solução veio de uma expressão regular (regex).

>>> import re
>>> re.findall('\d+(?:\,\d{2})?', 'Almoço, 10,50, 20/10/2015')
['10,50', '20', '10', '2015']
>>> re.findall('\d+(?:\,\d{2})?', 'Almoço, 10, 20/10/2015')
['10', '20', '10', '2015']
>>> re.findall('\d+(?:\,\d{2})?', 'Almoço, 10, 20 de dezembro')
['10', '20']
>>> re.findall('\d+(?:\,\d{2})?', 'Almoço, 10 reais, 20 de dezembro')
['10', '20']
>>> re.findall('\d+(?:\,\d{2})?', 'Almoço, R$ 10, 20 de dezembro')
['10', '20']
>>> re.findall('\d+(?:\,\d{2})?', 'Restaurante 1o de abril, R$ 10, 20 de dezembro')
['1', '10', '20']
>>> re.findall('\d+(?:\,\d{2})?', 'Restaurante 1o de abril, R$ 19,50, 20 de dezembro')
['1', '19,50', '20']

Simples, não? Como mostro acima, com essa regex eu consigo uma lista que me traz os números que foram encontrados, separando aqueles que tem exatamente dois dígitos depois de uma vírgula, que é o que me interessa. Depois simplesmente varro a string da mensagem recebida e substituo o valor identificado por ele mesmo, só que com um ponto no lugar da vírgula.

>>> import re
>>> text = 'Café da manhã, 19,50, dia 10/11'
>>> for value in re.findall('\d+(?:\,\d{2})?', text):
... value_replaced = value.replace(',', '.')
... text = text.replace(value, value_replaced)
...
>>> text
'Café da manhã, 19.50, dia 10/11'
>>> parts = text.split(',')
>>> parts
['Café da manhã', ' 19.50', ' dia 10/11']
>>> re.findall('[-+]?\d*\.\d+|\d+', parts[1])
['19.50']

Um item a menos para fechar o MVP do Fin. ;)

Dia 8

Reconhecendo datas

Para fechar o mínimo que gostaria que o Fin fizesse estava faltando apenas reconhecer a data na mensagem do usuário.

Recebendo uma data diferente da de hoje

Resolvi o problema com algo bem simples, não chega a ser infalível, mas funciona de maneira básica.

O primeiro passo é usar uma expressão regular simples para listar todos os números encontrados na string separada para a data.

>>> re.findall('\d*\d+', 'dia 20/10')
['20', '10']
>>> re.findall('\d*\d+', 'dia 20 de dezembro de 2016')
['20', '2016']

Como o Fin não foi feito para ser internacional, eu deduzo que o dia será sempre o primeiro número encontrado. O segundo pode ser um mês ou um ano.

Criei uma lista de nomes dos meses e varro a string original para procurar se o usuário escreveu algum dos meses por lá. Se encontrou, deduzo que o segundo número encontrado é o ano, senão é o mês.

Caso não encontre nenhum número, a data utilizada é a atual. Se encontrar apenas um, o mês e o ano da data atual serão utilizados. Se encontrar apenas dia e mês, uso o ano atual.

Uma solução simples e eficaz nesse primeiro momento.

Tratando erros

Para fechar a fase final do chatbot é preciso fazer o tratamento de erros. Nesse caso, seria o usuário colocar uma entrada não compatível com a que o bot está esperando.

Eu tratei alguns erros previsíveis, como não vir o tipo de dado correto ou no formato esperado, então envio mensagens mostrando exemplos de como devem ser as entradas.

Mesmo recebendo dados separados por vírgula, o bot percebe que não é o tipo correto

Fazendo o deploy da versão de teste

Todo esse desenvolvimento foi feito rodando o bot localmente com ajuda do ngrok para receber as requisições do webhook do Facebook Messenger.

Agora que tenho o básico do Fin pronto, quero deixá-lo online o tempo todo para que eu possa usá-lo por alguns dias e perceber falhas comuns e possíveis erros de funcionamento ou de usabilidade.

Para mantê-lo online, preciso fazer um deploy em um servidor e deixá-lo rodando. Como já deixei tudo pronto no Heroku no primeiro dia, basta fazer o deploy do branch master por lá e modificar a configuração do webhook no painel de controle de apps do Facebook.

Precisei criar um arquivo chamado runtime.txt e subí-lo para o repositório para que o Heroku rodasse meu código com Python3, pois o padrão de runtime deles é o Python 2.7.

No final acabou não dando certo porque estou usando SQLite e o Heroku não aceita esse tipo de banco de dados.

Dia 9

Conclusão

Como não consegui fazer o deploy no Heroku por causa do SQLite, estou tentando fazê-lo no webfaction. Mas isso pode levar um tempo porque o Facebook exige que a URL do chatbot contenha SSL, ou seja, que a conexão seja criptografada.

Isso está em andamento, mas como não interfere no objetivo final, que era fazer o bot guardar informações do orçamento, posso deixar isso para depois e finalizar o diário por aqui mesmo.

Outra coisa importante é passar pela aprovação do Facebook. É preciso submeter o chatbot no console de apps do Facebook para que eles avaliem e liberem para o público. Como o Fin ainda não está pronto, ele está limitado apenas a mim.

Existem muitas outras funcionalidades que eu ainda quero adicionar no futuro. A extração dos dados, um agendamento de alertas para vencimento de contas fixas, etc.

Mas o desafio era criar uma primeira versão que tenha uma interface boa o bastante para fazer as inserções de dados. Esse "MVP" está concluído. Veja o vídeo do funcionamento abaixo.

Para o futuro

Antes de voltar a desenvolver mais funcionalidades, quero migrar o Fin para um framework mais robusto. Estou pensando em utilizar o Django com Postgres como banco de dados.

O Flask é muito simples de utilizar e para começar o projeto foi, sem dúvida, a melhor opção. Mas o problema é que ele começou a ficar grande demais a medida que eu fui desenvolvendo e descobrindo o que é necessário fazer para aproveitar ao máximo as ferramentas de chatbot do Messenger.

Por isso, o primeiro passo para evoluir o Fin para um nível de produção é refatorá-lo de maneira que ele possa crescer sem problemas no futuro. Ele acabou se mostrando um projeto muito divertido de fazer e que pode ser promissor futuramente.

Conclusão

Se você leu tudo até aqui, parabéns. Fico feliz que tenha se interessado por esse diário de desenvolvimento. Tentei mostrar minhas dúvidas, os problemas que tive e as soluções que encontrei da melhor maneira possível. Espero que tenha gostado.

Cada entrada nesse diário foi feita contando apenas os dias em que eu tive tempo de parar e trabalhar de uma a três horas no desenvolvimento do chatbot. Portanto essa primeira versão do Fin me ocupou cerca 18 horas de desenvolvimento, contanto, é claro, com o tempo de pesquisa.

A pesquisa e o planejamento são as partes que consomem mais tempo em qualquer projeto, ainda mais em algo novo, com o qual nunca se trabalhou antes.

Vou continuar brincando com este bot nos meus tempos livres do ano que vem, como tenho feito ao longo desses 30 dias em que esse diário foi escrito, e espero que ele acabe se tornando uma ferramenta útil para o meu dia-a-dia. Afinal, cuidar do meu orçamento doméstico é o que realmente me impulsionou a começar esse projeto.

No ano que vem é muito provável que quem acompanha minhas publicações ouça falar muito do Fin ainda.

Estou usando esse espaço para colocar em prática minha vontade de escrever sobre qualquer coisa. Se você gostou e quer me incentivar a escrever mais, deixe seu “recomendar” clicando no coração abaixo do texto. ;)

--

--

I write code and stories - Senior Software Engineer @ Press Hook | Founder @ Fliptru