Simpan status obrolan di tumpukan

Semua yang baru sudah lama terlupakan!

Sekarang banyak orang menulis berbagai bot yang berkomunikasi dengan pengguna di IM dan entah bagaimana membantu pengguna hidup.



Jika Anda melihat kode banyak bot, maka biasanya turun ke pola yang sama:


  • pesan tiba
  • itu diteruskan ke penangan pesan pengguna ( callback )

Ini umumnya merupakan cara universal untuk menulis bot. Cocok untuk obrolan satu orang dan untuk bot yang terhubung dengan grup. Dengan metode ini, semuanya baik-baik saja kecuali satu: kode bot bahkan sederhana seringkali cukup membingungkan.


Mari kita coba mengungkapnya.


Saya akan mulai dengan disclaimer:


  1. Apa yang dijelaskan dalam artikel ini cocok untuk bot dari <-> tipe <-> .
  2. Kode dalam artikel ini adalah kode sketsa. Ditulis khusus untuk artikel ini dalam 15 menit. Jadi jangan menilai dengan ketat.
  3. Saya menggunakan pendekatan serupa dalam bisnis: dengan penyeimbangan beban. Tetapi, sayangnya, kode produksi saya memiliki banyak ketergantungan infrastruktur dan sangat mudah untuk tidak mempublikasikannya. Oleh karena itu, sketsa ini digunakan dalam artikel. Saya akan menyentuh pada isu-isu pengembangan paradigma (saya akan menjelaskan di mana dan bagaimana kami berkembang).

Nah, sekarang mari kita pergi.


Sebagai dukungan, pertimbangkan perpustakaan asinkron aiogram, python3.7 + . Tautan tersebut memiliki contoh bot gema sederhana.


Saya akan menyalinnya di sini:


Lihat kode
 """ 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) 

Kami melihat bahwa organisasi bot bersifat tradisional. Setiap kali pengguna menulis sesuatu kepada kami, fungsi handler dipanggil.


Apa yang salah dengan paradigma ini?


Bahwa fungsi pawang untuk mengimplementasikan dialog kompleks harus mengembalikan keadaannya dari beberapa jenis penyimpanan pada setiap panggilannya.


Jika Anda melihat sebagian besar bot yang mendukung beberapa jenis bisnis (misalnya, mempekerjakan), mereka mengajukan pertanyaan kepada pengguna 1..N, maka mereka melakukan sesuatu berdasarkan hasil dari pertanyaan ini (misalnya, mereka menyimpan profil dalam database).


Jika dimungkinkan untuk menulis bot dalam gaya tradisional (bukan gaya dering), maka mungkin untuk menyimpan data pengguna secara langsung di tumpukan.


Mari kita coba melakukannya.


Saya membuat sketsa sketsa modul, menghubungkan yang dapat Anda gunakan dengan perpustakaan ini:


Lihat kode
 #  - 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 

Sedikit penjelasan:


Kelas ChatDispatcher dengan parameter berikut:


  1. fungsi sharding dari pesan yang masuk (mengapa disebut sharding - nanti, saat kita menyentuh banyak muatan). Fungsi mengembalikan nomor unik yang menunjukkan dialog. Dalam contoh, ini hanya mengembalikan ID pengguna.
  2. fungsi yang akan melakukan pekerjaan layanan obrolan.
  3. Nilai batas waktu untuk ketidakaktifan pengguna.

Deskripsi Pekerjaan:


  1. Menanggapi pesan pertama pengguna, tugas asinkron dibuat yang akan melayani dialog. Tugas ini akan bekerja sampai dialog selesai.
  2. Untuk menerima pesan dari pengguna, kami memintanya secara eksplisit. Contoh obrolan echo :
     async def chat(get_message): message = await get_message() await message.answer(message.text) 
  3. Kami menanggapi pesan saat perpustakaan menawarkan kami ( message.answer ).

Mari kita coba menulis bot dalam paradigma ini


Contoh kode lengkap di sini
 #  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) 

Contoh bot tertulis - ia hanya menambahkan beberapa angka dan menghasilkan hasilnya.


Hasilnya terlihat seperti ini:



Baiklah, sekarang mari kita melihat lebih dekat pada kode. Contoh tidak boleh mengajukan pertanyaan.


Integrasi dengan sketsa kami dilakukan di handler standar yang kami sebut await chat_dispatcher.handle(message) . Dan kami menggambarkan chat di fungsi chat , saya akan mengulangi kodenya di sini:


 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') 

Kode layanan obrolan - hanya meminta data satu per satu dari pengguna. Respons pengguna hanya ditumpuk di tumpukan (variabel first , second , message ).


Fungsi get_message dapat mengeluarkan pengecualian jika pengguna tidak memasukkan apa pun selama batas waktu yang ditentukan (dan Anda dapat melewatkan batas waktu itu di tempat yang sama).


Keadaan dialog - terkait langsung dengan nomor baris di dalam fungsi ini. Turun kode, kita bergerak di sepanjang skema dialog . Membuat perubahan pada utas dialog tidak mudah, tetapi sangat sederhana!
Dengan demikian, mesin negara tidak diperlukan. Dalam paradigma ini, Anda dapat menulis dialog yang sangat kompleks dan memahami kode mereka akan jauh lebih sederhana daripada kode dengan callback .


Kekurangan


Di mana tanpa mereka.


  1. Untuk setiap pengguna aktif, ada satu tugas-corutin. Rata-rata, satu CPU biasanya melayani sekitar 1000 pengguna, kemudian penundaan dimulai.
  2. Restart seluruh daemon - mengakhiri semua dialog (dan me-restart-nya).
  3. Kode [dari contoh] tidak diadaptasi untuk memuat penskalaan dan internasionalisasi.

Jika dengan masalah kedua jelas apa yang harus dilakukan: mencegat sinyal berhenti dan memberi tahu pengguna "Saya memiliki keadaan darurat di sini, tembak, saya akan kembali sedikit nanti." Masalah terakhir itu dapat menyebabkan kesulitan. Mari kita melihatnya:


Memuat skala


Jelas, bot yang dimuat harus diluncurkan pada banyak backend sekaligus. Dengan demikian, mode operasi webHook akan digunakan.


Jika Anda hanya menyeimbangkan webHook antara, katakanlah, dua backend, maka jelas Anda perlu entah bagaimana memastikan bahwa pengguna yang sama datang ke coroutine yang sama yang sedang berbicara dengannya.


Kami melakukan ini sebagai berikut.


  1. Di balancer, parsing JSON dari pesan masuk ( message )
  2. Pilih ID pengguna dari itu
  3. Dengan menggunakan pengenal, kami menghitung angka backend (== shard). Misalnya, menggunakan user_id % Nshards .
  4. Kami mengarahkan permintaan ke beling.

ID Pengguna - menjadi kunci untuk sharding antara coroutine dari dialog dan dasar untuk menghitung nomor beling backend di penyeimbang.


Kode penyeimbang seperti itu sederhana - ditulis dalam bahasa apa pun dalam 10 menit. Saya tidak akan membawanya.


Kesimpulan


Jika Anda menulis bot dalam paradigma ini, maka Anda dapat dengan mudah mengulang dialog dari satu ke yang lain. Selain itu, yang penting adalah bahwa programmer baru dengan mudah memahami kode dialog yang dibuat seseorang sebelum dia.


Mengapa kebanyakan orang menulis bot dalam arsitektur cincin - saya tidak tahu.


Mereka biasa menulis dalam paradigma seperti itu. Melayani ruang obrolan dalam gaya ini diadopsi di era IRC dan bot untuk itu. Jadi saya tidak berpura-pura sebagai hal baru.


Dan lagi. Jika Anda menggunakan paradigma ini dalam bahasa dengan operator goto , maka ini akan menjadi contoh indah menggunakan goto (loop dalam dialog dilakukan dengan indah pada goto ). Sayangnya ini bukan tentang Python.

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


All Articles