feat: add FastAPI token server to pipecat-agent

Adds a lightweight HTTP token server (port 8080) alongside the voice
pipeline so web clients can obtain LiveKit access tokens without a
separate service. Adds fastapi and uvicorn to requirements. Adds
LIVEKIT_PUBLIC_URL env var (wss://voice.hobbs.farm) to docker-compose
so tokens carry the correct public WebSocket URL for browser clients.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-01 01:26:07 -05:00
parent 59856bd4fc
commit 68c3731c65
3 changed files with 59 additions and 8 deletions
+56 -8
View File
@@ -1,12 +1,16 @@
"""
Entry point for the Pipecat VoiceCoach agent.
Reads config from environment variables.
Runs both the token HTTP server (for web clients) and the voice pipeline.
"""
import asyncio
import logging
import os
import threading
import structlog
import uvicorn
from fastapi import FastAPI
from pydantic import BaseModel
from livekit import api as livekit_api
from pipeline import run_voice_coach_pipeline
@@ -14,15 +18,59 @@ from pipeline import run_voice_coach_pipeline
logging.basicConfig(level=logging.INFO)
logger = structlog.get_logger()
LIVEKIT_API_KEY = os.environ["LIVEKIT_API_KEY"]
LIVEKIT_API_SECRET = os.environ["LIVEKIT_API_SECRET"]
LIVEKIT_WS_URL = os.getenv("LIVEKIT_URL", "ws://livekit-server:7880")
LIVEKIT_PUBLIC_URL = os.getenv("LIVEKIT_PUBLIC_URL", "wss://voice.hobbs.farm")
# --- Token HTTP server (serves /token for web clients) ---
token_app = FastAPI(title="VoiceCoach Token Service")
class TokenRequest(BaseModel):
identity: str = "jeremy"
class TokenResponse(BaseModel):
token: str
url: str
@token_app.get("/health")
async def health():
return {"status": "ok"}
@token_app.post("/token")
async def get_token(req: TokenRequest):
token = livekit_api.AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET)
token.with_grants(livekit_api.VideoGrants(
room_join=True,
room="voicecoach",
can_publish=True,
can_subscribe=True,
))
token.identity = req.identity
return TokenResponse(token=token.to_jwt(), url=LIVEKIT_PUBLIC_URL)
def run_token_server():
uvicorn.run(token_app, host="0.0.0.0", port=8080, log_level="info")
# --- Main pipeline ---
async def main():
anthropic_key = os.environ["ANTHROPIC_API_KEY"]
livekit_api_key = os.environ["LIVEKIT_API_KEY"]
livekit_api_secret = os.environ["LIVEKIT_API_SECRET"]
livekit_url = os.getenv("LIVEKIT_URL", "ws://livekit-server:7880")
# Generate LiveKit access token for this agent
token = livekit_api.AccessToken(livekit_api_key, livekit_api_secret)
# Start token server in a background thread
server_thread = threading.Thread(target=run_token_server, daemon=True)
server_thread.start()
logger.info("Token server started on :8080")
# Generate agent's own LiveKit token
token = livekit_api.AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET)
token.with_grants(livekit_api.VideoGrants(room_join=True, room="voicecoach"))
token.identity = "voicecoach-agent"
livekit_token = token.to_jwt()
@@ -33,9 +81,9 @@ async def main():
"current_mode": "coaching mode",
}
logger.info("Starting VoiceCoach pipeline", livekit_url=livekit_url)
logger.info("Starting VoiceCoach pipeline", livekit_url=LIVEKIT_WS_URL)
await run_voice_coach_pipeline(
livekit_url=livekit_url,
livekit_url=LIVEKIT_WS_URL,
livekit_token=livekit_token,
anthropic_key=anthropic_key,
session_context=session_context,