Spaces:
Sleeping
Sleeping
| """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") | |
| async def index() -> FileResponse: | |
| return FileResponse(STATIC_DIR / "index.html") | |
| 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) | |
| 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"))) | |