Speichern Sie den Chat-Status auf dem Stapel

Alles Neue ist gut alt vergessen!

Jetzt schreiben viele Leute verschiedene Bots, die mit dem Benutzer in IM kommunizieren und ihm irgendwie helfen, zu leben.



Wenn Sie sich den Code vieler Bots ansehen, kommt es normalerweise auf dasselbe Muster an:


  • Nachricht kommt an
  • es wird an den User Message Handler übergeben ( callback )

Dies ist im Allgemeinen eine universelle Methode, um Bots zu schreiben. Es eignet sich für Ein-Personen-Chats und für Bots, die mit Gruppen verbunden sind. Mit dieser Methode ist bis auf eine alles in Ordnung: Der Code selbst einfacher Bots ist oft ziemlich verwirrend.


Lassen Sie uns versuchen, es zu entwirren.


Ich beginne mit den Haftungsausschlüssen:


  1. Was in diesem Artikel beschrieben wird, ist für Bots vom Typ <-> geeignet.
  2. Der Code in diesem Artikel ist ein Skizzencode. In 15 Minuten speziell für diesen Artikel geschrieben. Also nicht streng urteilen.
  3. In der Wirtschaft habe ich einen ähnlichen Ansatz gewählt: mit Lastausgleich. Leider hat mein Produktionscode viele Infrastrukturabhängigkeiten und es ist so einfach, ihn nicht zu veröffentlichen. Daher wird diese Skizze im Artikel verwendet. Ich werde auf die Probleme der Paradigmenentwicklung eingehen (ich werde beschreiben, wo und wie wir uns entwickelt haben).

Nun, jetzt lass uns gehen.


Betrachten Sie als Unterstützung die asynchrone Aiogrammbibliothek python3.7 + . Der Link enthält ein Beispiel für einen einfachen Echo-Bot.


Ich werde es hier kopieren:


Code anzeigen
 """ 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) 

Wir sehen, dass die Organisation des Bots traditionell ist. Jedes Mal, wenn ein Benutzer etwas an uns schreibt, wird eine Handlerfunktion aufgerufen.


Was ist falsch an diesem Paradigma?


Dass die Handler-Funktion zum Implementieren komplexer Dialoge ihren Zustand bei jedem Aufruf aus einem Speicher wiederherstellen muss.


Wenn Sie sich die meisten Bots ansehen, die eine Art von Geschäft unterstützen (z. B. Einstellungen), stellen sie dem Benutzer 1..N Fragen und führen dann basierend auf den Ergebnissen dieser Fragen eine Aktion aus (z. B. speichern sie das Profil in der Datenbank).


Wenn es möglich wäre, einen Bot in einem traditionellen Stil (anstelle eines Ringstils) zu schreiben, könnten Benutzerdaten direkt auf dem Stapel gespeichert werden.


Lass es uns versuchen.


Ich habe eine Skizze des Moduls entworfen, die Sie mit dieser Bibliothek verbinden können:


Code anzeigen
 #  - 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 

Eine kleine Erklärung:


Die ChatDispatcher Klasse wird mit den folgenden Parametern ChatDispatcher :


  1. Sharding-Funktionen eingehender Nachrichten (warum wird es Sharding genannt - später, wenn wir große Lasten berühren). Die Funktion gibt eine eindeutige Nummer zurück, die einen Dialog anzeigt. Im Beispiel wird einfach die Benutzer-ID zurückgegeben.
  2. Funktionen, die die Arbeit des Chat-Dienstes ausführen.
  3. Zeitüberschreitungswert für Benutzerinaktivität.

Stellenbeschreibung:


  1. Als Antwort auf die erste Nachricht des Benutzers wird eine asynchrone Aufgabe erstellt, die dem Dialog dient. Diese Aufgabe funktioniert, bis der Dialog abgeschlossen ist.
  2. Um eine Nachricht von einem Benutzer zu erhalten, fordern wir diese explizit an. echo Chat Beispiel:
     async def chat(get_message): message = await get_message() await message.answer(message.text) 
  3. Wir antworten auf Nachrichten, die die Bibliothek uns anbietet ( message.answer ).

Versuchen wir, in diesem Paradigma einen Bot zu schreiben


Vollständiges Codebeispiel hier
 #  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) 

Ein Beispiel für einen geschriebenen Bot - er fügt einfach ein paar Zahlen hinzu und erzeugt ein Ergebnis.


Das Ergebnis sieht so aus:



Nun schauen wir uns den Code genauer an. Instanzen sollten keine Fragen aufwerfen.


Die Integration mit unserer Skizze erfolgt in dem Standard-Handler, den wir await chat_dispatcher.handle(message) . Und wir haben den chat in der chat Funktion beschrieben, ich werde den Code hier wiederholen:


 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-Service-Code - fragt nacheinander nach den Daten des Benutzers. Benutzerantworten werden einfach auf den Stapel gestapelt (Variablen first , second , message ).


Die Funktion get_message kann eine Ausnahme get_message , wenn der Benutzer während des festgelegten Zeitlimits nichts get_message (und Sie können das Zeitlimit an derselben Stelle übergeben).


Dialogstatus - direkt bezogen auf die Zeilennummer innerhalb dieser Funktion. Wenn wir den Code nach unten bewegen, bewegen wir uns entlang des Dialogschemas . Änderungen am Dialog-Thread vorzunehmen ist nicht einfach, aber sehr einfach!
Somit werden keine Zustandsautomaten benötigt. In diesem Paradigma können Sie sehr komplexe Dialoge schreiben und verstehen, dass deren Code viel einfacher ist als Code mit callback .


Nachteile


Wo ohne sie.


  1. Für jeden aktiven Benutzer gibt es eine Aufgabenkorrektur. Im Durchschnitt bedient eine CPU normalerweise etwa 1000 Benutzer, und dann beginnen die Verzögerungen.
  2. Neustart des gesamten Daemons - Beendet alle Dialoge (und startet sie neu).
  3. Der Code [aus dem Beispiel] ist nicht an die Skalierung und Internationalisierung von Lasten angepasst.

Wenn beim zweiten Problem klar ist, was zu tun ist: Unterbrechen Sie das Stoppsignal und teilen Sie den Benutzern mit, "Ich habe hier einen Notfall, Feuer, ich bin etwas später zurück." Das letzte Problem kann zu Schwierigkeiten führen. Schauen wir es uns an:


Skalierung laden


Offensichtlich müssen geladene Bots auf vielen Backends gleichzeitig gestartet werden. Dementsprechend wird der webHook Betriebsmodus verwendet.


Wenn Sie den webHook einfach auf zwei Backends webHook , müssen Sie natürlich sicherstellen, dass derselbe Benutzer zu derselben Coroutine kommt, die mit ihm spricht.


Wir haben das wie folgt gemacht.


  1. Analysieren Sie auf dem Balancer den JSON-Wert der eingehenden Nachricht ( message ).
  2. Wählen Sie eine Benutzer-ID aus
  3. Mit dem Bezeichner berechnen wir die Backend-Nummer (== Shard). Zum Beispiel mit dem user_id % Nshards .
  4. Wir leiten die Anfrage an den Shard weiter.

Benutzer-ID - wird zum Schlüssel zum Wechseln zwischen den Coroutinen der Dialoge und zur Grundlage für die Berechnung der Shard-Nummer des Backends im Balancer.


Der Code eines solchen Balancers ist einfach - er ist in 10 Minuten in jeder Sprache geschrieben. Ich werde es nicht bringen.


Fazit


Wenn Sie Bots in diesem Paradigma schreiben, können Sie die Dialoge ganz einfach von einem zum anderen wiederholen. Darüber hinaus ist es wichtig, dass der neue Programmierer den Code der Dialoge, die jemand vor ihm erstellt hat, leicht versteht .


Warum die meisten Leute Bots in Ringarchitektur schreiben - ich weiß nicht.


Sie haben in einem solchen Paradigma geschrieben. Das Servieren von Chatrooms in diesem Stil wurde in der Ära von IRC und Bots übernommen. Ich gebe also nicht vor, irgendeine Neuheit zu sein.


Und vieles mehr. Wenn Sie dieses Paradigma in einer Sprache mit dem goto Operator verwenden, ist dies nur ein schönes Beispiel für die Verwendung von goto (Schleifen in Dialogen werden auf goto ). Leider geht es hier nicht um Python.

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


All Articles