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 @@ -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")
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,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())

# 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,
Copy link
Member

Choose a reason for hiding this comment

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

could you set a default for avatar_id? maybe a variable DEFAULT_AVATAR_ID

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

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(
Copy link
Member

Choose a reason for hiding this comment

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

could you create and use a TrugenException class? this would match the patterns of our avatar plugins

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

"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 retries exhaused; Unable to start TruGen.AI Avatar Session.")
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 = "[email protected]" }]
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