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

交互会話システム

Note

本章の内容は ErisPulse 2.8.0+ を必要とします。

ErisPulse は「ユーザーとの継続的な対話」をフレームワークレベルのインフラとして実装しています。wait_reply から始まり、定時通知、複数ルートの待機、セッションの排他、再起動時の復旧まで、すべてが統一された インタラクティブセッションマネージャー(Core/Event/interaction.py、sdk.interaction)によってスケジューリングされます。

{!--< tips >!--} 本文でカバーする各機能には所有者(owner)が付与されています。待機、リース、タイマーはすべて登録時のモジュール名を記録し、モジュールのアンロードやアダプターの停止時にフレームワークが自動的にクリーンアップし、待機側に即座に通知が届きます。これは、待機がタイムアウトするまで待つ必要がないことです。これは、所有権システムにおける待機の拡張です。 {!--< /tips >!--}

レプリーの待機:wait_reply

wait_reply はインタラクティブセッションの基盤です。現在のコルーチンを一時停止し、対象ユーザーが次のメッセージで「返信」するのを待ちます。

from ErisPulse.Core.Event import command

@command("ask")
async def ask_command(event):
    reply = await event.wait_reply(prompt="あなたの名前を入力してください:", timeout=30)
    if reply is None:
        await event.reply("タイムアウトしました")
        return
    await event.reply(f"こんにちは、{reply.get_text()}!")

全パラメータ一覧

パラメータ 説明 デフォルト
prompt 待機前に送信するプロンプト None
timeout 待機のタイムアウト(秒) 60
pattern glob フィルター(* / ? / [seq])、一致しない場合は待機を継続 None
regex 正規表現フィルター(pattern と同時に指定する場合、両方一致する必要あり)、一致しない場合は待機を継続 None
validator 検証関数(Event を受け取り、bool を返す)、失敗した場合は待機を継続 None
callback レプリー受信時のコールバック(戻り値形式の代わりの書き方) None
method prompt の送信方法 "Text"
session セッションレベルの待機:同じセッション(グループ / チャンネル)内の誰の返信でも有効 False
# 数字の金額のみを受け入れ、それ以外は待機を継続
reply = await event.wait_reply("金額を入力してください:", regex=r"\d+\s*元", timeout=30)

# セッションレベルの待機:グループ協働の場面、誰でも返信可能
reply = await event.wait_reply(session=True, prompt="誰か回答していただけますか?")

待機がいつキャンセルされるか

待機は「タイムアウトするまで待つ」だけではありません。以下の状況では待機が即座に終了(wait_reply は None を返す)し、呼び出し側がタイムアウトまで待つ必要はありません。

触発 キャンセル理由(InteractionCancelled.reason) 説明
所有モジュールがアンロード / 禁用された owner_unload 所有権のクリーンアップ:誰が登録した待機か、そのモジュールが消えたときに一緒に回収
アダプターが停止 / 再起動された platform_stop そのプラットフォームで一時停止された待機はすべてキャンセル
同じセッションで新しい待機 / リースが上書きされた conflict 下記「セッション仲裁」を参照
レプリーの送信者がブロックされた / 所有モジュールが解除された revoked レプリーが命じられた権限の再確認:スコープのアイデンティティ次元 + モジュール次元
ユーザーがレプリーを送信した —— 正常な経路、レプリーイベントを返す

低レベルの例外は InteractionCancelled(InteractionError 例外体系に属する)で、wait_reply はそれを None に変換しています。原因を必要とする呼び出し側は、sdk.interaction.register() の低レベル API を直接使用できます。

レプリーが命じられた完全な判定チェーン

レプリーのメッセージが到着した際、インタラクションマネージャーは以下の順序で判定します(コマンドのマッチングの前に実行され、会話の連続性が優先されます。メッセージが他の優先度の高い処理で既に認証されていても、一時停止された会話は完了します):

セッションキーのマッチ(正確な user 次元 → セッションレベルのバックアップ)
  → pattern / regex テキストフィルター(一致しない場合は待機を継続)
  → validator 検証(失敗した場合は待機を継続)
  → 権限の再確認(スコープのアイデンティティ次元 + 所有モジュール次元、失敗した場合は待機を終了)
  → 待機側の呼び出し + イベントの認証(mark_processed)

セッションタイマー:remind / escalate

「タイムアウト」を戻り値から編成可能な原語に変換します。タイマーはインタラクティブセッションに紐づき、モジュールのアンロード / アダプターの停止時に自動的にキャンセルされます。1セッションあたりのアクティブな remind の上限は 5 つです。

remind:返信がなければリマインド

@command("ticket")
async def ticket_command(event):
    await event.reply("チケットが提出されました。処理結果はここに通知されます。")
    # 5分間返信がなければ、優しくリマインド。ユーザーの返信はすべて自動的にキャンセルします
    event.remind(300, "まだいますか?結果が出たらすぐにご連絡します")
    reply = await event.wait_reply(timeout=3600)
    ...

escalate:期限に必ず届くアップグレード

event.escalate(1800, lambda e: notify_master(f"チケット 30 分未処理:{event.get_command_args()}"))

remind との唯一の違いは、ユーザーの返信でキャンセルされないことです。アップグレードアクション(通知、人間への転送)は「タイムアウト時に必ず到達」を約束し、手動で cancel() またはモジュールのアンロード / アダプターの停止でのみキャンセルされます。

remind escalate
到期時の動作 テキストを送信 / callback を実行 callback を実行
ユーザーの返信 自動的にキャンセル 影響を受けない
所有権のクリーンアップ(アンロード / アダプター停止) キャンセル キャンセル
1セッションあたりの上限 5 限界なし(所有権のクリーンアップでバックアップ)

多ルート待機:expect + select

同時に複数の期待を一時停止し、先着順で処理されます。典型的な場面:管理者の承認を待つと同時に、ユーザーの取り消しや、複数人による投票を待つ。

which, reply = await event.select(
    event.expect(pattern="同意*", user="10001"),
    event.expect(pattern="拒否*", user="10002"),
    event.expect(validator=lambda e: e.get_text() == "保留", session=True),
    timeout=60,
)
if which is None:
    await event.reply("60 秒以内に承認結果がありませんでした")
elif which == 0:
    await event.reply("承認しました")
elif which == 1:
    await event.reply("拒否しました")

{!--< tips >!--} select とマルチスレッド asyncio.wait の手動編集との比較:期待が命中しなかった場合の自動クリーンアップ、命中したイベントの自動認証、権限の再確認と所有権のクリーンアップがすべて有効です。自分で Future を管理する必要はありません。 {!--< /tips >!--}

セッションの排他:acquire / hold / get_owner_of

所有権は「リソース」から「セッション」へと移行しました。「このユーザーは現在誰に占有されているか」が一等のクエリになります。

# クエリ:このセッションは誰と対話中ですか?(空きなら None を返す)
owner = sdk.interaction.get_owner_of(event)
if owner and owner != "MyModule":
    return  # 他のモジュールが対話中なので、干渉しない

# 排他的リース:セッションを独占(deny 策略、占有されていれば None を返す)
lease = sdk.interaction.acquire(event)          # デフォルトで TTL 1 時間、ttl= を渡すことも可能
if lease is None:
    return  # すでに占有されている

try:
    ...  # 独占的な対話
finally:
    lease.release()

コンテキストマネージャー形式(取得失敗時は SessionOccupiedError をスロー):

with sdk.interaction.hold(event) as lease:
    ...  # 終了時に自動的に解放

リースは renew(ttl) で延長が可能;TTL は惰性で期限切れになります。期限切れのリースは、次回アクセス時に自動的にクリーンアップされます。

Conversation.resume() が会話を再開する際、フレームワークは自動的にリースを取得します(Conversation 多輪対話の「復帰即接続」を参照)——復帰した会話は天然にセッションを保持し、他のモジュールが挿入されることはありません。

セッションの受信箱:event.history

各セッションの最近のメッセージの流れを統一的に記録します(ユーザーとロボット両方)。AI のコンテキスト、重複防止、行動分析などのモジュールの共有事実ベースとして使用されます。各モジュールは個別に履歴を保存する必要がありません。

messages = await event.history(20)   # 最近の 20 件、時間順に昇順
for m in messages:
    print(m["role"], ":", m["text"])  # role: "user" / "bot"

メッセージトランザクション:message_tx

トランザクション内のすべての出力メッセージは自動的に記帳されます。例外が発生した場合、逆順に自動的に既に送信されたメッセージを撤回します(アダプターが delete_message を実装していない場合はスキップされますが、帳簿は正常に記録されます)。

async with event.message_tx():
    await event.reply("処理中です、少々お待ちください")
    result = await do_something()          # ここで例外が発生 →
    await event.reply(f"完了: {result}")   # 以前の「処理中」は自動的に撤回

トランザクション外の送信は記帳されません(ゼロコスト);get_send_receipts() で現在のトランザクションで送信された回執を確認できます。

リンク追跡:trace-id

各入力イベントは自動的に追跡 ID を取得します(event["id"] を再利用、存在しない場合は生成)、以下を貫きます:

1 つのメッセージが複数のモジュールによって処理された場合、全経路で同じ ID を使用して連携できます(ログ / 慢速クエリ / 審計)。

他のシステムとの関係

関連ドキュメント