프론트엔드 연결하기
프론트엔드 연결하기
프로바이더 키, 툴, 비즈니스 로직은 서버에 두세요. 사용자 디바이스는 프로바이더에 직접 연결하지 말고 백엔드에 연결하세요. 오디오가 디바이스와 모델 사이를 어떻게 이동하는지는 클라이언트와 프로바이더에 달려 있어요:
- 브라우저 WebRTC + 서버 사이드밴드 — OpenAI와 Azure OpenAI에서 권장하는 브라우저 경로예요. 브라우저가 프로바이더와 미디어를 직접 교환하고(최저 지연), 백엔드는 제어 평면 사이드밴드로 에이전트를 실행해요.
- 브라우저 → 백엔드 WebSocket 릴레이 — Gemini Live와 xAI를 포함한 모든 프로바이더에서 작동해요. 브라우저가 오디오를 미디어 브리지를 소유한 백엔드로 스트리밍해요.
- SIP / 텔레포니 브리지 — 전화 통화용, 텔레포니 프로바이더를 통해.
모든 설정에서 세션 — 툴, 이력, 사용량 한도와 함께 — 은 백엔드에서 실행돼요. 프로바이더의 자체 SDK로 브라우저를 프로바이더에 직접 연결하면 에이전트 루프를 클라이언트로 옮기고 그 모두를 포기하는 것이에요. 위 설정 중 하나를 선호하세요.
출처: 문서
본문
브라우저 WebRTC + 서버 사이드밴드
OpenAI와 Azure OpenAI의 브라우저 음성 에이전트를 위해, 브라우저는 마이크와 스피커 오디오를 WebRTC로 직접 운반하고 백엔드는 같은 호출에 제어 평면 사이드밴드를 붙여요. 미디어는 백엔드를 건드리지 않아 지연이 낮게 유지되고, 툴, 이력, 의존성, 프로바이더 자격증명은 서버에 남아요. Gemini Live와 xAI는 이 사이드밴드 전송을 제공하지 않아요. 거기서는 WebSocket 릴레이를 사용하세요.
browser ── WebRTC media ── provider
│ ▲
└─ SDP offer → backend ───┘ sideband identified by call_id
AgentRealtime.answer_webrtc_offer로 브라우저의 offer를 릴레이하고, SDP answer를 브라우저에 반환한 뒤, 반환된 호출 핸들을 붙이세요:
import asyncio
from pydantic_ai import Agent
agent = Agent(instructions='You are a concise voice assistant.')
realtime = agent.realtime('openai:gpt-realtime')
sideband_tasks: set[asyncio.Task[None]] = set()
async def handle_offer(sdp_offer: str) -> str:
answer = await realtime.answer_webrtc_offer(sdp_offer)
async def run_sideband() -> None:
async with realtime.session(provider_session=answer.session) as session:
async for event in session:
print(event)
task = asyncio.create_task(run_sideband())
sideband_tasks.add(task) # (1)
task.add_done_callback(sideband_tasks.discard)
return answer.sdp
asyncio는 태스크에 약한 참조만 유지하므로, 호출이 끝날 때까지 스스로 하나를 붙잡고 있으세요. done 콜백은 또한 사이드밴드 세션이 발생시킨 오류를 "never retrieved" 경고로 남기는 대신 드러내요.
보안 offer 릴레이 흐름은 브라우저에 토큰을 절대 주지 않아요. 대안으로 AgentRealtime.create_client_secret은 클라이언트 주도 협상을 위한 단기 자격증명을 발급해요. 어느 쪽이든 브라우저는 프로바이더 세션의 피어이고 프로바이더 네이티브 제어 이벤트를 보낼 수 있어요. 그래서 모든 서버 측 툴을 모델에 공급된 세션 인스트럭션이 아니라 신뢰된 deps로 인가하세요.
브라우저가 시드된 이력을 읽을 수 있다
message_history로 사이드밴드 세션을 시드하면 그 이전 턴들이 브라우저가 피어인 공유 프로바이더 대화로 보내져서, 호출 참가자가 데이터 채널로 그것들을(민감한 툴 결과 포함) 읽을 수 있어요(Azure의 webrtcfilter=on은 여전히 대화 항목 이벤트를 전달해요). 브라우저가 보기에 안전한 이력만 사이드밴드에 시드하세요. 민감한 컨텍스트는 deps와 툴 로직에 두세요.
사이드밴드는 오디오 전송을 소유하지 않아요. 그것의 send_audio(), commit_audio(), clear_audio(), stream_audio() 메서드는 예외를 던지고, audio_retention은 'transcript_only'로 남아야 해요. 사용자 음성이 이력에 나타나야 하면 입력 전사를 활성화하고, 말하는 표시에는 RealtimeOutputSpeechStartEvent / RealtimeOutputSpeechEndEvent를 사용하세요. interrupt()는 프로바이더의 아웃바운드 WebRTC 오디오 버퍼를 지워 barge-in이 재생을 멈추게 해요. 드롭된 사이드밴드는 재연결 규칙을 따르는데, 한 가지 특이점이 있어요. 깨끗한 종료가 브라우저가 끊는 것으로 취급된다는 점이에요.
realtime WebRTC 예제가 완전한 FastAPI와 브라우저 흐름을 보여줘요. 프로바이더별 설정(Azure의 Microsoft Entra ID와 webrtcfilter)은 Azure 페이지에 있어요.
브라우저 → 백엔드 WebSocket 릴레이
브라우저가 WebRTC를 쓸 수 없을 때 — 또는 프로바이더가 Gemini Live나 xAI일 때 — 백엔드에 브라우저의 마이크 오디오를 받아 send_audio()로 펌프하는 WebSocket 엔드포인트를 만들고, stream_audio() 출력을 재생을 위해 릴레이 back하세요. 여기서 백엔드가 미디어 브리지를 소유해요. realtime 카메라 예제가 이 설정을 처음부터 끝까지 보여줘요. 최소 FastAPI 릴레이(브라우저가 원시 PCM16 바이너리 프레임을 보내고 받은 프레임을 재생)는:
import asyncio
from fastapi import FastAPI, WebSocket
from pydantic_ai import Agent
agent = Agent(instructions='You are a helpful voice assistant.')
app = FastAPI()
@app.websocket('/voice')
async def voice_socket(websocket: WebSocket):
await websocket.accept()
async def microphone():
while True:
yield await websocket.receive_bytes()
async with agent.realtime('openai:gpt-realtime').session() as session:
async def playback():
async for chunk in session.stream_audio():
await websocket.send_bytes(chunk)
playback_task = asyncio.create_task(playback())
try:
await session.send_audio(microphone())
finally:
playback_task.cancel()
send_audio()는 전체 호출 동안 async 이터레이터를 소비해요. 그래서 브라우저가 연결을 끊으면 receive_bytes()가 예외를 던지고, finally가 재생을 멈추며, async with 블록을 떠나면 프로바이더 세션이 끊겨요. 대신 알몸의 asyncio.create_task로 입력을 구동하면 그 오류를 삼키고 아무도 듣지 않는 채 청구되는 세션을 열어두게 돼요.
handle_barge_in=True는 이 릴레이에서 무효예요. played_audio_bytes는 릴레이가 청크를 전달할 때 재생된 것으로 세는데, 브라우저가 실제로 재생하기 전이거든요. 그래서 세션은 플러시할 재생되지 않은 오디오를 보지 못해요. 릴레이는 브라우저의 실제 재생 위치를 얻어 그 수로 interrupt(played_bytes=...)를 호출하고, 브라우저에 재생을 멈추고 자체 버퍼를 지우라고 말해야 해요. 세션은 아직 전달하지 않은 것만 버릴 수 있으니까요. played_ms=를 전달하면 프로바이더 측 컷오프를 기록하지만 세션이 큐에 넣은 오디오나 브라우저에 버퍼링된 오디오를 절대 플러시하지 않아요.
SIP/텔레포니 브리지
Twilio 같은 텔레포니 프로바이더로 전화 통화를 종료하고, 그 미디어 스트림(예: WebSocket 위의 Twilio Media Streams)을 백엔드 세션에 연결하며, 라인 코덱과 PCM16 사이에서 트랜스코딩하는 서비스를 만드세요.