Bot de atendimento para o Food Commerce utilizando modelo GPT da OpenAI. Ele foi desenvolvido para o conteúdo da Master Class #013 da Dev Samurai.
O bot utiliza o modelo GPT da OpenAI para gerar respostas para as perguntas dos usuários simulando um atendimento humano. Este atendimento é feito através do WhatsApp utilizando o Venom.
Para que o bot siga um roteiro, um prompt padrão foi desenvolvido. Esse prompt pode ser visto no arquivo docs/prompt.md.
Com este prompt você poderá adaptar o bot para o seu negócio ou para outros nichos, como clinicas, etc.
Para executar o bot, você precisará de uma conta no WhatsApp, do Node.js e Docker instalados.
Você irá precisar também de uma conta e API Key no OpenAI.
Com isso em mãos, você precisará criar um arquivo .env na raiz do projeto com as seguintes variáveis:
OPENAI_API_KEY=sk-xxx <- Sua API Key do OpenAI REDIS_HOST=localhost REDIS_PORT=6379 REDIS_DB=0Após isso, você precisará instalar as dependências do projeto:
npm installE então, executar o bot:
npm run devPara que você possa testar o bot, você precisará de um aplicativo do WhatsApp instalado no seu celular e escanear o QR Code que será gerado no terminal.
Importante: devido ao fato de utilizar uma API não autorizada do WhatsApp pode gerar bloqueios e banimentos de números, por isso, teste com um número que você não se importe em perder. Não se responsabilizamos por qualquer dano causado pelo uso deste código.
- Criar o projeto backend Node.js em TypeScript.
- Instalar a lib Venom e criar o primeiro client.
- Integrar com o OpenAI e criar o primeiro prompt.
- Criar o roteiro do bot.
- Integrar com o Redis para armazenar o estado do usuário.
- Finalizar o pedido e armazenar a order.
Primeiro iremos criar a estrutura básica de um projeto Node.js com TypeScript. Para isso, crie uma pasta chamada backend e execute os comandos abaixo:
mkdir -p food-commerce-gpt cd food-commerce-gpt npm init -y npm install -D @types/node nodemon rimraf ts-node typecriptDepois de criado, abra o arquivo package.json e adicione os scripts abaixo:
{ "scripts": { "build": "rimraf ./build && tsc", "dev": "nodemon", "start": "node build/index.js" }, }E crie o arquivo nodemon.json:
{ "watch": ["src"], "ext": ".ts,.js", "ignore": [], "exec": "ts-node ./src/index.ts" }Com a nossa estrutura mínima chegou o momento de criar o arquivo src/index.ts com uma simples mensagem:
console.log('Hello World!')Na sequência criar o arquivo tsconfig.json com o comando:
npx tsc --initE por fim, ajustar o diretório de build no arquivo tsconfig.json:
{ "outDir": "./build", }Agora com a estrutura mínima necessária, vamos executar o projeto com o comando:
npm run devE você deverá ver a mensagem Hello World! no terminal.
Agora que temos a estrutura básica do projeto, vamos instalar a lib Venom para criar o nosso primeiro client do WhatsApp.
Para isso, execute o comando abaixo:
npm install venom-botCom a lib instalada, vamos criar o arquivo src/index.ts com o seguinte conteúdo:
import { Message, Whatsapp, create } from "venom-bot" create({ session: "food-gpt", disableWelcome: true, }) .then(async (client: Whatsapp) => await start(client)) .catch((err) => { console.log(err) }) async function start(client: Whatsapp) { client.onMessage(async (message: Message) => { if (!message.body || message.isGroupMsg) return const response = `Olá!` await client.sendText(message.from, response) }) }E rodar o comando npm run dev para executar o projeto para vincular o dispositivo no seu WhatsApp.
Após escanear o QR Code, você poderá enviar uma mensagem para o número que você vinculou e deverá receber a mensagem Olá! como resposta.
Perceba que o Venom já cria um arquivo de sessão para que você não precise escanear o QR Code novamente. Ele fica na pasta ./tokens.
Agora que temos o nosso client do WhatsApp, vamos integrar com o OpenAI para criar o nosso primeiro prompt.
Para isso, vamos instalar a lib do OpenAI e DotEnv:
npm install openai dotenvApós a instalação, iremos criar um "gerenciador de configurações" no projeto. Para isso, crie o arquivo src/config.ts com o seguinte conteúdo:
import dotenv from "dotenv" dotenv.config() export const config = { openAI: { apiToken: process.env.OPENAI_API_KEY, }, redis: { host: process.env.REDIS_HOST || "localhost", port: (process.env.REDIS_PORT as unknown as number) || 6379, db: (process.env.REDIS_DB as unknown as number) || 0, }, }Com o gerenciador de configurações criado, vamos criar o arquivo src/lib/openai.ts com o seguinte conteúdo:
import { Configuration, OpenAIApi } from "openai" import { config } from "../config" const configuration = new Configuration({ apiKey: config.openAI.apiToken, }) export const openai = new OpenAIApi(configuration)E no arquivo src/index.ts vamos importar o openai e criar uma função que será responsável por criar o prompt:
async function completion( messages: ChatCompletionRequestMessage[] ): Promise<string | undefined> { const completion = await openai.createChatCompletion({ model: "gpt-3.5-turbo", temperature: 0, max_tokens: 256, messages, }) return completion.data.choices[0].message?.content }E adaptar a função start para utilizar o completion e criar uma primeira interação com o modelo:
async function start(client: Whatsapp) { client.onMessage(async (message: Message) => { if (!message.body || message.isGroupMsg) return const response = (await completion([message.body])) || "Não entendi..." await client.sendText(message.from, content) }) }O nosso modelo já responde com uma mensagem, mas ainda não é o suficiente para criar uma interação com o usuário. Para isso, vamos criar um roteiro para o bot.
Mas antes disso, para que o bot funcione, é preciso o histórico de todas as mensagens entre o usuário e o bot, assim o modelo consegue entender o contexto da conversa.
import { Message, Whatsapp, create } from "venom-bot" import { ChatCompletionRequestMessage } from "openai" import { openai } from "./lib/openai" const customerChat: ChatCompletionRequestMessage[] = [] create({ session: "food-gpt", disableWelcome: true, }) .then(async (client: Whatsapp) => await start(client)) .catch((err) => { console.log(err) }) async function start(client: Whatsapp) { client.onMessage(async (message: Message) => { if (!message.body || message.isGroupMsg) return customerChat.push({ role: "user", content: message.body }) const response = (await completion(customerChat)) || "Não entendi..." customerChat.push({ role: "assistant", content: response }) await client.sendText(message.from, content) }) }Para que o bot funcione, o modelo precisa de um contexto inicial:
const customerChat = ChatCompletionRequestMessage[ { role: "system", content: "Você é uma assistente virtual de atendimento de uma pizzaria chamada Los Italianos. Você deve ser educada, atenciosa, amigável, cordial e muito paciente..." }, ]Isso posiciona o modelo para o contexto da conversa, deixando assim o modelo mais inteligente.
Um exemplo de roteiro de bot, encontra-se no arquivo docs/prompt.md.
Se você perceber, além de conter o contexto inicial — 'Você é...' — ainda iremos acrescentar um roteiro detalhado de atendimento.
Isso garante que o bot seja capaz de atender o cliente de forma mais natural possível, mas ainda seguir uma sequencia predefinida.
E para inciar o bot com um contexto, iremos criar um arquivo src/prompts/pizzaAgent.ts com o seguinte conteúdo:
export const prompt = `Você é uma assistente virtual de atendimento de uma pizzaria chamada {{ storeName }}. Você deve ser educada, atenciosa, amigável, cordial e muito paciente. Você não pode oferecer nenhum item ou sabor que não esteja em nosso cardápio. Siga estritamente as listas de opções. O código do pedido é: {{ orderCode }} O roteiro de atendimento é: 1. Saudação inicial: Cumprimente o cliente e agradeça por entrar em contato. 2. Coleta de informações: Solicite ao cliente seu nome para registro caso ainda não tenha registrado. Informe que os dados são apenas para controle de pedidos e não serão compartilhados com terceiros. 3. Quantidade de pizzas: Pergunte ao cliente quantas pizzas ele deseja pedir. 4. Sabores: Envie a lista resumida apenas com os nomes de sabores salgados e doces e pergunte ao cliente quais sabores de pizza ele deseja pedir. 4.1 O cliente pode escolher a pizza fracionada em até 2 sabores na mesma pizza. 4.2 Se o cliente escolher mais de uma pizza, pergunte se ele deseja que os sabores sejam repetidos ou diferentes. 4.3 Se o cliente escolher sabores diferentes, pergunte quais são os sabores de cada pizza. 4.4 Se o cliente escolher sabores repetidos, pergunte quantas pizzas de cada sabor ele deseja. 4.5 Se o cliente estiver indeciso, ofereça sugestões de sabores ou se deseja receber o cardápio completo. 4.6 Se o sabor não estiver no cardápio, não deve prosseguir com o atendimento. Nesse caso informe que o sabor não está disponível e agradeça o cliente. 5. Tamanho: Pergunte ao cliente qual o tamanho das pizzas. 5.1 Se o cliente escolher mais de um tamanho, pergunte se ele deseja que os tamanhos sejam repetidos ou diferentes. 5.2 Se o cliente escolher tamanhos diferentes, pergunte qual o tamanho de cada pizza. 5.3 Se o cliente escolher tamanhos repetidos, pergunte quantas pizzas de cada tamanho ele deseja. 5.4 Se o cliente estiver indeciso, ofereça sugestões de tamanhos. Se for para 1 pessoa o tamanho pequeno é ideal, para 2 pessoas o tamanho médio é ideal e para 3 ou mais pessoas o tamanho grande é ideal. 6. Ingredientes adicionais: Pergunte ao cliente se ele deseja adicionar algum ingrediente extra. 6.1 Se o cliente escolher ingredientes extras, pergunte quais são os ingredientes adicionais de cada pizza. 6.2 Se o cliente estiver indeciso, ofereça sugestões de ingredientes extras. 7. Remover ingredientes: Pergunte ao cliente se ele deseja remover algum ingrediente, por exemplo, cebola. 7.1 Se o cliente escolher ingredientes para remover, pergunte quais são os ingredientes que ele deseja remover de cada pizza. 7.2 Não é possível remover ingredientes que não existam no cardápio. 8. Borda: Pergunte ao cliente se ele deseja borda recheada. 8.1 Se o cliente escolher borda recheada, pergunte qual o sabor da borda recheada. 8.2 Se o cliente estiver indeciso, ofereça sugestões de sabores de borda recheada. Uma dica é oferecer a borda como sobremesa com sabor de chocolate. 9. Bebidas: Pergunte ao cliente se ele deseja pedir alguma bebida. 9.1 Se o cliente escolher bebidas, pergunte quais são as bebidas que ele deseja pedir. 9.2 Se o cliente estiver indeciso, ofereça sugestões de bebidas. 10. Entrega: Pergunte ao cliente se ele deseja receber o pedido em casa ou se prefere retirar no balcão. 10.1 Se o cliente escolher entrega, pergunte qual o endereço de entrega. O endereço deverá conter Rua, Número, Bairro e CEP. 10.2 Os CEPs de 12.220-000 até 12.330-000 possuem uma taxa de entrega de R$ 10,00. 10.3 Se o cliente escolher retirar no balcão, informe o endereço da pizzaria e o horário de funcionamento: Rua Abaeté, 123, Centro, São José dos Campos, SP. Horário de funcionamento: 18h às 23h. 11. Forma de pagamento: Pergunte ao cliente qual a forma de pagamento desejada, oferecendo opções como dinheiro, PIX, cartão de crédito ou débito na entrega. 11.1 Se o cliente escolher dinheiro, pergunte o valor em mãos e calcule o troco. O valor informado não pode ser menor que o valor total do pedido. 11.2 Se o cliente escolher PIX, forneça a chave PIX CNPJ: 1234 11.3 Se o cliente escolher cartão de crédito/débito, informe que a máquininha será levada pelo entregador. 12. Mais alguma coisa? Pergunte ao cliente se ele deseja pedir mais alguma coisa. 12.1 Se o cliente desejar pedir mais alguma coisa, pergunte o que ele deseja pedir. 12.2 Se o cliente não desejar pedir mais nada, informe o resumo do pedido: Dados do cliente, quantidade de pizzas, sabores, tamanhos, ingredientes adicionais, ingredientes removidos, borda, bebidas, endereço de entrega, forma de pagamento e valor total. 12.3 Confirmação do pedido: Pergunte ao cliente se o pedido está correto. 12.4 Se o cliente confirmar o pedido, informe o tempo de entrega médio de 45 minutos e agradeça. 12.5 Se o cliente não confirmar o pedido, pergunte o que está errado e corrija o pedido. 13. Despedida: Agradeça o cliente por entrar em contato. É muito importante que se despeça informando o número do pedido. Cardápio de pizzas salgadas (os valores estão separados por tamanho - Broto, Médio e Grande): - Muzzarella: Queijo mussarela, tomate e orégano. R$ 25,00 / R$ 30,00 / R$ 35,00 - Calabresa: Calabresa, cebola e orégano. R$ 30,00 / R$ 35,00 / R$ 40,00 - Nordestina: Carne de sol, cebola e orégano. R$ 35,00 / R$ 40,00 / R$ 45,00 - Frango: Frango desfiado, milho e orégano. R$ 30,00 / R$ 35,00 / R$ 40,00 - Frango c/ Catupiry: Frango desfiado, catupiry e orégano. R$ 35,00 / R$ 40,00 / R$ 45,00 - A moda da Casa: Carne de sol, bacon, cebola e orégano. R$ 40,00 / R$ 45,00 / R$ 50,00 - Presunto: Presunto, queijo mussarela e orégano. R$ 30,00 / R$ 35,00 / R$ 40,00 - Quatro Estações: Presunto, queijo mussarela, ervilha, milho, palmito e orégano. R$ 35,00 / R$ 40,00 / R$ 45,00 - Mista: Presunto, queijo mussarela, calabresa, cebola e orégano. R$ 35,00 / R$ 40,00 / R$ 45,00 - Toscana: Calabresa, bacon, cebola e orégano. R$ 35,00 / R$ 40,00 / R$ 45,00 - Portuguesa: Presunto, queijo mussarela, calabresa, ovo, cebola e orégano. R$ 35,00 / R$ 40,00 / R$ 45,00 - Dois Queijos: Queijo mussarela, catupiry e orégano. R$ 35,00 / R$ 40,00 / R$ 45,00 - Quatro Queijos: Queijo mussarela, provolone, catupiry, parmesão e orégano. R$ 40,00 / R$ 45,00 / R$ 50,00 - Salame: Salame, queijo mussarela e orégano. R$ 35,00 / R$ 40,00 / R$ 45,00 - Atum: Atum, cebola e orégano. R$ 35,00 / R$ 40,00 / R$ 45,00 Cardápio de pizzas doces (os valores estão separados por tamanho - Broto, Médio e Grande): - Chocolate: Chocolate ao leite e granulado. R$ 30,00 / R$ 35,00 / R$ 40,00 - Romeu e Julieta: Goiabada e queijo mussarela. R$ 30,00 / R$ 35,00 / R$ 40,00 - California: Banana, canela e açúcar. R$ 30,00 / R$ 35,00 / R$ 40,00 Extras/Adicionais (os valores estão separados por tamanho - Broto, Médio e Grande): - Catupiry: R$ 5,00 / R$ 7,00 / R$ 9,00 Bordas (os valores estão separados por tamanho - Broto, Médio e Grande): - Chocolate: R$ 5,00 / R$ 7,00 / R$ 9,00 - Cheddar: R$ 5,00 / R$ 7,00 / R$ 9,00 - Catupiry: R$ 5,00 / R$ 7,00 / R$ 9,00 Bebidas: - Coca-Cola 2L: R$ 10,00 - Coca-Cola Lata: R$ 8,00 - Guaraná 2L: R$ 10,00 - Guaraná Lata: R$ 7,00 - Água com Gás 500 ml: R$ 5,00 - Água sem Gás 500 ml: R$ 4,00Note que é um roteiro extremamente detalhado, para que possa atender a qualquer cliente de pizzaria. Você pode alterar o roteiro como quiser, mas lembre-se de que ele deve ser bem detalhado e sempre testado.
E depois iremos criar a função no arquivo src/utils/initPrompt.ts que carrega esse prompt e também possibilita ajustar alguns dados:
import { prompt } from "../prompts/pizzaAgent" export function initPrompt(storeName: string, orderCode: string): string { return prompt .replace(/{{[\s]?storeName[\s]?}}/g, storeName) // aqui é onde substituímos o nome da loja - {{ storeName }} .replace(/{{[\s]?orderCode[\s]?}}/g, orderCode) // aqui é onde substituímos o código do pedido - {{ orderCode }} }Depois desse roteiro 'monstro', iremos incorporar isso no nosso bot:
import { Message, Whatsapp, create } from "venom-bot" import { ChatCompletionRequestMessage } from "openai" import { openai } from "./lib/openai" import { initPrompt } from "./utils/initPrompt" const storeName = "Pizzaria Los Italianos" const orderCode = "#sk-123456" const customerChat = ChatCompletionRequestMessage[ { role: "system", content: initPrompt(storeName, orderCode), // Aqui é onde carregamos o prompt monstruoso com algumas informações como nome da loja e código. fique atendo a quantidade de texto do OpenAI }, ] create({ session: "food-gpt", disableWelcome: true, }) .then(async (client: Whatsapp) => await start(client)) .catch((err) => { console.log(err) }) async function start(client: Whatsapp) { client.onMessage(async (message: Message) => { if (!message.body || message.isGroupMsg) return customerChat.push({ role: "user", content: message.body }) const response = (await completion(customerChat)) || "Não entendi..." customerChat.push({ role: "assistant", content: response }) await client.sendText(message.from, content) }) }Com essas alterações já conseguimos ter um bot funcional, mas ainda não é multiusuário.
Para armazenar os dados de conversas e o status de cada pedido, iremos utilizar o Redis.
O Redis é um banco de dados em memória, que é extremamente rápido e simples de utilizar. Ele é muito utilizado para armazenar dados que precisam ser acessados rapidamente, como por exemplo, o status de um pedido.
Ele basicamente trabalha como um 'grande array' (arrayzão) com chave e valor.
A chave iremos armazenar o número do telefone do cliente, e o valor iremos armazenar o status do pedido e conversa.
Assim o bot não vai ficar perdido com a conversa de cada cliente, e também vai saber o status de cada pedido.
Para iniciar o uso do Redis, iremos instalar a biblioteca ioredis:
npm install ioredisE criar o arquivo src/lib/redis.ts que será responsável por criar a conexão com o Redis:
import { Redis } from "ioredis" import { config } from "../config" export const redis = new Redis({ host: config.redis.host, port: config.redis.port, db: config.redis.db, })O Redis é bem fácil de utilizar, basicamente ele possui duas funções principais: set e get.
redis.set("chave", "valor") const value = await redis.get("chave")O Redis consegue gravar valores apenas em string, por isso precisamos converter o objeto para string com JSON.stringify e depois converter novamente para objeto com JSON.parse.
redis.set("chave", JSON.stringify({ foo: "bar" })) const obj = JSON.parse((await redis.get("chave")) || "{}")Para que o nosso bot a conversa de cada cliente, iremos ajustar o código abaixo:
import { Message, Whatsapp, create } from "venom-bot" import { ChatCompletionRequestMessage } from "openai" import { openai } from "./lib/openai" import { redis } from "./lib/redis" import { initPrompt } from "./utils/initPrompt" create({ session: "food-gpt", disableWelcome: true, }) .then(async (client: Whatsapp) => await start(client)) .catch((err) => { console.log(err) }) async function start(client: Whatsapp) { client.onMessage(async (message: Message) => { if (!message.body || message.isGroupMsg) return const storeName = "Pizzaria Los Italianos" const customerPhone = `+${message.from.replace("@c.us", "")}` const customerName = message.author const customerKey = `customer:${customerPhone}:chat` const orderCode = `#sk-${("00000" + Math.random()).slice(-5)}` const lastChat = JSON.parse((await redis.get(customerKey)) || "[]") // carrega a conversa do cliente do Redis const customerChat: CustomerChat = lastChat.length > 0 ? lastChat : [ { role: "system", content: initPrompt(storeName, orderCode), } ] customerChat.push({ role: "user", content: message.body }) const response = (await completion(customerChat)) || "Não entendi..." customerChat.push({ role: "assistant", content: response }) await client.sendText(message.from, content) redis.set(customerKey, JSON.stringify(customerChat)) // grava a conversa do cliente no Redis }) }Se você não possuir o Redis instalado no seu computador, poderá utilizar o Docker para subir um container com o Redis através do docker-compose:
version: "3.1" services: redis: image: redis restart: always ports: - 6379:6379 volumes: - redis-data:/data volumes: redis-data:Para subir o container, basta executar o comando:
docker-compose up -dE em seguida, iremos subir o bot novamente:
npm run devAgora para que possamos controlar o status de cada pedido, iremos ajustar a estrutura de dados de mensagens e usuário:
import { ChatCompletionRequestMessage } from "openai" import { Message, Whatsapp, create } from "venom-bot" import { openai } from "./lib/openai" import { redis } from "./lib/redis" import { initPrompt } from "./utils/initPrompt" // declara a interface de mensagens interface CustomerChat { status?: "open" | "closed" orderCode: string chatAt: string customer: { name: string phone: string } messages: ChatCompletionRequestMessage[] orderSummary?: string } async function completion( messages: ChatCompletionRequestMessage[] ): Promise<string | undefined> { const completion = await openai.createChatCompletion({ model: "gpt-3.5-turbo", temperature: 0, max_tokens: 256, messages, }) return completion.data.choices[0].message?.content } create({ session: "food-gpt", disableWelcome: true, }) .then(async (client: Whatsapp) => await start(client)) .catch((err) => { console.log(err) }) async function start(client: Whatsapp) { const storeName = "Pizzaria Los Italianos" client.onMessage(async (message: Message) => { if (!message.body || message.isGroupMsg) return const customerPhone = `+${message.from.replace("@c.us", "")}` const customerName = message.author const customerKey = `customer:${customerPhone}:chat` const orderCode = `#sk-${("00000" + Math.random()).slice(-5)}` const lastChat = JSON.parse((await redis.get(customerKey)) || "{}") const customerChat: CustomerChat = lastChat?.status === "open" ? (lastChat as CustomerChat) // carrega a mensagem do cliente do Redis ou crie uma nova : { status: "open", orderCode, chatAt: new Date().toISOString(), customer: { name: customerName, phone: customerPhone, }, messages: [ { role: "system", content: initPrompt(storeName, orderCode), }, ], orderSummary: "", } console.debug(customerPhone, "👤", message.body) customerChat.messages.push({ role: "user", content: message.body, }) const content = (await completion(customerChat.messages)) || "Não entendi..." customerChat.messages.push({ role: "assistant", content, }) console.debug(customerPhone, "🤖", content) await client.sendText(message.from, content) // quando o bot repassar o número de pedido para o cliente, ele irá fechar o pedido e solicitar um resumo final para que possamos repassar a um atendente de forma resumida if ( customerChat.status === "open" && content.match(customerChat.orderCode) ) { customerChat.status = "closed" customerChat.messages.push({ role: "user", content: "Gere um resumo de pedido para registro no sistema da pizzaria, quem está solicitando é um robô.", }) const content = (await completion(customerChat.messages)) || "Não entendi..." console.debug(customerPhone, "📦", content) customerChat.orderSummary = content // armazena o resumo do pedido e NÃO envia para o cliente } redis.set(customerKey, JSON.stringify(customerChat)) }) }Neste tutorial, você aprendeu como criar um chatbot para WhatsApp usando o OpenAI e o Venom Bot. Você também aprendeu como usar o Redis para armazenar o histórico de conversas e o resumo do pedido.
Como falamos, isso pode ser usado para qualquer tipo de negócio, desde que você tenha um sistema de pedidos e um sistema de atendimento ao cliente.
Espero que tenha gostado 🧡
-- Felipe Fontoura, @DevSamurai
PS: Se você curtiu esse conteúdo, vai curtir também minha newsletter, inscreva-se em https://st.devsamurai.com.br/f7tvr6rx/index.html
