Улучшение простого чат-бота: концепция системы команд

В этой статье я расскажу про систему команд — улучшение простого чат-бота. Суть системы команд в том, чтобы создать возможность создания команд во время работы программы не изменяя ее кода. Эта идея является логическим продолжением идей о простом чат-боте, которые я описал в предыдущей статье «Как начать мыслить о ИИ». Поэтому, чтобы лучше понимать идею этой статьи, можете прочитать предыдущую тут.

Введение в концепцию «система команд»

Имея возможность создавать новые переменные и изменять их извне, можно создавать какую-то логику программы, взаимодействуя с ней через интерфейс. Система команд заключается в том, чтобы создавать различные команды на основе базовых команд (базового функционала). Можно соединить интерфейс взаимодействия с системой команд с интеллектуальной программой, чтобы она могла управлять этими командами.

Суть в том, чтобы создать инфраструктуру для системы ИИ, в которой она сможет создавать необходимые для выполнения задач команды. То есть нужно создать весь необходимый функционал на уровне кода, чтобы интеллектуальная программа смогла самостоятельно создавать нужные команды.

Команды представляют собой последовательность других команд, их можно рассматривать как функции, которые оперируют инструкциями реализованными на уровне кода. Каждая команда является новым уровнем абстракции над уже существующими функциями. Инструкциями является код, который выполняется, когда происходит его вызов через команды.

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

Визуализация команды

Визуализация команды

Базовые исходы представляют собой выполнение небольшого кусочка кода. Команды — это последовательность этих кусочков кода. Команда может состоять из других команд.

Основные элементы программы

В основе всего лежит некая память-буффер — buf, это словарь, в котором хранятся данные потоков ввода и вывода. В стек ввода записываются введенные строки, а в стек вывода — выведенные сообщения.

buf = {"input stack": [], "output stack": []}

В программе нужно как-то хранить сообщения, которые выводит система. Для этого можно добавить словарь, который будет хранить соответствия, то есть поля типа вопрос-ответ. Эти соответствия хранятся в словаре matches.

matches = {"input msg": "\nInput: "}

Базовые функции — это обычные функции, которые вызываются без аргументов. Информацию для работы эти функции берут в специальных структурах: буффер, переменные и так далее. Базовые функции можно рассматривать как инструкции системы команд. Чтобы выполнять функции нужно использовать вызов функции по ссылке. Базовые функции хранятся в словаре funcs (от слова functions).

def my_input(): buf["input stack"].append(input())
def my_output(): print(buf["output stack"][-1])

# системные команды - базовые исходы
funcs = {
    "input": my_input,
    "output": my_output,
}

Команды пользователя хранятся в виде последовательности подкоманд в словаре commands.

commands = {"make input": ["input msg", "output", "input"]}

Чтобы выполнять команды нужно сделать специальные обработчики, которые будут обрабатывать элементарные исходы команд — инструкции или изменение переменных. Функция обработки будет вызываться рекурсивно до тех пор, пока команда не дойдет до выполнения базовых функций. Эта функция называется process_command.

def process_command(command):  # функция обработки команды
    for key in commands[command]:
        if key in funcs:  # обработка базовых исходов (инструкций)
            funcs[key]()

        elif key in matches:  # обработка соответствий
            buf["output stack"].append(matches[key])

        elif key in commands:  # обработка команд пользователя
            process_command(key)

# главный цикл
while True:
    process_command("make input")  # ввод строки, занести строку в стек ввода
    process_command(buf["input stack"][-1])  # выполнить введенную строку

Сначала происходит ввод строки, эта строка заносится в стек ввода, затем происходит обработка введенной строки (команды), потом из commands извлекается эта команда и выполняются подкоманды в этой команде. Все ссылки на функции хранятся в словаре funcs. В matches хранятся обычные вопрос-ответ, там же хранятся сообщения, которые выводит программа, например, при выполнении команды ввода программа выводит сообщение «Input:».

В главном цикле происходит вызов функции process_command в которую передается команда «make input». Эта команда выводит сообщение «Input:», считывает введенную строку и добавляет ее в стек ввода. Затем введенная команда извлекается из стека ввода и выполняется в следующей строке.

Что происходит во время выполнения команды. Сначала происходит обращение к словарю matches и занесение значения ключа «input msg» в стек вывода. Потом происходит вызов функции my_output, которая выводит последнюю запись из стека вывода. Затем вызывается функция my_input в которой происходит ввод строки и ее занесение в стек ввода.

Другие элементы программы

Чтобы сделать новое соответствие нужно создать специальный функционал:

commands = {
  "enter": ["output", "input"],
  "make input": ["input msg", "enter"],

  # сделать соответствие
  "make key": ["key msg", "enter"],
  "make value": ["value msg", "enter"],
  "make match": ["make key", "make value", "match", "match created msg", "output"],
}

Так выглядит команда для создания нового соответствия. В funcs нужно добавить еще одну функцию:

def new_match(): matches[buf["input stack"][-2]] = buf["input stack"][-1]

funcs = {
    "input": my_input,
    "output": my_output,
    "match": new_match,
}

Базовые переменные. Это словарь, в котором хранятся различные переменные, которые нужны для выполнения команд. Каждая переменная представляет собой некую ячейку памяти, в которой хранится информация для какой-то определенной деятельности программы.

variables = {
    "program run": "true",
    "create new match": "false"
}

Переменные времени. Это способ изменения обычных переменных на основе значения времени. Если наступает определенное время, то обычная переменная изменяет свое значение.

time_variables = {"time var name": "time to activate"}

# использование
for time_var_name, time_to_activate in time_variables.items():
    if time_to_activate == time.ctime(time.time()).split()[-2]:
        variables[time_var_name] = "true"

Триггерные переменные. Это переменные, которые отвечают за выполнение каких-то команд, когда эти переменные активируются.

triggers = {"create new match": "make match"}

# использование:
for key, value in triggers.items():
    if variables[key] == "true":
        process_command(value)
        variables[key] = "false"

Потом эти обработчики нужно добавить в главный цикл.

Можно делать отдельные команды такие как «make key» или добавлять их сразу в основную команду «make match». Это похоже на принцип разделения обязанностей по функциям в программировании.

При изменении или создании переменной можно добавить проверку на существование введенной переменной. Это можно реализовать через добавление специальной переменной в variables, которая будет становиться активной, когда введенная переменная существует. Если же она не активна, то можно продолжать выполнение команды.

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

Обработчики

Для каждого типа действий нужно создавать свой обработчик. Обработчики, которые я сделал: обработка базовых функций, вопрос-ответ, обработка переменных времени, обработка триггерных команд, обработка самих команд.

Обработчик команд отвечает за то, чтобы обрабатывать каждую подкоманду соответствующим образом. Он последовательно рассматривает каждую подкоманду и передает ее на выполнение в определенные обработчики.

Обработчик базовых функций просто вызывает прописанные функции. Базовые функции лежат на самом нижнем уровне иерархии команд. Выполнение любой команды сводится к вызовам базовых функций.

Обработчик вопрос-ответ выводит соответственное значение ключа в словаре соответствий.

Переменные времени нужны для того, чтобы работать с системным временем. Эти переменные хранят значение времени, в которое они должны активировать какие-то обычные переменные. Для активации обычных переменных нужно сравнить текущее значение времени и указанное, если выполняется определенное условие (если они равны), то какая-то обычная переменная активируется.

Триггерные переменные нужны для того, чтобы выполнять какие-то команды автоматически. При активации определенной переменной выполняется соответствующая ей команда.

Если добавлять проверку существования переменной, то придется создавать новую логику. Например, можно добавить специальную простую переменную, которая будет активироваться определенным образом — если в словаре есть нужный ключ. Соответственно, нужно будет создать базовую функцию, которая будет проверять наличие ключа в словаре. Если ключ есть, то переменная активируется.

Циклические команды (процессы) можно выполнять через переменные, которые будут хранить ход выполнения команды (процесса) и продолжать с того состояния, на котором остановились.

Систему заметок тоже можно сделать через переменные и триггеры. Нужно создать новую переменную и триггер. Триггер должен выполнить команду по выводу сообщения. Переменная триггера активируется тогда, когда наступит определенное время.

Когда нужно воспользоваться переменной, нужно прописать саму переменную и действие с этой переменной, например «вывод переменной привет». Вместо «вывод» может быть любое другое действие: либо имеющееся, либо сгенерированное новое. Вместо «переменная привет» тоже может быть любое действие, команда, например, что-то сгенерировать.

Через ввод можно добавлять новые команды — в эту концепцию можно добавить нейросети, другие алгоритмы МО, сделать генерацию правдоподобного текста на запрос и различные другие идеи.

Заключение

В этой статье я рассказал про концепцию того, как можно создавать команды во время работы программы не изменяя ее кода. Если вы поймете принцип этой идеи, то сможете создавать любые команды. Это похоже на создание языка программирования, только идея в том, чтобы создавать новый «код» во время работы программы.

Основные идеи, о которых я рассказал в этой статье:

  • Создавать новые команды можно через интерфейс взаимодействия с системой команд;

  • Систему ИИ можно подключить к интерфейсу системы команд, чтобы она сама создавала нужные команды;

  • Команды представляют собой последовательное выполнение элементарных исходов — небольших кусочков кода;

  • Концепцию системы команд можно обобщить на любые типы данных: аудио, видео, движения и другие.

  • Чтобы создать новый вид действий, нужно просто создать необходимые функции или обработчики;

Пример кода с дополнительными командами тут (название файла «command-system.py»).

© Habrahabr.ru