LemonSlice transport updates

This commit is contained in:
jp-lemon
2026-04-10 07:10:41 -03:00
committed by filipi87
parent 5f75728207
commit c134110399
4 changed files with 54 additions and 14 deletions

4
changelog/3995.added.md Normal file
View File

@@ -0,0 +1,4 @@
- Updated LemonSlice transport:
- Added `on_avatar_connected` and `on_avatar_disconnected` events triggered when the avatar joins and leaves the room
- Added `api_url` parameter to `LemonSliceNewSessionRequest` to allow overriding the LemonSlice API endpoint
- Added support for passing arbitrary named parameters to the LemonSlice API endpoint

View File

@@ -114,6 +114,14 @@ async def main():
logger.info("Client disconnected") logger.info("Client disconnected")
await task.cancel() await task.cancel()
@transport.event_handler("on_avatar_connected")
async def on_avatar_connected(transport, participant):
logger.info("Avatar connected")
@transport.event_handler("on_avatar_disconnected")
async def on_avatar_disconnected(transport, participant, reason):
logger.info(f"Avatar disconnected. Reason: {reason}")
runner = PipelineRunner() runner = PipelineRunner()
await runner.run(task) await runner.run(task)

View File

@@ -44,7 +44,9 @@ class LemonSliceApi:
idle_timeout: Optional[int] = None, idle_timeout: Optional[int] = None,
daily_room_url: Optional[str] = None, daily_room_url: Optional[str] = None,
daily_token: Optional[str] = None, daily_token: Optional[str] = None,
properties: Optional[dict[str, Any]] = None, connection_properties: Optional[dict[str, Any]] = None,
extra_properties: Optional[dict[str, Any]] = None,
api_url: Optional[str] = None,
) -> dict: ) -> dict:
"""Create a new session with the specified agent_id or agent_image_url. """Create a new session with the specified agent_id or agent_image_url.
@@ -55,7 +57,9 @@ class LemonSliceApi:
idle_timeout: Idle timeout in seconds. idle_timeout: Idle timeout in seconds.
daily_room_url: Daily room URL to use for the session. daily_room_url: Daily room URL to use for the session.
daily_token: Daily token for authenticating with the room. daily_token: Daily token for authenticating with the room.
properties: Additional properties to pass to the session. connection_properties: Additional connection properties to pass to the session.
extra_properties: Additional properties to pass to the session.
api_url: LemonSlice API URL override.
Returns: Returns:
Dictionary containing session_id, room_url, and control_url. Dictionary containing session_id, room_url, and control_url.
@@ -64,16 +68,14 @@ class LemonSliceApi:
ValueError: If neither agent_id nor agent_image_url is provided. ValueError: If neither agent_id nor agent_image_url is provided.
""" """
if not agent_id and not agent_image_url: if not agent_id and not agent_image_url:
# Fallback to a default agent if none is provided raise ValueError("Provide an agent_id or agent_image_url")
logger.debug("No agent_id or agent_image_url provided, using default agent")
agent_id = "agent_080308d8b6e99f47"
if agent_id and agent_image_url: if agent_id and agent_image_url:
raise ValueError("Provide exactly one of agent_id or agent_image_url, not both") raise ValueError("Provide exactly one of agent_id or agent_image_url, not both")
logger.debug( logger.debug(
f"Creating LemonSlice session: agent_id={agent_id}, agent_image_url={agent_image_url}" f"Creating LemonSlice session: agent_id={agent_id}, agent_image_url={agent_image_url}"
) )
payload: dict[str, object] = {"transport_type": "daily"} payload: dict[str, Any] = {"transport_type": "daily"}
if agent_id is not None: if agent_id is not None:
payload["agent_id"] = agent_id payload["agent_id"] = agent_id
if agent_image_url is not None: if agent_image_url is not None:
@@ -82,16 +84,19 @@ class LemonSliceApi:
payload["agent_prompt"] = agent_prompt payload["agent_prompt"] = agent_prompt
if idle_timeout is not None: if idle_timeout is not None:
payload["idle_timeout"] = idle_timeout payload["idle_timeout"] = idle_timeout
properties_dict: dict[str, Any] = dict(properties) if properties else {} properties_dict: dict[str, Any] = (
dict(connection_properties) if connection_properties else {}
)
if daily_room_url is not None: if daily_room_url is not None:
properties_dict["daily_url"] = daily_room_url properties_dict["daily_url"] = daily_room_url
if daily_token is not None: if daily_token is not None:
properties_dict["daily_token"] = daily_token properties_dict["daily_token"] = daily_token
if properties_dict: if properties_dict:
payload["properties"] = properties_dict payload["properties"] = properties_dict
async with self._session.post( if extra_properties:
self.LEMONSLICE_URL, headers=self._headers, json=payload payload.update(extra_properties)
) as r: url = api_url if api_url is not None else self.LEMONSLICE_URL
async with self._session.post(url, headers=self._headers, json=payload) as r:
r.raise_for_status() r.raise_for_status()
response = await r.json() response = await r.json()
logger.debug(f"Created LemonSlice session: {response}") logger.debug(f"Created LemonSlice session: {response}")

View File

@@ -16,7 +16,7 @@ from typing import Any, Awaitable, Callable, Mapping, Optional
import aiohttp import aiohttp
from daily.daily import AudioData from daily.daily import AudioData
from loguru import logger from loguru import logger
from pydantic import BaseModel from pydantic import BaseModel, ConfigDict
from pipecat.frames.frames import ( from pipecat.frames.frames import (
BotStartedSpeakingFrame, BotStartedSpeakingFrame,
@@ -54,9 +54,12 @@ class LemonSliceNewSessionRequest(BaseModel):
idle_timeout: Idle timeout in seconds. idle_timeout: Idle timeout in seconds.
daily_room_url: Daily room URL to use for the session. daily_room_url: Daily room URL to use for the session.
daily_token: Daily token for authenticating with the room. daily_token: Daily token for authenticating with the room.
lemonslice_properties: Additional properties to pass to the session. lemonslice_properties: Additional connection properties to pass to the session.
api_url: Override the LemonSlice API URL.
""" """
model_config = ConfigDict(extra="allow")
agent_image_url: Optional[str] = None agent_image_url: Optional[str] = None
agent_id: Optional[str] = None agent_id: Optional[str] = None
agent_prompt: Optional[str] = None agent_prompt: Optional[str] = None
@@ -64,6 +67,7 @@ class LemonSliceNewSessionRequest(BaseModel):
daily_room_url: Optional[str] = None daily_room_url: Optional[str] = None
daily_token: Optional[str] = None daily_token: Optional[str] = None
lemonslice_properties: Optional[dict] = None lemonslice_properties: Optional[dict] = None
api_url: Optional[str] = None
class LemonSliceCallbacks(BaseModel): class LemonSliceCallbacks(BaseModel):
@@ -135,6 +139,8 @@ class LemonSliceTransportClient:
async def _initialize(self) -> str: async def _initialize(self) -> str:
"""Initialize the conversation and return the room URL.""" """Initialize the conversation and return the room URL."""
connection_properties = dict(self._session_request.lemonslice_properties or {})
extra_properties = self._session_request.model_extra
response = await self._api.create_session( response = await self._api.create_session(
agent_image_url=self._session_request.agent_image_url, agent_image_url=self._session_request.agent_image_url,
agent_id=self._session_request.agent_id, agent_id=self._session_request.agent_id,
@@ -142,7 +148,9 @@ class LemonSliceTransportClient:
idle_timeout=self._session_request.idle_timeout, idle_timeout=self._session_request.idle_timeout,
daily_room_url=self._session_request.daily_room_url, daily_room_url=self._session_request.daily_room_url,
daily_token=self._session_request.daily_token, daily_token=self._session_request.daily_token,
properties=self._session_request.lemonslice_properties, connection_properties=connection_properties if connection_properties else None,
extra_properties=extra_properties if extra_properties else None,
api_url=self._session_request.api_url,
) )
self._session_id = response["session_id"] self._session_id = response["session_id"]
self._control_url = response["control_url"] self._control_url = response["control_url"]
@@ -669,6 +677,8 @@ class LemonSliceTransport(BaseTransport):
- on_client_connected(transport, participant): Participant connected to the session - on_client_connected(transport, participant): Participant connected to the session
- on_client_disconnected(transport, participant): Participant disconnected from the session - on_client_disconnected(transport, participant): Participant disconnected from the session
- on_avatar_connected(transport, participant): LemonSlice avatar connected to the session
- on_avatar_disconnected(transport, participant, reason): LemonSlice avatar disconnected from the session
Example:: Example::
@@ -722,11 +732,15 @@ class LemonSliceTransport(BaseTransport):
# these handlers. # these handlers.
self._register_event_handler("on_client_connected") self._register_event_handler("on_client_connected")
self._register_event_handler("on_client_disconnected") self._register_event_handler("on_client_disconnected")
self._register_event_handler("on_avatar_connected")
self._register_event_handler("on_avatar_disconnected")
async def _on_participant_left(self, participant, reason): async def _on_participant_left(self, participant, reason):
"""Handle participant left events.""" """Handle participant left events."""
ls_bot_name = await self._client.get_bot_name() ls_bot_name = await self._client.get_bot_name()
if participant.get("info", {}).get("userName", "") != ls_bot_name: if participant.get("info", {}).get("userName", "") == ls_bot_name:
await self._on_avatar_disconnected(participant, reason)
else:
await self._on_client_disconnected(participant) await self._on_client_disconnected(participant)
async def _on_participant_joined(self, participant): async def _on_participant_joined(self, participant):
@@ -736,6 +750,7 @@ class LemonSliceTransport(BaseTransport):
# Ignore the LemonSlice bot's microphone # Ignore the LemonSlice bot's microphone
if participant.get("info", {}).get("userName", "") == ls_bot_name: if participant.get("info", {}).get("userName", "") == ls_bot_name:
self._lemonslice_participant_id = participant["id"] self._lemonslice_participant_id = participant["id"]
await self._on_avatar_connected(participant)
else: else:
await self._on_client_connected(participant) await self._on_client_connected(participant)
if self._lemonslice_participant_id: if self._lemonslice_participant_id:
@@ -782,6 +797,14 @@ class LemonSliceTransport(BaseTransport):
self._output = LemonSliceOutputTransport(client=self._client, params=self._params) self._output = LemonSliceOutputTransport(client=self._client, params=self._params)
return self._output return self._output
async def _on_avatar_connected(self, participant: Any):
"""Handle avatar connected events."""
await self._call_event_handler("on_avatar_connected", participant)
async def _on_avatar_disconnected(self, participant: Any, reason: str):
"""Handle avatar disconnected events."""
await self._call_event_handler("on_avatar_disconnected", participant, reason)
async def _on_client_connected(self, participant: Any): async def _on_client_connected(self, participant: Any):
"""Handle client connected events.""" """Handle client connected events."""
await self._call_event_handler("on_client_connected", participant) await self._call_event_handler("on_client_connected", participant)