-
Notifications
You must be signed in to change notification settings - Fork 2.6k
LemonSlice Plugin #4539
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
LemonSlice Plugin #4539
Conversation
📝 WalkthroughWalkthroughA new LemonSlice avatar plugin for LiveKit Agents is introduced, including comprehensive documentation, an example agent worker, API client implementation with retry logic, avatar session management with JWT credential handling, and workspace integration configuration. Changes
Sequence Diagram(s)sequenceDiagram
participant Worker as Agent Worker
participant Agent as AgentSession
participant Avatar as AvatarSession
participant API as LemonSlice API
participant Room as LiveKit Room
participant Output as DataStreamAudioOutput
Worker->>Agent: Create with STT/LLM/TTS
Worker->>Avatar: Create with image_url, prompt
Avatar->>Avatar: Ensure HTTP session
Avatar->>Avatar: Build JWT AccessToken
Avatar->>API: start_agent_session(agent_id/image_url, prompt, livekit_url/token)
API->>API: POST with retry logic
API-->>Avatar: return session_id
Avatar->>Output: Configure audio stream (sample_rate, identity)
Output->>Room: Bind to room, await participant
Worker->>Agent: Start agent in room
Worker->>Room: Agent publishes messages
Room->>Output: Route audio to avatar destination
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@examples/avatar_agents/lemonslice/agent_worker.py`:
- Around line 27-31: The environment variable lemonslice_image_url (from
os.getenv("LEMONSLICE_IMAGE_URL")) is used directly as agent_image_url when
constructing lemonslice.AvatarSession which can pass None to the API; add a
validation check after reading lemonslice_image_url to verify it is non-empty
and raise a clear exception (or call sys.exit) if missing, so
AvatarSession(agent_image_url=lemonslice_image_url, ...) is only called with a
valid URL.
In
`@livekit-plugins/livekit-plugins-lemonslice/livekit/plugins/lemonslice/__init__.py`:
- Around line 15-18: The module docstring in __init__.py contains a malformed
documentation URL with a duplicated "https://"
("https://https://docs.livekit.io/..."); update the docstring to remove the
extra scheme so the link reads
"https://docs.livekit.io/agents/models/avatar/plugins/lemonslice/" - edit the
top-level string literal in livekit/plugins/lemonslice/__init__.py accordingly.
In
`@livekit-plugins/livekit-plugins-lemonslice/livekit/plugins/lemonslice/api.py`:
- Around line 96-121: The retry loop currently iterates
range(self._conn_options.max_retry) and always retries; update it to
range(self._conn_options.max_retry + 1) so attempts = max_retry + 1, and when
catching exceptions, if isinstance(e, APIStatusError) and not e.retryable:
re-raise to avoid retrying 4xx non-retryable errors; keep handling
APIConnectionError separately as before. Replace usage of
self._conn_options.retry_interval with self._conn_options._interval_for_retry(i)
for backoff, and change the sleep/continue condition to i <
self._conn_options.max_retry so you only sleep when another attempt remains.
Ensure these changes are applied around the async with self._session.post(...)
block and the except handler where APIStatusError, APIConnectionError, and i are
referenced.
🧹 Nitpick comments (8)
pyproject.toml (1)
28-28: Minor formatting inconsistency.Missing space before the closing brace. Other entries use
{ workspace = true }(with space).🔧 Suggested fix
-livekit-plugins-lemonslice = { workspace = true} +livekit-plugins-lemonslice = { workspace = true }livekit-plugins/livekit-plugins-lemonslice/pyproject.toml (2)
13-13: Minor formatting: missing space after comma in keywords.🔧 Suggested fix
-keywords = ["voice", "ai", "realtime", "audio", "video", "livekit", "webrtc", "avatar", "agent","lemonslice"] +keywords = ["voice", "ai", "realtime", "audio", "video", "livekit", "webrtc", "avatar", "agent", "lemonslice"]
20-23: Consider adding classifiers for Python 3.11 and 3.12.Since
requires-python = ">=3.9.0", the package supports newer Python versions. Adding classifiers improves discoverability on PyPI.🔧 Suggested addition
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3 :: Only",examples/avatar_agents/lemonslice/agent_worker.py (1)
18-41: Add a Google-style docstring toentrypoint.
As per coding guidelines, public entrypoints should have Google-style docstrings.livekit-plugins/livekit-plugins-lemonslice/livekit/plugins/lemonslice/api.py (2)
30-67: Avoid leakingNOT_GIVENinto credentials/payload.
UsingorwithNOT_GIVENcan keep the sentinel (if it’s truthy), andlivekit_url/tokenare always serialized into JSON even when not provided. Safer to useutils.is_givenfor defaults and only include provided fields.🔧 Suggested fix
- ls_api_key = api_key or os.getenv("LEMONSLICE_API_KEY") + ls_api_key = api_key if utils.is_given(api_key) else os.getenv("LEMONSLICE_API_KEY") if ls_api_key is None: raise LemonSliceException("LEMONSLICE_API_KEY must be set") self._api_key = ls_api_key - self._api_url = api_url or DEFAULT_API_URL + self._api_url = api_url if utils.is_given(api_url) else DEFAULT_API_URL- payload: dict[str, Any] = { - "transport_type": "livekit", - "properties": { - "livekit_url": livekit_url, - "livekit_token": livekit_token, - }, - } + payload: dict[str, Any] = {"transport_type": "livekit", "properties": {}} + if utils.is_given(livekit_url): + payload["properties"]["livekit_url"] = livekit_url + if utils.is_given(livekit_token): + payload["properties"]["livekit_token"] = livekit_token + if not payload["properties"]: + payload.pop("properties")
34-44: Manageaiohttp.ClientSessionlifecycle.
Creating a session internally without a close path can leak sockets. Consider addingclose()/ async context manager support whensessionisn’t provided.livekit-plugins/livekit-plugins-lemonslice/livekit/plugins/lemonslice/avatar.py (2)
56-77: Preferutils.is_givenfor NOT_GIVEN defaults.
UsingorwithNOT_GIVENcan keep the sentinel (if it’s truthy), which may bypass required config checks and leak into identity fields. Safer to useutils.is_givenconsistently.🔧 Suggested fix
- self._avatar_participant_identity = avatar_participant_identity or _AVATAR_AGENT_IDENTITY - self._avatar_participant_name = avatar_participant_name or _AVATAR_AGENT_NAME + self._avatar_participant_identity = ( + avatar_participant_identity + if utils.is_given(avatar_participant_identity) + else _AVATAR_AGENT_IDENTITY + ) + self._avatar_participant_name = ( + avatar_participant_name if utils.is_given(avatar_participant_name) else _AVATAR_AGENT_NAME + )- livekit_url = livekit_url or (os.getenv("LIVEKIT_URL") or NOT_GIVEN) - livekit_api_key = livekit_api_key or (os.getenv("LIVEKIT_API_KEY") or NOT_GIVEN) - livekit_api_secret = livekit_api_secret or (os.getenv("LIVEKIT_API_SECRET") or NOT_GIVEN) + livekit_url = ( + livekit_url if utils.is_given(livekit_url) else (os.getenv("LIVEKIT_URL") or NOT_GIVEN) + ) + livekit_api_key = ( + livekit_api_key + if utils.is_given(livekit_api_key) + else (os.getenv("LIVEKIT_API_KEY") or NOT_GIVEN) + ) + livekit_api_secret = ( + livekit_api_secret + if utils.is_given(livekit_api_secret) + else (os.getenv("LIVEKIT_API_SECRET") or NOT_GIVEN) + )
65-73: Add a Google-style docstring tostart().
As per coding guidelines, public methods should include Google-style docstrings.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (12)
examples/avatar_agents/README.mdexamples/avatar_agents/lemonslice/README.mdexamples/avatar_agents/lemonslice/agent_worker.pylivekit-plugins/livekit-plugins-lemonslice/README.mdlivekit-plugins/livekit-plugins-lemonslice/livekit/plugins/lemonslice/__init__.pylivekit-plugins/livekit-plugins-lemonslice/livekit/plugins/lemonslice/api.pylivekit-plugins/livekit-plugins-lemonslice/livekit/plugins/lemonslice/avatar.pylivekit-plugins/livekit-plugins-lemonslice/livekit/plugins/lemonslice/log.pylivekit-plugins/livekit-plugins-lemonslice/livekit/plugins/lemonslice/py.typedlivekit-plugins/livekit-plugins-lemonslice/livekit/plugins/lemonslice/version.pylivekit-plugins/livekit-plugins-lemonslice/pyproject.tomlpyproject.toml
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
**/*.py: Format code with ruff
Run ruff linter and auto-fix issues
Run mypy type checker in strict mode
Maintain line length of 100 characters maximum
Ensure Python 3.9+ compatibility
Use Google-style docstrings
Files:
livekit-plugins/livekit-plugins-lemonslice/livekit/plugins/lemonslice/version.pylivekit-plugins/livekit-plugins-lemonslice/livekit/plugins/lemonslice/__init__.pyexamples/avatar_agents/lemonslice/agent_worker.pylivekit-plugins/livekit-plugins-lemonslice/livekit/plugins/lemonslice/avatar.pylivekit-plugins/livekit-plugins-lemonslice/livekit/plugins/lemonslice/log.pylivekit-plugins/livekit-plugins-lemonslice/livekit/plugins/lemonslice/api.py
🧠 Learnings (1)
📚 Learning: 2026-01-16T07:44:56.353Z
Learnt from: CR
Repo: livekit/agents PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T07:44:56.353Z
Learning: Follow the Plugin System pattern where plugins in livekit-plugins/ are separate packages registered via the Plugin base class
Applied to files:
pyproject.toml
🧬 Code graph analysis (3)
examples/avatar_agents/lemonslice/agent_worker.py (3)
livekit-agents/livekit/agents/voice/agent.py (2)
Agent(34-642)instructions(99-104)livekit-agents/livekit/agents/job.py (1)
JobContext(132-598)livekit-plugins/livekit-plugins-lemonslice/livekit/plugins/lemonslice/avatar.py (2)
AvatarSession(27-111)start(65-111)
livekit-plugins/livekit-plugins-lemonslice/livekit/plugins/lemonslice/avatar.py (3)
livekit-agents/livekit/agents/job.py (2)
api(288-296)get_job_context(56-63)livekit-plugins/livekit-plugins-lemonslice/livekit/plugins/lemonslice/api.py (3)
LemonSliceAPI(27-122)LemonSliceException(20-21)start_agent_session(45-81)livekit-agents/livekit/agents/utils/http_context.py (1)
http_session(40-51)
livekit-plugins/livekit-plugins-lemonslice/livekit/plugins/lemonslice/api.py (3)
livekit-agents/livekit/agents/_exceptions.py (2)
APIConnectionError(84-88)APIStatusError(45-81)livekit-agents/livekit/agents/types.py (1)
APIConnectOptions(54-88)livekit-agents/livekit/agents/utils/misc.py (1)
is_given(25-26)
🔇 Additional comments (5)
livekit-plugins/livekit-plugins-lemonslice/livekit/plugins/lemonslice/log.py (1)
1-3: LGTM!Standard logger setup following the established plugin pattern with the correct namespace.
livekit-plugins/livekit-plugins-lemonslice/README.md (1)
1-5: LGTM!README follows the standard plugin documentation pattern with appropriate links.
examples/avatar_agents/lemonslice/README.md (1)
1-28: LGTM!Clear documentation with all required environment variables and startup command.
livekit-plugins/livekit-plugins-lemonslice/livekit/plugins/lemonslice/__init__.py (1)
20-40: LGTM!Plugin structure correctly follows the established pattern with proper exports and auto-registration. Based on learnings, this aligns with the Plugin System pattern where plugins are registered via the
Pluginbase class.livekit-plugins/livekit-plugins-lemonslice/livekit/plugins/lemonslice/version.py (1)
15-15: Version constant looks good.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| lemonslice_image_url = os.getenv("LEMONSLICE_IMAGE_URL") | ||
| avatar = lemonslice.AvatarSession( | ||
| agent_image_url=lemonslice_image_url, | ||
| agent_prompt="Be expressive in your movements and use your hands while talking.", # Prompt to guide the avatar's movements | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validate LEMONSLICE_IMAGE_URL before use.
If the env var is unset, None will be treated as a provided value and sent to the API, leading to a runtime failure. Fail fast with a clearer error.
🔧 Suggested fix
- lemonslice_image_url = os.getenv("LEMONSLICE_IMAGE_URL")
+ lemonslice_image_url = os.getenv("LEMONSLICE_IMAGE_URL")
+ if not lemonslice_image_url:
+ raise ValueError("LEMONSLICE_IMAGE_URL must be set")
avatar = lemonslice.AvatarSession(
agent_image_url=lemonslice_image_url,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| lemonslice_image_url = os.getenv("LEMONSLICE_IMAGE_URL") | |
| avatar = lemonslice.AvatarSession( | |
| agent_image_url=lemonslice_image_url, | |
| agent_prompt="Be expressive in your movements and use your hands while talking.", # Prompt to guide the avatar's movements | |
| ) | |
| lemonslice_image_url = os.getenv("LEMONSLICE_IMAGE_URL") | |
| if not lemonslice_image_url: | |
| raise ValueError("LEMONSLICE_IMAGE_URL must be set") | |
| avatar = lemonslice.AvatarSession( | |
| agent_image_url=lemonslice_image_url, | |
| agent_prompt="Be expressive in your movements and use your hands while talking.", # Prompt to guide the avatar's movements | |
| ) |
🤖 Prompt for AI Agents
In `@examples/avatar_agents/lemonslice/agent_worker.py` around lines 27 - 31, The
environment variable lemonslice_image_url (from
os.getenv("LEMONSLICE_IMAGE_URL")) is used directly as agent_image_url when
constructing lemonslice.AvatarSession which can pass None to the API; add a
validation check after reading lemonslice_image_url to verify it is non-empty
and raise a clear exception (or call sys.exit) if missing, so
AvatarSession(agent_image_url=lemonslice_image_url, ...) is only called with a
valid URL.
| """LemonSlice virtual avatar plugin for LiveKit Agents | ||
|
|
||
| See https://https://docs.livekit.io/agents/models/avatar/plugins/lemonslice/ for more information. | ||
| """ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix duplicate https:// in documentation URL.
The URL has a typo with double https:// prefix.
🔧 Suggested fix
"""LemonSlice virtual avatar plugin for LiveKit Agents
-See https://https://docs.livekit.io/agents/models/avatar/plugins/lemonslice/ for more information.
+See https://docs.livekit.io/agents/models/avatar/plugins/lemonslice/ for more information.
"""📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| """LemonSlice virtual avatar plugin for LiveKit Agents | |
| See https://https://docs.livekit.io/agents/models/avatar/plugins/lemonslice/ for more information. | |
| """ | |
| """LemonSlice virtual avatar plugin for LiveKit Agents | |
| See https://docs.livekit.io/agents/models/avatar/plugins/lemonslice/ for more information. | |
| """ |
🤖 Prompt for AI Agents
In
`@livekit-plugins/livekit-plugins-lemonslice/livekit/plugins/lemonslice/__init__.py`
around lines 15 - 18, The module docstring in __init__.py contains a malformed
documentation URL with a duplicated "https://"
("https://https://docs.livekit.io/..."); update the docstring to remove the
extra scheme so the link reads
"https://docs.livekit.io/agents/models/avatar/plugins/lemonslice/" - edit the
top-level string literal in livekit/plugins/lemonslice/__init__.py accordingly.
| for i in range(self._conn_options.max_retry): | ||
| try: | ||
| async with self._session.post( | ||
| self._api_url, | ||
| headers={ | ||
| "Content-Type": "application/json", | ||
| "X-API-Key": self._api_key, | ||
| }, | ||
| json=payload, | ||
| timeout=aiohttp.ClientTimeout(sock_connect=self._conn_options.timeout), | ||
| ) as response: | ||
| if not response.ok: | ||
| text = await response.text() | ||
| raise APIStatusError( | ||
| "Server returned an error", status_code=response.status, body=text | ||
| ) | ||
| return await response.json() # type: ignore | ||
| except Exception as e: | ||
| if isinstance(e, APIConnectionError): | ||
| logger.warning("failed to call LemonSlice api", extra={"error": str(e)}) | ||
| else: | ||
| logger.exception("failed to call lemonslice api") | ||
|
|
||
| if i < self._conn_options.max_retry - 1: | ||
| await asyncio.sleep(self._conn_options.retry_interval) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
find . -name "api.py" | grep lemonsliceRepository: livekit/agents
Length of output: 136
🏁 Script executed:
git ls-files | grep -i lemonslice | head -20Repository: livekit/agents
Length of output: 738
🏁 Script executed:
# Check the file structure
fd "api.py" --path "*lemonslice*"Repository: livekit/agents
Length of output: 273
🏁 Script executed:
cat -n ./livekit-plugins/livekit-plugins-lemonslice/livekit/plugins/lemonslice/api.py | head -130Repository: livekit/agents
Length of output: 5209
🏁 Script executed:
# Find APIStatusError definition
rg "class APIStatusError" --type pyRepository: livekit/agents
Length of output: 134
🏁 Script executed:
# Find conn_options definition
rg "_conn_options\|class.*ConnOptions" --type py -A 5 | head -50Repository: livekit/agents
Length of output: 40
🏁 Script executed:
cat ./livekit-agents/livekit/agents/_exceptions.pyRepository: livekit/agents
Length of output: 2755
🏁 Script executed:
# Find APIConnectOptions definition
rg "class APIConnectOptions" --type py -A 20Repository: livekit/agents
Length of output: 1381
🏁 Script executed:
# Check full APIConnectOptions class definition
rg "class APIConnectOptions" --type py -A 50 | head -60Repository: livekit/agents
Length of output: 3490
🏁 Script executed:
# Search for _interval_for_retry method anywhere in the codebase
rg "_interval_for_retry" --type pyRepository: livekit/agents
Length of output: 1318
🏁 Script executed:
# Check if there are other API implementations in the codebase to see retry patterns
rg "for i in range.*max_retry" --type py -B 2 -A 15 | head -100Repository: livekit/agents
Length of output: 10495
🏁 Script executed:
# Verify the sleep condition logic
# With range(max_retry + 1) and i < max_retry, when max_retry=3:
# i=0: sleep (0 < 3) ✓
# i=1: sleep (1 < 3) ✓
# i=2: sleep (2 < 3) ✓
# i=3: no sleep (3 < 3) ✗
# This means 3 sleeps for 4 attempts, which is correct
# Check if there are any ruff formatting issues in the file
rg "for i in range" ./livekit-plugins/livekit-plugins-lemonslice/livekit/plugins/lemonslice/api.py -A 30 | wc -lRepository: livekit/agents
Length of output: 60
Fix retry loop semantics and respect non-retryable errors.
The retry loop should allow max_retry retries (i.e., max_retry + 1 total attempts), and should not retry 4xx client errors which are marked as non-retryable in APIStatusError. Additionally, use _interval_for_retry() for backoff consistency with other API integrations in the codebase.
Changes needed:
- Change
range(self._conn_options.max_retry)torange(self._conn_options.max_retry + 1)to allow proper retry semantics - Check
if isinstance(e, APIStatusError) and not e.retryable: raiseto respect non-retryable errors (4xx responses) - Replace
self._conn_options.retry_intervalwithself._conn_options._interval_for_retry(i)for consistent backoff - Update sleep condition from
i < self._conn_options.max_retry - 1toi < self._conn_options.max_retry
🤖 Prompt for AI Agents
In `@livekit-plugins/livekit-plugins-lemonslice/livekit/plugins/lemonslice/api.py`
around lines 96 - 121, The retry loop currently iterates
range(self._conn_options.max_retry) and always retries; update it to
range(self._conn_options.max_retry + 1) so attempts = max_retry + 1, and when
catching exceptions, if isinstance(e, APIStatusError) and not e.retryable:
re-raise to avoid retrying 4xx non-retryable errors; keep handling
APIConnectionError separately as before. Replace usage of
self._conn_options.retry_interval with self._conn_options._interval_for_retry(i)
for backoff, and change the sleep/continue condition to i <
self._conn_options.max_retry so you only sleep when another attempt remains.
Ensure these changes are applied around the async with self._session.post(...)
block and the except handler where APIStatusError, APIConnectionError, and i are
referenced.
LemonSlice Integration Docs
LemonSlice API Docs
Summary by CodeRabbit
Release Notes
New Features
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.