Tworzenie bota na Telegramie w języku R (część 4): Budowanie sekwencyjnego, logicznego dialogu z botem

Jeśli już zapoznaliście się z poprzednimi trzema artykułami z tej serii, to już potraficie pisać pełnoprawnych botów telegramowych z klawiaturą.

W tym artykule nauczymy się, jak napisać bota, który będzie wspierał sekwencyjny dialog. Tzn. bot będzie zadawał wam pytania i czekał na wprowadzenie jakiejkolwiek informacji. W zależności od wprowadzonych przez was danych, bot będzie wykonywał różne działania.

Nauczymy się również wykorzystywać do działania bota bazę danych, w naszym przykładzie będzie to SQLite, ale możecie użyć dowolnego innego systemu zarządzania bazami danych. Bardziej szczegółowo o interakcji z bazami danych pisaliśmy w w tym artykule.

Tworzenie bota na Telegramie w języku R (część 4): Budowanie sekwencyjnego, logicznego dialogu z botem

Wszystkie artykuły z serii „Tworzymy bota Telegram w języku R”

  1. Tworzymy bota i wysyłamy z jego pomocą wiadomości w Telegramie
  2. Dodajemy wsparcie dla komend i filtry wiadomości
  3. Jak dodać wsparcie dla klawiatury do bota
  4. Budowanie sekwencyjnego, logicznego dialogu z botem

Spis treści

Jeśli interesujesz się analizą danych, to mogą Cię zainteresować moje telegram i youtube kanały. Większość zawartości poświęcona jest językowi R.

  1. Wprowadzenie
  2. Proces budowy bota
  3. Struktura projektu bota
  4. Konfiguracja bota
  5. Tworzymy zmienną środowiskową
  6. Tworzymy bazę danych
  7. Pisanie funkcji do pracy z bazą danych
  8. Metody bota
  9. Filtry wiadomości
  10. Obsługiwacze
  11. Kod uruchamiający bota
  12. Podsumowanie

Wprowadzenie

Aby bot mógł prosić was o dane i czekać na wprowadzenie jakichkolwiek informacji, będziecie musieli zarejestrować bieżący stan dialogu. Najlepszym sposobem, by to osiągnąć, jest użycie jakiejś wbudowanej bazy danych, na przykład SQLite.

Tzn. logika będzie wyglądać następująco. Wywołujemy metodę bota, a bot kolejno prosi nas o jakąś informację, przy tym na każdym kroku czeka na wprowadzenie tych informacji i może dokonywać ich sprawdzenia.

Napiszemy maksymalnie prostego bota, na początku zapyta on o twoje imię, później o wiek, a zdobyte dane zapisze w bazie danych. Przy zapytaniu o wiek sprawdzi, czy wprowadzone dane są liczbą, a nie tekstem.

Taki prosty dialog będzie miał tylko trzy stany:

  1. start — normalny stan bota, w którym nie czeka na od was żadnych informacji
  2. wait_name — stan, w którym bot oczekuje na wprowadzenie imienia
  3. wait_age — stan, w którym bot oczekuje na wprowadzenie waszego wieku, liczby pełnych lat.

Proces budowy bota

W trakcie artykułu krok po kroku stworzymy bota, cały proces można schematycznie przedstawić w następujący sposób:
Tworzenie bota na Telegramie w języku R (część 4): Budowanie sekwencyjnego, logicznego dialogu z botem

  1. Tworzymy konfigurację bota, w której będziemy przechowywać pewne ustawienia. W naszym przypadku token bota oraz ścieżkę do pliku bazy danych.
  2. Tworzymy zmienną środowiskową, w której będzie przechowywana ścieżka do projektu z botem.
  3. Tworzymy bazę danych oraz szereg funkcji, dzięki którym bot będzie mógł z nią współpracować.
  4. Pisanie metod bota, czyli funkcji, które będzie wykonywał.
  5. Dodajemy filtry wiadomości, dzięki którym bot będzie mógł odwoływać się do odpowiednich metod, w zależności od aktualnego stanu czatu.
  6. Dodajemy obsługiwacze, które połączą polecenia i wiadomości z odpowiednimi metodami bota.
  7. Uruchamiamy bota.

Struktura projektu bota

Dla wygody podzielimy kod naszego bota oraz inne związane z nim pliki na następującą strukturę.

  • bot.R — główny kod naszego bota
  • db_bot_function.R — blok kodu z funkcjami do pracy z bazą danych
  • bot_methods.R — kod metod bota
  • message_filters.R — filtry wiadomości
  • handlers.R — obsługiwacze
  • config.cfg — konfiguracja bota
  • create_db_data.sql — skrypt SQL do tworzenia tabeli z danymi czatu w bazie danych
  • create_db_state.sql — skrypt SQL do tworzenia tabeli aktualnego stanu czatu w bazie danych
  • bot.db — baza danych bota

Cały projekt bota można zobaczyć, lub pobierz z mojego repozytorium na GitHubie.

Konfiguracja bota

Jako konfigurację użyjemy standardowego pliku ini, w następującym formacie:

[bot_settings]
bot_token=TWÓJ_TOKEN_BOTA

[db_settings]
db_path=C:/ŚCIEŻKA/DO/FOLDERU/PROJEKTU/bot.db

W konfiguracji zapisujemy token bota oraz ścieżkę do bazy danych, tzn. do pliku bot.db, którego stworzymy w następnej chwili.

Dla bardziej złożonych botów można tworzyć również bardziej skomplikowane konfiguracje, ponadto nie ma obowiązku używania formatu ini, można używać dowolnego formatu, w tym JSON.

Tworzymy zmienną środowiskową

Na każdym komputerze folder z projektem bota może znajdować się w różnych lokalizacjach i na różnych dyskach, dlatego w kodzie ścieżka do folderu projektu zostanie określona przez zmienną środowiskową TG_BOT_PATH.

Zmienną środowiskową można utworzyć na kilka sposobów, najprostszym jest zapisanie jej w pliku .Renviron.

Można utworzyć lub edytować ten plik za pomocą polecenia file.edit(path.expand(file.path("~", ".Renviron"))). Wykonaj je i dodaj w pliku jeden wiersz:

TG_BOT_PATH=C:/ŚCIEŻKA/DO/TWOJEGO/PROJEKTU

Następnie zapisz plik .Renviron i zrestartuj RStudio.

Tworzymy bazę danych

Następny krok to stworzenie bazy danych. Potrzebujemy 2 tabel:

  • chat_data — dane, które bot uzyskał od użytkownika
  • chat_state — aktualny stan wszystkich czatów

Te tabele można stworzyć za pomocą następującego zapytania SQL:

CREATE TABLE chat_data (
    chat_id BIGINT  PRIMARY KEY
                    UNIQUE,
    name    TEXT,
    age     INTEGER
);

CREATE TABLE chat_state (
    chat_id BIGINT PRIMARY KEY
                   UNIQUE,
    state   TEXT
);

Jeśli pobrałeś projekt bota z GitHub, aby stworzyć bazę danych, możesz skorzystać z poniższego kodu w języku R.

# Скрипт создания базы данных
library(DBI)     # интерфейс для работы с СУБД
library(configr) # чтение конфига
library(readr)   # чтение текстовых SQL файлов
library(RSQLite) # драйвер для подключения к SQLite

# директория проекта
setwd(Sys.getenv('TG_BOT_PATH'))

# чтение конфига
cfg <- read.config('config.cfg')

# подключение к SQLite
con <- dbConnect(SQLite(), cfg$db_settings$db_path)

# Создание таблиц в базе
dbExecute(con, statement = read_file('create_db_data.sql'))
dbExecute(con, statement = read_file('create_db_state.sql'))

Pisanie funkcji do pracy z bazą danych

Mamy już gotowy plik konfiguracyjny i utworzoną bazę danych. Teraz musimy napisać funkcje do odczytu i zapisu danych do tej bazy.

Jeśli pobrałeś projekt z GitHub, to funkcje możesz znaleźć w pliku db_bot_function.R.

Kod funkcji do pracy z bazą danych

# ###########################################################
# Function for work bot with database

# получить текущее состояние чата
get_state <- function(chat_id) {

  con <- dbConnect(SQLite(), cfg$db_settings$db_path)

  chat_state <- dbGetQuery(con, str_interp("SELECT state FROM chat_state WHERE chat_id == ${chat_id}"))$state

  return(unlist(chat_state))

  dbDisconnect(con)
}

# установить текущее состояние чата
set_state <- function(chat_id, state) {

  con <- dbConnect(SQLite(), cfg$db_settings$db_path)

  # upsert состояние чата
  dbExecute(con, 
            str_interp("
            INSERT INTO chat_state (chat_id, state)
                VALUES(${chat_id}, '${state}') 
                ON CONFLICT(chat_id) 
                DO UPDATE SET state='${state}';
            ")
  )

  dbDisconnect(con)

}

# запись полученных данных в базу
set_chat_data <- function(chat_id, field, value) {

  con <- dbConnect(SQLite(), cfg$db_settings$db_path)

  # upsert состояние чата
  dbExecute(con, 
            str_interp("
            INSERT INTO chat_data (chat_id, ${field})
                VALUES(${chat_id}, '${value}') 
                ON CONFLICT(chat_id) 
                DO UPDATE SET ${field}='${value}';
            ")
  )

  dbDisconnect(con)

}

# read chat data
get_chat_data <- function(chat_id, field) {

  con <- dbConnect(SQLite(), cfg$db_settings$db_path)

  # upsert состояние чата
  data <- dbGetQuery(con, 
                     str_interp("
            SELECT ${field}
            FROM chat_data
            WHERE chat_id = ${chat_id};
            ")
  )

  dbDisconnect(con)

  return(data[[field]])

}

Utworzyliśmy 4 proste funkcje:

  • get_state() — uzyskać aktualny stan czatu z bazy danych
  • set_state() — zapisać aktualny stan czatu w bazie danych
  • get_chat_data() — uzyskać dane przesłane przez użytkownika
  • set_chat_data() — zapisać dane otrzymane od użytkownika

Wszystkie funkcje są dość proste, albo odczytują dane z bazy za pomocą komendy dbGetQuery(), albo wykonują UPSERT operację (zmiana istniejących danych lub zapis nowych danych w bazie), za pomocą funkcji dbExecute().

Składnia operacji UPSERT wygląda następująco:

INSERT INTO chat_data (chat_id, ${field})
VALUES(${chat_id}, '${value}') 
ON CONFLICT(chat_id) 
DO UPDATE SET ${field}='${value}';

Tzn. w naszych tabelach pole chat_id ma ograniczenie unikalności i jest kluczem podstawowym tabel. Początkowo próbujemy dodać informacje do tabeli, a w przypadku błędu, gdy dane dla aktualnego czatu już istnieją, po prostu aktualizujemy informacje dla danego czatu.

Następnie te funkcje będziemy używać w metodach i filtrach bota.

Metody bota

Następnym krokiem w budowie naszego bota będzie stworzenie metod. Jeśli pobrałeś projekt z GitHub, to wszystkie metody znajdują się w pliku bot_methods.R.

Kod metod bota

# ###########################################################
# bot methods

# start dialog
start <- function(bot, update) {

  # 

  # Send query
  bot$sendMessage(update$message$chat_id, 
                  text = "Введи своё имя")

  # переключаем состояние диалога в режим ожидания ввода имени
  set_state(chat_id = update$message$chat_id, state = 'wait_name')

}

# get current chat state
state <- function(bot, update) {

  chat_state <- get_state(update$message$chat_id)

  # Send state
  bot$sendMessage(update$message$chat_id, 
                  text = unlist(chat_state))

}

# reset dialog state
reset <- function(bot, update) {

  set_state(chat_id = update$message$chat_id, state = 'start')

}

# enter username
enter_name <- function(bot, update) {

  uname <- update$message$text

  # Send message with name
  bot$sendMessage(update$message$chat_id, 
                  text = paste0(uname, ", приятно познакомится, я бот!"))

  # Записываем имя в глобальную переменную
  #username <<- uname
  set_chat_data(update$message$chat_id, 'name', uname) 

  # Справшиваем возраст
  bot$sendMessage(update$message$chat_id, 
                  text = "Сколько тебе лет?")

  # Меняем состояние на ожидание ввода имени
  set_state(chat_id = update$message$chat_id, state = 'wait_age')

}

# enter user age
enter_age <- function(bot, update) {

  uage <- as.numeric(update$message$text)

  # проверяем было введено число или нет
  if ( is.na(uage) ) {

    # если введено не число то переспрашиваем возраст
    bot$sendMessage(update$message$chat_id, 
                    text = "Ты ввёл некорректные данные, введи число")

  } else {

    # если введено число сообщаем что возраст принят
    bot$sendMessage(update$message$chat_id, 
                    text = "ОК, возраст принят")

    # записываем глобальную переменную с возрастом
    #userage <<- uage
    set_chat_data(update$message$chat_id, 'age', uage) 

    # сообщаем какие данные были собраны
    username <- get_chat_data(update$message$chat_id, 'name')
    userage  <- get_chat_data(update$message$chat_id, 'age')

    bot$sendMessage(update$message$chat_id, 
                    text = paste0("Тебя зовут ", username, " и тебе ", userage, " лет. Будем знакомы"))

    # возвращаем диалог в исходное состояние
    set_state(chat_id = update$message$chat_id, state = 'start')
  }

}

Utworzyliśmy 5 metod:

  • start — Rozpoczęcie dialogu
  • state — Uzyskanie aktualnego stanu czatu
  • reset — Resetowanie aktualnego stanu czatu
  • enter_name — Bot pyta o twoje imię
  • enter_age — Bot pyta o twój wiek

Metoda start pyta o twoje imię i zmienia stan czatu na wait_name, tzn. w tryb oczekiwania na wprowadzenie twojego imienia.

Następnie wysyłasz imię, a ono jest przetwarzane przez metodę enter_name, bot cię wita, zapisuje otrzymane imię w bazie i zmienia czat na stan wait_age.

Na tym etapie bot czeka na wprowadzenie twojego wieku. Wysyłasz swój wiek, bot sprawdza wiadomość, a jeśli zamiast liczby wysłałeś jakiś tekst, powie: Wprowadziłeś niepoprawne dane, wprowadź liczbę., i będzie oczekiwał od ciebie ponownego wprowadzenia danych. Jeśli wysłałeś liczbę, bot poinformuje cię, że przyjął twój wiek, zapisze otrzymane dane w bazie, przekaże wszystkie uzyskane od ciebie informacje i przywróci stan czatu do pierwotnego, tj. do start.

Wywołując metodę stan możesz w każdej chwili zapytać o bieżący stan czatu, a metodą reset przywrócić czat do pierwotnego stanu.

Filtry wiadomości

W naszym przypadku to jedna z najważniejszych części budowy bota. To właśnie dzięki filtrom wiadomości bot będzie rozumiał, jaką informację od ciebie oczekuje, i jak ją przetworzyć.

W projekcie na GitHub filtrowanie zostało zapisane w pliku message_filters.R.

Kod filtrów wiadomości:

# ###########################################################
# message state filters

# фильтр сообщений в состоянии ожидания имени
MessageFilters$wait_name <- BaseFilter(function(message) {
  get_state( message$chat_id )  == "wait_name"
}
)

# фильтр сообщений в состоянии ожидания возраста
MessageFilters$wait_age <- BaseFilter(function(message) {
  get_state( message$chat_id )   == "wait_age"
}
)

W filtrach wykorzystujemy wcześniej napisaną funkcję get_state(), aby zapytać o bieżący stan czatu. Ta funkcja wymaga tylko 1 argumentu, id czatu.

Następnie filtr wait_name przetwarza wiadomości, gdy czat znajduje się w stanie wait_name, a odpowiednio filtr wait_age przetwarza wiadomości, gdy czat znajduje się w stanie wait_age.

Obsługiwacze

Plik z obsługującymi go funkcjami nosi nazwę handlers.R, i zawiera następujący kod:

# ###########################################################
# handlers

# command handlers
start_h <- CommandHandler('start', start)
state_h <- CommandHandler('state', state)
reset_h <- CommandHandler('reset', reset)

# message handlers
## !MessageFilters$command - означает что команды данные обработчики не обрабатывают, 
## только текстовые сообщения
wait_age_h  <- MessageHandler(enter_age,  MessageFilters$wait_age  & !MessageFilters$command)
wait_name_h <- MessageHandler(enter_name, MessageFilters$wait_name & !MessageFilters$command)

Najpierw tworzymy obsługujące komendy, które pozwolą ci uruchamiać metody do rozpoczęcia dialogu, jego resetowania i zapytania o bieżący stan.

Następnie tworzymy 2 obsługujące wiadomości przy użyciu wcześniej stworzonych filtrów i dodajemy do nich filtr !MessageFilters$command, abyśmy w dowolnym stanie czatu mogli używać komend.

Kod uruchamiający bota

Teraz wszystko jest gotowe do uruchomienia, główny kod uruchamiający bota znajduje się w pliku bot.R.

library(telegram.bot)
library(tidyverse)
library(RSQLite)
library(DBI)
library(configr)

# przechodzimy do folderu projektu
setwd(Sys.getenv('TG_BOT_PATH'))

# czytamy konfigurację
cfg <- read.config('config.cfg')

# tworzymy instancję bota
updater <- Updater(cfg$bot_settings$bot_token)

# Ładowanie komponentów bota
source('db_bot_function.R') # funkcje do pracy z bazą danych
source('bot_methods.R')     # metody bota
source('message_filters.R') # filtry wiadomości
source('handlers.R') # obsługujące wiadomości

# Dodajemy obsługujące do menedżera
updater <- updater +
  start_h +
  wait_age_h +
  wait_name_h +
  state_h +
  reset_h

# Uruchamiamy bota
updater$start_polling()

W rezultacie otrzymaliśmy takiego bota:
Tworzenie bota na Telegramie w języku R (część 4): Budowanie sekwencyjnego, logicznego dialogu z botem

W każdej chwili za pomocą komendy /state możemy zapytać o bieżący stan czatu, a za pomocą komendy /reset przywrócić czat do pierwotnego stanu i rozpocząć dialog od nowa.

Podsumowanie

W tym artykule zrozumieliśmy, jak używać baz danych w botach, oraz jak budować sekwencyjne logiczne dialogi poprzez rejestrowanie stanu czatu.

W tym przypadku przyjrzeliśmy się najbardziej podstawowemu przykładzie, aby łatwiej zrozumieć ideę budowania takich botów; w praktyce można tworzyć znacznie bardziej złożone dialogi.

W następnym artykule z tej serii nauczymy się ograniczać użytkownikom bota dostęp do różnych jego metod.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster