Всем привет!

Давно хотел сделать своего собственного Jarvis. Недавно удалась свободная минутка и я его сделал. Он умеет переписываться с Вами, а также искать ответы на Ваши вопросы в Wikipedia. Для его реализации я использовал язык Python.

Для начала установим все необходимые библиотеки. Их три: pyTelegramBotAPI, scikit-learn, а также Wikipedia. Устанавливаются они просто:

pip install pyTelegramBotAPI
pip install Wikipedia
pip install scikit-learn

После установки всех библиотек приступаем к разработке. Для начала импортируем все библиотеки, установим язык для Википедии и подключим телеграмм бота

import telebot, wikipedia, re
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.linear_model import LogisticRegression

wikipedia.set_lang("ru")
bot = telebot.TeleBot('Ваш ключ, полученный от BotFather')

Теперь напишем код, для очистки всех ненужных нам знаков, которые вводит пользователь:

def clean_str(r):
	r = r.lower()
	r = [c for c in r if c in alphabet]
	return ''.join(r)

alphabet = ' 1234567890-йцукенгшщзхъфывапролджэячсмитьбюёqwertyuiopasdfghjklzxcvbnm?%.,()!:;'

Также Вам необходимо создать в папке, где находится Ваш код файл dialogues.txt, в нем мы будем создавать реплики на которые должен отвечать бот. Вот пример данного файла:

привет\здравствуйте!
как дела\хорошо.
кто ты\я Джарвис.

Строка до знака \ означает вопрос пользователя, а после ответ нашего бота. После чего напишем такой код в наш файл с ботом:

def update():
	with open('dialogues.txt', encoding='utf-8') as f:
		content = f.read()
	
	blocks = content.split('\n')
	dataset = []
	
	for block in blocks:
		replicas = block.split('\\')[:2]
		if len(replicas) == 2:
			pair = [clean_str(replicas[0]), clean_str(replicas[1])]
			if pair[0] and pair[1]:
				dataset.append(pair)
	
	X_text = []
	y = []
	
	for question, answer in dataset[:10000]:
		X_text.append(question)
		y += [answer]
	
	global vectorizer
	vectorizer = CountVectorizer()
	X = vectorizer.fit_transform(X_text)
	
	global clf
	clf = LogisticRegression()
	clf.fit(X, y)

update()

Этот кусок кода читает файл dialogues.txt, потом превращает реплики в так называемые вектора, с помощью которых наш бот будет искать наиболее подходящий ответ к заданному нами вопросу. Например, если Вы написали в файле dialogues.txt вопрос "Ты знаешь Аню", а ответ на него "Да, конечно", то бот будет отвечать также и на похожие вопросы, например "Ты знаешь Васю".

Теперь напишем кусок кода, который будет генерировать ответы на основе векторов:

def get_generative_replica(text):
	text_vector = vectorizer.transform([text]).toarray()[0]
	question = clf.predict([text_vector])[0]
	return question

Этот кусок кода принимает вопрос от пользователя и возвращает ответ от бота.

Теперь напишем функцию для поиска информации в Википедии:

def getwiki(s):
    try:
        ny = wikipedia.page(s)
        wikitext=ny.content[:1000]
        wikimas=wikitext.split('.')
        wikimas = wikimas[:-1]
        wikitext2 = ''
        for x in wikimas:
            if not('==' in x):
                if(len((x.strip()))>3):
                   wikitext2=wikitext2+x+'.'
            else:
                break
        wikitext2=re.sub('\([^()]*\)', '', wikitext2)
        wikitext2=re.sub('\([^()]*\)', '', wikitext2)
        wikitext2=re.sub('\{[^\{\}]*\}', '', wikitext2)
        return wikitext2
    except Exception as e:
        return 'В Википедии нет информации об этом'

Этот кусок кода получает вопрос пользователя, потом ищет ответ на него в Википедии и если ответ найден, то отдает его пользователю, а если ответ не найден, то пишет, что "В Википедии нет информации об этом".

Теперь пишем последний кусок кода:

@bot.message_handler(commands=['start'])
def start_message(message):
	bot.send_message(message.chat.id,"Здравствуйте, Сэр.")

question = ""

@bot.message_handler(content_types=['text'])
def get_text_messages(message):
	command = message.text.lower()
	if command =="не так":
		bot.send_message(message.from_user.id, "а как?")
		bot.register_next_step_handler(message, wrong)
	else:
		global question
		question = command
		reply = get_generative_replica(command)
		if reply=="вики ":
			bot.send_message(message.from_user.id, getwiki(command))
		else:
			bot.send_message(message.from_user.id, reply)

def wrong(message):
	a = f"{question}\{message.text.lower()} \n"
	with open('dialogues.txt', "a", encoding='utf-8') as f:
		f.write(a)
	bot.send_message(message.from_user.id, "Готово")
	update()

В этом куске кода телеграмм бот при получении сообщения от пользователя отвечает на него и если ответ не верный, то пользователь пишет "не так". Если бот получает сообщение "не так", то он берет последний вопрос пользователя и спрашивает "а как?", после чего пользователь должен отправить ему правильный ответ. После этого бот обновляет свою базу данных вопросов и ответов и при следующих вопросах пользователя отвечает на них правильно. И если ответ на вопрос бот должен был взять из Википедии, то пользователь в ответ на вопрос "а как?", должен написать "wiki". Осталось в конце приписать строчку:

bot.polling(none_stop=True)

И можно запускать и тестировать бота.

Весь код файла с ботом прилагаю ниже:

import telebot, wikipedia, re
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.linear_model import LogisticRegression

bot = telebot.TeleBot('Ваш ключ от BotFather')

wikipedia.set_lang("ru")

def clean_str(r):
	r = r.lower()
	r = [c for c in r if c in alphabet]
	return ''.join(r)

alphabet = ' 1234567890-йцукенгшщзхъфывапролджэячсмитьбюёqwertyuiopasdfghjklzxcvbnm?%.,()!:;'

def update():
	with open('dialogues.txt', encoding='utf-8') as f:
		content = f.read()
	
	blocks = content.split('\n')
	dataset = []
	
	for block in blocks:
		replicas = block.split('\\')[:2]
		if len(replicas) == 2:
			pair = [clean_str(replicas[0]), clean_str(replicas[1])]
			if pair[0] and pair[1]:
				dataset.append(pair)
	
	X_text = []
	y = []
	
	for question, answer in dataset[:10000]:
		X_text.append(question)
		y += [answer]
	
	global vectorizer
	vectorizer = CountVectorizer()
	X = vectorizer.fit_transform(X_text)
	
	global clf
	clf = LogisticRegression()
	clf.fit(X, y)

update()

def get_generative_replica(text):
	text_vector = vectorizer.transform([text]).toarray()[0]
	question = clf.predict([text_vector])[0]
	return question

def getwiki(s):
    try:
        ny = wikipedia.page(s)
        wikitext=ny.content[:1000]
        wikimas=wikitext.split('.')
        wikimas = wikimas[:-1]
        wikitext2 = ''
        for x in wikimas:
            if not('==' in x):
                if(len((x.strip()))>3):
                   wikitext2=wikitext2+x+'.'
            else:
                break
        wikitext2=re.sub('\([^()]*\)', '', wikitext2)
        wikitext2=re.sub('\([^()]*\)', '', wikitext2)
        wikitext2=re.sub('\{[^\{\}]*\}', '', wikitext2)
        return wikitext2
    except Exception as e:
        return 'В энциклопедии нет информации об этом'

@bot.message_handler(commands=['start'])
def start_message(message):
	bot.send_message(message.chat.id,"Здравствуйте, Сэр.")

question = ""

@bot.message_handler(content_types=['text'])
def get_text_messages(message):
	command = message.text.lower()
	if command =="не так":
		bot.send_message(message.from_user.id, "а как?")
		bot.register_next_step_handler(message, wrong)
	else:
		global question
		question = command
		reply = get_generative_replica(command)
		if reply=="вики ":
			bot.send_message(message.from_user.id, getwiki(command))
		else:
			bot.send_message(message.from_user.id, reply)

def wrong(message):
	a = f"{question}\{message.text.lower()} \n"
	with open('dialogues.txt', "a", encoding='utf-8') as f:
		f.write(a)
	bot.send_message(message.from_user.id, "Готово")
	update()

bot.polling(none_stop=True)

Надеюсь, статья Вам понравилась :)

Комментарии (8)


  1. bigmazy
    22.05.2022 13:21

    Почему называется само-обучаемый? До-обучаемый, сам он ничему не учится, по коду.


  1. Levin-Alexey
    22.05.2022 13:21
    +1

    Отличная и подробная статья! Попробую сделать.


  1. AASelivanov
    22.05.2022 13:22

    Статья понравилась! Интересно было бы посмотреть на бота в действии

    Понятно, что проект для себя и написан в свободную минуту, поэтому нет смысла спрашивать про вещи, которые могут сломаться при эксплуатации суровым пользователем.

    Но спрошу про некоторые вещи из любопытства:

    1. Из файла с записями берётся 10 тысяч, потому что на данном этапе предполагается, что там больше точно не будет?

    2. Вроде бы параметры Count Vectorizer по умолчанию рассчитаны на английский язык. Там регулярка не выбрасывает русские буквы? Также думаю замена списка стоп-слов может улучшить работу. Не пробовали TfIdfVectorizer или даже word2vec/fasttext?

    3. Проводили ли какую-нибудь оценку эффективности работы ML- алгоритма?

    4. Не уверен, насколько задачу поиска ближайшего соседа из датасета стоит решать как классификационную. Не является ли более удобным вариантом расчёт дистанции, скажем, косинусной меры, между векторами из базы и пришедшем?


    1. nrr
      22.05.2022 20:08

      Вот здесь можете посмотреть на бота в действии: https://t.me/tgbot20220522_bot.

      Ниже смотрите как сделать запрос к вики: https://habr.com/ru/post/667008/#comment_24366630

      Еще думаю, что в боте могут быть проблемы при его использовании несколькими пользователями одновременно, так как проверки вида if command =="не так", if reply=="вики " и другие подобные не привязаны к конкретному пользователю.


      1. night_admin
        23.05.2022 11:00

        Запустил вашего бота, в очередной раз удивился развитию технологий :)

        Я так понял, в одной из функций вы фильтруете "грязный" ответ из Википедии. Однако на вопрос о гипотизе Римана он мне выплюнул это:

        Ответ

        Гипо́теза Ри́мана — сформулированная немецким математиком Бернхардом Риманом в 1859 году математическая гипотеза о том, что дзета-функция Ри́мана

        ζ

        принимает нулевые значения только в отрицательных чётных числах:

        0

        =

        ζ

        =

        ζ

        =

        ζ

        =

        , и комплексных числах с вещественной частью

        1

        2

        {\displaystyle }

        .

        Думаю, было бы круто добавить показ формул (например, конвертировать Latex в картинки) и убрать сломанные теги викиразметки.


  1. nrr
    22.05.2022 18:55

    Спасибо за код, благодаря ему примерно понял как работают боты .

    Протестировал код, запустил его на Heroku. Для деплоя на Хероку помог вот этот ответ: https://ru.stackoverflow.com/a/896235/363117 .

    Реализация немного запутанная. Запутанность заключается в том, что непонятно когда нужно ввести слово "вики". А нужно ввести в такой последовательности:

    1. задать вопрос;

    2. написать "не так";

    3. ввести "вики";

    4. ввести еще раз тот же вопрос, и в этом случае сработает запрос к Википедии.


  1. prtolem
    23.05.2022 11:35

    принципе бот хороший, но я бы посоветовал перейти на aiogram


  1. theGrove
    23.05.2022 13:41

    Почему у меня ругаеться на эту строчку кода?

      a = f"{question}\{message.text.lower()} \n"
                                                  ^
    SyntaxError: invalid syntax