Skip to content

Overview

Dharuan Luigi edited this page Apr 22, 2023 · 9 revisions

Se você deseja melhorar o processo de desenvolvimento e manutenção do seu contato inteligente ou chatbot de acordo com sua cultura, este artigo é para você.

Introdução

Antes de iniciar

  • Este artigo é para desenvolvedores e tech leads que desejam implementar um Design Pattern em seu projeto que ajudará na construção, manutenção e escalabilidade de seu Contato Inteligente.
  • O exemplo que usarei aqui utilizará a plataforma Blip, mas o padrão de projeto pode ser usado com qualquer plataforma que segue o conceito de microsserviços, no qual é possível utilizar a segmentação de grandes responsabilidades em sub-bots com contextos delimitados.

Considerações

Para facilitar o entendimento durante a leitura do artigo, colocarei aqui as palavras que usarei e como são comumente definidas, para utilizarmos uma linguagem ubíqua.

  • Contato Inteligente: Chatbot, bot - uma solução que entrega o valor via automação de fluxo de conversa.
  • Sub-bots: Skills ou serviços menores que juntos formam um único contato inteligente com todas as habilidades divididas em contextos menores. O conjunto de um ou mais sub-bots compõem o que chamo de chatbot (contato inteligente).
  • Design Pattern: Pattern - padrão de desenvolvimento, padrão de projeto; o padrão atual sendo apresentado neste artigo.

Visão geral

Esse Pattern vem sendo implementado, estudado e testado por mim pelos últimos 12 meses. Durante esse tempo, pude ajustar e validar o que funciona e o que não funciona. Ele está maduro e, inclusive, está implementado no projeto em que estou trabalhando atualmente e tem funcionado bem para nós.

Durante o artigo, trarei alguns exemplos positivos e negativos para, ao final, você avaliar e ver se faz sentido para sua solução.

O foco desse Pattern é tratar alguns dos assuntos mais complicados e chatos que vejo durante a evolução de um chatbot em que não se segue um padrão de desenvolvimento. Especificamente, quando se trata de transições de sub-bots para atribuição de suas devidas responsabilidades.

Resumo

O propósito do Pattern, resumidamente, é solucionar os problemas de transição entre os sub-bots e diminuir o atrito de um ambiente poluído de blocos utilizados no processo. Ele ajuda a manter um melhor design de código e organização em um sub-bot utilizando do bom e velho Clean Code na solução, separando responsabilidades para quem é responsável por executar determinada tarefa e melhorando a reusabilidade dos fluxos, possibilitando melhor manutenção, escalabilidade e evolução.

Contexto

Durante todos esses anos trabalhando com o desenvolvimento de contatos inteligentes, tive a oportunidade de trabalhar com muitas soluções que eu considerava boas, mas que, ao longo da vida do chatbot, iam perdendo a qualidade, tanto em chatbots iniciados do zero ou já desenvolvidos, em que seriam implementadas features sem qualquer tipo de acompanhamento de evolução. Em um determinado momento, sempre acabava sendo necessário comunicar ao GP ou PM que seria necessário fazer a refatoração, pois estava inviável adicionar mais features, ajustar ou até mesmo dar suporte.

Lembro uma vez em que estava tentando resolver uma issue(problema) em um dos chatbots do projeto em que estava e passei quase um mês tentando encontrar e resolver o problema. O chatbot havia crescido sem nenhum tipo de controle nem padrão e já tinha passado na mão de vários desenvolvedores. Foi então que comecei a estudar o caso e me aprofundar para tentar entender o porquê de acontecerem coisas assim. Não foi difícil achar a resposta, visto o conjunto de processos de desenvolvimento que existiam e não eram praticados pelos desenvolvedores.

Então, pude notar quatro itens no padrão desses problemas, resumidamente:

  1. Falta de adequação aos guidelines propostos;
  2. Falta de documentação técnica eficiente e atualizada;
  3. Falta de refatoração constante durante o processo de desenvolvimento;
  4. Falta de um padrão de desenvolvimento.

Eu poderia começar pelo item número 1, mas o artigo ficaria muito extenso. Pretendo trazer estudos mais aprofundados de cada item e sugestões de novos padrões futuramente. No momento, falarei do quarto item: O padrão de desenvolvimento.

O padrão vem para atuar exatamente nesse ponto da dor, ajudar a manter e escalar o chatbot em larga escala e diminuir os atritos durante sua evolução ao longo dos anos, atuando fortemente no ponto entre transição de muitos sub-bots.

Sobre o nome

Acredito que entender o nome do padrão é fundamental para entendermos a abstração que ele carrega da solução que ele se propõe.

Dynamic: palavra vinda do inglês, que significa dinâmico. Homing: em sua tradução literal, quer dizer "teleguiado". Por exemplo: "A SpaceX tem um foguete com um sistema avançado de homing, que permite que o foguete saia de sua origem para o seu destino e depois volte novamente para sua origem de forma independente".

O Pattern é basicamente isso, mas de forma dinâmica, como o nome sugere.

Utilizando o mesmo exemplo da frase acima, ficaria algo assim: "A SpaceX tem um foguete com um sistema avançado de homing, que permite que o foguete saia da sua origem para o seu destino de forma independente. Caso necessário, no momento de retornar à origem, ele pode alterar sua antiga origem, indo para outro caminho. Caso não seja necessário, ele volta para origem inicial".

No nosso caso, o foguete é o nosso chatbot.

Aplicabilidade

Entendido o porquê do nome, agora vamos ao exemplo, podendo ser usado das seguintes maneiras:

  1. Um sistema de homing dinâmico, que permite que o chatbot saia de um ponto de conversa (um estado/bloco de conversa), vá até um destino em outro sub-bot em um ponto de conversa e volte para sua origem.
  2. Ao sair da sua origem de conversa e chegar em seu destino da primeira vez, ele pode alterar novamente seu novo destino e, ao invés de voltar para sua origem inicial, ele vai para outro ponto de conversa, possibilitando fazer isso por quantas vezes forem necessárias.

Parece confuso? Sim, um pouco. Com os próximos detalhes, ficará mais claro.

Por exemplo, a imagem abaixo:

Cenário simples:
image

Na conversa acima, podemos notar que há duas mensagens (em laranja) que, teoricamente, não estão no mesmo contexto de responsabilidade das mensagens anteriores.

Isso acontece pelo fato de que, em uma conversa em um chatbot, podemos ter N menus em N pontos diferentes de interação com o usuário. Não é recomendado criar um fluxo de tratativa de erro exclusivamente para cada menu em um mesmo sub-bot e em todos os seus pontos únicos de conversa, pelos seguintes fatos:

  1. Alteração global: caso eu deseje que um menu tenha o mesmo comportamento que outro, terei que duplicar a mesma lógica em outro ponto do chatbot, gerando muitos pontos de correção, o que é péssimo para quando der um problema e eu precisar lembrar onde foi que dupliquei aquela lógica.
  2. Responsabilidade única: quando adicionamos mais de uma responsabilidade em um sub-bot, ele acaba sendo onerado a fazer mais de uma coisa, prejudicando o escopo de atuação. Isso acaba gerando confusão no momento em que precisamos ampliar essa responsabilidade na qual ele foi construído, resultando em um alto número de lógica centralizada (não reusabilidade de lógica).

Ou seja, separando adequadamente, ficaria mais ou menos assim:
image

Nesse caso, dando nome aos sub-bots, temos um principal e outro responsável por enviar a mensagem de erro ao usuário.

Quando o sub-bot principal percebe o erro, ele chama o sub-bot de erro para dizer a mensagem que ele precisa que o usuário veja. Na sequência, o sub-bot principal assume a conversa novamente.

Repare que existe apenas uma flecha que sai do sub-bot principal e vai até ao sub-bot de erro, mas não existe uma flecha que sai do sub-bot de erro e volta para o sub-bot principal. Isso acontece porque a skill principal é a responsável por indicar isso para a skill de erro. A skill de erro apenas executa a mensagem e vai para o ponto indicado pela skill principal, aqui no exemplo, voltar para ela mesma. (Guarde essa informação, entrarei em detalhes mais a frente quando falar de skills de serviços e de negócios).

Este é o núcleo do Dynamic Homing Pattern: ele auxilia para que esse processo fique mais automatizado e simples de ser mantido, escalado e melhorado conforme a necessidade de crescimento dos sub-bots e também da solução por completo.

O exemplo acima serviu apenas para explicar no que o Pattern irá solucionar. Agora, vamos imaginar um cenário um pouco mais complexo.

Digamos que queremos coletar algumas informações do nosso usuário antes de podermos fechar seu pedido. A thread de conversa ficaria assim:

Cenário mais complexo:

image

Repare que, na conversa dessa skill, há um ponto desse sub-bot em que precisamos iniciar uma coleta de informações do usuário assim que o usuário inicia a finalização de seu pedido.

Seguindo os princípios de boas práticas, a skill de pedidos não é responsável por pedir essas informações (mensagens em verde) ao usuário. Por mais que ela precise dessas informações para finalizar o pedido, ela não é responsável por pedir nem validar esses dados do usuário. Nesse caso, podemos criar uma skill dedicada para coleta de dados e transferir essa responsabilidade para essa skill.

Resumindo, a skill de pedido fica responsável apenas por fazer o pedido e, quando for necessário pedir algum dado, ela direciona para a skill de coleta de dados dizendo qual dado precisa ser pedido ao usuário. Ela também precisa indicar para onde a skill de coleta de dados deve redirecionar o usuário quando o dado passado estiver válido.

A skill de coleta de dados fica responsável por pedir ao usuário a informação que foi solicitada pela skill de pedidos e validar se a informação está no formato esperado.

Caso a informação passada pelo usuário esteja certa, a skill de coleta de dados manda o usuário para o próximo destino indicado pela própria skill de pedidos. Nesse exemplo, a skill de pedidos pediu que voltasse para ela mesma, mas em outro bloco. Esse outro bloco repetiu o processo, mandando o usuário novamente para skill de coleta de dados e indicando solicitar o CEP, e assim sucessivamente até que a skill de pedidos solicite que o fluxo continue dentro dela mesma, para no final indicar para o bloco que manda a mensagem de "obrigado".

Caso o dado informado pelo usuário esteja inválido, a skill de coleta de dados direciona o usuário para a skill de erros e diz para ela mandar uma mensagem para o usuário informando que aquele dado está inválido. Logo em seguida, a skill de erros diz a mensagem solicitada e redireciona para o ponto indicado pela skill de coleta de dados, que nesse exemplo vai executar a volta para coletar a informação novamente dentro da skill de coleta de dados. Isso se repete até que o dado esteja correto e seja possível voltar para a skill de pedidos.

Ou seja, a validação ocorre por cascata de responsabilidade, e o usuário só volta para a skill de pedido quando o dado estiver correto.

Em uma visão geral, ficaria algo assim:

image

Dessa forma, conseguimos modularizar o pedido de dados para cada parte do chatbot em que precisamos coletar uma informação, e então podemos definir e criar formulários customizados com qualquer sequência e qualquer quantidade de dados que precisarmos, o que nos ajuda a ganhar nos seguintes pontos:

  1. Escalabilidade: podemos adicionar independentemente quantos dados forem necessários, visto que está dentro de um mesmo contexto, sem ter dependência de outros fluxos de conversa. Se hoje precisamos pedir apenas 10, amanhã poderemos pedir 300, pois o fluxo não se altera.
  2. Manutenção: caso a validação de CPF dê problema, por exemplo, temos apenas um ponto para dar manutenção e corrigir, que é a validação dos dados. Se forem usadas várias validações de CPF em vários pontos diferentes, teremos uma enorme dor de cabeça para manter atualizado.
  3. Reusabilidade: é possível reutilizar toda a skill de forma independente, podendo ser usada por todos os outros sub-bots que precisarem solicitar coleta e validação de um dado. Se quisermos que outra skill colete uma sequência diferente de dados, ou que colete menos dados que a skill de pedidos, precisamos apenas indicar isso definindo a sequência desses direcionamentos para a skill de coleta de dados.

Esses dois exemplos foram apenas uma introdução do Pattern e do problema que ele nos ajuda a resolver.

Agora, para entender um pouco mais, vamos falar um pouco sobre suas funcionalidades e regras.


Part 2

Regras

Para implementar corretamente o padrão, é necessário seguir algumas regras e restrições. Caso as regras sejam quebradas, podemos estar arriscando a saúde do desenvolvimento do chatbot. Mas, para poder aplicá-las, é necessário entender um pouco o porquê delas.

Divisão

Os sub-bots são divididos em papéis, sendo eles:

  • Sub-bots de serviço: são sub-bots que não representam uma atuação direta no tipo de serviço prestado ao usuário. Ex: sub-bot de erros, sub-bot de coleta de dados, sub-bot de finalização, sub-bot principal. Sem eles, o chatbot pode existir, ainda que em um modelo ruim, e pode prestar o devido serviço para o qual ele foi planejado.

  • Sub-bots de negócio: esses são os sub-bots responsáveis pelo negócio para qual o chatbot está sendo construído. Ex: sub-bot de pedido, sub-bot de produtos, sub-bot de boletos, sub-bot de atendimento. Esses são sub-bots necessários para entregarmos o devido valor de negócio ao cliente e fazer o negócio acontecer.

Direcionamentos e Redirecionamentos

A mais importante das regras é: o sub-bot de origem é o único responsável por saber de onde o usuário saiu e para onde ele vai, sempre, independente do cenário.

A skill de destino é apenas responsável por executar sua responsabilidade e logo em seguida tratar o que será necessário, verificar se pode seguir com o que foi indicado pela skill de origem ou se vai precisar ela mesma trocar o sentido de direcionamento e usar uma outra skill, e assim sucessivamente por quantas vezes forem necessárias.

O Pattern é dividido por tipos de direcionamentos e redirecionamentos, indo do mais simples até o mais complexo.

Em um cenário padrão sem o Pattern, existe apenas um tipo de direcionamento, o que chamo aqui de O.D. (Origem → Destino), pois usando o redirecionamento de serviços do Blip temos apenas um sentido único de direcionamento. Nesse sentido único, indicamos a um sub-bot para onde ele tem que enviar o usuário até outro sub-bot, apenas partindo de uma origem e chegando até um destino.

Com o Pattern, adicionamos mais alguns tipos de direcionamentos, como:

  • O.D.R. (Origem, Destino, Retorno) - Esse tipo de direcionamento é o mais simples, sendo geralmente utilizado para casos em que precisamos apenas partir de uma origem, ir até um destino e voltar para essa mesma origem. Por exemplo, no primeiro caso dos exemplos da parte de introdução, temos uma skill principal que tem um menu e existe uma exceção de menu. Para esse cenário, utilizamos o tipo de direcionamento O.D.R.

  • O.D.T.R.(Origem, Destino, Tratativa, Retorno) - Esse tipo é um pouco mais complexo. Nesse caso, o primeiro destino é responsável por tratar alguma informação antes de poder retornar para a origem inicial. Seria: skill de pedido → skill de coleta de dados (dado inválido) → skill de erros → skill de coleta de dados (dado válido) → skill de pedido.

  • O.D.D.O.P. (Origem, Destino, Tratativa, Retorno, Próximo) - Esse complemento é necessário sempre que implementamos o direcionamento tipo O.D.T.R. Ele é implementado para indicar a skill de origem a dizer para a skill de destino para onde ela deve retornar caso a informação coletada esteja correta. Ela pode simplesmente mandar a conversa para outro ponto dentro da mesma skill mudando o fluxo de conversa, ou para outra skill para dar sequência, e assim por diante. É uma adaptação necessária para funcionar no Blip. Esse tipo de direcionamento encapsula um conceito do padrão chamado Closures com Observer, no qual temos um conjunto de funções internas completadas à medida que as anteriores forem sendo completadas, chamando as demais que precisam ser completadas em sequência. Geralmente é utilizado o nome next, aqui traduzido para Próximo, para identificar quem será a próxima função a ser executada.

  • O.D.D. (Origem, Destino Diferente) - Esse é o caso no qual precisamos que, no direcionamento da primeira skill de origem, o retorno seja para uma skill diferente ao invés da skill origem. Ex: Imagine que estamos coletando os dados do usuário, mas ele está errando muitas vezes ao passar o dado. Ao invés de ficar em loop entre skill de coleta de dados ←→ skill de erros, identificamos o direcionamento repetido para skill de erros e, ao invés de mandar novamente para a skill de coleta de dados, mandamos para uma nova skill responsável por enviar o usuário para o atendimento humano, evitando loop de conversa. Ficaria mais ou menos assim: skill de pedido → skill de coleta de dados (dado inválido) → 1x skill de erros → skill de coleta de dados (dado inválido) → 2x skill de erro → skill de coleta de dados (dado inválido) → 3x skill de erro (mensagem diferente de erro para excesso de tentativas) → skill de atendimento humano (aplicado o conceito de O.D.D.).

Na teoria parece ser complicado, mas na prática é bem simples de ser implementado.

Transição entre skills

Para as saídas das skills, sempre devemos utilizar o redirecionamento de serviço da própria plataforma, o redirect service. Já para retornar a uma skill, caso necessário, deve ser utilizado o bloco de retorno automático implementado com o Pattern. Se precisarmos voltar para o ponto de origem ou ir para um ponto específico em outro sub-bot, não devemos utilizar apenas o redirecionamento de serviço, mas sim o processamento de comando para setar o state id de origem e logo em seguida, usar o redirecionamento de serviço nativo com ou sem mensagem dependendo do comportamento que deseja adicionar. Esse conjunto permite ser possível ir para um ponto específico. Dessa forma, conseguimos ter controle do fluxo de transição de skills, no qual utilizamos o redirecionamento de serviço apenas para sair de uma skill e chegar até uma outra skill de destino. No final, para voltar a skill de origem, temos apenas um ponto que faz isso, em que iremos processar o comando de retorno.

Direcionamentos internos dentro do mesmo sub-bot que não dependem de outro sub-bot podem ser feitos usando blocos de roteamento como go-to, router-blocks e afins.

Sub-bot de negócio

O bloco de start é o único responsável por direcionar o usuário para o início do fluxo e nada mais, não devem existir sub-blocos dentro do start que façam esse direcionamento interno; ex: bloco de roteamento, bloco de go-to que são vindos do bloco start de uma skill. Caso necessário, deve existir apenas um bloco a mais que sai do bloco de start, que servirá para fazer algum tipo de tratativa antes de entrar na skill, mas o destino deve ser sempre um: iniciar a skill.

Esses sub-blocos de direcionamento não são necessários quando se utiliza o Pattern, salvo quando tiver vários direcionamentos internos dentro da própria skill sem interferências externas, por exemplo, ir do menu da skill até outro ponto dentro da mesma skill, mas nada que venha do direcionamento do bloco start ou do redirecionamento originado de uma outra skill.

Para direcionar o usuário até uma skill de serviço, basta dizer de onde ele está saindo de dentro da skill atual de negócio e qual o nome do serviço interno que ele deseja acessar a partir do direcionamento.

Se por algum acaso precisarmos direcionar o usuário para uma outra skill de negócio e depender de outros blocos dessa skill de destino, será necessário, talvez, repensar se esse bloco está no lugar correto e na skill correta de implementação e considerar mover essa funcionalidade para uma skill de serviço, pois pode ser que outras skills precisem acessar esse mesmo recurso. Assim, já aproveitamos para refatorar essa parte e deixar tudo em seu devido lugar.

Sub-bot de serviços

Essas são as skills responsáveis por fazer o direcionamento do usuário e fazer todo o processo de segmentação das responsabilidades. O bloco de start deve conter um sub-bloco que tenha implementado o roteamento para onde o usuário precisa ir dentro da skill e fazer determinada ação, ou seja, aqueles famosos blocos go-to, bloco roteador e afins que retiramos das skills de negócio, passamos a usar exclusivamente dentro desse tipo de skill. Isso porque o direcionamento é apenas dentro da mesma skill, casando com o princípio de que podemos apenas usar esses blocos para direcionamento interno na skill.

Quando uma skill de negócio requisitar determinado ponto, ela precisa apenas saber qual o nome do serviço interno que quer pedir. Não é necessário ela saber exatamente qual é o ponto para onde o usuário vai, pois quem cuida disso é somente a skill de serviço que está recebendo a requisição do direcionamento. A skill de serviço faz esse roteamento interno e, caso seja preciso voltar o usuário para a origem, ela já sabe que precisa usar o bloco de retorno automático e voltar o usuário usando o processador de comandos, setando o ID do bloco no qual o usuário estava antes de entrar no serviço.

Ao receber o direcionamento de uma skill de negócios ou mesmo de serviço, precisamos analisar qual o contexto para conseguir decidir qual será o fluxo pelo qual o usuário irá passar dentro da skill usando os blocos de go-to que comentamos anteriormente.

Como aplicá-lo

Após entender um pouco sobre o Pattern, vamos ver exemplos das formas que podemos fazer para implementá-lo. Os exemplos serão diretos, práticos e bem simples, abstraindo o máximo que for possível. Trabalharemos apenas com as descrições, não teremos código. Caso queira ver todo o conteúdo com mais detalhes técnicos, basta acessar o repositório do Pattern no Github.

Explicarei a forma mais simples de aplicação do Pattern, do tipo de direcionamento O.D.R (Origem, Destino, Retorno), caso contrário, esse artigo ficaria ainda mais extenso. No repositório, cada um está explicado com uma maior riqueza de detalhes. Mas esse primeiro exemplo é uma base bem sólida e já irá facilitar muito o entendimento dos próximos.

Tipo O.D.R

Começando do exemplo mais simples, vamos relembrar nosso primeiro cenário: temos duas skills, uma chamada skill principal e a outra chamada skill de erro, e é preciso tratar um erro de menu.

Toda vez que precisamos redirecionar um usuário de uma skill de origem do tipo negócio para uma skill de destino do tipo serviço, utilizamos a forma nativa da plataforma, executando o tipo de ação chamada Redirect Service. Nela, indicamos para qual skill mandaremos o usuário naquele formato padrão que comentamos anteriormente, em que indicamos apenas de onde estamos saindo e para onde queremos ir.

Então, com esse primeiro cenário, podemos fazer o seguinte:

No bloco onde o usuário precisa escolher uma opção do menu, colocamos uma identificação para saber se o usuário escolheu uma opção válida que foi dada a ele.

  • Se a opção for válida, segue o fluxo de conversa.
  • Caso contrário, podemos direcionar o usuário para um bloco de exceção em que setamos algumas informações. Criamos um objeto de direcionamento contendo algumas informações sobre onde estávamos antes de ir para a skill de destino, que no caso será a skill de erros.  Também precisamos indicar para a skill de destino qual será o fluxo que queremos que ela nos leve para apresentar a mensagem de erro ao usuário, além de indicar o bloco de origem, para onde queremos voltar após a skill de erros apresentar a mensagem de erro ao usuário.

Algo que ficaria neste formato:

{
  "origin": {
    "id": "flow-id",
    "name": "principal",
    "block": {
      "id": "1234c636-4ee7-42ea-bbbd-939fe75a8b21",
      "name": "Menu Inicial"
    }
  },
  "destination": {
    "id": "flow-id",
    "name": "erros",
    "block": {
      "id": null,
      "name": "menu"
    }
  }
}

Note que não é necessário sabermos muito sobre o destino, precisamos saber apenas qual é o nome do destino e que queremos ir para um ponto que trate de erros de menu, indicando pelo nome do bloco no json que estou enviado para a skill de erros.

Neste caso, o nome do bloco que estou indicando no json não precisa ser o mesmo nome dentro da skill de erros, pois dentro da skill de erros ela mesma vai saber que estamos com um erro de menu pelo conteúdo do json que foi enviado a ela.

No caso da origem é o oposto, pois precisamos saber exatamente qual é o ponto em que o usuário estava antes de ser enviado para a skill de erros.

É fundamental enviarmos todas as informações corretas da origem do usuário. Estamos na skill e conseguimos facilmente acessar essas informações, e elas são o que dirá para a skill de erro exatamente para onde devemos voltar.

Dica: podemos adquirir o id do bloco de origem acessando o state.id do bloco em uma ação global que executa sempre que tem o input do usuário. Dessa forma, fica fácil saber e indicar que o usuário estava em determinado bloco que indicamos automaticamente pelas ações globais dentro do json de direcionamento.

Agora que fizemos a implementação da skill principal, vamos para a skill de erros.

Dentro da skill de erros, no bloco de start, podemos recuperar o json de direcionamento e criar um objeto único e imutável para ser usado com o valor que nos foi enviado através do input.content. Nesse caso, vamos supor que criamos um objeto chamado objHandleErrors.

Na sequência, criamos um segundo bloco que serve como um roteador de destinos dentro da skill, verificando qual é o fluxo interno solicitado pela skill de origem e acessando o valor através da propriedade [email protected]. Assim, sabemos que a skill de origem quer acessar um fluxo sobre erro de menu, então colocamos a saída para ir até o bloco que mostra o erro do menu.

O sub-bot irá mandar a mensagem do bloco e, logo em seguida, mandamos o fluxo de conversa para o bloco que executa a ação de retorno para a origem. Dentro desse bloco, criamos um script apenas com o propósito de inverter o que era destino para origem: se a origem era a skill principal, agora a skill principal irá virar destino.

Criamos o json com o mesmo modelo citado acima, mas agora vamos dizer que irá se chamar objRedirectFromError.

Logo em seguida criamos uma ação de processar comando. Essas ações simplesmente fazem chamadas nativas para a API do Blip. Dentro desse processador de comandos, configuramos para executar uma chamada com o método "set". No uri, colocamos a "/contexts/{{user-identity}}/stateid@{{flow-id}}". No nosso caso para recuperar o flow id da skill de origem, a uri ficaria assim: "/contexts/{{contact.identity}}/stateid@{{[email protected]}}".

No recurso, adicionamos o resource com o valor do bloco de destino, que no nosso caso é o id do bloco chamado Menu Principal que fica dentro da skill principal, informado antes de entrar na skill de erros e passado para o objeto chamado objHandleErrors. Nosso resource ficaria assim: {{[email protected]]}}.

Dessa forma, estamos usando a API do Blip para reposicionar o usuário dentro do nosso chatbot e mandar ele para onde ele estava originalmente antes de entrar na skill de erros.

Agora para commitar as alterações, basta fazer o redirecionamento de serviços usando o Redirect Service, mas agora usando os dados da variável {{objRedirectFromError}}, para indicar o nome do serviço que deseja retornar.

Link da documentação oficial de como funciona o change state id, que foi usado acima: Blip Docs - change user state

Repetição de mensagem

Diante disso, surgem dois cenários que ficam totalmente a critério do designer da solução e da equipe, com base no que funciona melhor para o público alvo. Toda vez que der um erro porque o usuário fez algo inesperado, deve-se decidir:

  1. Vamos apresentar a mensagem de erro e depois repetir a pergunta novamente;
  2. Vamos apenas mostrar a mensagem de erro e não repetir a pergunta, visto o contexto da conversa.
Ex - 1: 
Chatbot: Me informe seu CPF? 
User: batatinha   
Chatbot: Não consegui identificar nenhum CPF válido em sua resposta, preciso que me envie apenas os números.
Chatbot: Me informe seu CPF?
User: 00000000000
Ex - 2:
Chatbot: Me informe seu CPF?
User: batatinha
Chatbot: Não consegui identificar nenhum CPF válido em sua resposta, preciso que me envie apenas os números.
User: 00000000000

Isso é design de fluxo; estou apresentando esse ponto apenas porque o Pattern interfere nele, e fazer essa decisão é importante antes de implementá-lo em uma solução do zero. É também importante para o comportamento padrão caso queiramos adequar em uma solução existente, pois esse é o ponto que precisa ser ajustado nesse comportamento.

É importante definir isso quando fazemos um redirecionamento usando o change user state do Blip, pois ele não chama automaticamente a skill de destino. Ou seja, ele não manda uma mensagem que invoca a skill de destino automaticamente, por isso precisamos usar em conjunto o Redirect service e habilitar ou não o envio de mensagem, o sub-bot simplesmente posiciona o usuário no bloco em que ele estava antes de receber a mensagem de erro e, caso o usuário escolha a opção correta, segue com o cenário dois acima. Por padrão, ele estará no mesmo ponto do qual partiu, o usuário não irá receber a repetição da mensagem do sub-bot principal.

Caso o cenário precise ser o primeiro, é necessário indicar qual é o id do bloco anterior ao input inesperado do usuário no momento da criação do json de direcionamento . É possível pegar esse id nas ações globais que executam antes do input do usuário e utilizando a variável state.previous.id. Ao fazer isso, o usuário será redirecionado para um bloco antes daquele que ele deu o input, que no nosso caso enviaria a mensagem novamente, caso essa seja a intenção.

Se não houver essa divisão, será necessário criar esse bloco de apoio para servir de retorno dos destinos, e nesse bloco de retorno não pode haver um input, pois o usuário ficará preso nesse bloco sem ver a mensagem novamente.

Obs.: isso é um comportamento da plataforma Blip, e essa foi a saída encontrada para essa versão 1.0 do Pattern.


Final

Desvantagens

Aqui estão listadas as principais desvantagens de se utilizar o Pattern. Essas desvantagens estão no meu radar para serem aprimoradas em futuras novas versões. Algumas se dão por conta exclusivamente do funcionamento do Blip, em outros frameworks isso não necessariamente se aplicaria.

  • Complexidade: acredito que a utilização desse Pattern deixa a arquitetura do desenvolvimento mais complexa, devido a questões de modelos de objetos, processamento de comandos e estruturação do processo dos tipos de skills para que o Pattern seja aplicado corretamente. Se não houver cuidado na implementação, pode piorar ainda mais a situação de complexidade do contato inteligente.

  • Definição global de repetição de mensagem: esse é um ponto que precisa ser definido globalmente e no início do projeto, pois como o Pattern trata a repetição, ainda não temos uma alternativa a essa customização, permitindo em certos momentos repetir a pergunta ao usuário ou não. Para implementar em um projeto já existente, é preciso ter um cuidado maior, quebrar as implementações em partes menores e ir implementando o Pattern gradualmente.

  • Necessidade de envio para o bloco anterior ao input do usuário: é necessário sempre informar o bloco anterior ao que o usuário está, o que entra em conjunto com a limitação anterior. Essa particularidade é uma limitação que precisamos definir para o tipo de mensagem e comportamento de fluxo da conversa entre o usuário e o chatbot, quando for necessário voltar ao último ponto que o usuário estava.

  • Não executa as ações de entrada do bloco e mensagem: ao utilizar o Pattern, precisamos ter atenção a este ponto. Ao fazer o change user state, além dele não mandar novamente a mensagem para o usuário que foi exibida naquele bloco de destino, também não executa nenhuma das ações de entrada. Para executar algum tracking script nas ações de entrada, o ideal é mandar voltar para o bloco anterior, ou colocar essas ações nas ações de saída do bloco. Novamente casando com esse conjunto de particularidades exclusivas do Blip.

Vantagens

Agora que vimos algumas desvantagens, veremos algumas vantagens de sua utilização. Para não ser tendencioso, listarei também apenas as primeiras quatro vantagens principais de utilização do Pattern.

  • Manutenção: essa parte ajuda muito no momento de fazer as análises dos problemas de direcionamento no chatbot. Com o auxílio dos objetos de redirecionamento, temos o conceito de imutabilidade de entrada dentro da skill. Ou seja, quando acontecer algum bug, saberemos que está atrelado exclusivamente ao lugar em que estamos montando o objeto de direcionamento. Caso seja necessário fazer alguma alteração, melhoria ou manutenção, teremos que fazer em um único ponto.

  • Escalabilidade: se hoje tivermos cinco skills e amanhã, cinquenta, o processo é o mesmo para todos os direcionamentos. Basta implementar o mesmo modelo de direcionamento e estruturação de objetos que teremos configurado automaticamente todas as skills de serviços, não sendo necessário refazer todo o trabalho de mapeamento.

  • Divisão de responsabilidade: o Pattern facilita a utilização do princípio de responsabilidade única. Quando precisamos fazer determinada tarefa e vemos que será muito custoso adaptar no modelo, ele provavelmente precisa ser movido para uma responsabilidade única de skill. O Pattern facilita essa visualização e evita que sejam criadas muitas responsabilidades em uma mesma skill.

  • Solução modular: como a solução fica mais modular, é simples trocarmos as skills, adicionar mais skills, mudar qualquer direcionamento ou comportamento. Não existe acoplamento direto entre ligamentos técnicos, ajudando caso seja necessário transferir skills e responsabilidades futuramente. Visto que cada skill sabe o que precisa fazer, se depois for preciso trocar a skill principal por uma skill chamada master, não importa; desde que a skill master faça o uso do Pattern, tudo funcionará.

Conhecidas as vantagens e desvantagens, basta você analisar se faz sentido e usar em sua solução.

Conclusão

Entender que precisamos adicionar um padrão no que achamos complicado e no que gastamos muito tempo para dar suporte, adicionar escalabilidade, dividir os sub-bots em responsabilidades, padronizar o que é complexo. Todas essas são tarefas difíceis, e quando não usamos padrões, tudo fica ainda mais complicado de ser mantido. Quando o projeto troca de desenvolvedor, troca de equipe, trocam-se os padrões e tudo fica sem controle. Como vimos até aqui, o Dynamic Homing Pattern pode ajudar a resolver esses problemas.

Aplicar o Pattern ou não é uma decisão que depende da sua necessidade, do tamanho que você pretende escalar o seu contato inteligente e do tamanho da sua dor. O Pattern é, de fato, apenas a moldura por trás de tudo, de toda ideologia de padrão de desenvolvimento, trazendo consigo todos os outros benefícios provados e colocados em prática nesse último ano.

Como disse no início, o Pattern se prova necessário enquanto a implementação da solução cresce e se torna necessário padronizar o que antes eram apenas implementações por cima de implementações, features e códigos por cima de códigos. Ele não funciona sozinho, toda a boa prática é feita em conjunto com o time de desenvolvimento e com os padrões de desenvolvimento das equipes. Ele é apenas um módulo a mais que nos traz benefícios quando falamos de escalabilidade e manutenção.

Fique à vontade para explorá-lo e fazer modificações para atender sua necessidade. Todas as alterações ao projeto inicial são muito bem-vindas desde que sigam as mesmas ideologias iniciais e sua licença de ser livre e aberta para comunidade.

Quer bater um papo? - fique à vontade para usar os canais de comunicação no repositório do projeto, me mandar uma mensagem, fazer um pull request da sua ideia.

Acredito que há muito a ser explorado aqui.