コンテンツにスキップ

セルフトリガの活用

セルフトリガの活用

このガイドでは、SelfTriggerコンポーネントを使って ユーザー入力なしで自動的に連続対話を行う 方法を解説します。


概念

SelfTriggerとは

SelfTriggerは、 ツリーの処理中に次のツリーを自動起動する ためのコンポーネントです。

通常のドラマは、ユーザーからのevent.triggerを受けて処理を開始し、レスポンスを返して終了します。SelfTriggerを使うと、レスポンスを返した後に 自動的に次の処理を開始 できます。

【通常のドラマ】
ユーザー入力 → 処理 → レスポンス → 終了(ユーザー入力待ち)

【SelfTrigger使用時】
ユーザー入力 → 処理 → レスポンス → SelfTrigger → 処理 → レスポンス → SelfTrigger → ...
                                      ↑                        ↑
                                  自動起動                  自動起動(条件次第で継続)

主なユースケース

ユースケース 説明
複数キャラ会話 複数のキャラクターが交互に発話する対話シーン
連投制御 1回のユーザー入力に対してキャラクターが複数回発話
Agenticな挙動実現 目的達成のためにGPT等を繰り返し呼び出し、完了後にキャラクターが応答

タイミング制御について

基本的には、クライアント側からevent.triggerを送信してタイミングを制御することを推奨します。

クライアント制御の利点:

  • 音声再生完了後に次を開始するなど、クライアント状態に基づいた正確なタイミング制御
  • ネットワーク遅延やクライアント処理時間を考慮した調整が可能
  • ユーザー操作(スキップ、中断など)への柔軟な対応

SelfTriggerは、クライアント側でのタイミング制御が難しい場合や、サーバー主導で連続処理を行いたい場合に使用してください。


基本的な使い方

設定項目

パラメータ 説明
trigger 起動条件(lambda式) lambda: int(states.counter) < 5
message_data 次のトリガーに渡すデータ lambda: {'reason': 'continue'}
wait_time 起動前の待機時間(秒) 0.5 または lambda: states.delay
execution_mode 実行モード wait_for_completion

最小構成の例

カウンターが3未満の間、自動的に連続発話を行う例です。

graph LR
    A[Entry] --> B[StateSet<br/>counter+1]
    B --> C[EventResponse<br/>回数表示]
    C --> D[SelfTrigger<br/>counter < 3]
    D -.->|再トリガー| A

    style A fill:#e8f5e9
    style D fill:#ffe8e8
chain_elements:
  # エントリーポイント
  - id: event.trigger
    type: EntryChain
    config:
      entry_id: event.trigger

  # カウンター更新
  - id: counter
    type: StateSetChain
    config:
      update_states:
        counter: '{{ (states.counter | default(0) | int) + 1 }}'

  # レスポンス送信
  - id: response
    type: EventResponseChain
    config:
      fn:
        message: '回数: {{ states.counter }}'
      mode: once

  # セルフトリガー(3回まで)
  - id: self_trigger
    type: SelfTrigger
    config:
      trigger: 'lambda: int(states.counter or 0) < 3'
      message_data: 'lambda: {"reason": "continue"}'
      wait_time: '0.5'

動作の流れ:

  1. event.triggerで開始
  2. counterを1に更新
  3. responseで「回数: 1」を送信
  4. self_triggerでトリガー条件を評価(1 < 3 → True)
  5. 0.5秒待機後、自動的に再起動
  6. 2回目、3回目...と繰り返し
  7. counter=3でトリガー条件がFalseになり終了

実行モードの選択

なぜ実行モードが必要か

理想的には、現在のツリー処理が完全に終了してからSelfTriggerが発火されるべきです。しかし実際には、 SelfTriggerに到達した時点で、ツリー内の他のコンポーネント(TTS、EventResponseなど)がまだ処理中 であることが多いです。

execution_modeは、SelfTriggerの条件が満たされwait_timeが経過した時点で、 現在実行中のツリー処理をどう扱うか を制御します。

execution_mode: wait_for_completion  # 推奨

3つのモード比較

モード 動作 利点 欠点 用途
continue_immediately 現在のツリーを継続しながら次をトリガー 最速 複数ツリーが並行実行される 高速性重視
wait_for_completion 現在のツリー完了を待ってからトリガー 安定・リソースリーク防止 ツリーにバグがあると永遠に次が呼ばれない 一般的な用途(推奨)
stop_and_execute 現在のツリーを中断して次をトリガー 即座に切り替え 処理が途中で止まる 割り込み処理

選び方の目安

特別な理由がなければ wait_for_completion を使用してください。

  • wait_for_completion: TTS再生やEventResponseの送信が確実に完了してから次へ進みたい場合
  • continue_immediately: レイテンシを極限まで下げたい場合(並行実行を許容)
  • stop_and_execute: ユーザー割り込みなど、現在の処理を即座に中断したい場合

次の発話にデータを渡す方法

SelfTriggerで連続処理を行う際、次の発話に情報を引き継ぐ方法は3つあります。

比較表

方法 クライアント可視 永続性 プロンプト影響 用途
message_data 一時的な制御情報(フラグ、モード切替)
states.xxx △(明示的に参照) 永続的な状態、クライアント監視が必要な値
DialogAdd (system) ✓(強い) 会話文脈、LLMへの強い指示

1. message_data(一時的な制御情報)

次のトリガーにのみ渡される一時的なデータです。処理が終わると消えます。

self_trigger:
  type: SelfTrigger
  config:
    message_data: 'lambda: {"use_tool": True, "mode": "search"}'

Entryで受け取り:

entry:
  type: EntryChain
  config:
    output_items:
      use_tool: 'lambda message_data: message_data.get("use_tool", False)'
      mode: 'lambda message_data: message_data.get("mode", "default")'

用途: フラグ、モード切替、一時的な指示


2. states.xxx(永続的な状態)

セッション中ずっと保持される状態です。クライアントからEventResponseで送信すれば監視可能です。

# 状態の保存
save_state:
  type: StateSetChain
  config:
    update_states:
      current_speaker: '{{ values.next_speaker }}'
      turn_count: '{{ (states.turn_count | default(0) | int) + 1 }}'

# SelfTriggerから参照
self_trigger:
  type: SelfTrigger
  config:
    trigger: 'lambda: states.turn_count < 5'

クライアントへの送信:

response:
  type: EventResponseChain
  config:
    fn:
      speaker: '{{ states.current_speaker }}'
      turn: '{{ states.turn_count }}'

用途: ターン数、現在の話者、モード状態など


3. DialogAdd(会話履歴への追加)

会話履歴に追加され、LLMのプロンプトに強く影響します。systemロールで追加すると、LLMへの指示として機能します。

add_context:
  type: DialogAddChain
  config:
    role: system
    text: |-
      【ツール実行結果】
      質問: {{ states.question }}
      回答: {{ states.answer }}

      上記の情報を踏まえて、ユーザーに回答してください。

LLMでの参照(自動的にhistoryに含まれる):

llm:
  type: GeppettoLLMChain
  config:
    # history_formatで自動的にsystemメッセージも含まれる
    # 特別な設定なしで、追加したsystemメッセージがプロンプトに影響

用途: ツール実行結果、文脈情報、LLMへの強い指示


応用例

例1: 連投制御(1キャラが3回連続発話)

キャラクターAが3回連続で発話し、回数に応じてプロンプトを切り替える例です。

graph LR
    A[Entry] --> B[DialogHistory]
    B --> C[LLM<br/>プロンプト出し分け]
    C --> D[TTS3]
    C --> E[EventResponse<br/>テキスト]
    C --> F[DialogAdd]
    D --> G[EventResponse<br/>音声]
    G --> H[SelfTrigger<br/>count < 3]

    style A fill:#e8f5e9
    style C fill:#fff4e1
    style H fill:#ffe8e8

ポイント:

  • message_data.countで発話回数を管理(初回は0)
  • Jinja2でプロンプトを回数に応じて出し分け
  • LLM後は3分岐:TTS、テキストEventResponse、DialogAdd
  • 3回発話したらSelfTriggerが停止

Entry(message_dataからcountを取得):

entry:
  type: EntryChain
  config:
    output_items:
      data: 'lambda message_data: message_data'
      count: 'lambda message_data: message_data.get("count", 0)'

LLMのプロンプト(Jinja2で出し分け):

prompt: |-
  {% set my_count = values.count %}

  {% if my_count == 0 %}
  【1回目】最初の挨拶をしてください。自己紹介を含めて。
  {% elif my_count == 1 %}
  【2回目】話を展開してください。質問を投げかけて。
  {% else %}
  【3回目】会話を締めくくってください。また会えることを伝えて。
  {% endif %}

  対話履歴:
  {% for item in values.history[-5:] %}
  - {{ item.role }}: {{ item.text }}
  {% endfor %}

SelfTrigger設定:

self_trigger:
  type: SelfTrigger
  config:
    trigger: 'lambda: values.count < 2'  # 0,1,2の3回実行
    message_data: 'lambda: {"count": values.count + 1}'
    wait_time: '0.5'
    execution_mode: wait_for_completion

例2: 2キャラ交互会話(GPT判断で話者決定)

2人のキャラクターが交互に会話し、GPTが次の話者を決定する例です。レスポンス速度を優先するため、GPT判断は最後に行います。

graph LR
    A[Entry] --> B[DialogHistory]
    B --> C{Gate<br/>speaker判定}
    C -->|キャラA| D1[LLM-A]
    C -->|キャラB| D2[LLM-B]
    D1 --> E[TTS3]
    D2 --> E
    D1 --> F[EventResponse<br/>テキスト]
    D2 --> F
    D1 --> G[DialogAdd]
    D2 --> G
    E --> H[EventResponse<br/>音声]
    H --> I[GPT判断<br/>次の話者]
    I --> J[SelfTrigger]

    style A fill:#e8f5e9
    style C fill:#fff4e1
    style I fill:#e1f5ff
    style J fill:#ffe8e8

ポイント:

  • GPT判断は 最後 に配置(レスポンス速度優先)
  • message_dataで次の話者を渡す(初回はランダム選択)
  • Gateで話者を分岐し、それぞれのLLMを実行
  • LLM後は3分岐:TTS、テキストEventResponse、DialogAdd

Entry(message_dataの受け取り):

entry:
  type: EntryChain
  config:
    output_items:
      data: 'lambda message_data: message_data'
      # message_dataがない場合はランダム選択
      speaker: 'lambda message_data: message_data.get("speaker") or random.choice(["char_a", "char_b"])'
      count: 'lambda message_data: message_data.get("count", 0)'

Gate(話者分岐):

gate_char_a:
  type: Gate
  config:
    trigger: 'lambda: values.speaker == "char_a"'

gate_char_b:
  type: Gate
  config:
    trigger: 'lambda: values.speaker == "char_b"'

GPT判断(最後に配置、次の話者を決定):

decision_maker:
  type: PublicLLMChain
  config:
    prompt: |-
      2人のキャラクターが会話しています。
      - char_a: 元気で明るい女の子
      - char_b: 落ち着いた知的な男性

      今回の発話者: {{ values.speaker }}
      発話内容: {{ values.response }}

      次に話すべきキャラクターと、会話を続けるかを判断してください。
    response_schema:
      type: object
      properties:
        next_speaker:
          type: string
          enum: ["char_a", "char_b"]
        should_continue:
          type: boolean
      required: ["next_speaker", "should_continue"]

SelfTrigger(message_dataで次の話者とカウントを渡す):

self_trigger:
  type: SelfTrigger
  config:
    trigger: 'lambda: values.response.should_continue'
    message_data: |-
      lambda: {
        "speaker": values.response.next_speaker,
        "count": values.count + 1
      }
    wait_time: '0.3'
    execution_mode: wait_for_completion

例3: キーワード検知で会話終了

1キャラがずっと話し続け、「さよなら」「おやすみ」などのキーワード検知、または連投数5回で会話を終了する例です。

graph LR
    A[Entry] --> B[DialogHistory]
    B --> C[LLM]
    C --> D[LambdaFunc<br/>キーワード検知]
    D --> E[TTS3]
    D --> F[EventResponse<br/>テキスト]
    D --> G[DialogAdd]
    E --> H[EventResponse<br/>音声]
    H --> I[SelfTrigger<br/>終了条件判定]

    style A fill:#e8f5e9
    style C fill:#fff4e1
    style D fill:#e1f5ff
    style I fill:#ffe8e8

ポイント:

  • LambdaFuncで応答テキストからキーワードを検知
  • 「さよなら」「おやすみ」が含まれていたら終了
  • 連投数(message_data.count)が5回以上でも終了
  • LLM/LambdaFunc後は3分岐:TTS、テキストEventResponse、DialogAdd

Entry(message_dataからcountを取得):

entry:
  type: EntryChain
  config:
    output_items:
      data: 'lambda message_data: message_data'
      count: 'lambda message_data: message_data.get("count", 0)'

LambdaFunc(キーワード検知):

keyword_detector:
  type: LambdaFunc
  config:
    func: |-
      async def my_func(input_message, output_message):
          text = input_message.values.response.x or ""
          count = input_message.values.count.x or 0

          # 終了キーワードのリスト
          end_keywords = ["さよなら", "おやすみ", "またね", "バイバイ", "じゃあね"]

          # キーワード検知
          should_end = any(kw in text for kw in end_keywords)

          await output_message.update('response', text, is_last=True)
          await output_message.update('should_end', should_end, is_last=True)
          await output_message.update('count', count, is_last=True)
    gates:
      - values.response
    process_mode: gate

SelfTrigger(終了条件判定):

self_trigger:
  type: SelfTrigger
  config:
    # キーワード検知 OR 5回以上で終了
    trigger: 'lambda: not values.should_end and values.count < 5'
    message_data: 'lambda: {"count": values.count + 1}'
    wait_time: '0.5'
    execution_mode: wait_for_completion

終了条件の解説:

条件 説明
not values.should_end 終了キーワードが検知されていない
values.count < 5 連投数が5回未満

両方の条件を満たす場合のみ継続します。どちらかがFalseになると会話終了です。


例4: Agenticな挙動(ツール呼び出しループ)

通常はGeppettoで即座に応答しつつ、必要に応じてGPTを繰り返し呼び出して情報収集し、完了後にGeppettoで応答する例です。

graph TB
    A[Entry] --> B[DialogHistory]
    B --> C{Gate<br/>use_tool?}

    C -->|False| D[Geppetto<br/>通常応答]
    D --> E[TTS3]
    E --> F[EventResponse]
    F --> G[GPT判断<br/>ツール必要?]
    G --> H[SelfTrigger<br/>use_tool設定]

    C -->|True| I[GPT<br/>質問に回答]
    I --> J[StateSet<br/>answer保存]
    J --> K[GPT判断<br/>回答十分?]
    K --> L[SelfTrigger<br/>継続/完了判定]

    style A fill:#e8f5e9
    style C fill:#fff4e1
    style D fill:#e1f5ff
    style I fill:#ffe8e8
    style H fill:#ffcdd2
    style L fill:#ffcdd2

処理フロー:

  1. 通常パス(use_tool=False): Geppettoで応答 → 構造化出力で「高度なAIが必要か」を判断
  2. ツールパス(use_tool=True): GPTで質問に回答 → 回答が十分か判断 → 不十分なら再質問、十分なら通常パスへ戻る
  3. 統合応答: 通常パスに戻った際、states.answerがあればプロンプトに含めて応答

Entry(use_toolフラグの受け取り):

entry:
  type: EntryChain
  config:
    output_items:
      data: 'lambda message_data: message_data'
      use_tool: 'lambda message_data: message_data.get("use_tool", False)'

Gate(use_toolによる分岐):

# 通常パス(Geppettoで応答)
gate_normal:
  type: Gate
  config:
    trigger: 'lambda: not values.use_tool'

# ツールパス(GPTで情報収集)
gate_tool:
  type: Gate
  config:
    trigger: 'lambda: values.use_tool == True'

Geppetto(通常応答 + ツール結果の統合):

geppetto_response:
  type: GeppettoLLMChain
  config:
    scene: |-
      あなたはアシスタントです。ユーザーの質問に答えてください。

      {% if states.question and states.answer %}
      【参考情報】
      質問: {{ states.question }}
      回答: {{ states.answer }}

      上記の情報を踏まえて、ユーザーに分かりやすく説明してください。
      {% endif %}
    # ... その他の設定

GPT判断(ツール呼び出し要否):

通常応答後、高度なAIの呼び出しが必要かを判断します。

tool_decision:
  type: PublicLLMChain
  config:
    prompt: |-
      ユーザーの質問に対して、キャラクターが回答しました。

      ユーザー質問: {{ values.user_input }}
      キャラクター回答: {{ values.response }}

      この回答で十分ですか?それとも、より正確な情報を得るために
      高度なAI(GPT-4など)を呼び出すべきですか?

      呼び出す場合は、AIへの質問内容も指定してください。
    response_schema:
      type: object
      properties:
        use_tool:
          type: boolean
          description: "高度なAIを呼び出すか"
        question:
          type: string
          description: "AIへの質問内容(use_tool=trueの場合)"
      required: ["use_tool"]

StateSet(ツール呼び出し情報を保存):

save_tool_decision:
  type: StateSetChain
  config:
    update_states:
      question: '{{ values.response.question if values.response.use_tool else "" }}'

SelfTrigger(ツール呼び出しへ):

trigger_tool:
  type: SelfTrigger
  config:
    trigger: 'lambda: states.use_tool == True'
    message_data: 'lambda: {"use_tool": True}'
    wait_time: '0.1'
    execution_mode: wait_for_completion

GPT(質問に回答):

tool_gpt:
  type: PublicLLMChain
  config:
    provider: openai
    model: gpt-4o
    prompt: |-
      以下の質問に正確に回答してください。

      質問: {{ states.question }}
    max_tokens: 500

StateSet(回答を保存):

save_answer:
  type: StateSetChain
  config:
    update_states:
      answer: '{{ values.response }}'

GPT判断(回答の十分性):

answer_check:
  type: PublicLLMChain
  config:
    prompt: |-
      質問: {{ states.question }}
      回答: {{ states.answer }}

      この回答は質問に対して十分ですか?
      不十分な場合は、追加で確認すべき質問を指定してください。
    response_schema:
      type: object
      properties:
        is_sufficient:
          type: boolean
        follow_up_question:
          type: string
          description: "追加質問(不十分な場合)"
      required: ["is_sufficient"]

SelfTrigger(継続/完了判定):

trigger_loop:
  type: SelfTrigger
  config:
    trigger: 'lambda: True'  # 常にトリガー(分岐は内部で制御)
    message_data: |-
      lambda: {
        "use_tool": not states.is_sufficient,  # 不十分ならツール継続
      }
    wait_time: '0.1'
    execution_mode: wait_for_completion

StateSet(追加質問または完了処理):

update_or_clear:
  type: StateSetChain
  config:
    update_states:
      # 不十分なら追加質問を設定
      question: '{{ values.response.follow_up_question if not values.response.is_sufficient else states.question }}'

通常パスに戻った後のStateSetクリア:

Geppettoで応答した後、使用したstatesをクリアします。

clear_tool_states:
  type: StateSetChain
  config:
    update_states:
      question: ''
      answer: ''

処理の流れ(例):

ステップ use_tool states.question states.answer 動作
1 False - - Geppetto応答「調べてみますね」
2 - - - GPT判断: use_tool=True, question="最新の為替レート"
3 True "最新の為替レート" - GPT回答「1ドル=150円です」
4 - "最新の為替レート" "1ドル=150円" 回答十分 → use_tool=False
5 False "最新の為替レート" "1ドル=150円" Geppetto「調べた結果、1ドル150円でした!」
6 - "" "" statesクリア

安全性とベストプラクティス

無限ループについて

SelfTriggerには回数制限がありません。無限ループが発生しても、セッションを切断すればループは停止します

セッション切断でループ停止

WebSocket接続が切断されると、セッションが無効化され、新しいトリガーの起動先がなくなるため、ループは自動的に停止します。

トリガー条件の設計

明確な終了条件を設計することが重要です。

# 良い例: 明確な終了条件
lambda: int(states.counter or 0) < 5

# 良い例: フラグによる制御
lambda: states.should_continue == True

# 悪い例: 常にTrue(無限ループの危険)
lambda: True

execution_modeの選択

リソースリークを防ぐため、特別な理由がなければwait_for_completionを使用してください。

execution_mode: wait_for_completion  # 推奨

トラブルシューティング

トリガーが発火しない

原因 対策
条件が常にFalse ログでstatesの値を確認
前のツリーが完了していない execution_modeを確認

無限ループになる

原因 対策
終了条件がない triggerに明確な条件を設定
カウンターが更新されない StateSetChainの設定を確認
意図しないループ セッションを切断して停止

メモリ使用量が増加する

原因 対策
continue_immediatelyで並行実行 wait_for_completionに変更
履歴が蓄積している DialogHistoryChainmax_history_itemsを調整