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")
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()
await runner.run(task)

View File

@@ -44,7 +44,9 @@ class LemonSliceApi:
idle_timeout: Optional[int] = None,
daily_room_url: 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:
"""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.
daily_room_url: Daily room URL to use for the session.
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:
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.
"""
if not agent_id and not agent_image_url:
# Fallback to a default agent if none is provided
logger.debug("No agent_id or agent_image_url provided, using default agent")
agent_id = "agent_080308d8b6e99f47"
raise ValueError("Provide an agent_id or agent_image_url")
if agent_id and agent_image_url:
raise ValueError("Provide exactly one of agent_id or agent_image_url, not both")
logger.debug(
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:
payload["agent_id"] = agent_id
if agent_image_url is not None:
@@ -82,16 +84,19 @@ class LemonSliceApi:
payload["agent_prompt"] = agent_prompt
if idle_timeout is not None:
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:
properties_dict["daily_url"] = daily_room_url
if daily_token is not None:
properties_dict["daily_token"] = daily_token
if properties_dict:
payload["properties"] = properties_dict
async with self._session.post(
self.LEMONSLICE_URL, headers=self._headers, json=payload
) as r:
if extra_properties:
payload.update(extra_properties)
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()
response = await r.json()
logger.debug(f"Created LemonSlice session: {response}")

View File

@@ -16,7 +16,7 @@ from typing import Any, Awaitable, Callable, Mapping, Optional
import aiohttp
from daily.daily import AudioData
from loguru import logger
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict
from pipecat.frames.frames import (
BotStartedSpeakingFrame,
@@ -54,9 +54,12 @@ class LemonSliceNewSessionRequest(BaseModel):
idle_timeout: Idle timeout in seconds.
daily_room_url: Daily room URL to use for the session.
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_id: Optional[str] = None
agent_prompt: Optional[str] = None
@@ -64,6 +67,7 @@ class LemonSliceNewSessionRequest(BaseModel):
daily_room_url: Optional[str] = None
daily_token: Optional[str] = None
lemonslice_properties: Optional[dict] = None
api_url: Optional[str] = None
class LemonSliceCallbacks(BaseModel):
@@ -135,6 +139,8 @@ class LemonSliceTransportClient:
async def _initialize(self) -> str:
"""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(
agent_image_url=self._session_request.agent_image_url,
agent_id=self._session_request.agent_id,
@@ -142,7 +148,9 @@ class LemonSliceTransportClient:
idle_timeout=self._session_request.idle_timeout,
daily_room_url=self._session_request.daily_room_url,
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._control_url = response["control_url"]
@@ -669,6 +677,8 @@ class LemonSliceTransport(BaseTransport):
- on_client_connected(transport, participant): Participant connected to 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::
@@ -722,11 +732,15 @@ class LemonSliceTransport(BaseTransport):
# these handlers.
self._register_event_handler("on_client_connected")
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):
"""Handle participant left events."""
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)
async def _on_participant_joined(self, participant):
@@ -736,6 +750,7 @@ class LemonSliceTransport(BaseTransport):
# Ignore the LemonSlice bot's microphone
if participant.get("info", {}).get("userName", "") == ls_bot_name:
self._lemonslice_participant_id = participant["id"]
await self._on_avatar_connected(participant)
else:
await self._on_client_connected(participant)
if self._lemonslice_participant_id:
@@ -782,6 +797,14 @@ class LemonSliceTransport(BaseTransport):
self._output = LemonSliceOutputTransport(client=self._client, params=self._params)
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):
"""Handle client connected events."""
await self._call_event_handler("on_client_connected", participant)