コンテンツにスキップ

Twilio 上の Realtime Agent

Twilio は、電話通話の元音声を WebSocket サーバーに送信する Media Streams API を提供しています。 この構成を使うと、あなたの 音声エージェントの概要 を Twilio に接続できます。 Twilio から届くイベントを RealtimeSession に接続するには、デフォルトの Realtime Session transport を websocket モードで使用できます。 ただし、電話通話は Web ベースの会話より自然に遅延が大きくなるため、適切な音声フォーマットの設定と、割り込みタイミングの独自調整が必要です。

セットアップ体験を改善するために、Twilio への接続を処理する専用の transport layer を用意しました。 これには、割り込み処理と音声転送が含まれます。

  1. Twilio アカウントと Twilio 電話番号を用意してください

  2. Twilio からのイベントを受信できる WebSocket サーバーを設定してください

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

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

    Terminal window
    npm install @openai/agents-extensions
  4. アダプターとモデルを import して 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 イベントを listen できます。 Twilio からの各イベントは twilio_message という type を持ち、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/mcp',
}),
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);
});