Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelog/3995.added.md
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions changelog/3995.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Fixed LemonSlice transport example to use a publicly accessible ElevenLabs voice ID
10 changes: 9 additions & 1 deletion examples/foundational/56-lemonslice-transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ async def main():
tts = ElevenLabsTTSService(
api_key=os.getenv("ELEVENLABS_API_KEY", ""),
settings=ElevenLabsTTSService.Settings(
voice="71a7ad14-091c-4e8e-a314-022ece01c121", # British Reading Lady
voice="21m00Tcm4TlvDq8ikWAM", # Public voice ID
),
)

Expand Down Expand Up @@ -114,6 +114,14 @@ async def on_client_disconnected(transport, participant):
logger.info("Client disconnected")
await task.cancel()

@transport.event_handler("on_avatar_connected")
async def on_avatar_connected(transport, participant):
logger.info(f"Avatar connected")

@transport.event_handler("on_avatar_disconnected")
async def on_avatar_disconnected(transport, participant):
logger.info(f"Avatar disconnected")

runner = PipelineRunner()

await runner.run(task)
Expand Down
17 changes: 9 additions & 8 deletions src/pipecat/transports/lemonslice/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ async def create_session(
daily_room_url: Optional[str] = None,
daily_token: Optional[str] = None,
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.

Expand All @@ -56,6 +57,7 @@ async def create_session(
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.
api_url: LemonSlice API URL override.

Returns:
Dictionary containing session_id, room_url, and control_url.
Expand All @@ -64,16 +66,14 @@ async def create_session(
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:
Expand All @@ -82,16 +82,17 @@ async def create_session(
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] = {}
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 properties:
payload.update(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}")
Expand Down
29 changes: 26 additions & 3 deletions src/pipecat/transports/lemonslice/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
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,
Expand Down Expand Up @@ -55,15 +55,19 @@ class LemonSliceNewSessionRequest(BaseModel):
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.
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
idle_timeout: Optional[int] = None
daily_room_url: Optional[str] = None
daily_token: Optional[str] = None
lemonslice_properties: Optional[dict] = None
api_url: Optional[str] = None


class LemonSliceCallbacks(BaseModel):
Expand Down Expand Up @@ -135,14 +139,18 @@ def __init__(

async def _initialize(self) -> str:
"""Initialize the conversation and return the room URL."""
properties = dict(self._session_request.lemonslice_properties or {})
if self._session_request.model_extra:
properties.update(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,
agent_prompt=self._session_request.agent_prompt,
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,
properties=properties if properties else None,
api_url=self._session_request.api_url,
)
self._session_id = response["session_id"]
self._control_url = response["control_url"]
Expand Down Expand Up @@ -668,6 +676,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::

Expand Down Expand Up @@ -721,11 +731,15 @@ def __init__(
# 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)
else:
await self._on_client_disconnected(participant)

async def _on_participant_joined(self, participant):
Expand All @@ -735,6 +749,7 @@ async def _on_participant_joined(self, participant):
# 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:
Expand Down Expand Up @@ -781,6 +796,14 @@ def output(self) -> FrameProcessor:
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):
"""Handle avatar disconnected events."""
await self._call_event_handler("on_avatar_disconnected", participant)

async def _on_client_connected(self, participant: Any):
"""Handle client connected events."""
await self._call_event_handler("on_client_connected", participant)
Expand Down
Loading