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
1 change: 1 addition & 0 deletions examples/avatar_agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ These providers work with pre-configured avatars using unique avatar identifiers
- **[LiveAvatar](./liveavatar/)** - [Platform](https://www.liveavatar.com/)
- **[Simli](./simli/)** - [Platform](https://app.simli.com/)
- **[Tavus](./tavus/)** - [Platform](https://www.tavus.io/)
- **[TruGen](./trugen/)** - [Platform](https://app.trugen.ai/)

### 🖼️ Cloud-Based with Image Upload

Expand Down
28 changes: 28 additions & 0 deletions examples/avatar_agents/trugen/README.md
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
```
36 changes: 36 additions & 0 deletions examples/avatar_agents/trugen/agent_worker.py
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)
17 changes: 17 additions & 0 deletions livekit-plugins/livekit-plugins-trugen/README.md
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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use descriptive link text instead of “link”.

Markdownlint flags this; swap to a descriptive anchor for clarity.

✏️ Proposed fix
-Generate an API key from our Developer Studio [link](https://app.trugen.ai) and set the `TRUGEN_API_KEY` environment variable with it:
+Generate an API key from our [Developer Studio](https://app.trugen.ai) and set the `TRUGEN_API_KEY` environment variable with it:
📝 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.

Suggested change
Generate an API key from our Developer Studio [link](https://app.trugen.ai) and set the `TRUGEN_API_KEY` environment variable with it:
Generate an API key from our [Developer Studio](https://app.trugen.ai) and set the `TRUGEN_API_KEY` environment variable with it:
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

13-13: Link text should be descriptive

(MD059, descriptive-link-text)

🤖 Prompt for AI Agents
In `@livekit-plugins/livekit-plugins-trugen/README.md` at line 13, Replace the
non-descriptive anchor text "link" in the sentence "Generate an API key from our
Developer Studio [link](https://app.trugen.ai) and set the `TRUGEN_API_KEY`
environment variable with it" with a clear label such as "Trugen Developer
Studio" (e.g., use [Trugen Developer Studio](https://app.trugen.ai)), updating
the markdown so the anchor text is descriptive while keeping the
`TRUGEN_API_KEY` environment variable mention unchanged.


```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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*.py" | head -20

Repository: livekit/agents

Length of output: 984


🏁 Script executed:

# Find the Plugin base class definition
rg "class Plugin" --type py -A 3

Repository: livekit/agents

Length of output: 360


🏁 Script executed:

# Search for register_plugin method
rg "def register_plugin" --type py -A 10

Repository: 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__.py

Repository: 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 2

Repository: livekit/agents

Length of output: 461


🏁 Script executed:

# Check if TrugenPlugin class is missing docstring
rg "class TrugenPlugin" --type py -A 5

Repository: 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 -80

Repository: livekit/agents

Length of output: 1888


🏁 Script executed:

# Check other plugin implementations for comparison
find . -path "*/plugins/*/livekit/plugins/*/__init__.py" -type f | head -5

Repository: livekit/agents

Length of output: 40


Add class docstring and document threading requirement.

The Plugin.register_plugin method enforces main-thread registration and raises RuntimeError if called from a worker thread. This module-level call at import time will crash if imported from a worker context. Current imports appear main-thread safe, but future imports should avoid worker threads.

Additionally, TrugenPlugin class is missing a docstring, which violates the Google-style docstring requirement.

🤖 Prompt for AI Agents
In `@livekit-plugins/livekit-plugins-trugen/livekit/plugins/trugen/__init__.py`
around lines 31 - 36, Add a Google-style docstring to the TrugenPlugin class
describing its purpose and the threading requirement, and remove the
module-level Plugin.register_plugin(TrugenPlugin()) call; instead create a
function like register_trugen_plugin() that instantiates TrugenPlugin and calls
Plugin.register_plugin(…) only after verifying it’s running on the main thread
(e.g., check threading.current_thread() or raise a clear RuntimeError), so
imports from worker threads won’t crash; references: TrugenPlugin and
Plugin.register_plugin.


# 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,
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(
"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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use TrugenException and fix credential error message typos.

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
In `@livekit-plugins/livekit-plugins-trugen/livekit/plugins/trugen/avatar.py`
around lines 79 - 83, Replace the generic Exception with the project-specific
TrugenException when validating livekit credentials (checks for livekit_url,
livekit_api_key, livekit_api_secret) and correct the error message typos: change
"either pass then as arguments" to "either pass them as arguments" and
"enviroment" to "environment"; ensure this change occurs in the credential
validation block that references livekit_url/livekit_api_key/livekit_api_secret
(e.g., in the avatar initialization or factory function that currently raises
the Exception).


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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Retry loop undercounts attempts and skips any try when max_retry=0.

max_retry typically means “number of retries after the first attempt.” The current loop makes only max_retry total attempts and performs zero attempts when max_retry is 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
In `@livekit-plugins/livekit-plugins-trugen/livekit/plugins/trugen/avatar.py`
around lines 109 - 142, The retry loop currently iterates
range(self._conn_options.max_retry) which performs only max_retry total attempts
and zero attempts when max_retry=0; change the loop to perform the initial
attempt plus the configured retries by iterating
range(self._conn_options.max_retry + 1) (or otherwise compute attempts = 1 +
max_retry) so that at least one attempt is made and the intended number of
retries occur; update the loop variable name (e.g., i -> attempt) and keep the
existing backoff/sleep logic and final raise of APIConnectionError unchanged.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import logging

logger = logging.getLogger("livekit.plugins.trugen")
Empty file.
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"
39 changes: 39 additions & 0 deletions livekit-plugins/livekit-plugins-trugen/pyproject.toml
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"]
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ livekit-plugins-speechify = { workspace = true }
livekit-plugins-speechmatics = { workspace = true }
livekit-plugins-spitch = { workspace = true }
livekit-plugins-tavus = { workspace = true }
livekit-plugins-trugen = { workspace = true }
livekit-plugins-turn-detector = { workspace = true }
livekit-plugins-ultravox = { workspace = true }
livekit-plugins-upliftai = { workspace = true }
Expand Down
Loading
Loading