grok-voice-agent / server.py
Vrda's picture
Deploy Grok voice agent UI
a3ffe5c verified
Raw
History Blame Contribute Delete
3.59 kB
"""FastAPI backend for the Grok Voice Agent web UI.
Serves the static frontend and exposes a single WebSocket endpoint (`/realtime`)
that acts as a transparent proxy to the xAI Voice Agent realtime API. The xAI
API key never leaves the server; the browser only ever talks to this app.
Run locally:
python server.py
# or: uvicorn server:app --reload --port 7860
Deploy: this is the entry point for a Hugging Face Space (SDK: docker).
Set the `XAI_API_KEY` secret in the Space settings.
"""
from __future__ import annotations
import asyncio
import os
from pathlib import Path
import websockets
from dotenv import load_dotenv
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
load_dotenv()
# The local .env uses `xai_api_key`; HF Space secrets use `XAI_API_KEY`.
XAI_API_KEY = os.getenv("XAI_API_KEY") or os.getenv("xai_api_key")
AGENT_ID = os.getenv("XAI_AGENT_ID", "agent_otGoZoOqjIvC6jMv")
WS_URL = f"wss://api.x.ai/v1/realtime?agent_id={AGENT_ID}"
STATIC_DIR = Path(__file__).parent / "static"
app = FastAPI(title="Grok Voice Agent")
@app.get("/")
async def index() -> FileResponse:
return FileResponse(STATIC_DIR / "index.html")
@app.get("/health")
async def health() -> JSONResponse:
return JSONResponse(
{"ok": True, "agent_id": AGENT_ID, "api_key_configured": bool(XAI_API_KEY)}
)
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
async def _connect_upstream():
"""Open the WebSocket to xAI, tolerating the websockets header kwarg rename."""
headers = {"Authorization": f"Bearer {XAI_API_KEY}"}
try:
return await websockets.connect(WS_URL, additional_headers=headers, max_size=None)
except TypeError:
# websockets < 12 used `extra_headers`.
return await websockets.connect(WS_URL, extra_headers=headers, max_size=None)
@app.websocket("/realtime")
async def realtime(client: WebSocket) -> None:
"""Relay JSON events between the browser and the xAI Voice Agent."""
await client.accept()
if not XAI_API_KEY:
await client.send_json(
{"type": "error", "error": {"message": "XAI_API_KEY is not set on the server."}}
)
await client.close()
return
try:
upstream = await _connect_upstream()
except Exception as exc: # noqa: BLE001 - surface any handshake failure to the UI
await client.send_json(
{"type": "error", "error": {"message": f"Could not reach xAI: {exc}"}}
)
await client.close()
return
async def browser_to_xai() -> None:
try:
while True:
message = await client.receive_text()
await upstream.send(message)
except (WebSocketDisconnect, RuntimeError):
pass
async def xai_to_browser() -> None:
try:
async for message in upstream:
await client.send_text(message)
except websockets.exceptions.ConnectionClosed:
pass
up_task = asyncio.create_task(browser_to_xai())
down_task = asyncio.create_task(xai_to_browser())
_, pending = await asyncio.wait(
{up_task, down_task}, return_when=asyncio.FIRST_COMPLETED
)
for task in pending:
task.cancel()
await upstream.close()
try:
await client.close()
except RuntimeError:
pass
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=int(os.getenv("PORT", "7860")))