简体中文 English 繁體中文 日本語 Русский
本文为静态镜像,内容以交互版为准 在交互式文档中心打开 →

Conversation Многошаговый диалог

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

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

Создание с помощью метода conversation() объекта Event:

from ErisPulse.Core.Event import command

@command("quiz")
async def quiz_handler(event):
    conv = event.conversation(timeout=30)

    await conv.say("🎮 Добро пожаловать в викторину!")

    answer = await conv.choose("Первый вопрос: Кто создатель Python?", [
        "Guido van Rossum",
        "James Gosling",
        "Dennis Ritchie",
    ])

    if answer is None:
        await conv.say("Время вышло, попробуйте в другой раз!")
        return

    if answer == 0:
        await conv.say("Правильно!")
    else:
        await conv.say("Неверно, правильный ответ — Guido van Rossum")

    conv.stop()

Основные API

say(content, **kwargs)

Отправка сообщения, возвращает self для цепочечного вызова:

await conv.say("Первая строка").say("Вторая строка").say("Третья строка")

Также можно указать метод отправки:

await conv.say("https://example.com/image.jpg", method="Image")

wait(prompt=None, timeout=None)

Ожидание ответа пользователя, возвращает объект Event или None (при таймауте):

# Простое ожидание
resp = await conv.wait()
if resp:
    text = resp.get_text()

# Ожидание с отправкой подсказки
resp = await conv.wait(prompt="Пожалуйста, введите ваше имя:")

# Использование пользовательского таймаута (переопределяет таймаут диалога)
resp = await conv.wait(prompt="Пожалуйста, ответьте в течение 10 секунд:", timeout=10)

confirm(prompt=None, **kwargs)

Ожидание подтверждения пользователя (да/нет), возвращает True / False / None (при таймауте):

result = await conv.confirm("Вы уверены, что хотите удалить все данные?")
if result is True:
    await conv.say("Удалено")
elif result is False:
    await conv.say("Отменено")
else:
    await conv.say("Таймаут, ответ не получен")

Встроенные слова для подтверждения: да/yes/y/confirm/confirm/ok/true/правильно/да/хорошо/конечно/можно/согласен/нет проблем/в порядке...

Встроенные слова для отрицания: нет/no/n/cancel/отменить/не надо/нельзя/отказаться/не правильно/откажись/отказ...

choose(prompt, options, **kwargs)

Ожидание выбора пользователя из списка опций, возвращает индекс (начиная с 0) или None:

choice = await conv.choose("Пожалуйста, выберите цвет:", ["красный", "зеленый", "синий"])
if choice is not None:
    colors = ["красный", "зеленый", "синий"]
    await conv.say(f"Вы выбрали {colors[choice]}")

Пользователь может выбрать, введя номер (1/2/3) или текст опции (красный).

options_format="auto" (по умолчанию) автоматически выбирает стиль в зависимости от метода: Markdown→ненумерованный список, Html→нумерованный список, иначе→простой текстовый список. Также поддерживаются "list"、"inline"、"md"、"html" или пользовательская функция.

Поддержка merge_prompt=True для объединения в одно сообщение, а также поддержка плейсхолдера для позиционирования списка опций (по умолчанию {options}, можно изменить с помощью placeholder):

choice = await conv.choose(
    "## Пожалуйста, выберите\n{options}",
    ["Вариант A", "Вариант B"],
    method="Markdown",
    merge_prompt=True,
)

# Пользовательский плейсхолдер
choice = await conv.choose(
    "Выберите: [choices]",
    ["Вариант A", "Вариант B"],
    placeholder="[choices]",
)

collect(fields, **kwargs)

Сбор информации в несколько шагов, возвращает словарь данных или None:

data = await conv.collect([
    {"key": "name", "prompt": "Пожалуйста, введите имя"},
    {"key": "age", "prompt": "Пожалуйста, введите возраст",
     "validator": lambda e: e.get("alt_message", "").strip().isdigit(),
     "retry_prompt": "Возраст должен быть числом, пожалуйста, повторите ввод"},
    {"key": "city", "prompt": "Пожалуйста, введите город"},
])

if data:
    await conv.say(f"Регистрация успешна!\nИмя: {data['name']}\nВозраст: {data['age']}\nГород: {data['city']}")
else:
    await conv.say("Процесс регистрации прерван")

Конфигурация полей:

Параметр Описание Значение по умолчанию
key Ключ поля (обязательно) -
prompt Подсказка "Пожалуйста, введите {key}"
validator Функция валидации, принимает Event, возвращает bool Нет
retry_prompt Подсказка при ошибке валидации "Ввод неверен, пожалуйста, повторите ввод"
max_retries Максимальное количество попыток 3
condition Функция условия, принимает словарь уже собранных данных, возвращает bool Нет

Условные поля: Использование condition позволяет реализовать динамическую форму, поле собирается только при выполнении условия:

data = await conv.collect([
    {"key": "has_car", "prompt": "У вас есть машина? (да/нет)"},
    {"key": "car_brand", "prompt": "Пожалуйста, введите марку автомобиля",
     "condition": lambda d: d.get("has_car", "").lower() in ("да", "yes", "y")},
])

stop()

Ручное завершение диалога, устанавливает is_active в False:

conv.stop()

is_active

Является ли диалог активным:

if conv.is_active:
    await conv.say("Диалог продолжается")

Управление активным состоянием

stateDiagram-v2
    state "Активный" as active
    state "Неактивный" as inactive
    [*] --> active: event.conversation()
    active --> active: say / wait / confirm / choose / collect
    active --> inactive: stop()
    active --> inactive: wait() таймаут
    active --> inactive: collect() таймаут или исчерпание попыток
    inactive --> [*]

Диалог автоматически становится неактивным в следующих случаях:

  1. Вызов метода stop()
  2. wait() возвращает None по таймауту
  3. collect() возвращает None из-за таймаута или исчерпания попыток

После перехода в неактивное состояние все методы взаимодействия (wait/confirm/choose/collect) немедленно возвращают None, не ожидая ответа пользователя.

Ветвление и переходы

@conv.branch(name) декоратор

Использование branch() для регистрации ветви диалога, переход между ветвями с помощью goto():

@command("menu")
async def menu_handler(event):
    conv = event.conversation(timeout=60)

    @conv.branch("main")
    async def main_menu():
        await conv.say("=== Главное меню ===\n1. Личная информация\n2. Настройки\n3. Выход")
        resp = await conv.wait()
        if resp is None:
            return
        text = resp.get_text().strip()
        if text == "1":
            await conv.goto("profile")
        elif text == "2":
            await conv.goto("settings")
        elif text == "3":
            await conv.say("До свидания!")
            conv.stop()

    @conv.branch("profile")
    async def profile():
        await conv.say("=== Личная информация ===\nИмя: Alice\n0. Вернуться")
        resp = await conv.wait()
        if resp and resp.get_text().strip() == "0":
            await conv.goto("main")

    @conv.branch("settings")
    async def settings():
        await conv.say("=== Настройки ===\n1. Переключатель уведомлений\n0. Вернуться")
        resp = await conv.wait()
        if resp and resp.get_text().strip() == "0":
            await conv.goto("main")

    await conv.start()  # Начинаем с первой зарегистрированной ветви

conv.start(name=None)

Запуск диалога, по умолчанию с первой зарегистрированной ветви:

await conv.start()          # Начинаем с первой ветви
await conv.start("settings") # Начинаем с указанной ветви

Контекст и сохранение

conv.context

Внутренний словарь context каждого экземпляра диалога используется для обмена состоянием между ветвями:

@conv.branch("step1")
async def step1():
    conv.context["username"] = resp.get_text().strip()
    await conv.goto("step2")

@conv.branch("step2")
async def step2():
    name = conv.context.get("username", "неизвестный")
    await conv.say(f"Привет, {name}!")

save() / resume() / clear_saved()

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

# Сохранение состояния диалога (обычно не нужно вызывать вручную, см. "Автоматические контрольные точки")
await conv.save()

# ... позже в том же сеансе ...
conv2 = event.conversation()
if await conv2.resume():
    await conv2.say("Добро пожаловать обратно! Продолжим предыдущий диалог")
else:
    await conv2.say("Нет сохраненного диалога")

# Очистка сохраненного диалога
await conv.clear_saved()

Ключ сохранения содержит измерение target (conversation:{platform}:{user_id}:{target_id}), диалоги одного пользователя в разных сессиях не перезаписываются друг друга; старые архивы без target автоматически мигрируются при resume().

Автоматические контрольные точки и восстановление после перезапуска

Автоматическое архивирование

Фреймворк автоматически поддерживает контрольные точки в следующих случаях, обычно нет необходимости вызывать save() вручную:

Событие Действие
goto() / start() переход в ветвь Автоматически сохранить (текущая ветвь + context)
stop() / wait() таймаут / collect() неудача Автоматически удалить (конечное состояние диалога)

TTL контрольной точки

Архивы содержат метку времени, и архивы, превышающие ErisPulse.interaction.checkpoint_ttl (по умолчанию 24 часа), будут удалены:

[ErisPulse.interaction]
checkpoint_ttl = 86400  # секунд

Автоматическое восстановление после перезапуска

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

from ErisPulse.Core.Event.wrapper import Conversation

@Conversation.register_resume_handler()  # можно передать platform="onebot11" для ограничения платформой
def make_conversation(event) -> Conversation:
    # Задача фабрики: восстановить диалог и заново зарегистрировать все ветви
    conv = event.conversation(timeout=60)

    @conv.branch("menu")
    async def menu(conv, event):
        ...

    return conv

После регистрации, когда пользователь, находившийся в ветви menu перед перезапуском, отправит первое сообщение, фреймворк автоматически: восстановит context → захватит это сообщение → продолжит диалог из сохраненной ветви. При отсутствии зарегистрированной фабрики эта механизм не несёт никаких дополнительных затрат.

Восстановление и передача управления

При успешном resume() фреймворк автоматически выполняет две вещи:

  1. Передача управления сессией: автоматически acquire аренды исключительного доступа к сессии — другие модули могут определить "этот пользователь занят диалогом" с помощью sdk.interaction.get_owner_of(event); если сессия уже занята другим модулем, восстановление отменяется (возвращается False), предотвращая конфликт диалогов
  2. Восстановление истории: из почтового ящика сессии извлекаются последние 10 сообщений в conv.recent_history (контекст LLM не обрывается при восстановлении модуля AI); resume(with_history=0) можно использовать для отключения
if await conv.resume(with_history=20):
    for m in conv.recent_history:
        print(m["role"], ":", m["text"])

Ручное восстановление (при отключении автоматического механизма)

@command("continue")
async def continue_handler(event):
    conv = event.conversation()
    # ... регистрация ветвей ...
    if await conv.resume():
        conv.goto(conv.get_current_branch())

Типичные сценарии диалога

Навигационная регистрация

@command("register")
async def register_handler(event):
    conv = event.conversation(timeout=60)

    await conv.say("Добро пожаловать на регистрацию!")

    data = await conv.collect([
        {"key": "username", "prompt": "Пожалуйста, введите имя пользователя (от 3 до 20 символов)",
         "validator": lambda e: 3 <= len(e.get_text().strip()) <= 20},
        {"key": "email", "prompt": "Пожалуйста, введите адрес электронной почты",
         "validator": lambda e: "@" in e.get_text() and "." in e.get_text(),
         "retry_prompt": "Неверный формат электронной почты, повторите ввод"},
    ])

    if not data:
        await event.reply("Регистрация отменена")
        return

    confirmed = await conv.confirm(
        f"Подтвердите регистрационную информацию?\nИмя пользователя: {data['username']}\nЭлектронная почта: {data['email']}"
    )

    if confirmed:
        await conv.say("✅ Регистрация успешна!")
    else:
        await conv.say("❌ Регистрация отменена")

Циклический диалог

@command("chat")
async def chat_handler(event):
    conv = event.conversation(timeout=120)
    await conv.say("Вход в диалоговый режим, введите «выход» для завершения")

    while conv.is_active:
        resp = await conv.wait()
        if resp is None:
            await conv.say("Таймаут, диалог завершен")
            break

        text = resp.get_text().strip()

        if text == "выход":
            await conv.say("До свидания!")
            conv.stop()
        elif text == "помощь":
            await conv.say("Доступные команды: выход, помощь, статус")
        elif text == "статус":
            await conv.say("Диалог активен")
        else:
            await conv.say(f"Вы сказали: {text}")

Связанная документация