コンテンツにスキップ

Realtime Agent を Twilio に接続

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

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

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

  2. Twilio からのイベントを受け取れる WebSocket サーバーをセットアップすること

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

  3. extensions パッケージをインストールして 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 の type を持ち、元のイベントデータを含む message プロパティを持ちます。

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

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

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

Example server using 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);
});