Twilio용 Realtime 에이전트
Twilio는 전화 통화의 원문 오디오를 WebSocket 서버로 전송하는 Media Streams API를 제공합니다. 이 설정을 사용하면 음성 에이전트 개요를 Twilio에 연결할 수 있습니다. websocket 모드에서 기본 Realtime Session 전송 방식을 사용하여 Twilio에서 수신되는 이벤트를 Realtime Session에 연결할 수 있습니다. 하지만 전화 통화는 웹 기반 대화보다 자연스럽게 지연 시간이 길어지므로, 올바른 오디오 형식을 설정하고 인터럽션(중단 처리) 타이밍을 직접 조정해야 합니다.
설정 환경을 개선하기 위해 인터럽션(중단 처리) 처리와 오디오 전달을 포함하여 Twilio 연결을 관리하는 전용 전송 계층을 만들었습니다.
-
Twilio 계정과 Twilio 전화번호가 있는지 확인합니다.
-
Twilio의 이벤트를 수신할 수 있는 WebSocket 서버를 설정합니다.
로컬에서 개발하는 경우 로컬 서버에 Twilio가 접근할 수 있도록
ngrok또는 Cloudflare Tunnel 같은 로컬 터널을 구성해야 합니다.TwilioRealtimeTransportLayer를 사용하여 Twilio에 연결할 수 있습니다. -
확장 패키지를 설치하여 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의 모든 디버그 로그가 표시됩니다. 또는DEBUG=openai-agents:extensions:twilio*를 사용하여 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: 'deepwiki', serverUrl: 'https://mcp.deepwiki.com/mcp', }), 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-2.1', 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);});