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 @@ -16,6 +16,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(voice="kore"),
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](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,164 @@
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 = "7d881c1b"


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 = (
_DEFAULT_AVATAR_ID if avatar_id is NOT_GIVEN or avatar_id is None else avatar_id
)
self._api_key = os.getenv("TRUGEN_API_KEY") if api_key is NOT_GIVEN else api_key
if not self._api_key:
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"
)
if avatar_participant_identity is NOT_GIVEN or avatar_participant_identity is None:
self._avatar_participant_identity: str = _AVATAR_AGENT_IDENTITY
else:
self._avatar_participant_identity = avatar_participant_identity
if avatar_participant_name is NOT_GIVEN or avatar_participant_name is None:
self._avatar_participant_name: str = _AVATAR_AGENT_NAME
else:
self._avatar_participant_name = avatar_participant_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:
if livekit_url is NOT_GIVEN:
livekit_url = os.getenv("LIVEKIT_URL") or NOT_GIVEN
if livekit_api_key is NOT_GIVEN:
livekit_api_key = os.getenv("LIVEKIT_API_KEY") or NOT_GIVEN
if livekit_api_secret is NOT_GIVEN:
livekit_api_secret = os.getenv("LIVEKIT_API_SECRET") or NOT_GIVEN

if (
livekit_url is NOT_GIVEN
or livekit_api_key is NOT_GIVEN
or livekit_api_secret is NOT_GIVEN
or 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."
)

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 + 1):
try:
async with self._ensure_http_session().post(
f"{_BASE_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, APIStatusError):
logger.warning(
"API Error; Unable to trigger TruGen.AI API backend.",
extra={"status_code": e.status_code, "body": e.body},
)
if not e.retryable:
raise
else:
logger.warning(
"API Error; Unable to trigger TruGen.AI API backend.",
extra={"error": str(e)},
)

if i < self._conn_options.max_retry:
await asyncio.sleep(self._conn_options.retry_interval)

raise APIConnectionError("Max retries exhausted; 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.11"]

[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 @@ -50,6 +50,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