Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -13,6 +13,7 @@ These providers work with pre-configured avatars using unique avatar identifiers
- **[BitHuman](./bithuman/)** (Cloud mode) - [Platform](https://bithuman.ai/) | [Integration Guide](https://sdk.docs.bithuman.ai/#/preview/livekit-cloud-plugin)
- **[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") or "45e3f732"
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)
1 change: 1 addition & 0 deletions livekit-agents/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ speechify = ["livekit-plugins-speechify>=1.3.10"]
speechmatics = ["livekit-plugins-speechmatics>=1.3.10"]
spitch = ["livekit-plugins-spitch>=1.3.10"]
tavus = ["livekit-plugins-tavus>=1.3.10"]
trugen = ["livekit-plugins-trugen>=1.3.10"]
turn-detector = ["livekit-plugins-turn-detector>=1.3.10"]
ultravox = ["livekit-plugins-ultravox>=1.3.10"]
upliftai = ["livekit-plugins-upliftai>=1.3.10"]
Expand Down
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:

```bash
export TRUGEN_API_KEY=<trugen-api-key>
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# 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
from .version import __version__

__all__ = [
"AvatarSession",
"__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())

# 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,137 @@
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"


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
self._api_url = _BASE_API_URL
self._api_key = api_key or os.getenv("TRUGEN_API_KEY")
if self._api_key is None:
raise Exception(
"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."
)

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 retry exhaused; Unable to start TruGen.AI Avatar Session.")
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
raise APIConnectionError("Max retry exhaused; Unable to start TruGen.AI Avatar Session.")
raise APIConnectionError("Max retries exhausted; Unable to start TruGen.AI Avatar Session.")

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hey @tinalenguyen,
This is implemented.

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

Choose a reason for hiding this comment

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

🔴 Plugin version 1.3.10 is below the >=1.4.1 requirement declared in livekit-agents optional deps, making installation impossible

The trugen plugin's own version is 1.3.10 (version.py:15), but livekit-agents/pyproject.toml:110 declares the optional dependency as trugen = ["livekit-plugins-trugen>=1.4.1"]. Since 1.3.10 < 1.4.1, running pip install livekit-agents[trugen] will fail with a version resolution error — pip cannot find a livekit-plugins-trugen version satisfying >=1.4.1.

Root Cause and Comparison with Other Plugins

Every other avatar plugin in the repo has its version.py set to 1.4.1 (e.g. livekit-plugins/livekit-plugins-bey/livekit/plugins/bey/version.py:15 has __version__ = "1.4.1") and their corresponding optional dependency in livekit-agents/pyproject.toml also requires >=1.4.1. The trugen plugin appears to have been created from an older template without updating the version.

Additionally, the trugen pyproject.toml:25 declares dependencies = ["livekit-agents>=1.3.11"] instead of >=1.4.1 like all other plugins, which is a related inconsistency that could allow installation alongside an incompatible older agents version.

Impact: pip install livekit-agents[trugen] will fail outright. Even manual installation with pip install livekit-plugins-trugen would install a package advertising version 1.3.10, which is inconsistent with the rest of the monorepo's 1.4.1 release cadence.

Suggested change
__version__ = "1.3.10"
__version__ = "1.4.1"
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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