Spaces:
Running
Running
Commit
·
76744fc
1
Parent(s):
c63b8b6
Now generating the credentials for Twilio inside the service
Browse files
examples/voice_agent_webrtc_langgraph/agents/requirements.txt
CHANGED
|
@@ -11,4 +11,5 @@ docling
|
|
| 11 |
pymongo
|
| 12 |
yt_dlp
|
| 13 |
requests
|
| 14 |
-
protobuf==6.31.1
|
|
|
|
|
|
| 11 |
pymongo
|
| 12 |
yt_dlp
|
| 13 |
requests
|
| 14 |
+
protobuf==6.31.1
|
| 15 |
+
twilio
|
examples/voice_agent_webrtc_langgraph/pipeline.py
CHANGED
|
@@ -66,6 +66,71 @@ pcs_map: dict[str, SmallWebRTCConnection] = {}
|
|
| 66 |
contexts_map: dict[str, OpenAILLMContext] = {}
|
| 67 |
|
| 68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
ice_servers = (
|
| 70 |
[
|
| 71 |
IceServer(
|
|
@@ -392,7 +457,9 @@ async def websocket_endpoint(websocket: WebSocket):
|
|
| 392 |
logger.info(f"Reusing existing connection for pc_id: {pc_id}")
|
| 393 |
await pipecat_connection.renegotiate(sdp=request["sdp"], type=request["type"])
|
| 394 |
else:
|
| 395 |
-
|
|
|
|
|
|
|
| 396 |
await pipecat_connection.initialize(sdp=request["sdp"], type=request["type"])
|
| 397 |
|
| 398 |
@pipecat_connection.event_handler("closed")
|
|
@@ -450,26 +517,18 @@ async def get_prompt():
|
|
| 450 |
# RTC config endpoint must be registered before mounting static at "/"
|
| 451 |
@app.get("/rtc-config")
|
| 452 |
async def rtc_config():
|
| 453 |
-
"""Expose browser RTC ICE configuration based on environment variables.
|
| 454 |
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
Always includes a public STUN as a fallback.
|
| 458 |
"""
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
server["username"] = turn_user
|
| 467 |
-
if turn_pass:
|
| 468 |
-
server["credential"] = turn_pass
|
| 469 |
-
ice_servers.append(server)
|
| 470 |
-
# Public STUN fallback to aid connectivity when TURN is not provided
|
| 471 |
-
ice_servers.append({"urls": "stun:stun.l.google.com:19302"})
|
| 472 |
-
return {"iceServers": ice_servers}
|
| 473 |
|
| 474 |
|
| 475 |
# Serve static UI (if bundled) after API/WebSocket routes so they still take precedence
|
|
|
|
| 66 |
contexts_map: dict[str, OpenAILLMContext] = {}
|
| 67 |
|
| 68 |
|
| 69 |
+
# Helper: Build ICE servers for client (browser) using Twilio token if configured
|
| 70 |
+
def _build_client_ice_servers() -> list[dict]:
|
| 71 |
+
# Prefer Twilio dynamic credentials
|
| 72 |
+
sid = os.getenv("TWILIO_ACCOUNT_SID")
|
| 73 |
+
tok = os.getenv("TWILIO_AUTH_TOKEN")
|
| 74 |
+
if sid and tok:
|
| 75 |
+
try:
|
| 76 |
+
# Import lazily to avoid hard dependency when not configured
|
| 77 |
+
from twilio.rest import Client # type: ignore
|
| 78 |
+
|
| 79 |
+
client = Client(sid, tok)
|
| 80 |
+
token = client.tokens.create()
|
| 81 |
+
servers: list[dict] = []
|
| 82 |
+
# Twilio may return either 'ice_servers' with 'url' or 'urls'
|
| 83 |
+
for s in getattr(token, "ice_servers", []) or []:
|
| 84 |
+
url_val = s.get("urls") if isinstance(s, dict) else getattr(s, "urls", None)
|
| 85 |
+
if not url_val:
|
| 86 |
+
url_val = s.get("url") if isinstance(s, dict) else getattr(s, "url", None)
|
| 87 |
+
entry: dict = {"urls": url_val}
|
| 88 |
+
u = s.get("username") if isinstance(s, dict) else getattr(s, "username", None)
|
| 89 |
+
c = s.get("credential") if isinstance(s, dict) else getattr(s, "credential", None)
|
| 90 |
+
if u:
|
| 91 |
+
entry["username"] = u
|
| 92 |
+
if c:
|
| 93 |
+
entry["credential"] = c
|
| 94 |
+
if entry.get("urls"):
|
| 95 |
+
servers.append(entry)
|
| 96 |
+
# Always include a public STUN fallback
|
| 97 |
+
servers.append({"urls": "stun:stun.l.google.com:19302"})
|
| 98 |
+
return servers
|
| 99 |
+
except Exception as e: # noqa: BLE001
|
| 100 |
+
logger.warning(f"Twilio TURN fetch failed, falling back to env/static: {e}")
|
| 101 |
+
# Static env fallback
|
| 102 |
+
servers: list[dict] = []
|
| 103 |
+
turn_url = os.getenv("TURN_SERVER_URL") or os.getenv("TURN_URL")
|
| 104 |
+
turn_user = os.getenv("TURN_USERNAME") or os.getenv("TURN_USER")
|
| 105 |
+
turn_pass = os.getenv("TURN_PASSWORD") or os.getenv("TURN_PASS")
|
| 106 |
+
if turn_url:
|
| 107 |
+
server: dict = {"urls": turn_url}
|
| 108 |
+
if turn_user:
|
| 109 |
+
server["username"] = turn_user
|
| 110 |
+
if turn_pass:
|
| 111 |
+
server["credential"] = turn_pass
|
| 112 |
+
servers.append(server)
|
| 113 |
+
servers.append({"urls": "stun:stun.l.google.com:19302"})
|
| 114 |
+
return servers
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
# Helper: Convert client ICE dicts to server IceServer objects
|
| 118 |
+
def _build_server_ice_servers() -> list[IceServer]:
|
| 119 |
+
out: list[IceServer] = []
|
| 120 |
+
for s in _build_client_ice_servers():
|
| 121 |
+
urls = s.get("urls")
|
| 122 |
+
username = s.get("username", "")
|
| 123 |
+
credential = s.get("credential", "")
|
| 124 |
+
# urls may be a list or a string. Normalize to list for safety.
|
| 125 |
+
if isinstance(urls, list):
|
| 126 |
+
for u in urls:
|
| 127 |
+
out.append(IceServer(urls=u, username=username, credential=credential))
|
| 128 |
+
elif isinstance(urls, str) and urls:
|
| 129 |
+
out.append(IceServer(urls=urls, username=username, credential=credential))
|
| 130 |
+
return out
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
# Backward-compatible static servers (unused when Twilio configured)
|
| 134 |
ice_servers = (
|
| 135 |
[
|
| 136 |
IceServer(
|
|
|
|
| 457 |
logger.info(f"Reusing existing connection for pc_id: {pc_id}")
|
| 458 |
await pipecat_connection.renegotiate(sdp=request["sdp"], type=request["type"])
|
| 459 |
else:
|
| 460 |
+
# Build dynamic servers (Twilio or env) for new connections
|
| 461 |
+
dynamic_servers = _build_server_ice_servers()
|
| 462 |
+
pipecat_connection = SmallWebRTCConnection(dynamic_servers if dynamic_servers else ice_servers)
|
| 463 |
await pipecat_connection.initialize(sdp=request["sdp"], type=request["type"])
|
| 464 |
|
| 465 |
@pipecat_connection.event_handler("closed")
|
|
|
|
| 517 |
# RTC config endpoint must be registered before mounting static at "/"
|
| 518 |
@app.get("/rtc-config")
|
| 519 |
async def rtc_config():
|
| 520 |
+
"""Expose browser RTC ICE configuration based on environment variables or Twilio.
|
| 521 |
|
| 522 |
+
Uses Twilio dynamic TURN credentials when TWILIO_ACCOUNT_SID/TWILIO_AUTH_TOKEN are set.
|
| 523 |
+
Falls back to TURN_* env vars. Always includes a public STUN fallback.
|
|
|
|
| 524 |
"""
|
| 525 |
+
try:
|
| 526 |
+
servers = _build_client_ice_servers()
|
| 527 |
+
return {"iceServers": servers}
|
| 528 |
+
except Exception as e: # noqa: BLE001
|
| 529 |
+
logger.warning(f"rtc-config dynamic build failed: {e}")
|
| 530 |
+
# Final safe fallback
|
| 531 |
+
return {"iceServers": [{"urls": "stun:stun.l.google.com:19302"}]}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 532 |
|
| 533 |
|
| 534 |
# Serve static UI (if bundled) after API/WebSocket routes so they still take precedence
|