Skip to content

Latest commit

 

History

History
297 lines (211 loc) · 15.5 KB

plugin-dev-guide.md

File metadata and controls

297 lines (211 loc) · 15.5 KB

Руководство по разработке плагинов

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

Создание плагина

В простейшем случае, плагин для Ирины представляет собой одиночный файл с расширением .py. Чтобы файл был успешно распознан в качестве плагина:

  • он должен быть расположен в папке $IRENE_HOME/plugins (обычно $HOME/irene/plugins). Или в другом месте, если настройки плагина discover_plugins отличаются от стандартных.
  • его имя должно начинаться на plugin_ (опять же, можно изменить в настройках плагина discover_plugins).
  • в нём должны быть определены следующие переменные:
    # имя плагина. Латиницей, без спец. символов, желательно в snake case
    name = 'my_plugin'
    
    # версия плагина, желательно в формате семантического версионирования
    version = '1.235.432-rc42'
    См. https://semver.org/lang/ru/

Так же, желательно добавить docstring с описанием того, что делает плагин в самом начале файла:

"""
Мой замечательный плагин.

Делает кое-что очень полезное.
"""

import

...
...

name = ...
version = ...

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

from irene.plugin_loader.magic_plugin import MagicPlugin


class MyPlugin(MagicPlugin):
  """
  Мой замечательный плагин.
  
  Делает кое-что очень полезное.
  """
  # В случае класса, эти переменные не обязательны, но желательны
  name = 'my_plugin'
  version = '1.235.432-rc42'

  # В классе можно объявлять все переменные и функции, описанные далее
  # (но у функций добавляется дополнительный первый параметр self)
  ...

Если плагин состоит из нескольких файлов, то следует создать для него подпапку в папке с плагинами:

$HOME/irene/plugins/my_plugin/
$HOME/irene/plugins/my_plugin/plugin_my.py
$HOME/irene/plugins/my_plugin/helpers.py
$HOME/irene/plugins/my_plugin/...

Работа с конфигурацией

Поведение плагина может настраиваться посредством изменения конфигурации. Конфигурация каждого плагина хранится на диске в виде YAML или JSON файла и загружается загрузчиком конфигурации при запуске приложения.

Чтобы отметить, что плагину нужна своя конфигурация и задать её значения по-умолчанию нужно объявить переменную config:

config = {
  "имя_параметра": "значение по-умолчанию",
}

Конфигурация всегда является словарём со строковыми ключами.

Желательно добавить описание поддерживаемых параметров в переменной config_comment:

config_comment = """
Настройки (моего плагина).

Поддерживаются следующие параметры:
- `(имя_параметра)` - (описание параметра)

(какие-нибудь рекомендации по настройке плагина)
"""

Эта строка будет добавляться в качестве комментария к файлам конфигурации, а так же будет отображаться в графическом интерфейсе настроек.

Загрузчик плагинов будет обновлять значение переменной config при загрузке конфигурации из файла или обновлении её через графический интерфейс. Он может как присваивать новое значение переменной config, так и изменять словарь, хранящийся в этой переменной.

Если нужно предпринимать какие-то действия при изменении конфигурации, то можно определить функцию reveive_config:

def receive_config(config: dict[str, Any], *_args, **_kwargs):
  # Единственный позиционный параметр - актуальная конфигурация плагина.
  # Рекомендуется разрешать передачу дополнительных аргументов
  # для сохранения совместимости в будущем.
  ...

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

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

Добавление команд

Чтобы добавить команды голосового ассистента, нужно определить в плагине переменную define_commands:

define_commands = {
  "привет": _say_hi,
}

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

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

def define_commands(*_args, **_kwargs):
  return {
    "привет": _say_hi,
  }

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

Обработчик команды в простейшем случае - функция, принимающая на вход экземпляр API ассистента и дополнительный текст команды:

from irene.brain.abc import VAApiExt


def _say_hi(va: VAApiExt, text: str):
  # Просто отвечает пользователю текстом и/или голосом
  # См. документацию к VAApiExt чтобы узнать, что ещё можно сделать
  va.say("привет")

Для более сложных сценариев можно определить функцию-генератор:

from irene.brain.abc import VAApiExt


def _say_hi(va: VAApiExt, text: str):
  # yield задаёт вопрос и ждёт ответа от пользователя
  name = yield "Как тебя зовут?"

  va.say(f"Привет, {name}")

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

from irene.brain.abc import VAApiExt


def _say_hi_to_name(va: VAApiExt, text: str):
  va.say(f"Привет, {text}")


def _say_hi(va: VAApiExt, text: str):
  va.say("Как тебя зовут?")

  # Сигнатура функции аналогична обработчику команды, но вторым аргументом будет передан ответ пользователя полностью
  va.context_set(_say_hi_to_name)

Ключ в словаре команд может содержать несколько вариантов команды, разделённых |:

# пример из плагина plugin_random:
define_commands = {
  "подбрось|брось": {
    "монету|монетку": _play_coin,
    "кубик|кость": _play_dice,
  }
}
# Это определение эквивалентно следующему:
define_commands = {
  "подбрось монетку": _play_coin,
  "подбрось монету": _play_coin,
  "брось монетку": _play_coin,
  "брось монету": _play_coin,
  "подбрось кубик": _play_dice,
  "подбрось кость": _play_dice,
  "брось кубик": _play_dice,
  "брось кость": _play_dice,
}

Выполнение действий при запуске/остановке

Определив в плагине следующие функции можно выполнять определённые действия при запуске и завершении приложения:

def init(*_args, **_kwargs):
  # Функция будет вызвана при запуске приложения
  ...


def terminate(*_args, **_kwargs):
  # Функция будет вызвана при завершении работы приложения
  ...

Так же, можно определить функцию run, которая будет вызвана в отдельном потоке после запуска приложения:

def run(*_args, **_kwargs):
  ...

Если функция run не завершается сама по себе в течение некоторого времени, то желательно сделать так, чтобы вызов функции terminate завершал её выполнение.

Функции init, run и terminate могут быть асинхронными:

async def init(*_args, **_kwargs):
  ...

async def run(*_args, **_kwargs):
  # Асинхронная функция будет выполняться в основном event loop'е приложения вместо отдельного потока.
  # Вызов может быть отменён (см. https://docs.python.org/3/library/asyncio-task.html#task-cancellation) если процесс
  # получит сигнал прерывания.
  ...

async def terminate(*_args, **_kwargs):
  ...

Распространение плагинов

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

TODO: Добавить информацию о том, где можно рассказывать о своих плагинах

Простые однофайловые плагины

Если плагин не имеет зависимостей помимо стандартной библиотеки питона, Ирины и пакетов, от которых зависит Ирина, то плагин можно распространять в виде отдельного .py файла. Имя этого файла должно начинаться с plugin_.

Пользователь может положить такой плагин в папку $IRENE_HOME/plugins (обычно $HOME/irene/plugins) и использовать его.

Более сложные плагины

Плагины, состоящие из нескольких файлов и/или имеющие сторонние зависимости рекомендуется публиковать в виде отдельного git-репозитория. В корне такого репозитория должны находиться:

  • plugin_*.py файл плагина
  • дополнительные .py файлы если часть компонентов вынесено в отдельные файлы
  • файл requirements.txt со списком зависимостей в стандартном формате, если плагин имеет зависимости
  • желательно, файл README.md с описанием плагина и инструкциями по установке и настройке

Пользователь может склонировать такой репозиторий себе в папку $IRENE_HOME/plugins (обычно $HOME/irene/plugins), установить зависимости из requirements.txt и пользоваться плагином.

Упаковка в пакет и распространение через pypi

Если плагин достаточно качественен и стабилен, то его можно распространять в виде python-пакета.

К пакетам плагинов предъявляются следующие требования:

  • имя пакета должно начинаться на irene_plugin_
  • файлы плагинов должны располагаться в корне пакета и их имя должно начинаться на plugin_

Пользователь может просто установить такой плагин через pip.