Olá, pessoal! Tudo bem?
No último post, fizemos um exemplo bem simples utilizando o Amazon Lex, com o propósito de pedir uma pizza. Porém a configuração feita no exemplo não é muito flexível, e dificilmente seria utilizada em produção.
Neste post nós iremos mais além. A ideia é darmos mais poder ao Amazon Lex, através da integração com a AWS Lambda e alguns outros serviços da AWS.
Arquitetura exemplo
Agora que nós conhecemos os principais elementos do Amazon Lex, vamos definir o restante dos componentes que irão compor a nossa solução.
A arquitetura básica do nosso exemplo será a seguinte:

A ideia é tornar o exemplo simples, então testaremos o chatbot no Postman, e, como o “cérebro” do bot, iremos utilizar uma Lambda que terá como data source o DynamoDB. Para algo mais production-ready, poderíamos integrar outros serviços, como o Cognito, usado para autenticação/autorização.

Projeto
O projeto a seguir, foi desenvolvido usando o AWS Serverless Application Model, ou SAM. Para quem quiser conhecer, o código fonte está disponível no Github.
Estrutura do DynamoDB
Para deixar o exemplo mais completo, adicionamos uma tabela no DynamoDB (através do SAM), e incluímos o preço dos sabores de pizza e da bordas da pizza. Então a ideia é somar o valor de ambos e apresentar o valor final para o cliente.

Uma observação importante aqui é que os sabores de pizza e borda também precisam ser usados nos slot types do Lex, para que os valores sejam utilizados no treinamento do bot.

Lambda para inicialização e validação
A primeira coisa que faremos nesse exemplo, é a integração entre o Amazon Lex e a lambda que irá atuar na inicialização e validação dos dados das interações com o usuário. Para utilizá-la no Lex, precisamos selecioná-la em Lambda Initialization and Validation.

A função desta integração é obter todas as informações (slots) necessárias para completar o intent. Não entrarei em detalhes sobre a estrutura das mensagens trocadas entre o Amazon Lex e a Lambda, porém a documentação oficial está disponível aqui. Abaixo segue o código fonte.
const aws = require("aws-sdk");
const createInvalidSlotResponse = (slots, slotToElicit, message) => {
slots[slotToElicit] = null;
return {
dialogAction: {
type: "ElicitSlot",
message: {
contentType: "PlainText",
content: message
},
intentName: "OrderAPizza",
slots,
slotToElicit: slotToElicit
}
}
};
const createConfirmationResponse = (slots, totalPrice) => {
return {
sessionAttributes: {
totalPrice
},
dialogAction: {
type: "ConfirmIntent",
message: {
contentType: "PlainText",
content: `Ok. Do you confirm your order for a ${slots.flavour} pizza, with ${slots.crust} stuffed edge to be delivered at ${slots.address} for U$${totalPrice.toFixed(2)}?`
},
intentName: "OrderAPizza",
slots,
responseCard: {
version: 1,
contentType: "application/vnd.amazonaws.card.generic",
genericAttachments: [
{
title: "Order confirmation",
subTitle: "Can I place your order?",
buttons: [
{
text: "Yes",
value: "yes"
}, {
text: "No",
value: "no"
}
]
}
]
}
}
}
};
const createDelegateResponse = (slots) => {
return {
dialogAction: {
type: "Delegate",
slots
}
}
};
const getFlavourPrice = (type, flavour) => {
const dynamoDB = new aws.DynamoDB({
apiVersion: "2012-08-10",
region: "us-east-1"
});
const queryParams = {
TableName: "PizzaFlavours",
KeyConditionExpression: "#tp = :flavourType and flavour = :flavour",
ExpressionAttributeNames: {
"#tp": "type"
},
ExpressionAttributeValues: {
":flavourType": {
"S": type
},
":flavour": {
"S": flavour
}
},
};
return new Promise((resolve, reject) => {
dynamoDB.query(queryParams, function (err, data) {
if (err) return reject(err);
if (data.Items.length <= 0) return resolve();
console.log(JSON.stringify(data.Items[0]))
return resolve(parseFloat(data.Items[0].price.N));
})
});
};
exports.handler = async (event) => {
console.log(event);
const { slots, confirmationStatus } = event.currentIntent;
if (confirmationStatus !== 'None') return createDelegateResponse(slots);
if (!slots.flavour) return createInvalidSlotResponse(slots, 'flavour', 'What is the pizza flavour that you want?');
const flavourPrice = await getFlavourPrice('pizza', slots.flavour);
if (flavourPrice === undefined) return createInvalidSlotResponse(slots, 'flavour', 'This pizza flavour is not available. Can you choose another one? :-)');
if (!slots.crust) return createInvalidSlotResponse(slots, 'crust', 'What is the crust flavour?');
const crustPrice = await getFlavourPrice('crust', slots.crust);
if (crustPrice === undefined) return createInvalidSlotResponse(slots, 'crust', 'This crust flavour is not available. Can you choose another one? :-)');
if (!slots.address) return createInvalidSlotResponse(slots, 'address', 'What is the delivery address?');
return createConfirmationResponse(slots, flavourPrice + crustPrice);
};
O handler acima simplesmente valida os valores de cada um dos slots enviados pelo Lex, tentando obter o preço no DynamoDB através da função getFlavourPrice. Caso o valor do slot não tenha sido informado ou não exista, o mesmo será solicitado ao usuário através da mensagem de resposta criada em createInvalidSlotResponse.
Um ponto importante a ser observado é o armazenamento do totalPrice. Como não existe um slot para ele, armazenamos em SessionAttributes. Estes atributos permitem trafegar informações de contexto durante toda a interação com o Lex, inclusive entre intents diferentes. Eles estão disponíveis enquanto a sessão for válida, baseado no session timeout configurado na criação do bot.
Ao final, quando todos os slots estiverem preenchidos, enviamos uma mensagem ao usuário solicitando a confirmação do intent, informando o preço total (ver createConfirmationResponse). Como a resposta para está mensagem ainda é do tipo DialogCodeHook, ela volta para esta Lambda. Neste caso, apenas delegamos para o Lex na função createDelegateResponse, para deixá-lo seguir o fluxo. Caso a resposta do usuário seja negativa (“no“), ele resolverá a mensagem configurada no console (imagem abaixo), cancelando o intent. E em caso positivo, o fluxo seguirá para o fullfilment, onde será invocada a próxima lambda.

Lambda para finalização
Para fins de demonstração, a Lambda utilizada em fullfilment é bem simples. Ela irá salvar o pedido do usuário e finalizar o intent.
const aws = require("aws-sdk");
const uuid = require('uuid/v4');
const saveOrder = (order) => {
const dynamoDB = new aws.DynamoDB.DocumentClient({
apiVersion: "2012-08-10",
region: "us-east-1"
});
const params = {
TableName: 'PizzaOrders',
Item: order
};
return dynamoDB.put(params).promise();
};
const createOrder = (slots, sessionAttributes) => {
return {
orderId: uuid(),
...slots,
total: sessionAttributes.totalPrice
};
};
const createCloseResponse = () => {
return {
dialogAction: {
type: "Close",
fulfillmentState: "Fulfilled",
message: {
contentType: "PlainText",
content: "Your order has been placed. Thank you! :-D"
},
responseCard: {
version: 1,
contentType: "application/vnd.amazonaws.card.generic",
genericAttachments: [
{
title: "Your order has been placed!",
subTitle: "Do you need anything else?",
imageUrl: "https://i.ibb.co/D5c7xSB/Resized-pizza.png",
buttons: [
{
text: "Order another pizza",
value: "I want a pizza"
}
]
}
]
}
}
}
};
exports.handler = async (event) => {
console.log(event);
const { slots } = event.currentIntent;
const order = createOrder(slots, event.sessionAttributes);
await saveOrder(order);
return createCloseResponse();
};
O primeiro passo é salvarmos a ordem utilizando as informações dos slots obtidos do usuário, e o totalPrice armazenado como um atributo de sessão (SessionAttributes). Por fim, respondemos ao Lex com uma mensagem tipo Close, informando que o intent foi finalizado com sucesso (fulfillmentState: Fulfilled).
Para utilizarmos a Lambda no Lex, precisamos selecioná-la em Fullfilment.

Integração com o API Gateway
Como o Lex não possui uma integração direta com o API Gateway, precisamos de uma Lambda que faça o meio campo entre os dois. Neste cenário, ela irá receber as mensagens da API Gateway, invocar o Lex passando os parâmetros necessários, e devolver a resposta do Lex para o usuário.
const aws = require('aws-sdk');
const proxyResponse = (status, message) => {
console.log(message);
return {
statusCode: status,
body: JSON.stringify(message)
};
};
exports.handler = async (event) => {
console.log(event);
const payload = JSON.parse(event.body);
const lex = new aws.LexRuntime({
region: "us-east-1"
});
const params = {
botAlias: '$LATEST',
botName: 'PizzaBot',
inputText: payload.message,
userId: payload.userId,
sessionAttributes: payload.sessionAttributes || {}
};
try {
var lexResponse = await lex.postText(params).promise();
return proxyResponse(200, lexResponse);
} catch (e) {
console.log(e);
return proxyResponse(400, 'Ops. Houston, we have a problem! :-(');
}
};
O Chatbot em ação
Agora que temos todos os artefatos necessários, vamos simular o pedido de uma pizza.
Na primeira requisição enviaremos a utterance (attributo message), para iniciar o intent, e o userId, que será usado para identificar a sessão. Na resposta do Lex, podemos identificar que ele iniciou o intent OrderAPizza, que o slot com o sabor da pizza já foi identificado, a mensagem solicitando o preenchimento do próximo slot, o slot que será preenchido, e o estado do diálogo (ElicitSlot).

Enviando novamente a mensagem informando o sabor da borda, ele retorna uma resposta similar a resposta acima, solicitando o último slot.

Ao enviarmos a resposta do último slot, ele irá solicitar a confirmação do pedido (ConfirmIntent), incluindo o seu valor total como um atributo de sessão. Na resposta também está incluso um card , para facilitar a interação com o usuário.

Se informarmos no, o Lex irá cancelar o pedido. Se informarmos yes, devemos enviar junto o atributo de sessão, para que a lambda vinculada a fullfilment obtenha o preço (isso é adequado em caráter de exemplo, mas esse campo é passível de manipulação, então, em produção, devemos usar estratégias diferentes) e crie o pedido.


Para garantir que o pedido tenha sido criado, podemos conferir o DynamoDB.

Conclusão
Por fim, as possibilidades com o Amazon Lex são infinitas. Através da integração com Lambda, podemos construir basicamente qualquer tipo de serviço que exija interações com o usuário.
Um grande abraço e boa semana.