Armazene o status do bate-papo na pilha

Tudo novo é velho e esquecido!

Agora, muitas pessoas estão escrevendo vários bots que se comunicam com o usuário em mensagens instantâneas e de alguma forma ajudam o usuário a viver.



Se você olhar para o código de muitos bots, geralmente ele se resume ao mesmo padrão:


  • mensagem chega
  • é passado para o manipulador de mensagens do usuário ( callback )

Geralmente, é uma maneira universal de escrever bots. É adequado para bate-papos com uma pessoa e para bots conectados a grupos. Com esse método, está tudo bem, exceto um: o código de bots mesmo simples geralmente é bastante confuso.


Vamos tentar desvendar isso.


Vou começar com as isenções de responsabilidade:


  1. O que é descrito neste artigo é adequado para bots do tipo <-> .
  2. O código neste artigo é um código de esboço. Escrito especificamente para este artigo em 15 minutos. Portanto, não julgue estritamente.
  3. Eu usei uma abordagem semelhante nos negócios: com balanceamento de carga. Mas, infelizmente, meu código de produção tem muitas dependências de infraestrutura e é tão fácil não publicá-lo. Portanto, este esboço é usado no artigo. Vou abordar as questões de desenvolvimento de paradigma (descreverei onde e como nos desenvolvemos).

Bem, agora vamos.


Como suporte, considere a biblioteca assíncrona do aiograma, python3.7 + . O link tem um exemplo de um bot de eco simples.


Vou copiá-lo aqui:


Ver código
 """ This is a echo bot. It echoes any incoming text messages. """ import logging from aiogram import Bot, Dispatcher, executor, types API_TOKEN = 'BOT TOKEN HERE' # Configure logging logging.basicConfig(level=logging.INFO) # Initialize bot and dispatcher bot = Bot(token=API_TOKEN) dp = Dispatcher(bot) @dp.message_handler(regexp='(^cat[s]?$|puss)') async def cats(message: types.Message): with open('data/cats.jpg', 'rb') as photo: ''' # Old fashioned way: await bot.send_photo( message.chat.id, photo, caption='Cats are here ', reply_to_message_id=message.message_id, ) ''' await message.reply_photo(photo, caption='Cats are here ') @dp.message_handler() async def echo(message: types.Message): # old style: # await bot.send_message(message.chat.id, message.text) await message.answer(message.text) if __name__ == '__main__': executor.start_polling(dp, skip_updates=True) 

Vemos que a organização do bot é tradicional. Toda vez que um usuário escreve algo para nós, uma função de manipulador é chamada.


O que há de errado com esse paradigma?


A função do manipulador de implementar diálogos complexos deve restaurar seu estado de algum tipo de armazenamento em cada uma de suas chamadas.


Se você observar a maioria dos bots que oferecem suporte a algum tipo de negócio (por exemplo, contratação), eles fazem perguntas ao usuário 1..N, então fazem algo com base nos resultados dessas perguntas (por exemplo, salvam o perfil no banco de dados).


Se fosse possível escrever um bot em um estilo tradicional (em vez de em um estilo de anel), seria possível armazenar dados do usuário diretamente na pilha.


Vamos tentar fazer isso.


Esbocei um esboço do módulo, conectando o qual você pode usar com esta biblioteca:


Ver código
 #  - chat_dispatcher.py import asyncio class ChatDispatcher: class Timeout(RuntimeError): def __init__(self, last_message): self.last_message = last_message super().__init__('timeout exceeded') def __init__(self, *, chatcb, shardcb = lambda message: message.from_user.id, inactive_timeout = 15 * 60): self.chatcb = chatcb self.shardcb = shardcb self.inactive_timeout = inactive_timeout self.chats = {} async def handle(self, message): shard = self.shardcb(message) loop = asyncio.get_event_loop() if shard not in self.chats: self.chats[shard] = { 'task': self.create_chat(loop, shard), 'messages': [], 'wait': asyncio.Event(), 'last_message': None, } self.chats[shard]['messages'].append(message) self.chats[shard]['wait'].set() def create_chat(self, loop, shard): async def _chat_wrapper(): try: await self.chatcb(self.get_message(shard)) finally: del self.chats[shard] return loop.create_task(_chat_wrapper()) def get_message(self, shard): async def _get_message(inactive_timeout=self.inactive_timeout): while True: if self.chats[shard]['messages']: last_message = self.chats[shard]['messages'].pop(0) self.chats[shard]['last_message'] = last_message return last_message try: await asyncio.wait_for(self.chats[shard]['wait'].wait(), timeout=inactive_timeout) except asyncio.TimeoutError: self.chats[shard]['wait'].set() raise self.Timeout(self.chats[shard]['last_message']) if not self.chats[shard]['messages']: self.chats[shard]['wait'].clear() return _get_message 

Uma pequena explicação:


A classe ChatDispatcher é ChatDispatcher com os seguintes parâmetros:


  1. funções de compartilhamento de mensagens recebidas (por que é chamado de compartilhamento - mais tarde, quando tocamos em grandes cargas). A função retorna um número único indicando uma caixa de diálogo. No exemplo, ele simplesmente retorna o ID do usuário.
  2. funções que executarão o trabalho do serviço de bate-papo.
  3. Valor de tempo limite para inatividade do usuário.

Descrição do trabalho:


  1. Em resposta à primeira mensagem do usuário, é criada uma tarefa assíncrona que servirá ao diálogo. Esta tarefa funcionará até o diálogo ser concluído.
  2. Para receber uma mensagem de um usuário, solicitamos explicitamente. Exemplo de bate-papo de echo :
     async def chat(get_message): message = await get_message() await message.answer(message.text) 
  3. Respondemos às mensagens conforme a biblioteca nos oferece ( message.answer ).

Vamos tentar escrever um bot nesse paradigma


Exemplo de código completo aqui
 #  bot.py import asyncio import re from .chat_dispatcher import ChatDispatcher import logging from aiogram import Bot, Dispatcher, executor, types API_TOKEN ='    ' logging.basicConfig(level=logging.INFO) bot = Bot(token=API_TOKEN) dp = Dispatcher(bot) async def chat(get_message): try: message = await get_message() await message.answer('  ,   ') first = await get_message() if not re.match('^\d+$', str(first.text)): await first.answer('  ,  : /start') return await first.answer('  ') second = await get_message() if not re.match('^\d+$', str(second.text)): await second.answer('  ,  : /start') return result = int(first.text) + int(second.text) await second.answer(' %s (/start - )' % result) except ChatDispatcher.Timeout as te: await te.last_message.answer('-   ,  ') await te.last_message.answer(' - /start') chat_dispatcher = ChatDispatcher(chatcb=chat, inactive_timeout=20) @dp.message_handler() async def message_handle(message: types.Message): await chat_dispatcher.handle(message) if __name__ == '__main__': executor.start_polling(dp, skip_updates=True) 

Um exemplo de bot escrito - ele simplesmente adiciona alguns números e produz um resultado.


O resultado fica assim:



Bem, agora vamos dar uma olhada no código. Instâncias não devem levantar questões.


A integração com nosso esboço é feita no manipulador padrão que chamamos de await chat_dispatcher.handle(message) . E nós descrevemos o chat na função de chat - chat , repetirei o código aqui:


 async def chat(get_message): try: message = await get_message() await message.answer('  ,   ') first = await get_message() if not re.match('^\d+$', str(first.text)): await first.answer('  ,  : /start') return await first.answer('  ') second = await get_message() if not re.match('^\d+$', str(second.text)): await second.answer('  ,  : /start') return result = int(first.text) + int(second.text) await second.answer(' %s (/start - )' % result) except ChatDispatcher.Timeout as te: await te.last_message.answer('-   ,  ') await te.last_message.answer(' - /start') 

Código do serviço de bate-papo - apenas solicita os dados um a um ao usuário. As respostas do usuário são simplesmente empilhadas na pilha (variáveis first , second , message ).


A função get_message pode get_message uma exceção se o usuário não digitar nada durante o tempo limite definido (e você pode passar o tempo limite para ele no mesmo local).


Estado da caixa de diálogo - diretamente relacionado ao número da linha dentro desta função. Movendo o código, seguimos o esquema de diálogo . Fazer alterações no segmento de diálogo não é fácil, mas muito simples!
Portanto, máquinas de estado não são necessárias. Nesse paradigma, você pode escrever diálogos muito complexos e entender que o código deles será muito mais simples que o código com callback .


Desvantagens


Onde sem eles.


  1. Para cada usuário ativo, há uma tarefa-corutin. Em média, uma CPU normalmente atende a cerca de 1000 usuários e, em seguida, começam os atrasos.
  2. Reiniciar o daemon inteiro - encerra todas as caixas de diálogo (e as reinicia).
  3. O código [do exemplo] não está adaptado para carregar a escala e a internacionalização.

Se, com o segundo problema, ficar claro o que fazer: intercepte o sinal de parada e diga aos usuários: "Estou em uma emergência aqui, atire, voltarei um pouco mais tarde". Esse último problema pode causar dificuldades. Vejamos:


Escala de carga


Obviamente, os bots carregados devem ser lançados em muitos back-ends ao mesmo tempo. Assim, o modo de operação do webHook será usado.


Se você apenas equilibrar o webHook entre, digamos, dois back- webHook , obviamente precisará garantir de alguma forma que o mesmo usuário chegue à mesma rotina que está falando com ele.


Fizemos o seguinte.


  1. No balanceador, analise o JSON da mensagem recebida ( message )
  2. Escolha um ID do usuário
  3. Usando o identificador, calculamos o número de back-end (== shard). Por exemplo, usando o user_id % Nshards .
  4. Redirecionamos a solicitação para o shard.

ID do usuário - torna-se a chave para fragmentação entre as corotinas das caixas de diálogo e a base para o cálculo do número de fragmentos do back-end no balanceador.


O código desse balanceador é simples - é escrito em qualquer idioma em 10 minutos. Eu não vou trazê-lo.


Conclusão


Se você escrever bots nesse paradigma, poderá simplesmente refazer os diálogos de um para outro. Além disso, o importante é que o novo programador compreenda facilmente o código dos diálogos que alguém fez antes dele.


Por que a maioria das pessoas escreve bots na arquitetura de anel - eu não sei.


Eles costumavam escrever nesse paradigma. O atendimento de salas de bate-papo desse estilo foi adotado na era do IRC e dos bots. Portanto, não pretendo ser nenhum tipo de novidade.


E mais Se você usar esse paradigma em uma linguagem com o operador goto , este será apenas um belo exemplo do uso de goto (loops nas caixas de diálogo são muito bem feitos no goto ). Infelizmente, não se trata de Python.

Source: https://habr.com/ru/post/pt486006/


All Articles