Realtime 에이전트를 Twilio에 연결
Twilio는 전화 통화의 원문 오디오를 WebSocket 서버로 보내는 Media Streams API를 제공합니다. 이 설정을 사용하면 음성 에이전트 개요를 Twilio에 연결할 수 있습니다. 기본 Realtime Session 전송을 websocket
모드로 사용해 Twilio에서 오는 이벤트를 Realtime Session에 연결할 수 있습니다. 다만, 웹 기반 대화보다 통화가 자연스럽게 더 큰 지연을 유발하기 때문에 올바른 오디오 포맷을 설정하고 인터럽션(중단 처리) 타이밍을 조정해야 합니다.
설정 경험을 개선하기 위해, 인터럽션 처리와 오디오 포워딩을 포함해 Twilio 연결을 대신 처리하는 전용 전송 계층을 만들었습니다.
-
Twilio 계정과 Twilio 전화번호를 준비하세요.
-
Twilio에서 오는 이벤트를 받을 수 있는 WebSocket 서버를 설정하세요.
로컬에서 개발 중이라면, 로컬 서버를 Twilio가 접근할 수 있도록
ngrok
또는 Cloudflare Tunnel과 같은 로컬 터널 구성이 필요합니다.TwilioRealtimeTransportLayer
를 사용해 Twilio에 연결할 수 있습니다. -
extensions 패키지를 설치해 Twilio 어댑터를 설치하세요:
Terminal window npm install @openai/agents-extensions -
어댑터와 모델을 가져와
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 transporttransport: twilioTransport,}); -
RealtimeSession
을 Twilio에 연결하세요:session.connect({ apiKey: 'your-openai-api-key' });
RealtimeSession
에서 기대하는 모든 이벤트와 동작(함수 호출, 가드레일 등)이 정상 동작합니다. RealtimeSession
을 음성 에이전트와 함께 사용하는 방법에 대한 자세한 내용은 음성 에이전트 개요를 참고하세요.
팁 및 고려 사항
섹션 제목: “팁 및 고려 사항”-
속도가 핵심입니다.
Twilio에서 필요한 모든 이벤트와 오디오를 받으려면, WebSocket 연결에 대한 참조를 얻자마자 즉시
TwilioRealtimeTransportLayer
인스턴스를 만들고 곧바로session.connect()
를 호출하세요. -
Twilio의 원문 이벤트에 접근하세요.
Twilio에서 전송되는 원문 이벤트에 접근하려면,
RealtimeSession
인스턴스의transport_event
이벤트를 수신하면 됩니다. Twilio에서 오는 모든 이벤트는 타입이twilio_message
이며 원문 이벤트 데이터가 들어 있는message
속성을 가집니다. -
디버그 로그를 확인하세요.
문제가 발생해 상세 정보를 확인해야 할 때가 있습니다.
DEBUG=openai-agents*
환경 변수를 사용하면 Agents SDK의 모든 디버그 로그가 출력됩니다. 또는 Twilio 어댑터에 대한 디버그 로그만 활성화하려면DEBUG=openai-agents:extensions:twilio*
를 사용하세요.
전체 예제 서버
섹션 제목: “전체 예제 서버”아래는 Twilio로부터 요청을 받아 RealtimeSession
으로 전달하는 WebSocket 서버의 끝에서 끝까지 동작하는 예제입니다.
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 filedotenv.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 Fastifyconst 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 Routefastify.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 translationfastify.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-streamfastify.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);});