Chatbot for VK on Python on Callback API

Chatbots have already become very common, and appear in all instant messengers daily.

In this article, we will take a step-by-step look at creating a bot with a set of simple commands and find out how to expand its functionality in the future. This article will be useful for the most beginners who have never tried to create chat bots.

When I wanted to create a bot, I studied the available examples of bots for VKontakte and tried to achieve the maximum simplification of their structure.

To create the bot, I used Python 3.5 (other versions of the 3rd python are probably suitable) and additional Flask and VK libraries . They will need to be installed. There are many articles in Russian on installing Flask. If you have Pycharm, then it most likely was installed with it.

Let's start with the API itself. For our bot, we will use the Callback API, available for group messages. First of all, we need to create or already have a VKontakte group with connected messages.

In the section Community Managementworking with API, you need to create a key with access to community messages.

image

To work with Callback you need to have a web server that will receive requests for any events from the API, process them and send response requests. That is, we will write a “website” that will only respond to requests sent to it and send ours.

Since we write in python, the simplest thing you can use is hosting for python. I used the free hosting for Python. There you need to register, and then create an application for Python 3.5 on Flask (you can create it in the Web section). An initial file will be created:

# A very simple Flask Hello World app for you to get started with...
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
    return 'Hello from Flask!'

The only function that is now in the file is responsible for filling the page at the address provided during registration. If you go to the browser at username.pythonanywhere.com (with your nickname), you can only see the text "Hello from Flask!".

In the future, the code will be presented in blocks and after the completion of the whole block it will be possible to check, and in the process the code may be marked as erroneous by the environment. Do not be scared, it's better to just finish the block to the end.

So, BLOCK 1 .
To process requests sent to the site, add the following code at the end of the document:

@app.route('/', methods=['POST'])
def processing():
    return 'xxxxxxxx'

Where instead of X we substitute "the string that the server should return". It is listed in the group management in the Callback API section.

This feature will allow us to connect our site for notifications to the group.

Now we can check the work. Only need to restart the application. On the hosting after the files have been changed and saved so that the site starts working with new data, you need to reload it in the Web tab. After adding this code, we can enter the corresponding address username.pythonanywhere.com in the server address bar in the VKontakte group and click "Confirm".

You should see a green notification that the server address is connected successfully.

When you click "Confirm", VKontakte tries to contact our server and make sure that it really belongs to the group owner, and "waits" for the server to return a confirmation code in response to the request.

BLOCK 2
We can move on to the next step. Add the ability to write messages on behalf of the community. It's time to install the VK library on the hosting. In the Consoles section, launch the bash console and execute the command (or the appropriate one for the selected version of python):

pip3.5 install --user vk

How to install modules is described here .

Change the code of our function for processing incoming requests:

@app.route('/', methods=['POST'])
def processing():
    #Распаковываем json из пришедшего POST-запроса
    data = json.loads(request.data)
    #Вконтакте в своих запросах всегда отправляет поле типа
    if 'type' not in data.keys():
        return 'not vk'
    if data['type'] == 'confirmation':
        return confirmation_token
    elif data['type'] == 'message_new':
        session = vk.Session()
        api = vk.API(session, v=5.0)
        user_id = data['object']['user_id']
        api.messages.send(access_token=token, user_id=str(user_id), message='Привет, я новый бот!')
        # Сообщение о том, что обработка прошла успешно
        return 'ok'

The message that the processing was successful is needed by the VKontakte server. If an error occurs, or some other answer arrives, the server will continue to send a notification about the incoming message at some intervals (until we process it).

The structure of an incoming request notifying about a new message is as follows:

{"type":"message_new","object":{"id":43, "date":1492522323, "out":0, "user_id":xxxxxxxx, "read_state":0, "title":" ... ", "body":"помощь"}, "group_id":xxxxxxxxxxx}

Vkontakte passes three objects to our site: "type", "object", "group_id", and inside the "object" information about the message is stored.

All requests can be found in the documentation VKontakte.

We also add new “import” to the beginning of the file:

from flask import Flask, request, json
from settings import *
import vk

We created a new file in the same settings.py folder, in which the necessary login data is saved:

token = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
confirmation_token = 'xxxxxxxx'

They need to be replaced with your tokens. The first we created at the beginning of the article, the second is a confirmation code to connect the group to the server.

Now our bot can greet incoming messages and confirm that it belongs to the group whose code we gave it.

We can check it and write some message to it, you just need to enable notifications about incoming messages in the group settings in the Callback API section.

In order for the bot to send messages, you need to restart the application. After that, we write to the bot again and, if everything is in order, go to the next step.

BLOCK 3
If everything went well, and the bot greeted you in response to your message, go to the next step. We put all the interaction with the vk library into another file, I called it vkapi:

import vk
session = vk.Session()
api = vk.API(session, v=5.0)
def send_message(user_id, token, message, attachment=""):
    api.messages.send(access_token=token, user_id=str(user_id), message=message, attachment=attachment)

While there is only one function and initialization of the VKontakte session, then we will add others. Potentially, a function can also send attachments. We will take this opportunity later.

Next we’ll start a file - a message handler. It will process incoming messages, determine the appropriate commands when they appear, and issue the necessary answers.

File "messageHandler.py":

import vkapi
def get_answer(body):
   message = "Привет, я новый бот!"
   return message
def create_answer(data, token):
   user_id = data['user_id']
   message = get_answer(data['body'].lower())
   vkapi.send_message(user_id, token, message)

It remains to connect our new files to the main one. We change the request processing function in the main file:

@app.route('/', methods=['POST'])
def processing():
    data = json.loads(request.data)
    if 'type' not in data.keys():
        return 'not vk'
    if data['type'] == 'confirmation':
        return confirmation_token
    elif data['type'] == 'message_new':
        messageHandler.create_answer(data['object'], token)
        return 'ok'

And add the appropriate import to the beginning of the file:

import messageHandler

We can check what we did by restarting the application.

BLOCK 4
Let's start creating teams. Let's create a class of commands.

File "command_system.py":

command_list = []
class Command:
   def __init__(self):
       self.__keys = []
       self.description = ''
       command_list.append(self)
   @property
   def keys(self):
       return self.__keys
   @keys.setter
   def keys(self, mas):
       for k in mas:
           self.__keys.append(k.lower())
   def process(self):
       pass

The class has the keys property, where keys will be stored by which you can access this command. All keys are saved in lowercase letters when setting a property, and they need to be compared with user messages converted to lowercase so that the register does not affect the success of a command call.

The description field will be used to issue information on bot commands. The process function will be executed to generate a response message.

There is a general list to which all commands are saved when they are initialized. He is outside the classroom. We will use this list to search for the command that the user requested with his message.

Now let's create some teams for our bot. For convenience of loading, we will place the files in which we initialize the commands in the “commands” folder.

I will create several files, but you can place the commands in the same file

"hello.py"
import command_system
def hello():
   message = 'Привет, друг!\nЯ новый чат-бот.'
   return message, ''
hello_command = command_system.Command()
hello_command.keys = ['привет', 'hello', 'дратути', 'здравствуй', 'здравствуйте']
hello_command.description = 'Поприветствую тебя'
hello_command.process = hello

"Cat.py"

import command_system
import vkapi
import settings
def cat():
   # Получаем случайную картинку из паблика
   attachment = vkapi.get_random_wall_picture(-32015300, settings.access_token)
   message = 'Вот тебе котик :)\nВ следующий раз я пришлю другого котика.'
   return message, attachment
cat_command = command_system.Command()
cat_command.keys = ['котик', 'кошка', 'кот', 'котенок', 'котяра', 'cat']
cat_command.description = 'Пришлю картинку с котиком'
cat_command.process = cat

For the team sending the cat, we need a new token and a new function in the “vkapi” file, which returns a random picture from the wall of the group or user. In this case, we will receive a random photo from the wall of the public with cats.

Let's start by getting a token. We need a service access key. To do this, create a new Standalone application. It can be created by reference . Next, when the application is created, you need to go to its settings and copy what is in the “Service access key” field.
This needs to be added to our token file.
"Settings.py"
token = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
confirmation_token = 'xxxxxxxx'
access_token = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'


Now let's move on to creating a new vkapi method. Here we expand the range of API methods used a little.

This method looks like this:

def get_random_wall_picture(group_id, token):
    max_num = api.photos.get(owner_id=group_id, album_id='wall', count=0, access_token=token)['count']
    num = random.randint(1, max_num)
    photo = api.photos.get(owner_id=str(group_id), album_id='wall', count=1, offset=num, access_token=token)['items'][0]['id']
    attachment = 'photo' + str(group_id) + '_' + str(photo)
    return attachment

We add it to the "vkapi" file. Also, add the necessary import to the beginning of the “vkapi” file:

import random

And the last command is

info.py
import command_system
def info():
   message = ''
   for c in command_system.command_list:
        message += c.keys[0] + ' - ' + c.description + '\n'
   return message, ''
info_command = command_system.Command()
info_command.keys = ['помощь', 'помоги', 'help']
info_command.desciption = 'Покажу список команд'
info_command.process = info

Final file hierarchy:

image
botFlask is the main file that accepts incoming requests.

Now that we have described the commands, we need to make sure that our command sheet is full, and we can understand which of the commands the user accessed, since the command_list is filled only at the moment of launching files with specific commands.

We will automatically run for execution all files from the “commands” folder when starting our bot.

To do this, add the function in the messageHandler.py file:

def load_modules():
   # путь от рабочей директории, ее можно изменить в настройках приложения
   files = os.listdir("mysite/commands")
   modules = filter(lambda x: x.endswith('.py'), files)
   for m in modules:
       importlib.import_module("commands." + m[0:-3])

In this function, we load the list of files from the directory with the commands, filter only the python files and import them into our program, which ensures that the list is filled with commands.

The call to this function is added to create_answer. Now let's change the get_answer function so that it invokes the corresponding response.

The final form of the file:

import vkapi
import os
import importlib
from command_system import command_list
def load_modules():
   # путь от рабочей директории, ее можно изменить в настройках приложения
   files = os.listdir("mysite/commands")
   modules = filter(lambda x: x.endswith('.py'), files)
   for m in modules:
       importlib.import_module("commands." + m[0:-3])
def get_answer(body):
    # Сообщение по умолчанию если распознать не удастся
    message = "Прости, не понимаю тебя. Напиши 'помощь', чтобы узнать мои команды"
    attachment = ''
    for c in command_list:
        if body in c.keys:
            message, attachment = c.process()
    return message, attachment
def create_answer(data, token):
   load_modules()
   user_id = data['user_id']
   message, attachment = get_answer(data['body'].lower())
   vkapi.send_message(user_id, token, message, attachment)

That's it, our bot is ready! Now you know how to create the basis for the bot and add new teams for it.

BLOCK 5 The
rest of the article will be about one improvement that I consider necessary. However, the bot will work without it.

Approximate command recognition

If the user made a mistake in one character, most likely he had in mind the most similar command. Therefore, it would be nice if our bot still gave an answer, and did not say "I do not understand you."

For approximate recognition, we will use the Damerau-Levenshtein distance. It shows how many operations of deleting, inserting, replacing and moving characters can go from one line to another.

An algorithm for finding this distance is described, for example, on Wikipedia.

Add the function to the messageHandler.py file:

def damerau_levenshtein_distance(s1, s2):
   d = {}
   lenstr1 = len(s1)
   lenstr2 = len(s2)
   for i in range(-1, lenstr1 + 1):
       d[(i, -1)] = i + 1
   for j in range(-1, lenstr2 + 1):
       d[(-1, j)] = j + 1
   for i in range(lenstr1):
       for j in range(lenstr2):
           if s1[i] == s2[j]:
               cost = 0
           else:
               cost = 1
           d[(i, j)] = min(
               d[(i - 1, j)] + 1,  # deletion
               d[(i, j - 1)] + 1,  # insertion
               d[(i - 1, j - 1)] + cost,  # substitution
           )
           if i and j and s1[i] == s2[j - 1] and s1[i - 1] == s2[j]:
               d[(i, j)] = min(d[(i, j)], d[i - 2, j - 2] + cost)  # transposition
   return d[lenstr1 - 1, lenstr2 - 1]

It implements an algorithm for finding this distance, if you wish, you can change or improve it.

According to these lines, it will give out the number of operations for converting one into another. Now change the get_answer method:

def get_answer(body):
   message = "Прости, не понимаю тебя. Напиши 'помощь', чтобы узнать мои команды"
   attachment = ''
   distance = len(body)
   command = None
   key = ''
   for c in command_list:
       for k in c.keys:
           d = damerau_levenshtein_distance(body, k)
           if d < distance:
               distance = d
               command = c
               key = k
               if distance == 0:
                   message, attachment = c.process()
                   return message, attachment
   if distance < len(body)*0.4:
       message, attachment = command.process()
       message = 'Я понял ваш запрос как "%s"\n\n' % key + message
   return message, attachment

In this function, we calculate the distance for the message and each of the keys. If the match is not accurate, we write how the bot recognized each of the commands that were sent to it. If the distance exceeded 40% of the length of the submitted message, we consider that the user made a mistake too much and return the default message, where we offer to turn to help.

That's all, the working (at the time of this writing) code is posted on the github .

I hope this article makes your life a little easier if you decide to create your own bot for VK.

Also popular now: