コンテンツにスキップ

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. アダプターとモデルをインポートし、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 サーバーの完全な end-to-end の例です。

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-1.5',
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);
});