Skip to content

Руководство по созданию сценариев

📖 Полное руководство по созданию и настройке сценариев для Telegram-ботов с поддержкой плейсхолдеров, переходов и динамической логики.

📑 Содержание


📋 Структура сценария

Организация сценариев

Организация файлов — сценарии можно организовывать в папки и подпапки для удобства. Все YAML-файлы из папки scenarios/ (включая подпапки) автоматически загружаются.

Важные принципы:

  • Уникальность названий: Название сценария должно быть уникальным в рамках всего тенанта
  • Гибкая структура файлов: Можно создавать файлы с любыми названиями и организовывать их в подпапки — все файлы будут загружены
  • Рекурсивная загрузка: Все YAML-файлы из scenarios/ и всех подпапок автоматически парсятся
  • Глобальные переходы: Сценарии могут переходить к любым другим сценариям тенанта через jump_to_scenario

Пример структуры:

tenant_101/
  scenarios/
    commands.yaml           # Сценарии: start, menu, help
    settings.yaml           # Сценарии: settings, profile
    admin/                  # Подпапка для организации
      panel.yaml            # Сценарии: admin_panel, logs
      moderation.yaml       # Сценарии: ban, kick
    users/                  # Еще одна подпапка
      profile.yaml          # Сценарии: user_profile, edit_profile

В этом примере:

  • Все файлы из scenarios/ и подпапок загружаются автоматически
  • Сценарии могут переходить друг к другу независимо от расположения в папках
  • Подпапки используются только для логической организации файлов

Структура YAML файла:

yaml
# Название сценария
scenario_name:
  description: "Описание сценария (опционально)"

  # Условия запуска сценария (опционально для scheduled сценариев)
  trigger:
    - event_type: "message"
      event_text: "/start"
    - event_type: "callback"
      callback_data: "start"

  # Расписание запуска (опционально, cron выражение)
  schedule: "0 9 * * *"  # Каждый день в 9:00

  # Действия сценария
  step:
    - action: "send_message"
      params:
        text: "Привет!"
        inline:
          - [{"Кнопка": "callback_data"}]

Параметры сценария:

  • description — описание сценария (опционально)
  • trigger — условия запуска сценария (опционально для scheduled сценариев)
  • schedule — cron выражение для запуска по расписанию (опционально)
  • step — последовательность действий

Практический пример:

yaml
user_registration:
  description: "Процесс регистрации пользователя с проверками и уведомлениями"
  
  # ТРИГГЕРЫ: Каждый работает независимо - любой из них запустит сценарий
  trigger:
    - event_type: "message"      # Команда /register
      event_text: "/register"
    - event_type: "callback"     # Кнопка "Начать регистрацию"  
      callback_data: "start_registration"

  # ШАГИ: Выполняются последовательно один за другим
  step:
    # Шаг 1: Отправляем приветствие
    - action: "send_message"
      params:
        text: |
          👋 Добро пожаловать, {username|fallback:Гость}!
          
          Давайте зарегистрируем вас в системе.
        inline:
          - [{"✅ Продолжить": "continue_registration"}]

    # Шаг 2: Проверяем права пользователя с переходами
    - action: "validate"
      params:
        condition: "$user_role == 'admin'"
      transition:
        - action_result: "success"
          transition_action: "jump_to_scenario"
          transition_value: "admin_welcome"        # Переход к админскому приветствию
        - action_result: "error"
          transition_action: "continue"            # Обычный поток для пользователей
    
    # Шаг 3: Запрашиваем имя
    - action: "send_message"
      params:
        text: |
          📝 Шаг 2 из 3
          
          Пожалуйста, введите ваше имя:
        inline:
          - [{"❌ Отмена": "cancel_registration"}]
    
    # Шаг 4: Отправляем подтверждение с вложениями
    - action: "send_message"
      params:
        text: |
          ✅ Регистрация начата!
          
          Мы отправили вам инструкции на email.
          📎 Также прикрепляем полезные материалы:
        attachment:
          - file_id: "AgACAgIAAxkBAAIUJWjyLorr_d8Jwof3V_gVjVNdGHyXAAJ--zEb4vGQS-a9UiGzpkTNAQADAgADeQADNgW"
            type: "photo"
          - file_id: "AgACAgIAAxkBAAIUQWjyOH65wQYR4onkrrqM4J65yUD7AAM8-zEb4vGQS1mwvckFcZHlAQADAgADdwADNgQ"
            type: "photo"
        inline:
          - [{"🏠 Главное меню": "main_menu"}]
    
    # Шаг 5: Удаляем предыдущее сообщение
    - action: "delete_message"
      params:
        delete_message_id: "{last_message_id}"  # Используем ID предыдущего сообщения

admin_welcome:
  description: "Приветствие для админов (транзишн-сценарий)"
  # НЕТ ТРИГГЕРОВ - вызывается только программно
  
  step:
    - action: "send_message"
      params:
        text: |
          🔧 Добро пожаловать, администратор!
          
          У вас есть дополнительные права.
        inline:
          - [{"⚙️ Панель управления": "admin_panel"}]

Ключевые моменты:

  • Триггеры — логика ИЛИ: если сработал хотя бы один, сценарий запускается
  • Шаги — выполняются строго по порядку записи
  • Переходы — управляют потоком выполнения через transition:
    • continue — продолжить выполнение следующих действий
    • break — прервать выполнение только текущего сценария
    • abort — прервать всю цепочку выполнения текущего сценария (включая вложенные)
    • stop — прервать всю обработку события (все сценарии)
    • move_steps — переместиться на указанное количество шагов (положительное = вперед, отрицательное = назад)
    • jump_to_step — перейти к конкретному шагу по индексу (шаги нумеруются с 0)
    • jump_to_scenario — перейти к другому сценарию (к любому сценарию тенанта)
  • Транзишн-сценарии — сценарии без триггеров, вызываются программно из других сценариев
  • Scheduled сценарии — сценарии с полем schedule (cron выражение), запускаются автоматически по расписанию
  • Гибридные сценарии — могут иметь и trigger, и schedule одновременно (работают по событиям И по расписанию)
  • Данные события — доступны в параметрах действий (user_id, chat_id, timestamp и др.)
  • Вложения — можно пересылать файлы через attachment[0].file_id и attachment[0].type

🎯 Триггеры (trigger)

Назначение: Условия, при которых запускается сценарий.

Механика работы триггеров:

Простая форма (атрибуты):

yaml
trigger:
  - event_type: "message"
    event_text: "/start"

Преобразуется в условие:

python
$event_type == "message" and $event_text == "/start"

Сложная формаcondition):

yaml
trigger:
  - event_type: "message"
    condition: |
      $event_text == "test"
      and $user_id > 100
      or $username in ('@admin', '@moderator')

Преобразуется в условие:

python
$event_type == "message" and ($event_text == "test" and $user_id > 100 or $username in ('@admin', '@moderator'))

Логика триггеров:

  • Между триггерами — логика ИЛИ (OR): если сработал хотя бы один, сценарий запускается
  • Внутри триггера — логика И (AND): все условия должны выполняться
  • В condition — полная свобода: and, or, not, скобки, операторы

⚠️ Важно: Плейсхолдеры в условиях condition

Проблема: При использовании плейсхолдеров в condition триггеров для сравнения строк нужно оборачивать плейсхолдер в кавычки. Иначе плейсхолдер будет "разворачиваться" как атрибут для поиска данных в condition_parser, а не как строковое значение.

Решение: Всегда оборачивайте плейсхолдеры в кавычки при сравнении строк в условиях.

Примеры:

yaml
# ❌ ОШИБКА - плейсхолдер развернется в random_user без кавычек
# condition_parser попытается найти переменную random_user в данных
trigger:
  - event_type: "message"
    condition: "{_cache.detected_intent} == 'random_user'"
    # После обработки плейсхолдера: random_user == 'random_user'
    # condition_parser ищет переменную random_user в данных, а не сравнивает строки!

# ✅ ПРАВИЛЬНО - плейсхолдер обернут в кавычки
trigger:
  - event_type: "message"
    condition: "'{_cache.detected_intent}' == 'random_user'"
    # После обработки плейсхолдера: 'random_user' == 'random_user'
    # condition_parser корректно сравнивает строки

# ✅ ПРАВИЛЬНО - сравнение чисел (кавычки не нужны)
trigger:
  - event_type: "message"
    condition: "{user_id} > 100"
    # После обработки плейсхолдера: 12345 > 100
    # Числовое сравнение работает корректно

# ✅ ПРАВИЛЬНО - сравнение с модификаторами (кавычки не нужны для boolean)
trigger:
  - event_type: "message"
    condition: "{response_value.feedback|exists} == True"
    # Модификатор exists возвращает boolean, сравнение работает корректно

Правило:

  • Для сравнения строк — всегда оборачивайте плейсхолдер в кавычки: '{_cache.field}' == 'value'
  • Для сравнения чисел — кавычки не нужны: {user_id} > 100
  • Для boolean значений — кавычки не нужны: {field|exists} == True

Доступные поля для условий:

⚠️ Важно: Все поля в условиях должны иметь маркер $name (например, $event_type, $user_id, $event_text).

Данные события:

  • $event_text — текст сообщения
  • $event_type — тип события (message, callback)
  • $user_id — ID пользователя
  • $username — имя пользователя
  • $chat_id — ID чата
  • $callback_data — данные callback кнопки
  • $event_date — время события
  • $tenant_id — ID тенанта
  • $message_id — ID сообщения
  • $chat_type — тип чата
  • $is_group — является ли чат группой
  • И другие поля события (см. EVENT_GUIDE.md)

Данные предыдущих действий:

  • $last_message_id — ID последнего отправленного сообщения
  • $response_data — любые данные, возвращенные предыдущими действиями
  • Другие поля — в зависимости от действий в сценарии

Вложенные поля:

  • $message.text — доступ к вложенным полям через точку
  • $user.profile.name — многоуровневая вложенность

Операторы в условиях:

  • == — равно
  • != — не равно
  • >, <, >=, <= — сравнение чисел
  • in, not in — вхождение в список
  • ~ — содержит подстроку
  • !~ — не содержит подстроку
  • regex — регулярное выражение
  • is_null — поле пустое (None или пустая строка)
  • not is_null — поле не пустое (существует и имеет значение)

⏰ Scheduled сценарии (запуск по расписанию)

Назначение: Автоматический запуск сценариев по расписанию через cron выражения.

Структура scheduled сценария:

yaml
daily_report:
  description: "Ежедневный отчет в 9:00"
  
  # Cron выражение для запуска каждый день в 9:00
  schedule: "0 9 * * *"
  
  # Триггеры опциональны - сценарий будет работать по расписанию
  # Можно добавить триггеры для гибридного режима (по событиям И по расписанию)
  
  step:
    - action: "send_message"
      params:
        target_chat_id: 123456789
        text: |
          📊 Ежедневный отчет
          
          Время запуска: {scheduled_at|format:datetime}
          Сценарий: {scheduled_scenario_name}

Cron выражения:

Формат: минута час день месяц день_недели

ПримерОписание
* * * * *Каждую минуту
*/5 * * * *Каждые 5 минут
0 * * * *Каждый час в начале часа
0 9 * * *Каждый день в 9:00
0 9 * * 1-5Каждый рабочий день в 9:00
0 0 1 * *Первое число каждого месяца в 00:00

ℹ️ Важно: Cron выражения интерпретируются в локальном времени платформы.

Доступные поля в scheduled сценариях:

В scheduled сценариях доступны специальные поля события. Подробное описание всех полей см. в разделе Поля scheduled сценариев в EVENT_GUIDE.md.

Пример использования:

yaml
step:
  - action: "send_message"
    params:
      text: |
        ⏰ Scheduled сценарий запущен
        
        Время: {scheduled_at|format:datetime}
        Название: {scheduled_scenario_name}
        ID: {scheduled_scenario_id}

Гибридный режим (trigger + schedule):

Сценарий может работать и по событиям, и по расписанию одновременно:

yaml
hybrid_scenario:
  description: "Работает по событиям И по расписанию"
  
  # Запуск по событиям
  trigger:
    - event_type: "message"
      event_text: "/report"
  
  # Запуск по расписанию (каждый день в 9:00)
  schedule: "0 9 * * *"
  
  step:
    - action: "send_message"
      params:
        text: |
          📊 Отчет
          
          {scheduled_at|fallback:Запущено вручную|format:datetime}

⚠️ Важные ограничения и рекомендации:

1. Синхронизация сценариев:

  • При синхронизации сценариев (удаление и пересоздание) теряется информация о последнем запуске (last_scheduled_run)
  • После синхронизации scheduled сценарии пересчитывают следующее время запуска от текущего момента
  • Рекомендация: Синхронизируйте сценарии в периоды низкой активности scheduled задач

2. Пропуск запуска:

  • Если синхронизация происходит в момент, когда должен запускаться scheduled сценарий, запуск может быть пропущен
  • Рекомендация: Избегайте синхронизации сценариев за несколько секунд до запланированного времени запуска

3. Поведение при ошибках:

  • Scheduled сценарии обновляют время следующего запуска даже при ошибке выполнения
  • Это гарантирует предсказуемое поведение и предотвращает повторные запуски проблемных сценариев

4. Контекст выполнения:

  • Scheduled сценарии выполняются с системным контекстом (автоматически получают bot_id, tenant_id)
  • Общие поля события недоступны: В scheduled сценариях большинство общих полей (user_id, chat_id, event_text, message_id и др.) недоступны, так как событие не связано с пользователем или чатом
  • Передача параметров: Необходимо передавать большинство параметров вручную в действиях или извлекать данные из storage тенанта или пользователя (из базы данных)
  • Используйте target_chat_id в параметрах действий для указания конкретного чата

⚡ Действия (step)

Назначение: Последовательность действий, выполняемых в сценарии.

Важно:

  • Порядок выполнения шагов определяется их позицией в массиве step (сверху вниз). Шаги выполняются последовательно в том порядке, в котором они указаны в YAML. Нумерация шагов начинается с 0.
  • Последовательное выполнение: По умолчанию сценарий идет последовательно по шагам независимо от результата действия. Если у шага нет transition или нет подходящего перехода для результата действия, выполнение автоматически переходит к следующему шагу.
  • Изменение потока: Переходы (transition) позволяют изменить последовательность выполнения (перейти к другому шагу, сценарию, прервать выполнение и т.д.).

Отправка сообщения:

Inline кнопки:

yaml
step:
  - action: "send_message"
    params:
      text: |
        👋 Привет!
        
        📋 Выберите действие:
      inline:
        - [{"📋 Меню": "main_menu"}, {"📚 Справка": "help"}]
        - [{"🌐 Сайт": "https://example.com"}, {"📞 Поддержка": "tg://resolve?domain=support"}]
        - [{"🔙 Назад": "start"}]

Reply клавиатура:

yaml
step:
  - action: "send_message"
    params:
      text: |
        👋 Привет!
        
        📋 Выберите действие:
      reply:
        - ["📋 Меню", "📚 Справка"]
        - ["🔙 Назад"]

Убрать клавиатуру:

yaml
step:
  - action: "send_message"
    params:
      text: "Клавиатура убрана"
      reply: []  # Пустой массив убирает клавиатуру

Параметры сообщения:

  • text — текст сообщения (поддерживает многострочность с |)
  • inline — массив кнопок:
    • [{"Текст кнопки": "callback_data"}] — одна кнопка в строке
    • [{"Кнопка 1": "data1"}, {"Кнопка 2": "data2"}] — несколько кнопок в строке
    • [{"Ссылка": "https://example.com"}] — кнопка со ссылкой
  • reply — reply клавиатура:
    • [["Кнопка 1", "Кнопка 2"]] — одна строка кнопок
    • [["Кнопка 1"], ["Кнопка 2"]] — каждая кнопка в отдельной строке
    • [] — убрать клавиатуру
  • attachment — массив вложений (фото, документы, видео, аудио):
    • [{"file_id": "AgACAgI...", "type": "photo"}] — пересылка файла по file_id
    • [{"file_id": "{event_attachment[0].file_id}", "type": "{event_attachment[0].type}"}] — динамическая пересылка из события
    • [{...}, {...}] — несколько файлов за раз

Прикрепление файлов:

📎 Прикрепление файлов к сообщениям:

Можно отправлять фото, документы, видео, аудио. Файлы можно брать из событий или использовать сохранённые file_id.

Пример: Пересылка файла из события:

yaml
step:
  - action: "send_message"
    params:
      text: "Пересылаю ваш файл обратно"
      attachment:
        - file_id: "{event_attachment[0].file_id}"
          type: "{event_attachment[0].type}"

Пример: Отправка файла по известному file_id:

yaml
step:
  - action: "send_message"
    params:
      text: "Инструкция"
      attachment:
        - file_id: "AgACAgIAAxkBAAIUJWjyLorr_d8Jwof3V_gVjVNdGHyXAAJ--zEb4vGQS-a9UiGzpkTNAQADAgADeQADNgW"
          type: "photo"

Пример: Несколько файлов:

yaml
step:
  - action: "send_message"
    params:
      text: "Полезные материалы"
      attachment:
        - file_id: "{event_attachment[0].file_id}"
          type: "{event_attachment[0].type}"
        - file_id: "BQACAgIAAxkBAAI..."
          type: "document"

📋 Как получить file_id и type: см. пример сценария ниже 👇

Конфигурация тенанта в действиях:

Назначение: Автоматическое использование атрибутов конфигурации тенанта в действиях.

Некоторые действия автоматически используют параметры из конфигурации тенанта (_config). Такие параметры не нужно передавать явно в params — они автоматически берутся из _config.

Пример использования:

yaml
step:
  - action: "completion"
    params:
      prompt: "Привет, как дела?"
      # openrouter_token автоматически берется из _config.openrouter_token
      # Не нужно указывать openrouter_token явно!

Доступ через плейсхолдеры:

Конфигурация тенанта также доступна напрямую через плейсхолдеры:

yaml
step:
  - action: "send_message"
    params:
      text: |
        Конфигурация тенанта:
        Токен: {_config.openrouter_token|fallback:Не установлен}

Важно:

  • _config доступен во всех сценариях тенанта автоматически
  • Если атрибут не установлен в конфигурации тенанта, значение будет None
  • Используйте fallback в плейсхолдерах для обработки отсутствующих значений
  • Если параметр передан явно в params, явное значение имеет приоритет

📖 Подробнее о конфигурации тенанта: См. раздел config.yaml в TENANT_CONFIG_GUIDE.md

Кэширование данных в сценариях:

Назначение: Автоматическое сохранение результатов действий в _cache для использования в последующих шагах сценария.

⚠️ Важно: Область видимости кэша:

  • Кэш _cache сохраняется только в рамках одной цепочки выполнения сценариев (одна цепочка обработки одного события)
  • Данные доступны между сценариями в рамках одной цепочки выполнения (например, при переходе через jump_to_scenario или execute_scenario)
  • Кэш НЕ сохраняется между разными цепочками выполнения (даже если они триггерятся одним событием — каждый сценарий имеет свой изолированный кэш)
  • Кэш НЕ сохраняется между разными событиями (разными сообщениями, разными callback'ами)
  • Для долгосрочного хранения данных используйте set_storage / set_user_storage вместо set_cache
  • Подробнее см. раздел Сохранение контекста в рамках обработки события

Общая механика работы:

  • Каждое действие может вернуть response_data с любыми полями
  • Данные автоматически сохраняются в _cache и доступны в последующих действиях через плейсхолдеры
  • Система кэширования поддерживает два системных поля для управления структурой данных:
    • _namespace — для контроля перезаписи и организации данных в вложенные структуры
    • _response_key — для подмены ключа replaceable поля перед сохранением в _cache
  • Все механизмы работают на уровне движка сценариев и не требуют изменений в сервисах

Плоское кэширование (по умолчанию):

По умолчанию используется плоское кэширование — данные мержатся напрямую в _cache без вложенности. Доступ к данным осуществляется через {_cache.field}:

yaml
step:
  - action: "generate_int"
    params:
      min: 1
      max: 100
    # response_data: {"random_value": 42, "random_seed": 123}
    # Попадает в: _cache.random_value = 42, _cache.random_seed = 123
  
  - action: "send_message"
    params:
      text: "Число: {_cache.random_value}"  # ✅ ПРАВИЛЬНО

Пример с несколькими действиями:

yaml
step:
  - action: "generate_int"
    params:
      min: 1
      max: 100
    # _cache.random_value = 42, _cache.random_seed = 123
  
  - action: "generate_array"
    params:
      min: 1
      max: 10
      count: 5
    # _cache.random_list = [3, 7, 2, 9, 1], _cache.random_seed = 456
  
  - action: "send_message"
    params:
      text: |
        Число: {_cache.random_value}
        Массив: {_cache.random_list|comma}

⚠️ Проблемы плоского кэширования:

1. Конфликты имен между разными действиями:

yaml
step:
  - action: "get_storage_value"
    params:
      group_key: "settings"
      key: "api_key"
    # _cache.response_value = "key1"
  
  - action: "get_user_storage_value"
    params:
      key: "api_secret"
    # _cache.response_value = "secret1"  # ⚠️ Перезаписано!
  
  - action: "send_message"
    params:
      text: "Key: {_cache.response_value}"  # Покажет только "secret1"

2. Перезапись при повторных вызовах одного действия:

yaml
step:
  - action: "get_storage_value"
    params:
      group_key: "settings"
      key: "api_key"
    # _cache.response_value = "key1"
  
  - action: "get_storage_value"  # То же действие!
    params:
      group_key: "settings"
      key: "api_secret"
    # _cache.response_value = "secret1"  # ⚠️ Первый результат перезаписан!

Системное поле _namespace (контроль перезаписи):

Назначение: Сохранение данных в вложенную структуру _cache[_namespace] для контроля перезаписи и избежания конфликтов имен.

Механика работы:

  • Если указан _namespace, данные сохраняются в _cache[_namespace] вместо плоского _cache
  • Доступ к данным: {_cache[_namespace].field}
  • Позволяет сохранять результаты повторных вызовов одного действия
  • Позволяет избежать конфликтов имен между разными действиями

Пример использования:

yaml
step:
  - action: "generate_int"
    params:
      min: 1
      max: 100
      _namespace: "my_random"
    # Попадает в: _cache.my_random = {random_value: 42, random_seed: 123}
  
  - action: "send_message"
    params:
      text: "Число: {_cache.my_random.random_value}"  # ✅ ПРАВИЛЬНО

Решение конфликтов с _namespace:

yaml
step:
  - action: "get_storage_value"
    params:
      group_key: "settings"
      key: "api_key"
      _namespace: "first"
    # _cache.first.response_value = "key1"
  
  - action: "get_storage_value"
    params:
      group_key: "settings"
      key: "api_secret"
      _namespace: "second"
    # _cache.second.response_value = "secret1"
    # ✅ Оба результата сохранены!
  
  - action: "send_message"
    params:
      text: |
        Первый: {_cache.first.response_value}
        Второй: {_cache.second.response_value}

Системное поле _response_key (переименование основного поля):

Назначение: Позволяет задать кастомное имя для основного поля результата действия для удобного доступа к данным в _cache.

Механика работы:

  • Некоторые действия возвращают основное значение под определенным ключом (например, storage_values, user_storage_values, formatted_text)
  • По умолчанию данные сохраняются в _cache с оригинальным ключом из результата действия
  • Если указан _response_key, движок автоматически переименовывает основное поле перед сохранением в _cache
  • Работает только для действий, которые поддерживают переименование основного поля

Пример использования:

yaml
step:
  # Получаем storage значение (по умолчанию будет в _cache.storage_values)
  - action: "get_storage"
    params:
      tenant_id: "{tenant_id}"
      group_key: "settings"
      key: "api_key"
      _response_key: "api_key"  # Переименовываем storage_values в api_key
    # response_data: {"storage_values": "secret_key_123"}
    # Попадает в: _cache.api_key = "secret_key_123" (вместо _cache.storage_values)
  
  # Используем переименованное значение
  - action: "send_message"
    params:
      text: "API ключ: {_cache.api_key}"  # ✅ ПРАВИЛЬНО - используем кастомный ключ

Пример с несколькими вызовами одного действия:

yaml
step:
  # Первый вызов - сохраняем в api_key
  - action: "get_storage"
    params:
      tenant_id: "{tenant_id}"
      group_key: "settings"
      key: "api_key"
      _response_key: "api_key"
    # _cache.api_key = "secret_key_123"
  
  # Второй вызов - сохраняем в api_secret
  - action: "get_storage"
    params:
      tenant_id: "{tenant_id}"
      group_key: "settings"
      key: "api_secret"
      _response_key: "api_secret"
    # _cache.api_secret = "secret_secret_456"
    # ✅ Оба значения сохранены под удобными ключами!
  
  - action: "send_message"
    params:
      text: |
        API Key: {_cache.api_key}
        API Secret: {_cache.api_secret}

Пример с форматированием текста:

yaml
step:
  # Форматируем данные и сохраняем под кастомным ключом
  - action: "format_data_to_text"
    params:
      format_type: "list"
      title: "Доступные намерения:"
      item_template: '- "$id" - $description'
      input_data: "{storage_values.ai_router.intents}"
      _response_key: "formatted_intents"
    # response_data: {"formatted_text": "• intent1\n• intent2"}
    # Попадает в: _cache.formatted_intents = "• intent1\n• intent2"
    # (вместо _cache.format_data_to_text.formatted_text)
  
  - action: "send_message"
    params:
      text: "{_cache.formatted_intents}"  # ✅ Используем удобный ключ

Комбинация _namespace и _response_key:

yaml
step:
  # Используем и _namespace, и _response_key одновременно
  - action: "get_storage"
    params:
      tenant_id: "{tenant_id}"
      group_key: "settings"
      key: "api_key"
      _namespace: "first"
      _response_key: "api_key"
    # _cache.first.api_key = "secret_key_123"
    # (вместо _cache.first.storage_values)
  
  - action: "get_storage"
    params:
      tenant_id: "{tenant_id}"
      group_key: "settings"
      key: "api_secret"
      _namespace: "second"
      _response_key: "api_secret"
    # _cache.second.api_secret = "secret_secret_456"
  
  - action: "send_message"
    params:
      text: |
        Первый: {_cache.first.api_key}
        Второй: {_cache.second.api_secret}

Пример с форматированием текста:

yaml
step:
  # Форматируем данные и сохраняем под кастомным ключом
  - action: "format_data_to_text"
    params:
      format_type: "list"
      title: "Доступные намерения:"
      item_template: '- "$id" - $description'
      input_data: "{storage_values.ai_router.intents}"
      _response_key: "formatted_intents"
    # response_data: {"formatted_text": "• intent1\n• intent2"}
    # Попадает в: _cache.formatted_intents = "• intent1\n• intent2"
    # (вместо _cache.format_data_to_text.formatted_text)
  
  - action: "send_message"
    params:
      text: "{_cache.formatted_intents}"  # ✅ Используем удобный ключ

Рекомендации по использованию:

Общие рекомендации:

  • ✅ Плоское кэширование по умолчанию — меньше вложенности, проще доступ
  • ✅ Используйте _namespace для контроля перезаписи при повторных вызовах одного действия
  • ✅ Используйте _namespace для избежания конфликтов имен между разными действиями
  • ✅ Используйте _response_key для удобства доступа к основным значениям действий
  • ✅ Комбинируйте _namespace и _response_key для полного контроля над структурой данных в _cache
  • ✅ Используйте осмысленные имена для _namespace и _response_key
  • ✅ Используйте set_cache для выноса данных из вложенности во флэт при необходимости

⚠️ Важно:

  • _response_key работает только для действий, которые поддерживают подмену ключа основного значения
  • Если действие не поддерживает подмену ключа, _response_key игнорируется (без ошибки)
  • Подмена происходит автоматически перед сохранением в _cache

Сохранение контекста в рамках обработки события:

⚠️ Важно: Кэш _cache сохраняется только в рамках одной цепочки выполнения сценариев. Это означает, что данные доступны между сценариями в рамках одной цепочки (когда один сценарий переходит к другому через jump_to_scenario или execute_scenario), но не сохраняются между разными цепочками выполнения (даже если они триггерятся одним событием) и между разными событиями.

Что такое "одна цепочка выполнения":

  • Одна цепочка = последовательность сценариев, связанных переходами (jump_to_scenario, execute_scenario)
  • Если сценарий A переходит к сценарию B через jump_to_scenario — это одна цепочка, кэш сохраняется
  • Если сценарий A вызывает сценарий B через execute_scenario — это одна цепочка, кэш сохраняется
  • Если событие триггерит несколько независимых сценариев — это разные цепочки, у каждой свой изолированный кэш

Что такое "одно событие":

  • Одно событие = одно сообщение от пользователя, один callback от кнопки, один scheduled запуск и т.д.
  • Если пользователь отправил сообщение /start — это одно событие
  • Если пользователь нажал кнопку с callback_data: "menu" — это другое, отдельное событие
  • Каждое событие может триггерить несколько сценариев, но каждый сценарий выполняется независимо со своим кэшем

Что сохраняется в рамках одного события:

  • Все данные события (event data: user_id, chat_id, event_text и т.д.)
  • Все результаты действий (данные из response_data автоматически попадают в плоский _cache или _cache[_namespace] если указан _namespace, с учетом подмены ключа через _response_key)
  • Временные данные из _cache (сохраненные через действие set_cache или другие действия)
  • Любые другие данные, накопленные в контексте обработки события

Когда кэш сохраняется (в рамках одной цепочки выполнения):

✅ При переходе через jump_to_scenario:

yaml
# Сценарий 1: main_scenario
step:
  - action: "set_cache"
    params:
      cache:
        selected_user: "@username"
  
  - action: "send_message"
    transition:
      - action_result: "success"
        transition_action: "jump_to_scenario"
        transition_value: "next_scenario"

# Сценарий 2: next_scenario
# ✅ Все данные из main_scenario доступны (одна цепочка выполнения):
step:
  - action: "send_message"
    params:
      text: "Пользователь: {_cache.set_cache.selected_user}"  # ✅ Доступен

Когда кэш НЕ сохраняется:

❌ Между разными сценариями, которые триггерятся одним событием:

yaml
# Событие: message "/start"

# Сценарий 1: welcome_scenario (триггерится "/start")
step:
  - action: "set_cache"
    params:
      cache:
        welcome_sent: true

# Сценарий 2: analytics_scenario (тоже триггерится "/start", но выполняется независимо!)
step:
  - action: "send_message"
    params:
      text: "Welcome sent: {_cache.set_cache.welcome_sent}"  # ❌ НЕ доступно! Разные цепочки выполнения

❌ Между разными событиями:

yaml
# Событие 1: Пользователь отправил "/start"
step:
  - action: "set_cache"
    params:
      cache:
        step: 1

# Событие 2: Пользователь отправил "/menu" (другое событие!)
step:
  - action: "send_message"
    params:
      text: "Step: {_cache.set_cache.step}"  # ❌ НЕ доступно! Разные события

🔄 Переходы (transition)

Назначение: Управление потоком выполнения сценария.

yaml
step:
  - action: "validate"
    params:
      condition: "$user_role == 'admin'"
    transition:
      - action_result: "success"
        transition_action: "jump_to_scenario"
        transition_value: "admin_panel"      # Переход к сценарию админки
      - action_result: "error"
        transition_action: "continue"         # Обычный поток для пользователей
      - action_result: "timeout"
        transition_action: "break"           # Прервать только текущий сценарий при таймауте
      - action_result: "error"
        transition_action: "abort"           # Прервать всю цепочку при критической ошибке
      - action_result: "system_failure"
        transition_action: "stop"            # Прервать всю обработку события при системной ошибке

Типы переходов:

  • continue — продолжить выполнение следующих действий
  • break — прервать выполнение только текущего сценария
  • abort — прервать всю цепочку выполнения текущего сценария (включая вложенные)
  • stop — прервать всю обработку события (все сценарии)
  • move_steps — переместиться на указанное количество шагов (положительное число = вперед, отрицательное = назад). Например, move_step: 1 перейдет к следующему шагу, move_step: 2 пропустит 1 шаг и перейдет к следующему
  • jump_to_step — перейти к конкретному шагу по индексу (шаги нумеруются с 0, например, jump_to_step: 5 перейдет к шагу с индексом 5)
  • jump_to_scenario — перейти к другому сценарию (аналогично break + прыжок, текущий сценарий далее не выполняется)
  • any — универсальный переход, который обрабатывается первым независимо от результата действия

Особенности переходов:

  • transition_value — обязателен для move_steps (количество шагов), jump_to_step (индекс шага, начиная с 0) и jump_to_scenario (название сценария)
  • Поиск сценария — система ищет сценарий в том же tenant'е
  • Приоритет переходов — переход any обрабатывается первым, независимо от результата действия
  • Единственный переход — всегда выполняется только один переход из всех возможных
  • Блокировка других переходов — если есть переход any, остальные переходы (success, error, timeout и т.д.) не будут обработаны
  • Различие между переходами:
    • break — прерывает только текущий сценарий, другие сценарии (если есть) продолжают обработку события
    • abort — прерывает всю цепочку выполнения текущего сценария (включая все вложенные сценарии), но позволяет другим сценариям (из других триггеров) продолжить работу
    • stop — полностью прекращает обработку события во всех сценариях, возвращает управление системе
    • jump_to_scenario — прерывает выполнение текущего сценария и переходит к другому (аналогично break + прыжок)

🔧 Плейсхолдеры (Placeholders)

Назначение: Динамическая подстановка значений в параметры действий сценариев.

Механика работы:

Все атрибуты в params автоматически обрабатываются плейсхолдерами перед выполнением действия. Это означает:

  • Все значения в params — строки, числа, массивы, объекты — проходят через обработку плейсхолдеров
  • Все уровни вложенности — плейсхолдеры работают на любом уровне структуры params (в строках, элементах массивов, значениях объектов)
  • Автоматическая обработка — происходит для каждого шага сценария перед выполнением действия
  • Источник данных — система использует накопленные данные события и результаты предыдущих действий как источник значений для подстановки

Синтаксис плейсхолдеров:

Простая замена:

yaml
params:
  text: "Привет, {username}!"
  chat_id: "{user_id}"

С модификаторами:

yaml
params:
  text: "Привет, {username|fallback:Гость}!"
  price: "{amount|*0.9|format:currency}"

Литеральные значения в кавычках:

yaml
params:
  text: "{'hello'|upper}"                      # HELLO (без передачи через словарь)
  duration: "{'1d 2w'|seconds}"                # 1296000 (литеральное время)
  calculation: "{'100'|+50}"                   # 150 (литеральная арифметика)
  date_shift: "{'2024-12-25'|shift:+1 day}"   # 2024-12-26 (литеральная дата)
  # Двойные кавычки для строк с одинарными кавычками
  text: "{"it's working"}"                     # it's working
  # Экранированные кавычки
  text: "{'it\'s working'}"                    # it's working

Вложенные плейсхолдеры:

yaml
params:
  text: "Ответ: {status|equals:{expected}|value:OK|fallback:BAD}"

⚠️ Важно: Кавычки в YAML для плейсхолдеров с regex

Проблема: YAML по-разному обрабатывает escape-последовательности в одинарных и двойных кавычках, что критично для regex паттернов с обратными слешами.

Решение: Используйте двойные кавычки для regex паттернов с обратными слешами (\s, \d, \w и т.д.).

Примеры:

yaml
# ❌ ОШИБКА - в одинарных кавычках \\s не экранируется правильно
params:
  value: '{event_text|regex:^([^\\s]+)|lower}'  # \\s становится \s (обратный слеш + s), а не пробел!

# ✅ ПРАВИЛЬНО - двойные кавычки правильно экранируют обратные слеши
params:
  value: "{event_text|regex:^([^\\s]+)|lower}"  # \\s становится \s (пробел в regex)

# ✅ ПРАВИЛЬНО - для regex без обратных слешей можно использовать одинарные
params:
  tenant_id: '{user_state|regex:tenant_(\d+)}'  # Работает, так как \d не требует двойного экранирования

# ✅ ПРАВИЛЬНО - для простых regex с одним обратным слешем тоже двойные кавычки
params:
  value: "{event_text|regex:\\s+([^\\s]+)$|lower}"  # \\s для пробела, \\s для пробела

# ✅ ОБЫЧНЫЕ ПЛЕЙСХОЛДЕРЫ - без разницы
params:
  text: "Привет, {username}!"     # Работает
  text: 'Привет, {username}!'    # Тоже работает

Когда использовать двойные кавычки:

  • Regex паттерны с обратными слешами{field|regex:pattern} где pattern содержит \s, \d, \w, \n, \t и т.д.
  • Важно: В двойных кавычках нужно использовать двойное экранирование: \\s для пробела, \\d для цифры
  • Пример: "{event_text|regex:^([^\\s]+)|lower}"\\s становится \s (пробел в regex)

Когда можно использовать одинарные кавычки:

  • Regex паттерны без обратных слешей — простые паттерны без \s, \d, \w и т.д.
  • Обычные плейсхолдеры{username}, {user_id}
  • Простые модификаторы{name|upper}, {price|format:currency}
  • Текстовые значения — без специальных символов

Правило: Если в regex паттерне есть обратные слеши (\s, \d, \w и т.д.) — всегда используйте двойные кавычки с двойным экранированием (\\s, \\d, \\w).

Доступ к вложенным данным:

yaml
params:
  # Точечная нотация для объектов
  text: "Пользователь: {user.profile.name|fallback:Неизвестный}"
  text: "Сообщение: {message.text}"
  
  # Индексы для массивов
  text: "Первый файл: {attachment[0].file_id}"
  text: "Последний пользователь: {users[-1].name}"
  
  # Комбинированный доступ
  text: "Событие: {event.attachment[0].type}"
  text: "Данные: {response.data.items[0].value}"

Доступные данные:

Все данные события и предыдущих действий доступны для использования в плейсхолдерах:

Данные события:

  • user_id — ID пользователя
  • username — имя пользователя
  • chat_id — ID чата
  • event_text — текст сообщения
  • callback_data — данные callback кнопки
  • event_date — время ивента
  • tenant_id — ID тенанта
  • message_id — ID сообщения
  • reply_to_message_id — ID сообщения, на которое отвечаем
  • forward_from — данные о пересылке
  • И любые другие поля — в зависимости от типа события

Конфигурация тенанта:

  • _config — словарь с конфигурацией тенанта (атрибуты из config.yaml)
    • _config.openrouter_token — OpenRouter API ключ для тенанта (если установлен)
    • Другие атрибуты — в зависимости от конфигурации тенанта
    • Автоматически доступен во всех сценариях тенанта

Данные предыдущих действий:

  • last_message_id — ID последнего отправленного сообщения
  • response_data — любые данные, возвращенные предыдущими действиями
  • Другие поля — в зависимости от действий в сценарии

Доступ к вложенным элементам:

Универсальный доступ к данным! Поддержка точечной нотации для объектов и индексов для массивов.

СинтаксисОписаниеПримерРезультат
Объекты
object.fieldДоступ к полю объекта{message.text}Привет мир
object.field.subfieldВложенные поля{user.profile.name}Иван Петров
Массивы
array[index]Доступ к элементу массива{attachment[0].file_id}file_1 (первый файл)
array[-index]Отрицательный индекс{attachment[-1].file_id}file_3 (последний файл)
array[index].fieldПоле элемента массива{users[1].name}Боб (имя второго пользователя)
array[index][index]Вложенные массивы{matrix[0][1]}2 (элемент матрицы)
Комбинированный
object.array[index].fieldСмешанный доступ{event.attachment[0].file_id}file_1

Примеры использования:

yaml
params:
  # Объекты
  text: "Сообщение: {message.text}"
  text: "Пользователь: {user.profile.name|fallback:Неизвестный}"
  
  # Массивы
  text: "Первый файл: {attachment[0].file_id}"
  text: "Последний пользователь: {users[-1].name|upper}"
  text: "Файл: {attachment[0].file_id}, размер: {attachment[0].size}"
  
  # Комбинированный доступ
  text: "Событие: {event.attachment[0].type}"
  text: "Данные: {response.data.items[0].value}"
  
  # Конфигурация тенанта
  text: "Токен: {_config.openrouter_token|fallback:Не установлен}"
  
  # Безопасный доступ
  text: "{attachment[10].file_id|fallback:Файл не найден}"

Получение информации о файле

Практический пример: Получение file_id и type для пересылки файлов

Создайте сценарий, который покажет file_id и type всех вложений из события:

yaml
file_info:
  description: "Получение file_id и type вложений для последующей пересылки"
  trigger:
    - event_type: "message"
      event_text: "/file"

  step:
    - action: "send_message"
      params:
        text: |
          📎 Информация о файле:
          
          file_id: <code>{event_attachment[0].file_id|fallback:{reply_attachment[0].file_id|fallback:Файл не найден}}</code>
          type: <code>{event_attachment[0].type|fallback:{reply_attachment[0].type|fallback:Тип не найден}}</code>

Обработка ошибок:

  • При обращении к несуществующему полю/индексу возвращается None
  • Можно использовать модификатор fallback для значения по умолчанию
  • Отрицательные индексы работают как в Python: -1 = последний элемент
  • Поддерживается неограниченная глубина вложенности

Модификаторы:

МодификаторОписаниеПримерРезультат
Арифметические операции
+valueСложение{price|+100}1500 (если price=1400)
-valueВычитание{price|-50}1350 (если price=1400)
*valueУмножение{price|*0.9}1260 (если price=1400)
/valueДеление{seconds|/3600}2.5 (если seconds=9000)
%valueОстаток от деления{number|%7}3 (если number=10)
Регистр
upperВерхний регистр{name|upper}ИВАН (если name="Иван")
lowerНижний регистр{name|lower}иван (если name="Иван")
titleЗаглавные буквы каждого слова{name|title}Иван Петров (если name="иван петров")
capitalizeПервая заглавная буква{name|capitalize}Иван (если name="иван")
case:typeПреобразование регистра{name|case:upper}ИВАН (если name="Иван")
Списки
tagsПреобразование в теги{users|tags}@user1 @user2
listМаркированный список{items|list}• item1\n• item2
commaЧерез запятую{items|comma}item1, item2
expandРазворачивание массива массивов на один уровень (только в массивах){keyboard|expand}[[a, b], [c]][a, b], [c] (в массиве)
keysИзвлечение ключей из объекта (словаря) в массив{storage_values|keys}["group1", "group2"] (если storage_values={"group1": {...}, "group2": {...}})
Преобразования
codeОборачивание значения в code блок{field|code}<code>value</code>
Форматирование
truncate:lengthОбрезка текста{text|truncate:50}Очень длинный текст...
format:dateФормат даты{timestamp|format:date}25.12.2024
format:timeФормат времени{timestamp|format:time}14:30
format:time_fullФормат времени с секундами{timestamp|format:time_full}14:30:45
format:datetimeПолный формат{timestamp|format:datetime}25.12.2024 14:30
format:datetime_fullПолный формат с секундами{timestamp|format:datetime_full}25.12.2024 14:30:45
format:pg_dateФормат даты для PostgreSQL (YYYY-MM-DD){timestamp|format:pg_date}2024-12-25
format:pg_datetimeФормат даты и времени для PostgreSQL (YYYY-MM-DD HH:MM:SS){timestamp|format:pg_datetime}2024-12-25 14:30:45
secondsПреобразование временных строк в секунды (поддержка формата: Xw Yd Zh Km Ms){duration|seconds}9000 (если duration="2h 30m")
shift:±интервалСдвиг даты на интервал (PostgreSQL style: +1 day, -2 hours, +1 year 2 months). Поддержка всех форматов дат, корректная обработка месяцев/годов{created|shift:+1 day}2024-12-26 (если created="2024-12-25")
Приведение к началу периода
to_dateПриведение даты к началу дня (00:00:00), возвращает ISO формат{created|to_date}2024-12-25 00:00:00
to_hourПриведение даты к началу часа (минуты и секунды = 0), возвращает ISO формат{created|to_hour}2024-12-25 15:00:00 (если created="2024-12-25 15:30:45")
to_minuteПриведение даты к началу минуты (секунды = 0), возвращает ISO формат{created|to_minute}2024-12-25 15:30:00 (если created="2024-12-25 15:30:45")
to_secondПриведение даты к началу секунды (микросекунды = 0), возвращает ISO формат{created|to_second}2024-12-25 15:30:45
to_weekПриведение даты к началу недели (понедельник 00:00:00), возвращает ISO формат{created|to_week}2024-12-23 00:00:00 (если created="2024-12-25 15:30:45")
to_monthПриведение даты к началу месяца (1 число, 00:00:00), возвращает ISO формат{created|to_month}2024-12-01 00:00:00 (если created="2024-12-25 15:30:45")
to_yearПриведение даты к началу года (1 января, 00:00:00), возвращает ISO формат{created|to_year}2024-01-01 00:00:00 (если created="2024-12-25 15:30:45")
format:currencyФорматирование валюты{amount|format:currency}1000.00 ₽
format:percentФорматирование процентов{value|format:percent}25.5%
format:numberФорматирование чисел{value|format:number}1234.56
format:timestampПреобразование в timestamp{date|format:timestamp}1703512200
Условные
equals:valueПроверка равенства (строковое сравнение){status|equals:active}true или false
in_list:itemsПроверка вхождения в список{role|in_list:admin,moderator}true или false
trueПроверка истинности (рекомендуется для boolean){is_active|true}true или false
existsПроверка существования значения (не None и не пустая строка){field|exists}true или false
is_nullПроверка на null (None, пустая строка или строка "null"){field|is_null}true или false
value:resultВозврат значения при истинности{status|equals:active|value:Активен}Активен или null
Служебные
fallback:valueЗначение по умолчанию{username|fallback:Гость}Иван или Гость
lengthПодсчет длины{text|length}15 (количество символов)
lengthПодсчет длины массива{array|length}3 (количество элементов)
regex:patternИзвлечение по regex{text|regex:(\d+)}123 (первое число)
Асинхронные действия
readyПроверка готовности async действия{_async_action.ai_req_1|ready}true если завершено, false если выполняется
not_readyПроверка что действие еще выполняется{_async_action.ai_req_1|not_ready}true если выполняется, false если готово

Примеры использования:

yaml
params:
  # Простые операции
  result: "{value|+100|format:currency}"
  
  # Условная логика
  status: "{user_status|equals:active|value:Активен|fallback:Неактивен}"
  
  # Цепочка модификаторов
  formatted: "{price|*0.9|format:currency|fallback:0.00 ₽}"
  
  # Regex извлечение (используем двойные кавычки для обратных слешей!)
  duration: "{event_text|regex:(\\d+)\\s*мин|fallback:0}"
  
  # Точечная нотация
  name: "{user.profile.name|fallback:Неизвестный}"
  
  # Временные значения
  duration_seconds: "{duration|seconds}"
  duration_minutes: "{duration|seconds|/60}"
  duration_hours: "{duration|seconds|/3600}"
  formatted_time: "{duration|seconds|format:number}"
  
  # Сдвиг дат
  tomorrow: "{created|shift:+1 day}"
  next_month: "{created|shift:+1 month|format:date}"
  complex_shift: "{created|shift:+1 year 2 months|format:datetime}"
  
  # Приведение к началу периода
  start_of_day: "{created|to_date}"
  start_of_week: "{created|to_week}"
  formatted_month_start: "{created|to_month|format:date}"
  
  # Boolean поля (различаем True/False/None)
  is_active: "{is_active|equals:True|value:✅ Включен|fallback:{is_active|equals:False|value:❌ Выключен|fallback:❓ Неизвестно}}"
  is_polling: "{is_polling|equals:True|value:✅ Активен|fallback:{is_polling|equals:False|value:❌ Неактивен|fallback:❓ Неизвестно}}"
  
  # String поля (используем equals:value)
  status: "{user_status|equals:active|value:Активен|fallback:Неактивен}"
  
  # Упрощенный вариант (если None не ожидается)
  is_simple: "{is_active|true|value:✅ Включен|fallback:❌ Выключен}"
  
  # Проверка существования значений (в условиях)
  # В условиях используйте: {field|exists} == True или {field|exists} == False
  
  # Проверка на null (в условиях)
  # В условиях используйте: {field|is_null} == True или {field|is_null} == False

Пример использования exists в условиях:

yaml
step:
  - action: "validate"
    params:
      condition: "{response_value.feedback|exists} == True"
    transition:
      - action_result: "success"
        transition_action: "continue"  # Значение существует
      - action_result: "failed"
        transition_action: "jump_to_scenario"
        transition_value: "create_feedback"  # Значение не существует

Важно: boolean vs string поля

Для boolean полей (True/False):

  • Рекомендуется: {is_active|true|value:✅ Включен|fallback:❌ Выключен}
  • Не работает: {is_active|equals:true|value:✅ Включен} (сравнение строк)

Для string полей:

  • Используйте: {status|equals:active|value:Активен|fallback:Неактивен}
  • Модификатор true не подходит для строковых значений

Практические примеры:

Пример 1: Персонализированное приветствие

yaml
step:
  - action: "send_message"
    params:
      text: |
        👋 Привет, {username|fallback:Гость}!
        
        📊 Ваша статистика:
        • ID: {user_id}
        • Чатов: {chat_count|fallback:0}
        • Последний вход: {last_login|format:datetime|fallback:Неизвестно}

Пример 2: Динамическое меню с условиями

yaml
step:
  - action: "send_message"
    params:
      text: |
        🎛️ Главное меню
        
        Статус: {user_status|equals:active|value:✅ Активен|fallback:❌ Неактивен}
        Баланс: {balance|format:currency|fallback:0.00 ₽}
      inline:
        - [{"📊 Статистика": "stats"}, {"💰 Баланс": "balance"}]
        - [{"⚙️ Настройки": "settings"}]

Пример 3: Обработка ошибок с fallback

yaml
step:
  - action: "send_message"
    params:
      text: |
        ❌ Ошибка обработки
        
        Код ошибки: {error_code|fallback:UNKNOWN}
        Сообщение: {error_message|fallback:Неизвестная ошибка}
        Время: {timestamp|format:datetime}
      inline:
        - [{"🔄 Повторить": "retry"}, {"🏠 Главное меню": "main"}]

Пример 4: Математические вычисления

yaml
step:
  - action: "send_message"
    params:
      text: |
        💰 Расчет стоимости
        
        Цена: {base_price|format:currency}
        Скидка: {discount_percent|fallback:0}%
        Итого: {base_price|*{discount_percent|/100}|*{base_price|-1}|+{base_price}|format:currency}

Пример 5: Извлечение данных из текста

yaml
step:
  - action: "send_message"
    params:
      text: |
        📝 Обработка заказа
        
        Номер заказа: "{event_text|regex:заказ\\s*(\\d+)|fallback:Не найден}"
        Сумма: "{event_text|regex:сумма\\s*(\\d+)|format:currency|fallback:0.00 ₽}"
        Время доставки: "{event_text|regex:(\\d+)\\s*мин|fallback:30}" минут

Пример 6: Переход к сценарию (прерывание текущего)

yaml
step:
  - action: "check_user_role"
    params:
      required_role: "admin"
    transition:
      - action_result: "success"
        transition_action: "jump_to_scenario"
        transition_value: "admin_panel"      # Переход к админке, текущий сценарий прерывается
      - action_result: "error"
        transition_action: "continue"        # Продолжить выполнение текущего сценария
  
  # Этот шаг НЕ выполнится, если сработал jump_to_scenario
  - action: "send_message"
    params:
      text: "Обычное меню для пользователей"

Пример 7: Перемещение по шагам

yaml
step:
  - action: "check_permissions"
    params:
      required_role: "admin"
    transition:
      - action_result: "success"
        transition_action: "move_steps"
        transition_value: 2                 # Переместиться на 2 шага вперед (пропустить 1 шаг, перейти к следующему)
      - action_result: "error"
        transition_action: "continue"        # Выполнить все шаги

Пример 7.1: Переход к конкретному шагу

yaml
step:
  - action: "validate"
    params:
      condition: "$user_role == 'admin'"
    transition:
      - action_result: "success"
        transition_action: "jump_to_step"
        transition_value: 5                 # Перейти к шагу с индексом 5 (шаги нумеруются с 0)
      - action_result: "failed"
        transition_action: "continue"       # Продолжить выполнение

Пример 8: Универсальный переход "any"

yaml
step:
  - action: "send_message"
    params:
      text: "Сообщение отправлено"
    transition:
      - action_result: "any"                 # Выполняется независимо от результата
        transition_action: "abort"           # Всегда прерываем выполнение
      - action_result: "success"
        transition_action: "continue"        # ❌ Этот переход НЕ выполнится из-за "any"
      - action_result: "error"
        transition_action: "continue"        # ❌ Этот переход НЕ выполнится из-за "any"

⚠️ Важно: Переход any имеет высший приоритет и блокирует выполнение всех остальных переходов. Система всегда выполняет только один переход из всех возможных.

Пример 9: Динамическая клавиатура с модификатором expand

yaml
step:
  # Получаем список тенантов
  - action: "get_tenants_list"
    params: {}
  
  # Строим динамическую клавиатуру из публичных тенантов
  - action: "build_keyboard"
    params:
      items: "{public_tenant_ids}"
      keyboard_type: "inline"
      text_template: "Tenant $value$"
      callback_template: "select_tenant_$value$"
      buttons_per_row: 2
  
  # Отправляем сообщение с динамической клавиатурой + статичные кнопки
  - action: "send_message"
    params:
      text: "Выберите тенант:"
      inline:
        - "{keyboard|expand}"              # Разворачиваем динамическую клавиатуру
        - [{"🔙 Назад": "main_menu"}]      # Статичная кнопка

Как работает expand:

  • Модификатор expand разворачивает массив массивов на один уровень только при использовании в массиве
  • В примере выше {keyboard|expand} в массиве inline разворачивает [[{...}, {...}], [{...}]] в [{...}, {...}], [{...}]
  • Это позволяет комбинировать динамически сгенерированные строки клавиатуры со статичными строками
  • Важно: expand работает только в контексте массива. В строке или словаре значение остается без изменений

Пример без expand (неправильно):

yaml
inline:
  - "{keyboard}"                           # ❌ Получится [[[...]], [...]] - 3 уровня вложенности!
  - [{"🔙 Назад": "main_menu"}]

Пример с expand (правильно):

yaml
inline:
  - "{keyboard|expand}"                     # ✅ Получится [[...], [...]] - 2 уровня вложенности
  - [{"🔙 Назад": "main_menu"}]

⚡ Асинхронные действия (Async Actions)

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

Механика работы:

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

Структура async действия:

yaml
step:
  - step_id: 1
    action_name: "ai_request"
    async: true                    # Флаг асинхронного выполнения
    action_id: "ai_req_1"         # Уникальный ID для отслеживания (обязателен)
    params:
      prompt: "Привет!"

Параметры:

  • async: true — флаг, что действие должно выполняться асинхронно
  • action_id — уникальный идентификатор действия (обязателен для async действий)
  • action_name — имя действия для выполнения
  • params — параметры действия

Как работает механизм async действий:

Что происходит при запуске async действия:

  1. Запуск действия в фоне:

    • При запуске async действия оно начинает выполняться в фоне, не блокируя выполнение сценария
    • Сценарий продолжает работать и может выполнять другие действия, пока async действие выполняется
    • Система автоматически отслеживает состояние выполнения действия
  2. Сохранение ссылки на действие в _async_action:

    • Система сохраняет ссылку на выполняющееся действие в специальном поле _async_action[action_id]
    • _async_action — это системный словарь, который хранит все запущенные async действия
    • Ключ словаря — это action_id (который вы указали), значение — ссылка на выполняющееся действие
    • Важно: _async_action хранится отдельно от _cache, чтобы быть доступным для проверки готовности между шагами
  3. Автоматическое отслеживание состояния:

    • Когда действие завершается (успешно или с ошибкой), система автоматически обновляет его состояние
    • Обновление происходит автоматически в фоне, независимо от выполнения сценария
    • Можно проверить готовность через плейсхолдер {_async_action.action_id|ready} в любой момент

Почему используется синтаксис {_async_action.action_id|ready}:

  • _async_action — системное поле, которое автоматически создается при запуске async действий
  • action_id — это ID, который вы указали при запуске async действия (например, "ai_req_1")
  • ready — модификатор, который проверяет, завершено ли действие (возвращает True если готово, False если еще выполняется)
  • Это позволяет использовать проверку готовности в условиях валидации и создавать сложные сценарии с ожиданиями

Пример: После запуска нескольких async действий система автоматически создает структуру:

  • _async_action["ai_req_1"] — ссылка на первый AI запрос
  • _async_action["data_proc_1"] — ссылка на обработку данных
  • _async_action["ai_req_2"] — ссылка на второй AI запрос

Вы можете проверить готовность любого из них через плейсхолдер: {_async_action.ai_req_1|ready}

Проверка готовности:

Для проверки готовности асинхронного действия используются модификаторы ready и not_ready в плейсхолдерах. Это позволяет создавать сложные сценарии с ожиданиями через валидацию и переходы.

Модификаторы:

  • {_async_action.action_id|ready} — возвращает True если действие завершено, False если еще выполняется
  • {_async_action.action_id|not_ready} — возвращает True если действие еще выполняется, False если готово

Как это работает:

  • Плейсхолдер {_async_action.action_id|ready} обращается к системному полю _async_action и проверяет состояние действия с указанным action_id
  • Модификатор ready проверяет, завершено ли действие — возвращает True если действие готово, False если еще выполняется
  • Результат можно использовать в условиях валидации для создания циклов ожидания и сложной логики

Пример использования в валидации:

yaml
step:
  - action: "validate"
    params:
      condition: "{_async_action.ai_req_1|ready} == True"  # Проверяем готовность
    transition:
      - action_result: "failed"
        transition_action: "move_steps"
        transition_value: -2  # Возвращаемся назад, если еще не готово

Ожидание результата:

Для получения результата асинхронного действия используется действие wait_for_action:

yaml
step:
  - step_id: 3
    action_name: "wait_for_action"
    params:
      action_id: "ai_req_1"
      timeout: 60  # Опциональный таймаут в секундах

Параметры:

  • action_id — ID асинхронного действия для ожидания (обязателен)
  • timeout — таймаут ожидания в секундах (опционально)

Результат:

  • wait_for_action возвращает результат основного действия AS IS (как будто оно выполнилось напрямую)
  • Если основное действие завершилось успешно → возвращается его результат с response_data, который автоматически попадает в плоский _cache (или _cache[_namespace] если указан _namespace, с учетом подмены ключа через _response_key)
  • Если основное действие завершилось с ошибкой → возвращается его ошибка
  • Если ошибка ожидания (таймаут, Future не найден) → возвращается ошибка wait_for_action

Практический пример:

yaml
ai_processing:
  description: "Обработка запроса к AI с показом загрузки"
  trigger:
    - event_type: "message"
      event_text: "/ai"

  step:
    # Шаг 1: Запускаем AI запрос асинхронно
    - action: "completion"
      async: true
      action_id: "ai_req_1"
      params:
        prompt: "{event_text|regex:/ai\\s+(.+)}"
    
    # Шаг 2: Отправляем сообщение о загрузке
    - action: "send_message"
      params:
        text: "⏳ Обрабатываю запрос..."
    
    # Шаг 3: Задержка перед проверкой
    - action: "sleep"
      params:
        seconds: 1.0  # Задержка 1 секунда между проверками
    
    # Шаг 4: Проверяем готовность через валидацию
    # Используем плейсхолдер {_async_action.ai_req_1|ready} для проверки состояния действия
    - action: "validate"
      params:
        condition: "{_async_action.ai_req_1|ready} == True"
      transition:
        - action_result: "failed"
          transition_action: "move_steps"
          transition_value: -1  # Возвращаемся на шаг с задержкой (шаг 3), если еще не готово
    
    # Шаг 5: Удаляем сообщение о загрузке (выполнится только когда действие готово)
    - action: "delete_message"
      params:
        delete_message_id: "{last_message_id}"
    
    # Шаг 6: Ожидаем результат AI (получаем данные в контекст)
    - action: "wait_for_action"
      params:
        action_id: "ai_req_1"
        timeout: 60  # Таймаут 60 секунд
    # response_data попадает в плоский _cache = {response_completion: "Ответ от AI", prompt_tokens: 50, completion_tokens: 100}
    
    # Шаг 7: Отправляем результат AI
    - action: "send_message"
      params:
        text: "{_cache.response_completion|fallback:Ошибка обработки}"

Что происходит в этом примере:

  1. Запускается async действие — система сохраняет ссылку на него в _async_action["ai_req_1"]
  2. Отправляется сообщение о загрузке
  3. Делается задержка 1 секунда (предотвращает 100% CPU)
  4. Проверяется готовность через валидацию — плейсхолдер {_async_action.ai_req_1|ready} проверяет, завершено ли действие
  5. Если не готово — возврат на шаг с задержкой (цикл: задержка → проверка → задержка → проверка...)
  6. Если готово — удаление сообщения о загрузке и получение результата

Этот пример показывает:

  • Как использовать проверку готовности через валидацию для создания циклов ожидания
  • Как правильно организовать цикл с задержкой для предотвращения 100% CPU
  • Механизм работы плейсхолдера {_async_action.action_id|ready} в условиях валидации

Важные особенности:

1. Результат попадает в контекст только через wait_for_action:

  • Результат основного действия попадает в _cache сценария только после вызова wait_for_action
  • wait_for_action получает результат из завершенного действия и возвращает его как обычный результат действия
  • Результат автоматически сохраняется в плоский _cache (или _cache[_namespace] если указан _namespace, с учетом подмены ключа через _response_key)
  • Без wait_for_action результат остается в системе и не доступен в сценарии через плейсхолдеры

2. Несколько async действий:

  • Можно запустить несколько async действий одновременно с разными action_id
  • Каждое действие отслеживается независимо через свой action_id в _async_action
  • Можно проверять готовность каждого действия отдельно: {_async_action.action_id_1|ready}, {_async_action.action_id_2|ready}
  • Можно создавать сложные условия: {_async_action.ai_1|ready} and {_async_action.data_1|ready}

3. Таймауты:

  • Можно указать timeout в wait_for_action для ограничения времени ожидания
  • При превышении таймаута wait_for_action вернет ошибку timeout, но выполнение сценария продолжается
  • Основное действие продолжит выполняться в фоне даже после таймаута — ссылка на действие останется в _async_action и можно будет проверить готовность позже

Пример с несколькими async действиями:

yaml
parallel_processing:
  description: "Параллельная обработка нескольких запросов"
  trigger:
    - event_type: "message"
      event_text: "/process"

  step:
    # Запускаем несколько действий параллельно
    - action: "completion"
      async: true
      action_id: "ai_1"
      params:
        prompt: "Вопрос 1"
    # response_data: {"response_completion": "Ответ 1", "prompt_tokens": 50, "completion_tokens": 100}
    
    - action: "process_data"
      async: true
      action_id: "data_proc_1"
      params:
        data: "{event_text}"
    # response_data: {"processed_data": "Обработанные данные", "status": "success"}
    
    # Ожидаем результат AI (сохраняется в _cache)
    # ВАЖНО: Не используем цикл проверки готовности без задержки - это приведет к 100% CPU!
    # Просто ждем результат напрямую через wait_for_action с таймаутом
    - action: "wait_for_action"
      params:
        action_id: "ai_1"
        timeout: 60  # Таймаут 60 секунд
    # wait_for_action возвращает результат основного действия AS IS
    # response_data из результата попадает в плоский _cache = {response_completion: "Ответ 1", prompt_tokens: 50, completion_tokens: 100}
    
    # Ожидаем результат обработки данных (сохраняется в _cache)
    - action: "wait_for_action"
      params:
        action_id: "data_proc_1"
        timeout: 30  # Таймаут 30 секунд
        _namespace: "data_processing"  # Используем кастомный ключ для сохранения обоих результатов
    # response_data попадает в _cache.data_processing = {processed_data: "Обработанные данные", status: "success"}
    
    # Используем результаты (все данные доступны через _cache)
    - action: "send_message"
      params:
        text: |
          AI ответ: {_cache.response_completion|fallback:Не получен}
          Токены: {_cache.completion_tokens|fallback:0}
          Обработанные данные: {_cache.data_processing.processed_data|fallback:Не обработаны}
          Статус: {_cache.data_processing.status|fallback:Неизвестно}

ℹ️ Важно:

  • wait_for_action возвращает результат основного действия AS IS (как будто оно выполнилось напрямую)
  • Результат обрабатывается на уровне scenario_engine как обычный response_data
  • Данные попадают в плоский _cache (или _cache[_namespace] если указан _namespace, с учетом подмены ключа через _response_key)
  • Если wait_for_action вызывается дважды без _namespace, второй результат перезапишет первый (данные мержатся через глубокое слияние)
  • Решение: Используйте _namespace для сохранения результатов разных async действий в разные ключи
  • Дополнительно: Используйте _response_key для удобной подмены ключа replaceable поля в результатах async действий

Рекомендации:

  1. Всегда указывайте action_id для async действий — это единственный способ отслеживать их статус
  2. Добавляйте таймауты в wait_for_action для долгих операций — это предотвращает бесконечное ожидание
  3. Обрабатывайте ошибки — добавляйте transition для обработки error и timeout результатов в wait_for_action
  4. Используйте циклы проверки готовности с задержкой — если нужен цикл проверки готовности, обязательно добавляйте sleep перед возвратом к проверке
  5. Для нескольких async действий используйте wait_for_action напрямую — не создавайте циклы проверки готовности для нескольких действий, просто ждите каждое через wait_for_action с таймаутом
  6. Используйте _namespace для сохранения результатов разных async действий в разные ключи _cache

Coreness — Create. Automate. Scale.