E aí galera, tudo bem? Bom, depois de toda a teoria sobre serviços cognitivos, vamos voltar ao código! :D. Hoje falaremos sobre a estrutura do bot! (para isso, faremos uma análise do código que o template do Bot Builder gera ao criarmos um novo projeto). Ansioso? Vem comigo então! ^^

Pré-requisitos

Para saber como criar um bot com o BotFramework, leia o post do Gustavo Bigardi antes deste ^^.

Controller

O que é uma Controller?

Quando criamos um novo projeto de chatbot, é gerado um projeto como visto abaixo, onde temos uma pasta Controllers que contém a classe MessageController.cs.

Estrutura de um projeto recém-criado

Essa classe é a responsável default por receber as requisições do usuário e direcioná-las ao processamento adequado. Ela não é, nada mais, nada menos, que uma API; cada Controller do BotFramework deve receber mensagens vindas de um chat da mesma forma que uma API receberia requisições através de um método POST (note, inclusive, que o template já cria a action POST para nós), e então as repassa para serem processadas.

[BotAuthentication]
public class MessagesController : ApiController
{
	/// <summary>
	/// POST: api/Messages
	/// Receive a message from a user and reply to it
	/// </summary>
	public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
	{
		...
	}

	private Activity HandleSystemMessage(Activity message)
	{
		...
	}
}

Perceba que a action recebe como parâmetro um Activity – essa é uma classe padrão do BotFramework que serve para armazenar todas as informações ligadas ao canal de comunicação criado entre o bot e o usuário. Nela temos métodos como Text (o texto que o usuário enviou), ChannelId (o Id do canal de comunicação), MembersAdded e MembersRemoved (dados dos usuários do canal), etc.

Como capturar os eventos do usuário na Controller?

A classe Activity possui uma propriedade Type pela qual é possível “enxergar” o tipo da ação praticada pelo usuário. A propriedade retorna uma string que pode representar diferentes tipos de ação (para mais informações, leia a documentação oficial). Podemos usar isso em comparações básica (como a que temos abaixo) para, por exemplo, verificar se o usuário enviou uma mensagem para nosso bot.

if (activity.Type == ActivityTypes.Message)
{
//Faça algo
}

Também podemos usar esses “eventos” de forma mais criativa. Por exemplo: se seu bot estiver processando uma operação que vai demorar para ser concluída, ele pode enviar uma ActivityTypes.Typing para o usuário para que ele tenha o feedback de que o seu bot não travou. Só tem um problema: o Typing que enviamos aparece como o texto “digitando” para o usuário por, em média, 4 segundos antes de desaparecer. Assim, se quisermos enviá-lo durante uma operação que dure mais que 4 segundos, precisamos implementar algo mais complexo. Abaixo, criei um exemplo onde, enquanto o bot faz uma operação de busca de dados, ele também fica enviando um feedback Typing para o usuário. Como eu não quis arriscar sobrecarregar o canal de comunicação, coloquei um delay de 3 segundos entre cada Typing enviado.

private async Task sendTypingWhileWaitingOperation<T>(IDialogContext context, Task<T> operation)
{
	while (!operation.IsCompleted)
	{
		await SendIsTypingMessage(context);
		await Task.Delay(3000);
	}
}

private async Task SendIsTypingMessage(IDialogContext context)
{
	var reply = context.MakeMessage();
	reply.Type = ActivityTypes.Typing;

	await context.PostAsync(reply);
}

Esse código produz o seguinte resultado:

Exemplo de bot enviando Typing para usuário

Como chamo meu código a partir da Controller?

Teoricamente, seu código pode ser chamado através de uma instância comum dentro da controller. O padrão, entretanto, é mantê-lo dentro das chamadas Dialogs (ver tópico específico), onde os mesmos serão processados como resultantes de um fluxo de conversação específico. Veja abaixo um exemplo onde usamos a classe estática Conversation, responsável por gerenciar o fluxo de uma conversa, e chamados o método SendAync, que irá enviar uma resposta para o usuário, passando para ele o Activity (onde temos os dados do canal, do usuário, etc.) e a instância de uma nova Dialog que conterá toda a nossa lógica de negócio.

await Conversation.SendAsync(activity, () => new Dialogs.RootDialog());

O método segue o princípio de inversão de dependência e é responsável pelas seguintes atividades, como vemos neste link:

  • Instanciar os componentes necessários (como a Dialog desejada);
  • Deserializar a pilha de Dialogs e o estado de cada Dialog na pilha de dentro do IBotDataStore;
  • Continuar a conversa a partir do ponto em que o Bot parou da última vez (Por exemplo: a partir do callback de uma PromptDialog ou de um context.Wait());
  • Enviar respostas ao usuário;
  • Serializar, atualizar o estado da conversa (a pilha de Dialogs e o estado de cada Dialog) e salvá-la no IBotDataStore;

Assim, sempre que quisermos criar um diálogo com o usuário, devemos chamá-lo de dentro do Conversation, como mostrado acima, já que ele vai cuidar de manter todo o contexto das conversas com o usuário e os dados do mesmo salvos e armazenados, além de seguir o fluxo de conversas sempre que ele for interrompido (no caso, por exemplo, do bot estar esperando uma resposta do usuário; o fluxo é interrompido e, quando o usuário envia a resposta, o fluxo deve continuar do ponto do código em que estava antes).

Dialogs

O que são Dialogs?

Dialogs são estruturas criadas para conter todos os “processos”, “serviços” e “contextos” necessários numa conversa. Cada Dialog é uma abstração que guarda o próprio estado e implementa a interface IDialog, de onde herda uma série de métodos bem bacanas que permitem recuperar mensagens em diferentes “tempos de uma conversa”. Ok, mas o que isso tudo quer dizer? Uma Dialog representa o diálogo que seu bot e o usuário estão tendo sobre um dado assunto – assim, uma boa prática é criar diferentes Dialogs para lidar com diferentes assuntos dentro de uma conversa.

Muito abstrato? Olha só um exemplo: imagine que estamos conversando com um amigo sobre um filme que gostamos e, de repente (e isso acontece com mais frequência do que nos damos conta), isso nos faz falar sobre a época em que o assistimos.

-Cara, assisti Watchmen na estreia;
-Nossa, eu adorava esse filme!
-O só foi tenso que eu tava no colégio na época, aí tive de faltar à aula pra pegar a sessão mais barata.
-Onde você estudava?
-Num colégio público, ali no Tatuapé.
-Ah, legal. Eu estudava no […]

Percebe como a conversa flui naturalmente de um assunto para o outro? No cenário acima, se tivéssemos um bot criado para trocar ideia, por exemplo, teríamos, pelo menos, duas Dialogs: uma feita para tratar apenas de conversas sobre filmes, outra feita para falar sobre a “infância do bot” (sim, sei que é estranho dizer isso, mas você pegou a ideia). Nesse cenário, temos métodos que podem levar a execução do bot de uma Dialog para a outra, de modo que a conversa fique tão fluída quando o exemplo que eu dei (vamos falar um pouco mais sobre isso nos próximos tópicos).

Como as Dialogs são executadas?

Beleza, agora sabemos o que são as Dialogs. Mas, como elas funcionam? Bom, tudo começa de uma Dialog raiz (root) que é sempre executada na nossa Controller. Ela geralmente é responsável pelo primeiro contato com o Usuário, então costumo chamá-la de “Dialog Cumprimento”. Quando essa Dialog começa a ser executada, ela é colocada em uma stack, ou seja, uma pilha de execução, e o BotFramework direciona as mensagens que recebe do usuário para que ela processe.

Pilha de execução de Dialogs

Agora, imagine que tenho uma pilha de Dialogs como a abaixo. Queremos direcionar o fluxo de conversa para uma Dialog que realiza um pedido (imagine que é um chatbot de uma pizzaria); quando o fazemos, a nossa “Dialog Pedido” é colocada no topo da pilha, e o BotFramework começa a direcionar as mensagens para ela. Nossa “Dialog Fazer Pedido” ficará então responsável pelo processamento da conversa enquanto não a fecharmos propriamente (usando a chamada context.Done()) – uma vez que ela for fechada, o controle do fluxo da conversa retorna para a Dialog pai de “Pedido” que, em nosso cenário, é a “Dialog Cumprimento”.

Dialog sendo colocada no topo da pilha de execução

Você deve estar se perguntando: como uma única Dialog pode ficar responsável por todo o fluxo de comunicação? Bom, como eu disse lá em cima, uma Dialog guarda o próprio estado. O que isso quer dizer? Que a Dialog, depois de instanciada e colocada na stack, torna-se única ao longo da conversa entre o bot e o usuário. A cada mensagem que o bot troca com o usuário, os dados contidos em cada Dialog são serializados e enviados dentro do Connector (ou seja, do canal de comunicação entre o bot e o usuário). Com isso, o estado dos objetos fica salvo a todo instante, sendo enviado de lá para cá do canal de comunicação junto das mensagens usuário – é por isso que todas as classes de Dialog, assim como os serviços extras, devem conter a propriedade [Serializable] (essa regra só se altera no caso de você usar algum serviço de injeção de dependência, como o próprio Autofac, suportado pelo Bot framework)

Assim, toda vez que o bot receber uma mensagem do usuário, o bot framework vai extrair o conteúdo salvo de dentro do IBotDataBag, descobrir qual a Dialog no topo da pilha, e passar a mensagem do usuário e o controle da conversa para ela.

Se você der uma olhada na RootDialog, criada automaticamente quando criamos um novo projeto de chatbot, notará que todos os métodos dentro dela recebem uma instância da interface IDialogContext como parâmetro. Essa interface promove acesso à todos os serviços que uma Dialog necessitará para salvar o estado de uma conversa e os dados de comunicação do canal criado entre ela e o usuário; em outras palavras, é neste contexto que todos os dados da conversa, desde a resposta do usuário até o ponto em que o Bot parou sua execução para aguardar o usuário, estão contidos (para saber mais à respeito, dê uma lida aqui).

public Task StartAsync(IDialogContext context)
{
	...
}

private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
	...
}

Como fazer a comunicação entre Dialog?

O IDialogContext, que expliquei na seção anterior, possui alguns métodos que podem ser utilizados para chamar outras Dialogs de diferentes formas ou mesmo encerrar a execução da Dialog que estiver no topo da pilha. Os métodos são o seguinte:

Método Descrição
   
.Call() Chama uma Dialog filha e adiciona ela no topo da pilha – ou seja, diz ao framework para executá-la
.Done() Encerra a Dialog atual e retorna algum valor (a ser especificado, por exemplo: .Done<string>()), para a Dialog parent (pai) dela.
.Forward() Chama uma Dialog filha e adiciona ela no topo da pilha passando à ela uma mensagem para ser executada
.Fail() Gera uma exceção indicando que a Dialog atual “falhou” e retorna essa exceção para a Dialog parent (pai) da atual.
.Wait() Suspende a Dialog atual e espera até que um evento externo (usuário enviando uma mensagem) ocorra.

Se você é como eu, deve estar um confuso sobre quando usar o .Call() e o .Forward(). Vou usar um exemplo mais palpável para explicar: outro dia criei um chatbot de piadas. Antes de contar uma piada, eu precisava perguntar ao usuário que tipo de piada ele queria ouvir… uma vez com a informação, eu contava a piada para ele. Bom, nesse contexto, eu tinha três Dialogs: a primeira, a root Dialog, de onde eu chamaria a segunda, que era um formulário  (conhecido como FormDialog) com várias perguntas para o usuário que me ajudariam a saber qual o tipo de piada que ele queria ouvir. A terceira era a Dialog que cuidaria de contar a piada.

Lendo sobre essas três Dialogs, você deve ter entendido que eu tive dois cenários: o primeiro, onde eu só preciso chamar outra Dialog com o formulário, e o segundo, onde além de chamar outra Dialog também preciso passar para ela uma informação (no caso, o tipo de piada que o usuário queria ouvir). Assim, para o primeiro caso, usei o .Call() enquanto, para o segundo, o .Forward(). Veja um exemplo:

private async Task AskUserAboutJoke(IDialogContext context)
{
	var formDialog = new FormDialog<ChooseJokes>(new ChooseJokes(), ChooseJokes.BuildForm, FormOptions.PromptInStart);
	context.Call(formDialog, ExecuteAfterForm);
}

private async Task ExecuteAfterForm(IDialogContext context, IAwaitable<ChooseJokes> result)
{
	var data = await result;
	var message = context.MakeMessage();

	message.Text = data.JokeType.GetDescribe();           
	await context.Forward(dialogFactory.Create<JokeDialog>(), ExecuteAfterJokeDialog, message, CancellationToken.None);
}

Um último adendo: dentro do FormDialog ele dá um .Done() automático ao terminar de coletar os dados do usuário; é assim que o fluxo de execução volta dele e cai no método ExecuteAfterForm() que, aqui, é uma função de callback que chama o contex.Forward().

Consigo instanciar mais de uma Dialog na minha Controller?

Não. Quando você instância uma Dialog, ela vai para o topo da pilha. Como já disse antes, uma vez que o contexto se mantém ao longo de toda a conversa, o BotFramework irá pegar a Dialog no topo da pilha toda vez que receber uma mensagem e repassar a mensagem para ela. Se você quiser chamar outra Dialog, terá de usar os métodos que expliquei na seção anterior.

Finalizando…

E é isso aí. Agora você tem um conhecimento de como as Dialogs e as Controllers funcionam; a partir daqui, vai poder trabalhar melhor no design das conversas do seu bot, criando um código limpo e um fluxo que consiga levar o máximo de valor para seu usuário. Espero que tenham gostado! ^^

Obrigado pela atenção, pessoal, e até a próxima 🙂

William Pinto