Skip to content

Commit

Permalink
Merge branch 'main' into theo/v0.18.1
Browse files Browse the repository at this point in the history
  • Loading branch information
theomonnom authored Nov 14, 2024
2 parents 6961248 + bc48e91 commit c469c7b
Show file tree
Hide file tree
Showing 35 changed files with 1,483 additions and 481 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -162,4 +162,6 @@ cython_debug/
# vscode project settings
.vscode

.DS_Store
.DS_Store

docs
6 changes: 3 additions & 3 deletions examples/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@


async def main():
# will automatically use the LIVEKIT_API_KEY and LIVEKIT_API_SECRET env vars
lkapi = api.LiveKitAPI("http://localhost:7880")
# will automatically use LIVEKIT_URL, LIVEKIT_API_KEY and LIVEKIT_API_SECRET env vars
lkapi = api.LiveKitAPI()
room_info = await lkapi.room.create_room(
api.CreateRoomRequest(name="my-room"),
)
Expand All @@ -15,4 +15,4 @@ async def main():


if __name__ == "__main__":
asyncio.get_event_loop().run_until_complete(main())
asyncio.run(main())
27 changes: 26 additions & 1 deletion livekit-api/livekit/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,21 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""LiveKit API SDK"""
"""LiveKit Server APIs for Python
`pip install livekit-api`
Manage rooms, participants, egress, ingress, SIP, and Agent dispatch.
Primary entry point is `LiveKitAPI`.
See https://docs.livekit.io/reference/server/server-apis for more information.
"""

# flake8: noqa
# re-export packages from protocol
from livekit.protocol.agent_dispatch import *
from livekit.protocol.agent import *
from livekit.protocol.egress import *
from livekit.protocol.ingress import *
from livekit.protocol.models import *
Expand All @@ -28,3 +39,17 @@
from .access_token import VideoGrants, SIPGrants, AccessToken, TokenVerifier
from .webhook import WebhookReceiver
from .version import __version__

__all__ = [
"LiveKitAPI",
"room_service",
"egress_service",
"ingress_service",
"sip_service",
"agent_dispatch_service",
"VideoGrants",
"SIPGrants",
"AccessToken",
"TokenVerifier",
"WebhookReceiver",
]
8 changes: 5 additions & 3 deletions livekit-api/livekit/api/_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ def __init__(
self.api_secret = api_secret

def _auth_header(
self, grants: VideoGrants, sip: SIPGrants | None = None
self, grants: VideoGrants | None, sip: SIPGrants | None = None
) -> Dict[str, str]:
tok = AccessToken(self.api_key, self.api_secret).with_grants(grants)
tok = AccessToken(self.api_key, self.api_secret)
if grants:
tok.with_grants(grants)
if sip is not None:
tok = tok.with_sip_grants(sip)
tok.with_sip_grants(sip)

token = tok.to_jwt()

Expand Down
87 changes: 61 additions & 26 deletions livekit-api/livekit/api/access_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
import os
import jwt
from typing import Optional, List, Literal
from google.protobuf.json_format import MessageToDict, ParseDict

from livekit.protocol.room import RoomConfiguration

DEFAULT_TTL = datetime.timedelta(hours=6)
DEFAULT_LEEWAY = datetime.timedelta(minutes=1)
Expand All @@ -27,13 +30,13 @@
@dataclasses.dataclass
class VideoGrants:
# actions on rooms
room_create: bool = False
room_list: bool = False
room_record: bool = False
room_create: Optional[bool] = None
room_list: Optional[bool] = None
room_record: Optional[bool] = None

# actions on a particular room
room_admin: bool = False
room_join: bool = False
room_admin: Optional[bool] = None
room_join: Optional[bool] = None
room: str = ""

# permissions within a room
Expand All @@ -44,23 +47,22 @@ class VideoGrants:
# TrackSource types that a participant may publish.
# When set, it supersedes CanPublish. Only sources explicitly set here can be
# published
can_publish_sources: List[str] = dataclasses.field(default_factory=list)
can_publish_sources: Optional[List[str]] = None

# by default, a participant is not allowed to update its own metadata
can_update_own_metadata: bool = False
can_update_own_metadata: Optional[bool] = None

# actions on ingresses
ingress_admin: bool = False # applies to all ingress
ingress_admin: Optional[bool] = None # applies to all ingress

# participant is not visible to other participants (useful when making bots)
hidden: bool = False
hidden: Optional[bool] = None

# indicates to the room that current participant is a recorder
recorder: bool = False
# [deprecated] indicates to the room that current participant is a recorder
recorder: Optional[bool] = None

# indicates that the holder can register as an Agent framework worker
# it is also set on all participants that are joining as Agent
agent: bool = False
agent: Optional[bool] = None


@dataclasses.dataclass
Expand All @@ -75,12 +77,28 @@ class SIPGrants:
class Claims:
identity: str = ""
name: str = ""
video: VideoGrants = dataclasses.field(default_factory=VideoGrants)
sip: SIPGrants = dataclasses.field(default_factory=SIPGrants)
attributes: dict[str, str] = dataclasses.field(default_factory=dict)
metadata: str = ""
sha256: str = ""
kind: str = ""
metadata: str = ""
video: Optional[VideoGrants] = None
sip: Optional[SIPGrants] = None
attributes: Optional[dict[str, str]] = None
sha256: Optional[str] = None
room_preset: Optional[str] = None
room_config: Optional[RoomConfiguration] = None

def asdict(self) -> dict:
# in order to produce minimal JWT size, exclude None or empty values
claims = dataclasses.asdict(
self,
dict_factory=lambda items: {
snake_to_lower_camel(k): v
for k, v in items
if v is not None and v != ""
},
)
if self.room_config:
claims["roomConfig"] = MessageToDict(self.room_config)
return claims


class AccessToken:
Expand Down Expand Up @@ -141,16 +159,22 @@ def with_sha256(self, sha256: str) -> "AccessToken":
self.claims.sha256 = sha256
return self

def with_room_preset(self, preset: str) -> "AccessToken":
self.claims.room_preset = preset
return self

def with_room_config(self, config: RoomConfiguration) -> "AccessToken":
self.claims.room_config = config
return self

def to_jwt(self) -> str:
video = self.claims.video
if video.room_join and (not self.identity or not video.room):
if video and video.room_join and (not self.identity or not video.room):
raise ValueError("identity and room must be set when joining a room")

claims = dataclasses.asdict(
self.claims,
dict_factory=lambda items: {snake_to_lower_camel(k): v for k, v in items},
)
claims.update(
# we want to exclude None values from the token
jwt_claims = self.claims.asdict()
jwt_claims.update(
{
"sub": self.identity,
"iss": self.api_key,
Expand All @@ -164,7 +188,7 @@ def to_jwt(self) -> str:
),
}
)
return jwt.encode(claims, self.api_secret, algorithm="HS256")
return jwt.encode(jwt_claims, self.api_secret, algorithm="HS256")


class TokenVerifier:
Expand Down Expand Up @@ -208,7 +232,7 @@ def verify(self, token: str) -> Claims:
}
sip = SIPGrants(**sip_dict)

return Claims(
grant_claims = Claims(
identity=claims.get("sub", ""),
name=claims.get("name", ""),
video=video,
Expand All @@ -218,6 +242,17 @@ def verify(self, token: str) -> Claims:
sha256=claims.get("sha256", ""),
)

if claims.get("roomPreset"):
grant_claims.room_preset = claims.get("roomPreset")
if claims.get("roomConfig"):
grant_claims.room_config = ParseDict(
claims.get("roomConfig"),
RoomConfiguration(),
ignore_unknown_fields=True,
)

return grant_claims


def camel_to_snake(t: str):
return re.sub(r"(?<!^)(?=[A-Z])", "_", t).lower()
Expand Down
113 changes: 113 additions & 0 deletions livekit-api/livekit/api/agent_dispatch_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import aiohttp
from typing import Optional
from livekit.protocol.agent_dispatch import (
CreateAgentDispatchRequest,
AgentDispatch,
DeleteAgentDispatchRequest,
ListAgentDispatchRequest,
ListAgentDispatchResponse,
)
from ._service import Service
from .access_token import VideoGrants

SVC = "AgentDispatchService"
"""@private"""


class AgentDispatchService(Service):
"""Manage agent dispatches. Service APIs require roomAdmin permissions.
Recommended way to use this service is via `livekit.api.LiveKitAPI`:
```python
from livekit import api
lkapi = api.LiveKitAPI()
agent_dispatch = lkapi.agent_dispatch
```
"""

def __init__(
self, session: aiohttp.ClientSession, url: str, api_key: str, api_secret: str
):
super().__init__(session, url, api_key, api_secret)

async def create_dispatch(self, req: CreateAgentDispatchRequest) -> AgentDispatch:
"""Create an explicit dispatch for an agent to join a room.
To use explicit dispatch, your agent must be registered with an `agentName`.
Args:
req (CreateAgentDispatchRequest): Request containing dispatch creation parameters
Returns:
AgentDispatch: The created agent dispatch object
"""
return await self._client.request(
SVC,
"CreateDispatch",
req,
self._auth_header(VideoGrants(room_admin=True, room=req.room)),
AgentDispatch,
)

async def delete_dispatch(self, dispatch_id: str, room_name: str) -> AgentDispatch:
"""Delete an explicit dispatch for an agent in a room.
Args:
dispatch_id (str): ID of the dispatch to delete
room_name (str): Name of the room containing the dispatch
Returns:
AgentDispatch: The deleted agent dispatch object
"""
return await self._client.request(
SVC,
"DeleteDispatch",
DeleteAgentDispatchRequest(
dispatch_id=dispatch_id,
room=room_name,
),
self._auth_header(VideoGrants(room_admin=True, room=room_name)),
AgentDispatch,
)

async def list_dispatch(self, room_name: str) -> list[AgentDispatch]:
"""List all agent dispatches in a room.
Args:
room_name (str): Name of the room to list dispatches from
Returns:
list[AgentDispatch]: List of agent dispatch objects in the room
"""
res = await self._client.request(
SVC,
"ListDispatch",
ListAgentDispatchRequest(room=room_name),
self._auth_header(VideoGrants(room_admin=True, room=room_name)),
ListAgentDispatchResponse,
)
return list(res.agent_dispatches)

async def get_dispatch(
self, dispatch_id: str, room_name: str
) -> Optional[AgentDispatch]:
"""Get an Agent dispatch by ID
Args:
dispatch_id (str): ID of the dispatch to retrieve
room_name (str): Name of the room containing the dispatch
Returns:
Optional[AgentDispatch]: The requested agent dispatch object if found, None otherwise
"""
res = await self._client.request(
SVC,
"ListDispatch",
ListAgentDispatchRequest(dispatch_id=dispatch_id, room=room_name),
self._auth_header(VideoGrants(room_admin=True, room=room_name)),
ListAgentDispatchResponse,
)
if len(res.agent_dispatches) > 0:
return res.agent_dispatches[0]
return None
Loading

0 comments on commit c469c7b

Please sign in to comment.