-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Added TruGen Avatar Plugin. #4430
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?
Changes from all commits
30a61f8
40f7695
c2a9ac2
8c09c38
d8521b4
3771338
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| # LiveKit TruGen.AI Realtime Avatar | ||
|
|
||
| This example demonstrates how to create a realtime avatar session for your Livekit Voice Agents using [TruGen Developer Studio](https://app.trugen.ai/). | ||
|
|
||
| Select your avatar [list](https://docs.trugen.ai/docs/avatars/overview) | ||
|
|
||
| ## Usage | ||
|
|
||
| * Update the environment: | ||
|
|
||
| ```bash | ||
| # TruGen Config | ||
| export TRUGEN_API_KEY="..." | ||
|
|
||
| # Google config (or other models, tts, stt) | ||
| export GOOGLE_API_KEY="..." | ||
|
|
||
| # LiveKit config | ||
| export LIVEKIT_API_KEY="..." | ||
| export LIVEKIT_API_SECRET="..." | ||
| export LIVEKIT_URL="..." | ||
| ``` | ||
|
|
||
| * Start the agent worker: | ||
|
|
||
| ```bash | ||
| python examples/avatar_agents/trugen/agent_worker.py dev | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import logging | ||
| import os | ||
|
|
||
| from dotenv import load_dotenv | ||
|
|
||
| from livekit.agents import Agent, AgentServer, AgentSession, JobContext, cli | ||
| from livekit.plugins import google, trugen | ||
|
|
||
| logger = logging.getLogger("trugen-avatar-example") | ||
| logger.setLevel(logging.INFO) | ||
|
|
||
| load_dotenv() | ||
|
|
||
| server = AgentServer() | ||
|
|
||
|
|
||
| @server.rtc_session() | ||
| async def entrypoint(ctx: JobContext): | ||
| session = AgentSession( | ||
| llm=google.realtime.RealtimeModel(), | ||
| resume_false_interruption=False, | ||
| ) | ||
|
|
||
| avatar_id = os.getenv("TRUGEN_AVATAR_ID") | ||
| trugen_avatar = trugen.AvatarSession(avatar_id=avatar_id) | ||
| await trugen_avatar.start(session, room=ctx.room) | ||
|
|
||
| await session.start( | ||
| agent=Agent(instructions="You are a friendly AI Agent."), | ||
| room=ctx.room, | ||
| ) | ||
| session.generate_reply(instructions="Greet the user with a joke.") | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| cli.run_app(server) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| # TruGen AI plugin for LiveKit Agents | ||
|
|
||
| Adding support for [TruGen.AI](https://docs.trugen.ai) realtime avatars. | ||
|
|
||
| ## Installation | ||
|
|
||
| ```bash | ||
| pip install livekit-plugins-trugen | ||
| ``` | ||
|
|
||
| ## Pre-requisites | ||
|
|
||
| Generate an API key from our Developer Studio [link](https://app.trugen.ai) and set the `TRUGEN_API_KEY` environment variable with it: | ||
|
|
||
| ```bash | ||
| export TRUGEN_API_KEY=<trugen-api-key> | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| # Copyright 2023 LiveKit, Inc. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| """TruGen.AI plugin for LiveKit Agents""" | ||
|
|
||
| from .avatar import AvatarSession, TrugenException | ||
| from .version import __version__ | ||
|
|
||
| __all__ = [ | ||
| "AvatarSession", | ||
| "TrugenException", | ||
| "__version__", | ||
| ] | ||
|
|
||
| from livekit.agents import Plugin | ||
|
|
||
| from .log import logger | ||
|
|
||
|
|
||
| class TrugenPlugin(Plugin): | ||
| def __init__(self) -> None: | ||
| super().__init__(__name__, __version__, __package__, logger) | ||
|
|
||
|
|
||
| Plugin.register_plugin(TrugenPlugin()) | ||
|
Comment on lines
+31
to
+36
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find . -type f -name "*.py" | head -20Repository: livekit/agents Length of output: 984 🏁 Script executed: # Find the Plugin base class definition
rg "class Plugin" --type py -A 3Repository: livekit/agents Length of output: 360 🏁 Script executed: # Search for register_plugin method
rg "def register_plugin" --type py -A 10Repository: livekit/agents Length of output: 926 🏁 Script executed: # Look at the specific file mentioned in the review
cat -n livekit-plugins/livekit-plugins-trugen/livekit/plugins/trugen/__init__.pyRepository: livekit/agents Length of output: 1556 🏁 Script executed: # Search for imports of trugen module in the codebase
rg "from.*trugen|import.*trugen" --type py -B 2 -A 2Repository: livekit/agents Length of output: 461 🏁 Script executed: # Check if TrugenPlugin class is missing docstring
rg "class TrugenPlugin" --type py -A 5Repository: livekit/agents Length of output: 671 🏁 Script executed: # Look at the Plugin base class to understand the pattern
cat -n livekit-agents/livekit/agents/plugin.py | head -80Repository: livekit/agents Length of output: 1888 🏁 Script executed: # Check other plugin implementations for comparison
find . -path "*/plugins/*/livekit/plugins/*/__init__.py" -type f | head -5Repository: livekit/agents Length of output: 40 Add class docstring and document threading requirement. The Additionally, 🤖 Prompt for AI Agents |
||
|
|
||
| # Cleanup docs of unexported modules | ||
| _module = dir() | ||
| NOT_IN_ALL = [m for m in _module if m not in __all__] | ||
|
|
||
| __pdoc__ = {} | ||
|
|
||
| for n in NOT_IN_ALL: | ||
| __pdoc__[n] = False | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import asyncio | ||
| import os | ||
|
|
||
| import aiohttp | ||
|
|
||
| from livekit import api, rtc | ||
| from livekit.agents import ( | ||
| DEFAULT_API_CONNECT_OPTIONS, | ||
| NOT_GIVEN, | ||
| AgentSession, | ||
| APIConnectionError, | ||
| APIConnectOptions, | ||
| APIStatusError, | ||
| NotGivenOr, | ||
| get_job_context, | ||
| utils, | ||
| ) | ||
| from livekit.agents.voice.avatar import DataStreamAudioOutput | ||
| from livekit.agents.voice.room_io import ATTRIBUTE_PUBLISH_ON_BEHALF | ||
|
|
||
| from .log import logger | ||
|
|
||
| _BASE_API_URL = "https://api.trugen.ai" | ||
| _AVATAR_AGENT_IDENTITY = "trugen-avatar" | ||
| _AVATAR_AGENT_NAME = "Trugen Avatar" | ||
| _DEFAULT_AVATAR_ID = "45e3f732" | ||
|
|
||
|
|
||
| class TrugenException(Exception): | ||
| """Exception for TruGen.AI errors""" | ||
|
|
||
|
|
||
| class AvatarSession: | ||
| """TruGen Realtime Avatar Session""" | ||
|
|
||
| def __init__( | ||
| self, | ||
| *, | ||
| avatar_id: NotGivenOr[str | None] = NOT_GIVEN, | ||
tinalenguyen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| api_key: NotGivenOr[str] = NOT_GIVEN, | ||
| avatar_participant_identity: NotGivenOr[str] = NOT_GIVEN, | ||
| avatar_participant_name: NotGivenOr[str] = NOT_GIVEN, | ||
| conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS, | ||
| ) -> None: | ||
| self._avatar_id = avatar_id or _DEFAULT_AVATAR_ID | ||
| self._api_url = _BASE_API_URL | ||
| self._api_key = api_key or os.getenv("TRUGEN_API_KEY") | ||
| if self._api_key is None: | ||
| raise TrugenException( | ||
| "The api_key not found; set this by passing api_key to the client or " | ||
| "by setting the TRUGEN_API_KEY environment variable" | ||
| ) | ||
|
|
||
| self._avatar_participant_identity = avatar_participant_identity or _AVATAR_AGENT_IDENTITY | ||
| self._avatar_participant_name = avatar_participant_name or _AVATAR_AGENT_NAME | ||
| self._http_session: aiohttp.ClientSession | None = None | ||
| self._conn_options = conn_options | ||
|
|
||
| def _ensure_http_session(self) -> aiohttp.ClientSession: | ||
| if self._http_session is None: | ||
| self._http_session = utils.http_context.http_session() | ||
|
|
||
| return self._http_session | ||
|
|
||
| async def start( | ||
| self, | ||
| agent_session: AgentSession, | ||
| room: rtc.Room, | ||
| *, | ||
| livekit_url: NotGivenOr[str] = NOT_GIVEN, | ||
| livekit_api_key: NotGivenOr[str] = NOT_GIVEN, | ||
| livekit_api_secret: NotGivenOr[str] = NOT_GIVEN, | ||
| ) -> None: | ||
| 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) | ||
| if not livekit_url or not livekit_api_key or not livekit_api_secret: | ||
| raise Exception( | ||
tinalenguyen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| "livekit_url, livekit_api_key, and livekit_api_secret not found," | ||
| "either pass then as arguments here or set enviroment variables." | ||
| ) | ||
|
Comment on lines
+79
to
+83
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use The custom exception exists; using it here keeps error handling consistent and improves messaging. ✏️ Proposed fix- if not livekit_url or not livekit_api_key or not livekit_api_secret:
- raise Exception(
- "livekit_url, livekit_api_key, and livekit_api_secret not found,"
- "either pass then as arguments here or set enviroment variables."
- )
+ if not livekit_url or not livekit_api_key or not livekit_api_secret:
+ raise TrugenException(
+ "livekit_url, livekit_api_key, and livekit_api_secret not found; "
+ "either pass them as arguments here or set environment variables."
+ )🤖 Prompt for AI Agents |
||
|
|
||
| job_ctx = get_job_context() | ||
| local_participant_identity = job_ctx.local_participant_identity | ||
| livekit_token = ( | ||
| api.AccessToken(api_key=livekit_api_key, api_secret=livekit_api_secret) | ||
| .with_kind("agent") | ||
| .with_identity(self._avatar_participant_identity) | ||
| .with_name(self._avatar_participant_name) | ||
| .with_grants(api.VideoGrants(room_join=True, room=room.name)) | ||
| # allow the avatar agent to publish audio and video on behalf of your local agent | ||
| .with_attributes({ATTRIBUTE_PUBLISH_ON_BEHALF: local_participant_identity}) | ||
| .to_jwt() | ||
| ) | ||
|
|
||
| logger.debug("Starting Realtime Avatar Session") | ||
| await self._start_session(livekit_url, livekit_token) | ||
|
|
||
| agent_session.output.audio = DataStreamAudioOutput( | ||
| room=room, | ||
| destination_identity=self._avatar_participant_identity, | ||
| wait_remote_track=rtc.TrackKind.KIND_VIDEO, | ||
| ) | ||
|
|
||
| async def _start_session(self, livekit_url: str, livekit_token: str) -> None: | ||
| assert self._api_key is not None | ||
| for i in range(self._conn_options.max_retry): | ||
| try: | ||
| async with self._ensure_http_session().post( | ||
| f"{self._api_url}/v1/sessions", | ||
| headers={ | ||
| "x-api-key": self._api_key, | ||
| }, | ||
| json={ | ||
| "avatar_id": self._avatar_id, | ||
| "livekit_url": livekit_url, | ||
| "livekit_token": livekit_token, | ||
| }, | ||
| 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 | ||
|
|
||
| except Exception as e: | ||
| if isinstance(e, APIConnectionError): | ||
| logger.warning( | ||
| "API Error; Unable to trigger TruGen.AI API backend.", | ||
| extra={"error": str(e)}, | ||
| ) | ||
| else: | ||
| logger.exception("API Error; Unable to trigger TruGen.AI API backend.") | ||
|
|
||
| if i < self._conn_options.max_retry - 1: | ||
| await asyncio.sleep(self._conn_options.retry_interval) | ||
|
|
||
| raise APIConnectionError("Max retries exhaused; Unable to start TruGen.AI Avatar Session.") | ||
|
Comment on lines
+109
to
+142
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Retry loop undercounts attempts and skips any try when max_retry=0.
🐛 Proposed fix- for i in range(self._conn_options.max_retry):
+ for i in range(self._conn_options.max_retry + 1):
try:
async with self._ensure_http_session().post(
f"{self._api_url}/v1/sessions",
@@
- if i < self._conn_options.max_retry - 1:
+ if i < self._conn_options.max_retry:
await asyncio.sleep(self._conn_options.retry_interval)
- raise APIConnectionError("Max retries exhaused; Unable to start TruGen.AI Avatar Session.")
+ raise APIConnectionError("Max retries exhausted; Unable to start TruGen.AI Avatar Session.")🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| import logging | ||
|
|
||
| logger = logging.getLogger("livekit.plugins.trugen") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| # Copyright 2025 LiveKit, Inc. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| __version__ = "1.3.10" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| [build-system] | ||
| requires = ["hatchling"] | ||
| build-backend = "hatchling.build" | ||
|
|
||
| [project] | ||
| name = "livekit-plugins-trugen" | ||
| dynamic = ["version"] | ||
| description = "Livekit Agent framework plugin for realtime TruGen AI avatars" | ||
| readme = "README.md" | ||
| license = "Apache-2.0" | ||
| requires-python = ">=3.9.0" | ||
| authors = [{ name = "LiveKit", email = "support@livekit.io" }] | ||
| keywords = ["voice", "ai", "realtime", "audio", "video", "livekit", "webrtc"] | ||
| classifiers = [ | ||
| "Intended Audience :: Developers", | ||
| "License :: OSI Approved :: Apache Software License", | ||
| "Topic :: Multimedia :: Sound/Audio", | ||
| "Topic :: Multimedia :: Video", | ||
| "Topic :: Scientific/Engineering :: Artificial Intelligence", | ||
| "Programming Language :: Python :: 3", | ||
| "Programming Language :: Python :: 3.9", | ||
| "Programming Language :: Python :: 3.10", | ||
| "Programming Language :: Python :: 3 :: Only", | ||
| ] | ||
| dependencies = ["livekit-agents>=1.3.10"] | ||
|
|
||
| [project.urls] | ||
| Documentation = "https://docs.livekit.io" | ||
| Website = "https://livekit.io/" | ||
| Source = "https://github.com/livekit/agents" | ||
|
|
||
| [tool.hatch.version] | ||
| path = "livekit/plugins/trugen/version.py" | ||
|
|
||
| [tool.hatch.build.targets.wheel] | ||
| packages = ["livekit"] | ||
|
|
||
| [tool.hatch.build.targets.sdist] | ||
| include = ["/livekit"] |
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.
Use descriptive link text instead of “link”.
Markdownlint flags this; swap to a descriptive anchor for clarity.
✏️ Proposed fix
📝 Committable suggestion
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
13-13: Link text should be descriptive
(MD059, descriptive-link-text)
🤖 Prompt for AI Agents