Files
2026-07-13 13:39:38 +08:00

120 lines
4.1 KiB
Python

from __future__ import annotations
import asyncio
from typing import Any
import aiohttp
from livekit.agents import (
DEFAULT_API_CONNECT_OPTIONS,
APIConnectionError,
APIConnectOptions,
APIStatusError,
)
from .log import logger
DEFAULT_API_URL = "https://api.keyframelabs.com"
class KeyframeAPI:
"""Asynchronous client for the Keyframe Labs session API."""
def __init__(
self,
api_key: str,
api_url: str,
*,
conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS,
session: aiohttp.ClientSession | None = None,
) -> None:
self._api_key = api_key
self._api_url = api_url
self._conn_options = conn_options
self._session = session
self._own_session = session is None
async def __aenter__(self) -> KeyframeAPI:
if self._own_session:
self._session = aiohttp.ClientSession()
return self
async def __aexit__(
self, exc_type: type | None, exc_val: Exception | None, exc_tb: Any
) -> None:
if self._own_session and self._session and not self._session.closed:
await self._session.close()
async def create_plugin_session(
self,
*,
persona_id: str | None = None,
persona_slug: str | None = None,
room_name: str,
livekit_url: str,
livekit_token: str,
source_participant_identity: str,
) -> dict[str, Any]:
"""Create a plugin session via POST /v1/sessions/plugins/livekit.
Returns dict with reservation_id and avatar_participant_identity.
"""
payload: dict[str, Any] = {
"room_name": room_name,
"livekit_url": livekit_url,
"livekit_token": livekit_token,
"source_participant_identity": source_participant_identity,
}
if persona_id:
payload["persona_id"] = persona_id
if persona_slug:
payload["persona_slug"] = persona_slug
headers = {
"Authorization": f"Bearer {self._api_key}",
"Content-Type": "application/json",
}
return await self._post("/v1/sessions/plugins/livekit", payload, headers)
async def _post(
self, endpoint: str, payload: dict[str, Any], headers: dict[str, str]
) -> dict[str, Any]:
url = f"{self._api_url}{endpoint}"
session = self._session or aiohttp.ClientSession()
try:
for i in range(self._conn_options.max_retry + 1):
try:
async with session.post(
url,
headers=headers,
json=payload,
timeout=aiohttp.ClientTimeout(sock_connect=self._conn_options.timeout),
) as response:
if not response.ok:
text = await response.text()
raise APIStatusError(
f"Keyframe API error for {url}: {response.status}",
status_code=response.status,
body=text,
)
return await response.json() # type: ignore
except Exception as e:
if isinstance(e, APIStatusError) and not e.retryable:
raise APIConnectionError(
f"Failed to call Keyframe API at {url} with non-retryable error",
retryable=False,
) from e
if isinstance(e, APIConnectionError):
logger.warning("Failed to call Keyframe API", extra={"error": str(e)})
else:
logger.exception("Failed to call Keyframe API")
if i < self._conn_options.max_retry:
await asyncio.sleep(self._conn_options._interval_for_retry(i))
finally:
if not self._session:
await session.close()
raise APIConnectionError("Failed to call Keyframe API after all retries.")