セルフトリガの活用
セルフトリガの活用¶
このガイドでは、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'
動作の流れ:
event.triggerで開始counterを1に更新responseで「回数: 1」を送信self_triggerでトリガー条件を評価(1 < 3 → True)- 0.5秒待機後、自動的に再起動
- 2回目、3回目...と繰り返し
- counter=3でトリガー条件がFalseになり終了
実行モードの選択¶
なぜ実行モードが必要か¶
理想的には、現在のツリー処理が完全に終了してからSelfTriggerが発火されるべきです。しかし実際には、 SelfTriggerに到達した時点で、ツリー内の他のコンポーネント(TTS、EventResponseなど)がまだ処理中 であることが多いです。
execution_modeは、SelfTriggerの条件が満たされwait_timeが経過した時点で、 現在実行中のツリー処理をどう扱うか を制御します。
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 処理フロー:
- 通常パス(use_tool=False): Geppettoで応答 → 構造化出力で「高度なAIが必要か」を判断
- ツールパス(use_tool=True): GPTで質問に回答 → 回答が十分か判断 → 不十分なら再質問、十分なら通常パスへ戻る
- 統合応答: 通常パスに戻った際、
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(回答を保存):
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をクリアします。
処理の流れ(例):
| ステップ | 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を使用してください。
トラブルシューティング¶
トリガーが発火しない¶
| 原因 | 対策 |
|---|---|
| 条件が常にFalse | ログでstatesの値を確認 |
| 前のツリーが完了していない | execution_modeを確認 |
無限ループになる¶
| 原因 | 対策 |
|---|---|
| 終了条件がない | triggerに明確な条件を設定 |
| カウンターが更新されない | StateSetChainの設定を確認 |
| 意図しないループ | セッションを切断して停止 |
メモリ使用量が増加する¶
| 原因 | 対策 |
|---|---|
continue_immediatelyで並行実行 | wait_for_completionに変更 |
| 履歴が蓄積している | DialogHistoryChainのmax_history_itemsを調整 |