コンテンツにスキップ

Realtime Agent を Twilio に接続

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

セットアップ体験を向上させるために、Twilio への接続、割り込み処理、音声の転送まで行う専用のトランスポートレイヤーを用意しました。

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

  2. Twilio からのイベントを受け取れる WebSocket サーバーを設定します。

    ローカル開発の場合は、this will require you to configure a local tunnel like this will require you to configure a local tunnel like 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 のすべてのデバッグログが表示されます。 あるいは、Twilio アダプターのデバッグログだけを有効にするには DEBUG=openai-agents:extensions: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: 'dnd',
}),
hostedMcpTool({
serverLabel: 'deepwiki',
}),
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);
});