Closed4

OpenAI「Agents SDK」⑨リアルタイムエージェント

kun432kun432

クイックスタート

https://openai.github.io/openai-agents-python/ja/realtime/quickstart/

注意書きにもあるけど

ということで。

ローカルのMacでやる。uvでプロジェクト作成

uv init -p 3.12 openai-realtime-agents-work && cd $_

Agents SDKをインストール。extrasはいらないのかな?

uv add openai-agents
出力
(snip)
 + openai-agents==0.3.0
(snip)

サンプルコード。前回やったVoicePipeline の場合はエージェントをワークフローでラップして、さらにそれをパイプラインでラップする感じだったけど、リアルタイムの場合はより直接的にRealtimeAgentで定義してRealtimeRunnerでラップするという感じ。

sample.py
import asyncio
from agents.realtime import RealtimeAgent, RealtimeRunner

async def main():
    # エージェントを作成
    agent = RealtimeAgent(
        name="Assistant",
        instructions="You are a helpful voice assistant. Keep responses brief and conversational.",
    )
    
    # ランナーを設定
    runner = RealtimeRunner(
        starting_agent=agent,
        config={
            "model_settings": {
                "model_name": "gpt-realtime",
                "voice": "ash",
                "modalities": ["audio"],
                "input_audio_format": "pcm16",
                "output_audio_format": "pcm16",
                "input_audio_transcription": {"model": "gpt-4o-mini-transcribe"},
                "turn_detection": {"type": "semantic_vad", "interrupt_response": True},
            }
        },
    )
    
    # セッションを開始
    session = await runner.run()

    async with session:
        print("セッションが開始されました!エージェントはリアルタイムで音声応答をストリーミングします。")
        # イベントを処理
        async for event in session:
            try:
                if event.type == "agent_start":
                    print(f"エージェントが開始されました: {event.agent.name}")
                elif event.type == "agent_end":
                    print(f"エージェントが終了されました: {event.agent.name}")
                elif event.type == "handoff":
                    print(f"{event.from_agent.name} から {event.to_agent.name} へハンドオフされました")
                elif event.type == "tool_start":
                    print(f"ツールが開始されました: {event.tool.name}")
                elif event.type == "tool_end":
                    print(f"ツールが終了されました: {event.tool.name}; output: {event.output}")
                elif event.type == "audio_end":
                    print("オーディオが終了されました")
                elif event.type == "audio":
                    # メタデータ付きでコールバック再生用の音声をキューに入れる
                    # 非ブロッキングで投入。キューは無制限のためドロップは発生しない。
                    pass
                elif event.type == "audio_interrupted":
                    print("オーディオが中断されました")
                    # オーディオコールバックで穏やかなフェード+フラッシュを開始し、ジッタバッファを再構築する。
                elif event.type == "error":
                    print(f"エラーが発生しました: {event.error}")
                elif event.type == "history_updated":
                    pass  # 頻出イベントのためスキップ
                elif event.type == "history_added":
                    pass  # 頻出イベントのためスキップ
                elif event.type == "raw_model_event":
                    print(f"生のモデルイベント: {_truncate_str(str(event.data), 200)}")
                else:
                    print(f"不明なイベントタイプ: {event.type}")
            except Exception as e:
                print(f"イベント処理中にエラーが発生しました: {_truncate_str(str(e), 200)}")

def _truncate_str(s: str, max_length: int) -> str:
    if len(s) > max_length:
        return s[:max_length] + "..."
    return s

if __name__ == "__main__":
    # セッションを実行
    asyncio.run(main())

実行してみるとエラー。ってかやっぱりextrasが足りないんじゃないかな?

uv run sample.py
出力
ModuleNotFoundError: No module named 'websockets'

パッケージ追加

uv add websockets

再度実行、エラーは出ないけど、以下からうんともすんとも動かない。

セッションが開始されました!エージェントはリアルタイムで音声応答をストリーミングします。
生のモデルイベント: RealtimeModelRawServerEvent(data={'type': 'session.created', 'event_id': 'event_CGMCJoHPcyOp6wsH558sy', 'session': {'type': 'realtime', 'object': 'realtime.session', 'id': 'sess_CGMCJK7mQ4LAHj8PSXeYj'...
生のモデルイベント: RealtimeModelRawServerEvent(data={'type': 'session.updated', 'event_id': 'event_CGMCKG6ZvHEOxwBhw8tuQ', 'session': {'type': 'realtime', 'object': 'realtime.session', 'id': 'sess_CGMCJK7mQ4LAHj8PSXeYj'...
生のモデルイベント: RealtimeModelRawServerEvent(data={'type': 'session.updated', 'event_id': 'event_CGMCKE3zmaBoGP1S0RvKn', 'session': {'type': 'realtime', 'object': 'realtime.session', 'id': 'sess_CGMCJK7mQ4LAHj8PSXeYj'...

マイクから入力音声を取得するとかは自分で実装しないといけないんじゃないかなぁ。RealtimeAgentRealtimeRunner がどこまでやってくれるのかこの時点でわからないところに、これをもって「動作する完全な例」と書くのはどうしたもんかな、という気がする。

一応パラメータは以下。

カテゴリ オプション名 説明・選択肢例
モデル設定 model_name 利用可能なリアルタイムモデル(例: gpt-realtime
voice 音声の種類(alloyechofableonyxnovashimmer
modalities 有効化するモード(["text"] または ["audio"]
音声設定 input_audio_format 入力音声フォーマット(pcm16g711_ulawg711_alaw
output_audio_format 出力音声フォーマット
input_audio_transcription 文字起こしの設定
ターン検出 type 検出方法(server_vadsemantic_vad
threshold 音声活動のしきい値(0.0–1.0)
silence_duration_ms ターン終了を検出する無音時間(ミリ秒)
prefix_padding_ms 発話前の音声パディング(ミリ秒)

とりあえずサンプルは以下にもある。

https://github.com/openai/openai-agents-python/tree/main/examples/realtime

レポジトリをまるっとクローンしてサンプルのディレクトリに。

git clone https://github.com/openai/openai-agents-python && cd openai-agents-python
cd examples/realtime/

デモは3つあるけど、cliのサンプルを。

cd cli
uv run demo.py
出力
ModuleNotFoundError: No module named 'numpy'

パッケージ追加

uv add python

再実行

uv run demo.py

起動したみたい。

出力
Connecting, may take a few seconds...
Connected. Starting audio recording...
Audio recording started. You can start speaking - expect lots of logs!
Raw model event: RealtimeModelRawServerEvent(data={'type': 'session.created', 'event_id': 'event_CGMlYuMf7QLlUmL8QIdz4', 'session': {'type': 'realtime', 'object': 'realtime.session', 'id': 'sess_CGMlYD4ofpvLEIaxBwyzT'...
Raw model event: RealtimeModelRawServerEvent(data={'type': 'session.updated', 'event_id': 'event_CGMlYzsubGVIxMEyobReT', 'session': {'type': 'realtime', 'object': 'realtime.session', 'id': 'sess_CGMlYD4ofpvLEIaxBwyzT'...
Raw model event: RealtimeModelRawServerEvent(data={'type': 'session.updated', 'event_id': 'event_CGMlY7bzQawJ12CklChtA', 'session': {'type': 'realtime', 'object': 'realtime.session', 'id': 'sess_CGMlYD4ofpvLEIaxBwyzT'...

音声で発話するとこんな感じで認識されているのがわかる。

出力
Raw model event: RealtimeModelRawServerEvent(data={'type': 'input_audio_buffer.speech_started', 'event_id': 'event_CGMldigVaG6ki7t69NENf', 'audio_start_ms': 4780, 'item_id': 'item_CGMldPq1qS5Kp23GHErT3'}, type='raw_se...
Raw model event: RealtimeModelRawServerEvent(data={'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_CGMlfLHj7cZLiL9rNGdae', 'audio_end_ms': 7320, 'item_id': 'item_CGMldPq1qS5Kp23GHErT3'}, type='raw_serv...
Raw model event: RealtimeModelRawServerEvent(data={'type': 'input_audio_buffer.committed', 'event_id': 'event_CGMlfjousSV09J6L8o2LJ', 'previous_item_id': '', 'item_id': 'item_CGMldPq1qS5Kp23GHErT3'}, type='raw_server_...
Raw model event: RealtimeModelRawServerEvent(data={'type': 'conversation.item.added', 'event_id': 'event_CGMlfGZL0SZGPreSzRVp8', 'previous_item_id': '', 'item': {'id': 'item_CGMldPq1qS5Kp23GHErT3', 'type': 'message', ...
(snip)
Raw model event: RealtimeModelInputAudioTranscriptionCompletedEvent(item_id='item_CGMldPq1qS5Kp23GHErT3', transcript='おはようございます。', type='input_audio_transcription_completed')

そしてレスポンスが音声で返ってくる。

出力
Raw model event: RealtimeModelAudioEvent(data=b'\xff\xff\x00\x00\x01\x00\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\x04\x00\x02\x00\xff\xff\x07\x00\xff\xff\x00\x00\xfe\xff\x00\x00\xfa\xff\x00\x00\x01\x00\xff\xff\xfe\xff\...
Raw model event: RealtimeModelRawServerEvent(data={'type': 'response.output_audio.delta', 'event_id': 'event_CGMlg1dQIbXm9fTOfCcCA', 'response_id': 'resp_CGMlfVv0iVPwMOCbGGrvf', 'item_id': 'item_CGMlfSCxphE4X9OQtW3gm'...
Raw model event: RealtimeModelAudioEvent(data=b'\xde\x03\xec\x05\xbb\x04\xa3\x02\xd7\x029\x04#\x01m\xff}\xfe;\xfcy\xf9\x80\xf9\xa7\xf9\x0f\xf8\x8e\xf8R\xf99\xfbj\xfa*\xfbt\xfbA\xfbA\xfc\xf9\xfca\xfd\xe5\xfbg\xfb\x7f\x...
Raw model event: RealtimeModelRawServerEvent(data={'type': 'response.output_audio_transcript.delta', 'event_id': 'event_CGMlguz2WaS9VNTwFOAqS', 'response_id': 'resp_CGMlfVv0iVPwMOCbGGrvf', 'item_id': 'item_CGMlfSCxphE...
Raw model event: RealtimeModelTranscriptDeltaEvent(item_id='item_CGMlfSCxphE4X9OQtW3gm', delta=' to', response_id='resp_CGMlfVv0iVPwMOCbGGrvf', type='transcript_delta')
Raw model event: RealtimeModelRawServerEvent(data={'type': 'response.output_audio.delta', 'event_id': 'event_CGMlgrRtKI0tcf6CtrrwO', 'response_id': 'resp_CGMlfVv0iVPwMOCbGGrvf', 'item_id': 'item_CGMlfSCxphE4X9OQtW3gm'...
Raw model event: RealtimeModelAudioEvent(data=b'\xfa\xff\x02\x00\x0b\x00\x11\x00\x15\x00\x18\x00\x19\x00\x1b\x00\x1d\x00 \x00"\x00$\x00&\x00\'\x00$\x00!\x00\x1d\x00\x1a\x00\x18\x00\x16\x00\x15\x00\x12\x00\x10\x00\x0f\...
Raw model event: RealtimeModelRawServerEvent(data={'type': 'response.output_audio_transcript.delta', 'event_id': 'event_CGMlgqixqF2Hs0c2jhGwv', 'response_id': 'resp_CGMlfVv0iVPwMOCbGGrvf', 'item_id': 'item_CGMlfSCxphE...
Raw model event: RealtimeModelTranscriptDeltaEvent(item_id='item_CGMlfSCxphE4X9OQtW3gm', delta=' you', response_id='resp_CGMlfVv0iVPwMOCbGGrvf', type='transcript_delta')
Raw model event: RealtimeModelRawServerEvent(data={'type': 'response.output_audio_transcript.delta', 'event_id': 'event_CGMlg1FdBCVW0z0XGMHsn', 'response_id': 'resp_CGMlfVv0iVPwMOCbGGrvf', 'item_id': 'item_CGMlfSCxphE...
Raw model event: RealtimeModelTranscriptDeltaEvent(item_id='item_CGMlfSCxphE4X9OQtW3gm', delta='。', response_id='resp_CGMlfVv0iVPwMOCbGGrvf', type='transcript_delta')
Raw model event: RealtimeModelRawServerEvent(data={'type': 'response.output_audio.delta', 'event_id': 'event_CGMlgIynzqj2jR5U0Rytt', 'response_id': 'resp_CGMlfVv0iVPwMOCbGGrvf', 'item_id': 'item_CGMlfSCxphE4X9OQtW3gm'...
Raw model event: RealtimeModelAudioEvent(data=b'!\nZ\x05\xf7\x02H\x05:\x04,\xfe\xb7\xf7\xdd\xf4\x82\xee\xbd\xe7|\xe2\xc9\xdc\x8e\xd6K\xce\xf1\xc8\x8a\xc3\x9f\xc2\r\xc4j\xc4^\xc0!\xbd\xb4\xbdB\xc02\xc27\xc4B\xc9l\xcc\x...
Raw model event: RealtimeModelRawServerEvent(data={'type': 'response.output_audio.delta', 'event_id': 'event_CGMlgOGYOuqn0P83QLRR5', 'response_id': 'resp_CGMlfVv0iVPwMOCbGGrvf', 'item_id': 'item_CGMlfSCxphE4X9OQtW3gm'...
Raw model event: RealtimeModelAudioEvent(data=b'\x97\x03\x08\xff\xaf\xf8]\xf4\x89\xf0d\xee\xc0\xec|\xeb\xb5\xed\x96\xf1I\xf4=\xf9\xd4\xfdv\x01\xe2\x05M\x08a\x08S\t\x00\t\x02\x06\x03\x02P\xfe(\xfa\xc4\xf6:\xf4w\xf1[\xf...
Raw model event: RealtimeModelRawServerEvent(data={'type': 'response.output_audio_transcript.delta', 'event_id': 'event_CGMlgiJFBPjUf1ydtQxHX', 'response_id': 'resp_CGMlfVv0iVPwMOCbGGrvf', 'item_id': 'item_CGMlfSCxphE...
Raw model event: RealtimeModelTranscriptDeltaEvent(item_id='item_CGMlfSCxphE4X9OQtW3gm', delta='お', response_id='resp_CGMlfVv0iVPwMOCbGGrvf', type='transcript_delta')
Raw model event: RealtimeModelRawServerEvent(data={'type': 'response.output_audio_transcript.delta', 'event_id': 'event_CGMlgP6HTEqyrYQIR8RWj', 'response_id': 'resp_CGMlfVv0iVPwMOCbGGrvf', 'item_id': 'item_CGMlfSCxphE...
Raw model event: RealtimeModelTranscriptDeltaEvent(item_id='item_CGMlfSCxphE4X9OQtW3gm', delta='は', response_id='resp_CGMlfVv0iVPwMOCbGGrvf', type='transcript_delta')
Raw model event: RealtimeModelRawServerEvent(data={'type': 'response.output_audio.delta', 'event_id': 'event_CGMlgPZMIzLjRvnwaziAI', 'response_id': 'resp_CGMlfVv0iVPwMOCbGGrvf', 'item_id': 'item_CGMlfSCxphE4X9OQtW3gm'...
Raw model event: RealtimeModelAudioEvent(data=b'\xb2\xfa\n\x07\xae\x08\xb4\x06\xe5\xde"\x07c\x17\xf2\xebr\x07\\\xef\xde\xffa\x15\x19\xf3\x1f\xf7\xf6\xfcx\xf93\x17\xb8\x04\xc5\xe8f\xfb\x81\xf6\xa8\x11\xf4\x06\x8c\xf6\x...
Raw model event: RealtimeModelRawServerEvent(data={'type': 'response.output_audio.delta', 'event_id': 'event_CGMlgRSyDjs66ch3oF1hr', 'response_id': 'resp_CGMlfVv0iVPwMOCbGGrvf', 'item_id': 'item_CGMlfSCxphE4X9OQtW3gm'...
Raw model event: RealtimeModelAudioEvent(data=b'\xda\xf9G\xf9\x92\xf9\xc7\xf9\x07\xfa\x8d\xfa\xab\xfa\xc8\xfa=\xfa!\xfbD\xfd\x8e\xfd\xcb\xfe\xd1\xfe\x9c\xfex\xff\x06\xff\xca\xff\xab\x00u\x01:\x03\x81\x04\t\x05\x17\x06...
Raw model event: RealtimeModelRawServerEvent(data={'type': 'response.output_audio.delta', 'event_id': 'event_CGMlgfI1o3R86Qhe2wThk', 'response_id': 'resp_CGMlfVv0iVPwMOCbGGrvf', 'item_id': 'item_CGMlfSCxphE4X9OQtW3gm'...
Raw model event: RealtimeModelAudioEvent(data=b'\x04\x00\x04\x00\x03\x00\x03\x00\x01\x00\x02\x00\x02\x00\x03\x00\x03\x00\x02\x00\x02\x00\x04\x00\x02\x00\x03\x00\x03\x00\x02\x00\x03\x00\x02\x00\x03\x00\x02\x00\x03\x00\...
Raw model event: RealtimeModelRawServerEvent(data={'type': 'response.output_audio_transcript.delta', 'event_id': 'event_CGMlgioRqyca2jedDl8mo', 'response_id': 'resp_CGMlfVv0iVPwMOCbGGrvf', 'item_id': 'item_CGMlfSCxphE...
Raw model event: RealtimeModelTranscriptDeltaEvent(item_id='item_CGMlfSCxphE4X9OQtW3gm', delta='よう', response_id='resp_CGMlfVv0iVPwMOCbGGrvf', type='transcript_delta')
Raw model event: RealtimeModelRawServerEvent(data={'type': 'response.output_audio_transcript.delta', 'event_id': 'event_CGMlgsUrcBWL5DB3mlhB4', 'response_id': 'resp_CGMlfVv0iVPwMOCbGGrvf', 'item_id': 'item_CGMlfSCxphE...
Raw model event: RealtimeModelTranscriptDeltaEvent(item_id='item_CGMlfSCxphE4X9OQtW3gm', delta='ございます', response_id='resp_CGMlfVv0iVPwMOCbGGrvf', type='transcript_delta')
Raw model event: RealtimeModelRawServerEvent(data={'type': 'response.output_audio_transcript.delta', 'event_id': 'event_CGMlgZJ1bWwQy1SooLh8o', 'response_id': 'resp_CGMlfVv0iVPwMOCbGGrvf', 'item_id': 'item_CGMlfSCxphE...
Raw model event: RealtimeModelTranscriptDeltaEvent(item_id='item_CGMlfSCxphE4X9OQtW3gm', delta='!', response_id='resp_CGMlfVv0iVPwMOCbGGrvf', type='transcript_delta')
Raw model event: RealtimeModelRawServerEvent(data={'type': 'response.output_audio_transcript.delta', 'event_id': 'event_CGMlgu8nlMOJQ5kp8yqzq', 'response_id': 'resp_CGMlfVv0iVPwMOCbGGrvf', 'item_id': 'item_CGMlfSCxphE...
Raw model event: RealtimeModelTranscriptDeltaEvent(item_id='item_CGMlfSCxphE4X9OQtW3gm', delta='今日は', response_id='resp_CGMlfVv0iVPwMOCbGGrvf', type='transcript_delta')

トレースはこんな感じ。

ざっとコードを読んでみたけど、エージェント部分はシンプルに1エージェントが定義されているだけで、オーディオ周りの処理がほとんどを占めている感じ。

もう一つのサンプルも。

 cd ../app/
uv run server.py
出力
INFO:     Started server process [98695]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

ブラウザでアクセスするとこんな画面。

コードを見ると、受付エージェントがメインで、そこからFAQに答えるエージェント、座席の予約を行うエージェントにハンドオフされるようになっている。ハンドオフされるエージェントはツールが定義されている。

https://github.com/openai/openai-agents-python/blob/main/examples/realtime/app/agent.py

こんな感じで使える。

ざっとやってみた限り、リアルタイムモデルだけあってレスポンスはとても良好な感じ。

kun432kun432

ガイド

https://openai.github.io/openai-agents-python/ja/realtime/guide/

概要にある通り、Realtime APIを使うのが「リアルタイムエージェント」


アーキテクチャ

コンポーネント

コンポーネント名 説明
RealtimeAgent instructions、tools、handoffsで構成されるエージェント。会話の主役になる。
RealtimeRunner 設定を管理する。runner.run() でセッションを開始する。
RealtimeSession 単一の対話セッション。ユーザーが会話を開始してから終了するまで1セッション。
RealtimeModel 基盤となるモデルのインターフェイス。OpenAIのWebSocket実装。

セッションフロー

セッションフローの流れは以下とある

  1. RealtimeAgent を作成 し、instructions、tools、handoffs を設定します。
  2. RealtimeRunner をセットアップ し、エージェントと設定オプションを指定します。
  3. セッションを開始 します。await runner.run() を使用すると RealtimeSession が返ります。
  4. 音声またはテキストメッセージを送信 します。send_audio() または send_message() を使用します。
  5. イベントをリッスン します。セッションを反復処理して、音声出力、文字起こし、ツール呼び出し、ハンドオフ、エラーなどのイベントを受け取ります。
  6. **割り込みに対応 **します。ユーザーがエージェントの発話に重ねて話した場合、現在の音声生成は自動的に停止します。

以下のサンプルコードで見てみる。

https://github.com/openai/openai-agents-python/tree/efa88f79e759b17e65572919239cf9593ac2f686/examples/realtime/cli/demo.py

  1. RealtimeAgent を作成 し、instructions、tools、handoffs を設定します。

https://github.com/openai/openai-agents-python/blob/efa88f79e759b17e65572919239cf9593ac2f686/examples/realtime/cli/demo.py#L36-L46

  1. RealtimeRunner をセットアップ し、エージェントと設定オプションを指定します。

https://github.com/openai/openai-agents-python/blob/efa88f79e759b17e65572919239cf9593ac2f686/examples/realtime/cli/demo.py#L219-L230

  1. セッションを開始 します。await runner.run() を使用すると RealtimeSession が返ります。

https://github.com/openai/openai-agents-python/blob/efa88f79e759b17e65572919239cf9593ac2f686/examples/realtime/cli/demo.py#L231-L233

  1. 音声またはテキストメッセージを送信 します。send_audio() または send_message() を使用します。

https://github.com/openai/openai-agents-python/blob/efa88f79e759b17e65572919239cf9593ac2f686/examples/realtime/cli/demo.py#L300-L308

  1. イベントをリッスン します。セッションを反復処理して、音声出力、文字起こし、ツール呼び出し、ハンドオフ、エラーなどのイベントを受け取ります。

https://github.com/openai/openai-agents-python/blob/efa88f79e759b17e65572919239cf9593ac2f686/examples/realtime/cli/demo.py#L321-L357

  1. **割り込みに対応 **します。ユーザーがエージェントの発話に重ねて話した場合、現在の音声生成は自動的に停止します。

ここは4でも処理されている

https://github.com/openai/openai-agents-python/blob/efa88f79e759b17e65572919239cf9593ac2f686/examples/realtime/cli/demo.py#L341-L345


エージェントの設定

リアルタイムの場合は専用のエージェント、RealtimeAgent を使って、エージェントを定義する。VoicePipeline とはここが違うところだが、通常の Agent クラスとは以下が異なる。

  • エージェント単位でモデルは選択できない。セッション単位で設定する。まあリアルタイムモデルなのでそれはそうという気がする。
  • structured outputはサポートされない。音声出力なのでそれはそう。
  • 音声(alloyとかechoとか)はエージェント単位で設定できるが、最初のエージェントが話し始めたあとは変更できない。ここはちょっと意味がわからなかった。

セッションの設定

モデル設定

モデルの動作を設定可能

  • モデル名(gpt-realtime など)
  • 音声( alloyechofableonyxnovashimmer
  • モダリティ(テキスト and/or 音声)
  • 音声フォーマット(入力・出力の両方、デフォルトは PCM16)

音声設定

セッションの音声入力・出力の扱いを設定可能

  • 音声入力
    • Whisper などのモデルを使った入力音声の文字起こし
    • 言語設定
    • 文字起こしプロンプト
  • ターン検出
    • VADのしきい値
    • 無音時間
    • 検出した発話の前後のパディング

ツールと関数 / ハンドオフの作成

ここは通常のAgentと同じように思える。


イベント処理

イベント名 説明
audio エージェントの応答からの生の音声データ。音声再生に利用。
audio_end エージェントの発話が終了したタイミングで発生。
audio_interrupted ユーザーがエージェントの発話に割り込んだときに発生。音声再生を即座に停止する。
tool_start ツール(関数)が実行開始したときに発生。
tool_end ツール(関数)の実行が終了したときに発生。
handoff エージェントのハンドオフ(会話の引き継ぎ)が発生。
error 何かエラーが起きたときに発生。

詳細は以下。公式のサンプルコードも参考になりそう。

https://openai.github.io/openai-agents-python/ref/realtime/events/#agents.realtime.events.RealtimeSessionEvent


ガードレール

通常のAgentの場合との違い

  • RealtimeAgentがサポートするのは出力ガードレールのみ
  • まとまった出力単位で定期的に実行。
    • 「デバウンス」というらしい。
    • 逐次実行するとパフォーマンスに影響するため。
    • 出力単位の長さ=デバウンス長は設定可能でデフォルト100文字」
  • ガードレールが作動しても例外は発生しない。

音声処理

  • 音声の入力
    • session.send_audio(audio_bytes): 音声を送信
    • session.send_message(): テキストを送信
  • 音声の出力
    • audio イベントを監視して、任意の音声ライブラリで再生
    • audio_interrupted イベントを監視して、エージェント発話中のユーザの割り込みを検知
      • 再生中の再生を停止
      • キューにある音声をクリア

モデルへの直接アクセス

モデルに直接アクセスして、カスタムリスナーの設定などができるらしい。

session.model.add_listener(my_custom_listener)

RealtimeModel をより低レベルで制御したい場合に使うみたい。


コード例

https://github.com/openai/openai-agents-python/tree/main/examples/realtime

見た感じ、必要なものが一通り実装されているようなので、これをベースにすれば良さそう。

kun432kun432

まとめ

自分はAgents SDKをある程度触って大体の雰囲気は理解したつもりだけども、Realtime APIはほとんど触ってないので、とりあえず Realtime API の基本を理解した上で使うのが良さそうと感じた。

あと、VoicePipelineRealtimeAgent で、同じ音声エージェントだけどもコンポーネント構成が全然違ってるのも興味深い。

このスクラップは3日前にクローズされました