Бот для telegram зі станом в СУБД і класифікацією тексту

Т. к. мій класифікатор минулого посту таки працює (втім, параметри «з коробки» не завжди вдалі, бо я виніс можливість злегка налаштувати Conv1d-шари і прихований шар) — я вирішив прикрутити його до боту. Так, я запізнився на цей хайп ) до Речі, заздалегідь уточню, що прикрутити російську я поки таки не пробував, хоча це не повинно стати проблемою — в nltk підтримуються потрібні фічі, навчання word2vec концептуально не відрізняється від англійської, так і предобученные моделі начебто є.

Ну і відразу виникають питання:

  • під які платформи його пиляти — поки вирішив зупинитися на telegram. В теорії — конструкція дозволяє легко дописати обгортки для інших платформ (як ніби він комусь знадобиться )
  • як описувати «сценарій». Навелосипедил свою структуру з класами і сутностями поверх YAML
  • ну і непогано було б зберігати ботів/стан в який-небудь БД

Вимоги
Знадобляться:

До біса подробиці, давай приклад
Код на python
import logging
from robotframework_telegram import RobotFrameworkTelegram
from config import config


if __name__ == '__main__':
bots = RobotFrameworkTelegram({
'DATABASE_URI': 'URI',
'KEEP_WORD_VECTORS': 3,
'ROBOT_CONFIGURATIONS': 'robot_configurations',
'KEEP_ROBOTS': 5,
'OUTPUT_HANDLERS': {},
'TOKENS': [
# Your tokens here
],
'NO_ANSWER_MESSAGE': 'Sorry, I can\'t answer now.',
'NO_INSTANCE_MESSAGE': 'Sorry, robot not instantiated yet. '+ \
'You\'ll отримати answer after instantiation - it can take few minutes',
'TELEGRAM_WORKERS_PER_BOT': 1,
'WORKER_COUNT': 2,
'WORKER_SLEEP_TIME': 0.2,
'LOGGER': logging,
})
bots.start_polling()


За даними (структура і дамп postgres-ї БД, word2vec, конфіг навченого бота) — сюди.

Опис сценарію
В моєму випадку сценарій бота може бути описаний як дерево (ну, насправді — з урахуванням «goto» — швидше граф), вузол якого може містити умови:

  • про класі власного тексту
  • про які використовуються типи сутностей
Крім дерева в сценарії зазначені:

  • дані для навчання класифікатора
  • типи сутностей (умовне {«city»: [«Moscow»,«Tokeyo»,«Ottava»]})
  • їх синоніми (не менше умовне {«Moscow»: [«Default city»]})
  • (опціонально) додаткові параметри класифікатора. Наприклад, у разі мого «погодного» приклад конфігурація мережі, яка буде побудована за стандартним параметрам — лютейший оверкилл, бо я знизив кількість розмірів фільтрів і нейронів вихідного шару.
Наприклад,
купа yaml
script:
text: Hello. I'm weather robot. How can I help you?
name: main
children:
-
name: city_conditions
text: "Conditions in {%print $city%} is next."
conditions:
class: conditions
entity_variables:
city: $city
-
name: city_temperature
text: "Temperature in {%print $city%} is next."
conditions:
class: temperature
entity_variables:
city: $city
-
name: conditions
text: Okay, type city name.
conditions:
class: conditions
children:
-
name: conditions_city_selected
text: "{%goto city_conditions%}"
conditions:
entity_variables:
city: $city
-
name: temperature
text: Okay, type city name.
conditions:
class: temperature
children:
-
name: temperature_city_selected
text: "{%goto city_temperature%}"
conditions:
entity_variables:
city: $city
entities:
city:
- Moscow
- Tokyo
- Ottava
synonyms:
Moscow:
- default city
- DC
classes:
temperature:
- How is it hot today?
- Is it hot outside?
- It Will be uncomfortably hot?
- It Will be sweltering?
- How cold today is it?
- Is it cold outside?
- It Will be uncomfortably cold?
- It Will be frigid?
- What is the expected high for today?
- What is the expected temperature?
- Will high temperatures be dangerous?
- Is it dangerously cold?
- When will the heat subside?
- Is it hot?
- Is it cold?
- How cold is it now?
- Will we have a cold day today?
- When will the cold subside?
- What highs are we expecting?
- What lows are we expecting?
- Is it warm?
- Is it chilly?
- What's the current in temp Celsius?
- What is the temperature in Fahrenheit?
conditions:
- Is it windy?
- It Will rain today?
- What are the chances for rain?
- Will we get snow?
- Are we expecting sunny conditions?
- Is it overcast?
- It Will be cloudy?
- How much rain will fall today?
- How much snow are we expecting?
- Is it windy outside?
- How much snow do we expect?
- Is the forecast calling for snow today?
- Will we see some sun?
- When will the rain subside?
- Is it cloudy?
- Is it sunny now?
- It Will rain?
- Will we have much snow?
- Are the winds dangerous?
- What is the expected snowfall today?
- It Will be dry?
- It Will be breezy?
- It Will be humid?
- What is today's expected humidity?
- Will the blizzard hit us?
- Is it drizzling?
classifier_params:
filter_sizes: [1]
hidden_size: 100
nb_filter: 150
min_class_confidence: 0.8


Перемикання з 1 стану в інший (а заодно — витяг сутностей) в інше відбувається так:

  • класифікуємо enter. Витягаємо клас з найбільшим «схожістю». Дописуємо його ім'я перед користувальницьким текстом. Наприклад — «How cold in Moscow now?» перетвориться в «temperature How cold in Moscow now?»
  • отримуємо дочірні вузли поточного вузла (за замовчуванням — main)
  • для кожного нащадка (по порядку):

    • якщо він накладає обмеження на клас — перевіряємо входження в клас, якщо не входить — переходимо до наступного нащадку
    • якщо є типи сутностей, згадування яких потрібно зберегти — перевіряємо входження всіх можливих сутностей (і їх синонімів) цього типу і зберігаємо в словнику під відповідним ім'ям. Якщо знайдені не всі типи — переходимо до наступного нащадку. У процесі «шаблоном» {«city»: "$city"} «what temperature expected in Moscow and Ottava» витягнеться {"$city": [«Moscow», «Ottava»]}

Під капотом, частина 1
Жахливий код на bitbucket. Для початку — виділив такі сутності:

  • Класифікатор. З ним, ймовірно, начебто все зрозуміло
  • Сценарій. Містить повні відомості про сценарії — дерево, список класів, список типів сутностей, список еквівалентних найменувань, etc. robot/script.py
  • Вузол дерева сценарію. Містить дані про 1 вузлі — шаблон тексту, умови (включаючи клас і сутності), дочірні елементи. robot/script.py
  • Стан робота. Включає назву поточного вузла і виділені з користувальницького введення змінні. robot/robot_state.py
  • Оброблювач виводу. Може застосовуватися, наприклад для виведення значення змінної (print), переходу до іншого вузла (goto). robot/output_processing.py
  • Робот. Власне, з'єднує класифікатор, сценарій і обробник виводу. На вході має своє пройшло стан і текст, на виході — новий стан. robot/robot.py
Тепер можливо таке:

script = Script.load(os.path.join(os.path.dirname(__file__), "script.yaml"))
text_processor = TextProcessor("english",
[["turn", "on"], ["turn", "off"]],
Word2Vec.load_word2vec_format(
os.path.join(os.path.dirname(__file__), "100d.txt")
))
robot = Robot(script, text_processor)
state, output = robot.output()
self.assertEqual(output, "Hello. I'm weather robot. How can I help you?")
self.assertIsNotNone(state)
self.assertEqual(state.stage, "main")
self.assertEqual(state.variables, {})
state, output = robot.answer(state, "How cold it'll be in Moscow today?")
self.assertIsNotNone(state)
self.assertEqual(state.stage, "city_temperature")
self.assertEqual(state.variables, {"$city": ["Moscow"]})
self.assertEqual(output, "Temperature in Moscow is next.")

Вже можна намагатися що-небудь запустити, але я пішов далі.

Обгортка навколо robot і sqlalchemy. Поки без telegram
Ще більш жахливий код на bitbucket. Знову ділимо сутності:

  • Мова. Містить ім'я для nltk і набір виключень для вирізання стопслов. robotframework/models/language.py
  • Предметна область. Містить посилання на мову і word2vec, який навчений на текстах потрібної предметної області на цій мові (буде завантажений в пам'ять при першому зверненні і вивантажений у разі, якщо вже завантажили більше заданого кількості word2vec-в). robotframework/models/domain.py
  • Робот. Містить посилання на предметну область, сценарій і посилання на конфіг класифікатора (знову ж — класифікатор буде створений за першим зверненням. Якщо ж класифікатор не навчений — він буде попередньо навчений і збережений. Выгрузится за перевищенні відповідного параметра). robotframework/models/robot.py
  • Користувач. Все що від нього треба тут бути однозначно идентифицируемым. robotframework/models/user.py
  • Діалог. Просто посилається на юзера і робота. robotframework/models/conversation.py
  • Запис діалогу. Містить свій джерело (робот/користувач), текст і стан робота. robotframework/models/conversation_item.py
  • Додаток — одинак з всілякої технічної функціональністю. Зараз — ініціалізує sqlalchemy
Якось так:



Приклад коду:

from robotframework import *
from sqlalchemy import select

app = Application({
'DATABASE_URI': ",
'KEEP_WORD_VECTORS': 4
})
robot = Robot.filter(lambda query: query.where(Robot.id == 1))[0]
user = User.filter(lambda query: query.where(User.id == 1))[0]
conversation = robot.converse(user)
#print(robot.instance)
items = lambda: print([str(item) for item in conversation.items])
items()
conversation.output()
conversation.answer('How hot it\'ll be in Moscow today?')
conversation.answer('Okay, turn off')
items()

Ну і в результаті:

[]
["Hello. I'm weather robot. How can I help you?", "How hot it'll be in Moscow today", "Temperature in Moscow is next", ""]

Останній запис порожня, т. к. в сценарії немає сайту по якому можна перейти в цьому випадку.

У конфігурації — наступні опції:

{
'DATABASE_URI' : ", # URI для подлюченія sqlalchemy до СУБД
'KEEP_WORD_VECTORS': 3, # одночасно зберігається не більше заданого числа "словників" word2vec, інакше буде вивантажений той, до якого (відносно) давно не було звернень
'KEEP_ROBOTS': 3, # кількість збережених одночасно роботів
'ROBOT_CONFIGURATIONS': 'robot_configurations', # директорія, де зберігаються конфігурації навчених роботів,
'OUTPUT_HANDLERS': {} 
}

Тепер у нас начебто є всі засоби для більш чи менш адекватного зберігання роботів, їх станів і допоміжних даних. І — можливість прикручувати їх до різних месенджерів (та й не тільки), прикрутивши відповідну обгортку.

Telegram
Перед переглядом коду рекомендується запастися заспокійливими. Обгортка додає до моделей ще пару полів (telegram_chat_id в Conversation, telegram_bot_id в Robot). Ну і багатопоточність в усі поля.

Ось приклад:

import logging
from robotframework_telegram import RobotFrameworkTelegram
from config import config


if __name__ == '__main__':
bots = RobotFrameworkTelegram({
'DATABASE_URI': 'URI',
'KEEP_WORD_VECTORS': 3,
'ROBOT_CONFIGURATIONS': 'robot_configurations',
'KEEP_ROBOTS': 5,
'OUTPUT_HANDLERS': {},
'TOKENS': [
# Your tokens here
],
'NO_ANSWER_MESSAGE': 'Sorry, I can\'t answer now.',
'NO_INSTANCE_MESSAGE': 'Sorry, robot not instantiated yet. '+ \
'You\'ll отримати answer after instantiation - it can take few minutes',
'TELEGRAM_WORKERS_PER_BOT': 1,
'WORKER_COUNT': 2,
'WORKER_SLEEP_TIME': 0.2,
'LOGGER': logging,
})
bots.start_polling()

І діалог отриманий при тесті

[7:14:55 AM] #:
/start
[7:14:56 AM] robotframework_demo_weather:
Sorry, robot not instantiated yet. You'll get answer after instantiation — it can take few minuutes
Hello. I'm weather robot. How can I help you?
[7:16:07 AM] #:
It's cold?
[7:16:08 AM] robotframework_demo_weather:
Okay, type city name.
[7:16:11 AM] #:
Moscow
[7:16:11 AM] robotframework_demo_weather:
Temperature in Moscow is next.

[7:40:25 AM] #:
/start
[7:40:26 AM] robotframework_demo_weather:
Hello. I'm weather robot. How can I help you?
[7:40:48 AM] #:
What is your destination, robot? Seems like you mustn't answer
[7:40:48 AM] robotframework_demo_weather:
Sorry, I can't answer now.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.