콘텐츠로 이동

Realtime 에이전트를 Twilio에 연결

Twilio는 전화 통화에서 나오는 원문 오디오를 WebSocket 서버로 전송하는 Media Streams API를 제공합니다. 이 설정을 사용하여 음성 에이전트 개요를 Twilio에 연결할 수 있습니다. 기본 Realtime Session 전송을 websocket 모드로 사용해 Twilio에서 오는 이벤트를 Realtime Session에 연결할 수 있습니다. 다만, 올바른 오디오 포맷을 설정하고, 웹 기반 대화보다 전화 통화가 자연스럽게 더 높은 지연을 유발하므로 인터럽션(중단 처리) 타이밍을 직접 조정해야 합니다.

설정 경험을 개선하기 위해, 인터럽션(중단 처리)과 오디오 포워딩 등을 포함하여 Twilio와의 연결을 대신 처리하는 전용 전송 레이어를 만들었습니다.

  1. Twilio 계정과 Twilio 전화번호를 보유하고 있는지 확인하세요.

  2. Twilio에서 오는 이벤트를 받을 수 있는 WebSocket 서버를 설정하세요.

    로컬에서 개발하는 경우, Twilio가 로컬 서버에 접근할 수 있도록 ngrok 또는 Cloudflare Tunnel 같은 로컬 터널을 구성해야 합니다. 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이며 원문 이벤트 데이터가 담긴 message 속성을 가집니다.

  3. 디버그 로그를 확인하세요.

    문제 상황에서 더 많은 정보를 원할 수 있습니다. DEBUG=openai-agents* 환경 변수를 사용하면 Agents SDK의 모든 디버그 로그가 표시됩니다. 또는 Twilio 어댑터에 대한 디버그 로그만 활성화하려면 DEBUG=openai-agents:extensions: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);
});