# Assistant Fulfillment Helper
Esta biblioteta tem como objetivo facilitar a criação de *webhooks* para os nós de intenção da [Assistente TOTVS](https://produtos.totvs.com/ficha-tecnica/tudo-sobre-o-totvs-carol-assistente/). Com poucas linhas de código, é possivel criar uma regra de negócio customizada em sua própria estrutura de servidor.
## Pré-requisitos:
- [>= Python 3.7](https://www.python.org/downloads/)
## Por onde começar?
Crie um projeto novo e instale o módulo usando PyPI:
```sh
pip install assistant-fulfillment-helper
```
Importe e instancie a classe ``FulfillmentHelper`` para poder definir sua própria regra de negócio.
```python
from assistant_fulfillment_helper import FulfillmentHelper
fh = FulfillmentHelper()
fh.start() # Inicia um servidor local
```
O código acima irá instanciar o módulo e servirá um Webhook pronto para receber as requisições dos seus nós de intenção da Assistente. Para checar se está tudo certo, abra seu navegador e acesse http://127.0.0.1:5052 (endereço padrão).
O endereço (path) raíz possui dois roteamentos:
- ``GET``: Trás uma mensagem de sucesso com ``http response 200``, e pode ser utilizada como *health check*.
- ``POST``: Iniciará o processamento das [intenções definidas](#definindo-uma-intenção) na sua aplicação. É a chamada que o servidor espera receber da Assistente.
## Definindo uma Intenção
Para definir uma intenção, basta adicionar o decorator ``intent()`` no método que deverá ser o callback para aquela intenção. Uma intenção deverá estar vinculada à um Webhook, o qual é definido no momento da criação do callback.
Por exemplo:
```python
from assistant_fulfillment_helper import FulfillmentHelper
fh = FulfillmentHelper()
@fh.intent(webhook='pedidos', node='Novo pedido', token='{token}')
def cb_novo_pedido(args):
""" Sua regra de negócio aqui """
pass
```
Nesse exemplo, o "Novo pedido" no UI da Assistente deverá estar definido nas configurações do Fullfilment (webhook): ``http://meu-servidor.com.br/pedidos``.
### intent()
O decorator ``intent()`` definirá o callback pra cada intenção em uma lista de intenções dentro de um contexto de webhook. Será efetuado uma chamada para o método declarado como callback toda a vez que o servidor receber uma chamada vinda de um nó da Assistente para a intenção definida.
É possível registrar quantos callback de intenções e webhooks forem necessários, mas apenas um callback é permitido para cada intenção (nó).
**Parâmetros:**
| Parâmetro | Obrigatório? | Tipo | Descrição |
|-----------|--------------|------|-----------|
| ``webhook`` | Sim | ``Str`` | Path do webhook, para o qual a Assistant requisitará nesse nó |
| ``token`` | Sim | ``Str`` | Token disponibilizado pela Assistente na configuração do Nó de Intenção |
| ``node`` | Sim | ``Str`` | Nome do Nó de Intenção cadastrado na Assistente |
## Criando um callback
Na chamada do callback, será passado uma variável do tipo ``Dict`` com alguns argumentos que poderão ser utilizados na regra de negócio como quiser.
Exemplo de um método para callback de um Nó de Intenção:
```python
from assistant_fulfillment_helper import FulfillmentHelper, FulfillmentHelperResponse
fh = FulfillmentHelper()
@fh.intent(webhook='pedidos', node='Novo pedido', token='{token}')
def cb_novo_pedido(params):
session = params.get('sessionId')
message = f"Olá Usuário, nosso id de sessão é: {session}"
return FulfillmentHelperResponse(
message = message
)
```
Os parametros passados na chamada do callback são:
| Parâmetro | Tipo | Descrição |
|-----------|------|-----------|
| intent_name | ``Str`` | Nome do nó sendo processado |
| parameters | ``Dict`` | Todos os parametros desse contexto de conversa |
| sessionLog | ``List`` | Lista de ID de sessões até esse ponto da conversa |
| namedQueries | ``Dict`` | Resultados da Named Query (se houver) |
| query | ``Str`` | Query executada (se houver) |
| language | ``Str`` | Idioma da conversa |
| carolOrganization | ``Str`` | Nome da Organização |
| carolEnvironment | ``Str`` | Nome do Ambiente |
| carolOrganizationId | ``Str`` | Id da Organização |
| carolEnvironmentId | ``Str`` | Id do Ambiente |
| sessionId | ``Str`` | Id da sessão da conversa atual |
| isProduction | ``Bool`` | Informa se a convesa está acontecendo em Produção ou Desenvolvimento |
| channelName | ``Str`` | Nome do canal por onde a mensagem chegou |
### Retorno do Callback
O metodo de callback deve retornar uma classe do tipo ``FulfillmentHelperResponse``, como no [exemplo acima](#criando-um-callback). Essa classe possui os seguintes atributos para retorno:
| Parâmetro | Obrigatório? | Tipo | Descrição |
|-----------|--------------|------|-----------|
| ``message`` | Sim | ``Str`` | Mensagem que será retornada na conversa |
| ``short_message`` | Não | ``Str`` | Mensagem curta de retorno |
| ``jump_to`` | Não | ``Str`` | Nome do nó para o qual a conversa será direcionada |
| ``options`` | Não | ``List[Str]`` | Lista de opções pré-definidas que aparecerão como botões na resposta |
| ``logout`` | Não | ``Bool`` | Destrói a sessão de usuário na conversa ``(default: False)`` |
| ``parameters`` | Não | ``Dict`` | Parametros que serão adicionados no contexto da conversa |
Exemplo de uso:
```python
@fh.intent(webhook='boas-vindas', node='Boas vindas', token='{token}')
def callback_boas_vindas(params):
message = f"Olá, o que deseja fazer agora?"
return FulfillmentHelperResponse(
message = message,
short_message = "Boas vindas",
jump_to = "Pedidos",
options = [
"Criar Pedido",
"Consultar Pedido"
"Cancelar Pedido"
],
logout = False,
parameters = { 'onboarding' : True }
)
```
### Falha do Callback
Se por algum motivo houver uma excessão na execução do callback, o servidor responderá uma mensagem de fallback padrão para a assentente ("Desculpe, não consegui processar seu pedido no momento."). Essa mensagem pode ser alteradas na definição da intenção.
Por exemplo:
```python
@fh.intent(webhook='login', node='Login', token='{token}', fallback_message='Estamos passando por alguns problemas.')
def callback_login(params):
resposta = 'lorem'*1/params # raises a TypeError Exception
return FulfillmentHelperResponse(
message = resposta
)
```
Na execução desse callback, a excessão TypeError será capturada pelo servidor o qual, por sua vez, retornará para o usuaŕio na Assistente a mensagem de fallback definida para esse callback e o detalhes do erro será disponibilizado no console do servidor.
## Iniciando um servidor local
Existem diferentes formas de executar o servidor webhook. A biblioteca disponibiliza o método ``start()`` para rodar o servidor diretamente pelo código e o método ``get_app_context()`` para poder executar externamente por outra biblioteca (ex: gunicorn) e executar testes.
### get_app_context()
O método ``get_app_context()`` retornará o contexto do servidor em uma variavel, o que é util para rodar testes e rodar a aplicação utilizando bibliotecas externas.
Por exemplo:
```python
fh = FulfillmentHelper()
application = fh.get_app_context()
```
Agora a aplicação pode ser executada com o comando:
```sh
gunicorn my_app
```
> NOTA: nesse exemplo a variável ``application`` poderá ser acessada por bibliotecas de testes.
### start()
O método ``start()`` é responsável por iniciar um servidor local e deixar pronto para receber requisições como um Webhook. O servidor local pode ser configurado passando algumas propriedades no momento da chamada. Por exemplo:
```python
fh = FulfillmentHelper()
fh.start(
debug = True
)
```
As configurações customizáveis para o servidor local são:
| Parâmetro | Obrigatório? | Tipo | Default | Descrição |
|-----------|--------------|------|---------|-----------|
| ``debug`` | Não | ``Bool`` | ``False`` | O Debug ativo habilita verbosidade e reinicia o servidor em cada alteração de código |
| ``host`` | Não | ``Str`` | ``0.0.0.0`` | Nome ou IP do host local |
| ``port`` | Não | ``Int`` | ``5052`` | Porta do host local |
## Exceções
Os possíveis erros são tratados pelas exceções do módulo. Aqui está a lista das exceções existentes:
| Exceção | Problema |
|-----------|--------|
| ``DuplicatedIntentNodeException()`` | Foi tentado adicionar dois métodos de callback para o mesmo Nó de Intenção |
| ``IntentCallbackNotFoundException()`` | O WebHook recebeu uma chamada para um Nó de Intenção indefinido |
| ``IntentResponseInstanceException()`` | O Callback invocado não retornou a classe de resposta esperada (``FulfillmentHelperResponse()``) |
| ``InvalidWebhookTokenException()`` | O WebHook token utilizado na chamada é diferente do token informado no registro do nó |
| ``WebhookNotFoundException()`` | O servidor recebeu uma requisição para um webhook indefinido |
## Executando em ambiente de DEV:
> Dica: Ao invocar o metodo ``start()``, [habilite o Debug](#iniciando-um-servidor-local) para um desenvolvimento mais rápido.
Ao iniciar o webhook local, será necessario disponibilizar a aplicação para fora da sua rede. Para isso recomendamos a utilização de algum software de proxy local, como, por exemplo, o [ngrok](https://ngrok.com/download). Após instalação, execute o comando abaixo em seu terminal para obter a URL pública da sua aplicação. Essa URL poderá ser adicionada como WebHook nas configurações dos seus nós na Assistente para um teste local.
```sh
ngrok http http://127.0.0.1:5052
```
> NOTA: Informe o host e porta definida na inicialiazação do servidor WebHook.
## Executando com Gunicorn
É possivel executar o servidor do webhook utilizando [Gunicorn](https://gunicorn.org/), uma vez que o contexto da aplicação esteja acessível no arquivo de chamada raíz da sua aplicação. Há duas formas pra obter o contexto da aplicação no seu arquivo.
1. Utilizando o método `get_app_context()`:
```python
from assistant_fulfillment_helper import FulfillmentHelper
fh = FulfillmentHelper()
...
application = fh.get_app_context()
```
Ou simplesmente importar a "application" diretamente:
```python
from assistant_fulfillment_helper import FulfillmentHelper, application
fh = FulfillmentHelper()
...
```
Agora é só iniciar o servidor com [Gunicorn](https://gunicorn.org/):
```
gunicorn -w 4 main
```
## Licença
MIT (LICENSE)