diff --git a/CHANGELOG.md b/CHANGELOG.md index aaf7ec85a..4b558d0f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added `LiveKitRESTHelper` utility class for managing LiveKit rooms via REST API. + - Added `DeepgramSageMakerSTTService` which connects to a SageMaker hosted Deepgram STT model. Added `07c-interruptible-deepgram-sagemaker.py` foundational example. diff --git a/pyproject.toml b/pyproject.toml index f5ade679b..f3e7350c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,7 +72,7 @@ inworld = [] koala = [ "pvkoala~=2.0.3" ] krisp = [ "pipecat-ai-krisp~=0.4.0" ] langchain = [ "langchain~=0.3.20", "langchain-community~=0.3.20", "langchain-openai~=0.3.9" ] -livekit = [ "livekit~=1.0.13", "livekit-api~=1.0.5", "tenacity>=8.2.3,<10.0.0" ] +livekit = [ "livekit~=1.0.13", "livekit-api~=1.0.5", "tenacity>=8.2.3,<10.0.0", "pyjwt>=2.10.1" ] lmnt = [ "pipecat-ai[websockets-base]" ] local = [ "pyaudio~=0.2.14" ] local-smart-turn = [ "coremltools>=8.0", "transformers", "torch>=2.5.0,<3", "torchaudio>=2.5.0,<3" ] diff --git a/src/pipecat/transports/livekit/utils.py b/src/pipecat/transports/livekit/utils.py new file mode 100644 index 000000000..741b820b2 --- /dev/null +++ b/src/pipecat/transports/livekit/utils.py @@ -0,0 +1,96 @@ +# +# Copyright (c) 2024–2025, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""LiveKit REST Helpers. + +Methods that wrap the LiveKit API for room management. +""" + +import aiohttp + + +class LiveKitRESTHelper: + """Helper class for interacting with LiveKit's REST API. + + Provides methods for managing LiveKit rooms. + """ + + def __init__( + self, + *, + api_key: str, + api_secret: str, + api_url: str = "https://your-livekit-host.com", + aiohttp_session: aiohttp.ClientSession, + ): + """Initialize the LiveKit REST helper. + + Args: + api_key: Your LiveKit API key. + api_secret: Your LiveKit API secret. + api_url: LiveKit server URL (e.g. "https://your-livekit-host.com"). + aiohttp_session: Async HTTP session for making requests. + """ + self.api_key = api_key + self.api_secret = api_secret + self.api_url = api_url.rstrip("/") + self.aiohttp_session = aiohttp_session + + def _create_access_token(self, room_create: bool = True) -> str: + """Create a signed access token for LiveKit API authentication. + + Args: + room_create: Whether to grant roomCreate permission. + + Returns: + Signed JWT access token. + """ + import time + + import jwt + + claims = { + "iss": self.api_key, + "sub": self.api_key, + "nbf": int(time.time()), + "exp": int(time.time()) + 60, # Token valid for 60 seconds + "video": { + "roomCreate": room_create, + }, + } + + return jwt.encode(claims, self.api_secret, algorithm="HS256") + + async def delete_room_by_name(self, room_name: str) -> bool: + """Delete a LiveKit room by name. + + This will forcibly disconnect all participants currently in the room. + + Args: + room_name: Name of the room to delete. + + Returns: + True if deletion was successful. + + Raises: + Exception: If deletion fails. + """ + token = self._create_access_token(room_create=True) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } + + async with self.aiohttp_session.post( + f"{self.api_url}/twirp/livekit.RoomService/DeleteRoom", + headers=headers, + json={"room": room_name}, + ) as r: + if r.status != 200: + text = await r.text() + raise Exception(f"Failed to delete room [{room_name}] (status: {r.status}): {text}") + + return True diff --git a/uv.lock b/uv.lock index 7a937e54d..3db38f2a9 100644 --- a/uv.lock +++ b/uv.lock @@ -4522,6 +4522,7 @@ langchain = [ livekit = [ { name = "livekit" }, { name = "livekit-api" }, + { name = "pyjwt" }, { name = "tenacity" }, ] lmnt = [ @@ -4739,6 +4740,7 @@ requires-dist = [ { name = "pyaudio", marker = "extra == 'local'", specifier = "~=0.2.14" }, { name = "pydantic", specifier = ">=2.10.6,<3" }, { name = "pygobject", marker = "extra == 'gstreamer'", specifier = "~=3.50.0" }, + { name = "pyjwt", marker = "extra == 'livekit'", specifier = ">=2.10.1" }, { name = "pyloudnorm", specifier = "~=0.1.1" }, { name = "python-dotenv", marker = "extra == 'runner'", specifier = ">=1.0.0,<2.0.0" }, { name = "pyvips", extras = ["binary"], marker = "extra == 'moondream'", specifier = "~=3.0.0" }, @@ -4763,7 +4765,7 @@ requires-dist = [ { name = "wait-for2", marker = "python_full_version < '3.12'", specifier = ">=0.4.1" }, { name = "websockets", marker = "extra == 'websockets-base'", specifier = ">=13.1,<16.0" }, ] -provides-extras = ["aic", "anthropic", "assemblyai", "asyncai", "aws", "aws-nova-sonic", "azure", "cartesia", "cerebras", "deepseek", "daily", "deepgram", "elevenlabs", "fal", "fireworks", "fish", "gladia", "google", "grok", "groq", "gstreamer", "heygen", "hume", "inworld", "krisp", "koala", "langchain", "livekit", "lmnt", "local", "mcp", "mem0", "mistral", "mlx-whisper", "moondream", "nim", "neuphonic", "noisereduce", "openai", "openpipe", "openrouter", "perplexity", "playht", "qwen", "rime", "riva", "runner", "sambanova", "sarvam", "sentry", "local-smart-turn", "local-smart-turn-v3", "remote-smart-turn", "sagemaker", "silero", "simli", "soniox", "soundfile", "speechmatics", "strands", "tavus", "together", "tracing", "ultravox", "webrtc", "websocket", "websockets-base", "whisper"] +provides-extras = ["aic", "anthropic", "assemblyai", "asyncai", "aws", "aws-nova-sonic", "azure", "cartesia", "cerebras", "daily", "deepgram", "deepseek", "elevenlabs", "fal", "fireworks", "fish", "gladia", "google", "grok", "groq", "gstreamer", "heygen", "hume", "inworld", "koala", "krisp", "langchain", "livekit", "lmnt", "local", "local-smart-turn", "local-smart-turn-v3", "mcp", "mem0", "mistral", "mlx-whisper", "moondream", "neuphonic", "nim", "noisereduce", "openai", "openpipe", "openrouter", "perplexity", "playht", "qwen", "remote-smart-turn", "rime", "riva", "runner", "sagemaker", "sambanova", "sarvam", "sentry", "silero", "simli", "soniox", "soundfile", "speechmatics", "strands", "tavus", "together", "tracing", "ultravox", "webrtc", "websocket", "websockets-base", "whisper"] [package.metadata.requires-dev] dev = [