コンテンツにスキップ

Realtime Agent を Twilio に接続

Twilio は、電話の通話音声の 元 オーディオを WebSocket サーバーへ送る Media Streams API を提供しています。このセットアップを使って、あなたの 音声エージェントの概要 を Twilio に接続できます。デフォルトの Realtime Session トランスポートを websocket モードで使い、Twilio から来るイベントを Realtime Session に接続することも可能です。ただし、その場合は適切なオーディオ形式の設定や、Web ベースの会話よりも通話では遅延が大きくなるため、割り込みタイミングの調整が必要になります。

セットアップ体験を改善するために、Twilio への接続、割り込み処理、オーディオ転送を代わりに扱う専用のトランスポート層を用意しました。

  1. Twilio アカウントと Twilio の電話番号を用意する

  2. Twilio からのイベントを受け取れる WebSocket サーバーを用意する

    ローカル開発の場合は、ngrokCloudflare Tunnel などのローカルトンネルを設定して、ローカル サーバーを Twilio からアクセス可能にする必要があります。TwilioRealtimeTransportLayer を使って Twilio に接続できます。

  3. 拡張パッケージをインストールして Twilio アダプターを導入する

    Terminal window
    npm install @openai/agents-extensions
  4. アダプターとモデルをインポートして RealtimeSession に接続する

    import { TwilioRealtimeTransportLayer } from '@openai/agents-extensions';
    import { RealtimeAgent, RealtimeSession } from '@openai/agents/realtime';
    const agent = new RealtimeAgent({
    name: 'My Agent',
    });
    // Create a new transport mechanism that will bridge the connection between Twilio and
    // the OpenAI Realtime API.
    const twilioTransport = new TwilioRealtimeTransportLayer({
    twilioWebSocket: websocketConnection,
    });
    const session = new RealtimeSession(agent, {
    // set your own transport
    transport: twilioTransport,
    });
  5. RealtimeSession を Twilio に接続する

    session.connect({ apiKey: 'your-openai-api-key' });

RealtimeSession に期待できるイベントや挙動は、ツール呼び出し、ガードレールなどを含め、期待どおりに動作します。RealtimeSession を音声エージェントで使う方法の詳細は、音声エージェントの概要 を参照してください。

  1. スピードが最重要

    Twilio から必要なイベントとオーディオをすべて受け取るため、WebSocket 接続の参照を取得したらすぐに TwilioRealtimeTransportLayer インスタンスを作成し、その直後に session.connect() を呼び出してください。

  2. Twilio の 元 イベントへアクセスする

    Twilio から送られてくる 元 イベントにアクセスしたい場合は、RealtimeSession インスタンスの transport_event を購読します。Twilio の各イベントは twilio_message タイプで、 元 イベントデータを含む message プロパティを持ちます。

  3. デバッグログを確認する

    何が起きているかを詳しく知りたい場合があります。環境変数 DEBUG=openai-agents* を使うと Agents SDK からのすべてのデバッグログが表示されます。あるいは、DEBUG=openai-agents:extensions:twilio* を使って Twilio アダプターのデバッグログだけを有効にできます。

以下は、Twilio からのリクエストを受け取り、それを RealtimeSession に転送する WebSocket サーバーのエンドツーエンドの例です。

Fastify を使ったサーバー例
import Fastify from 'fastify';
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import dotenv from 'dotenv';
import fastifyFormBody from '@fastify/formbody';
import fastifyWs from '@fastify/websocket';
import {
RealtimeAgent,
RealtimeSession,
backgroundResult,
tool,
} from '@openai/agents/realtime';
import { TwilioRealtimeTransportLayer } from '@openai/agents-extensions';
import { hostedMcpTool } from '@openai/agents';
import { z } from 'zod';
import process from 'node:process';
// Load environment variables from .env file
dotenv.config();
// Retrieve the OpenAI API key from environment variables. You must have OpenAI Realtime API access.
const { OPENAI_API_KEY } = process.env;
if (!OPENAI_API_KEY) {
console.error('Missing OpenAI API key. Please set it in the .env file.');
process.exit(1);
}
const PORT = +(process.env.PORT || 5050);
// Initialize Fastify
const fastify = Fastify();
fastify.register(fastifyFormBody);
fastify.register(fastifyWs);
const weatherTool = tool({
name: 'weather',
description: 'Get the weather in a given location.',
parameters: z.object({
location: z.string(),
}),
execute: async ({ location }: { location: string }) => {
return backgroundResult(`The weather in ${location} is sunny.`);
},
});
const secretTool = tool({
name: 'secret',
description: 'A secret tool to tell the special number.',
parameters: z.object({
question: z
.string()
.describe(
'The question to ask the secret tool; mainly about the special number.',
),
}),
execute: async ({ question }: { question: string }) => {
return `The answer to ${question} is 42.`;
},
needsApproval: true,
});
const agent = new RealtimeAgent({
name: 'Greeter',
instructions:
'You are a friendly assistant. When you use a tool always first say what you are about to do.',
tools: [
hostedMcpTool({
serverLabel: 'deepwiki',
serverUrl: 'https://mcp.deepwiki.com/sse',
}),
secretTool,
weatherTool,
],
});
// Root Route
fastify.get('/', async (_request: FastifyRequest, reply: FastifyReply) => {
reply.send({ message: 'Twilio Media Stream Server is running!' });
});
// Route for Twilio to handle incoming and outgoing calls
// <Say> punctuation to improve text-to-speech translation
fastify.all(
'/incoming-call',
async (request: FastifyRequest, reply: FastifyReply) => {
const twimlResponse = `
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say>O.K. you can start talking!</Say>
<Connect>
<Stream url="wss://${request.headers.host}/media-stream" />
</Connect>
</Response>`.trim();
reply.type('text/xml').send(twimlResponse);
},
);
// WebSocket route for media-stream
fastify.register(async (scopedFastify: FastifyInstance) => {
scopedFastify.get(
'/media-stream',
{ websocket: true },
async (connection: any) => {
const twilioTransportLayer = new TwilioRealtimeTransportLayer({
twilioWebSocket: connection,
});
const session = new RealtimeSession(agent, {
transport: twilioTransportLayer,
model: 'gpt-realtime',
config: {
audio: {
output: {
voice: 'verse',
},
},
},
});
session.on('mcp_tools_changed', (tools: { name: string }[]) => {
const toolNames = tools.map((tool) => tool.name).join(', ');
console.log(`Available MCP tools: ${toolNames || 'None'}`);
});
session.on(
'tool_approval_requested',
(_context: unknown, _agent: unknown, approvalRequest: any) => {
console.log(
`Approving tool call for ${approvalRequest.approvalItem.rawItem.name}.`,
);
session
.approve(approvalRequest.approvalItem)
.catch((error: unknown) =>
console.error('Failed to approve tool call.', error),
);
},
);
session.on(
'mcp_tool_call_completed',
(_context: unknown, _agent: unknown, toolCall: unknown) => {
console.log('MCP tool call completed.', toolCall);
},
);
await session.connect({
apiKey: OPENAI_API_KEY,
});
console.log('Connected to the OpenAI Realtime API');
},
);
});
fastify.listen({ port: PORT }, (err: Error | null) => {
if (err) {
console.error(err);
process.exit(1);
}
console.log(`Server is listening on port ${PORT}`);
});
process.on('SIGINT', () => {
fastify.close();
process.exit(0);
});