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
125 changes: 125 additions & 0 deletions examples/voice_agents/telnyx_voice_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"""
Telnyx Voice Agent Example.

This example demonstrates a voice agent using:
- Telnyx STT (Speech-to-Text)
- Telnyx TTS (Text-to-Speech)
- OpenAI LLM (GPT-4.1-mini)

Usage:
export TELNYX_API_KEY='your_telnyx_api_key'
export OPENAI_API_KEY='your_openai_api_key'
export LIVEKIT_URL='wss://your-livekit-server'
export LIVEKIT_API_KEY='your_livekit_api_key'
export LIVEKIT_API_SECRET='your_livekit_api_secret'

python telnyx_voice_agent.py dev # Development mode with hot reload
python telnyx_voice_agent.py console # Terminal mode (no server needed)
"""

import logging

from dotenv import load_dotenv

from livekit.agents import (
Agent,
AgentServer,
AgentSession,
JobContext,
JobProcess,
RunContext,
cli,
metrics,
room_io,
)
from livekit.agents.llm import function_tool
from livekit.plugins import openai, silero, telnyx

logger = logging.getLogger("telnyx-voice-agent")

load_dotenv()


class TelnyxVoiceAgent(Agent):
def __init__(self) -> None:
super().__init__(
instructions=(
"You are a helpful voice assistant powered by Telnyx. "
"Keep your responses concise and conversational. "
"Do not use emojis, asterisks, or markdown in your responses. "
"You are friendly and professional."
),
)

async def on_enter(self):
self.session.generate_reply(allow_interruptions=False)

@function_tool
async def get_current_time(self, context: RunContext):
"""Called when the user asks for the current time."""
import datetime

now = datetime.datetime.now()
return f"The current time is {now.strftime('%I:%M %p')}."

@function_tool
async def lookup_weather(self, context: RunContext, location: str):
"""Called when the user asks about the weather.

Args:
location: The city or location to get weather for.
"""
logger.info(f"Looking up weather for {location}")
return f"The weather in {location} is sunny with a temperature of 72 degrees Fahrenheit."


server = AgentServer()


def prewarm(proc: JobProcess):
proc.userdata["vad"] = silero.VAD.load()


server.setup_fnc = prewarm


@server.rtc_session()
async def entrypoint(ctx: JobContext):
ctx.log_context_fields = {
"room": ctx.room.name,
}

session = AgentSession(
stt=telnyx.STT(
language="en",
transcription_engine="telnyx",
),
llm=openai.LLM(model="gpt-4.1-mini"),
tts=telnyx.TTS(
voice="Telnyx.NaturalHD.astra",
),
vad=ctx.proc.userdata["vad"],
)

usage_collector = metrics.UsageCollector()

@session.on("metrics_collected")
def _on_metrics_collected(ev):
metrics.log_metrics(ev.metrics)
usage_collector.collect(ev.metrics)

async def log_usage():
summary = usage_collector.get_summary()
logger.info(f"Usage: {summary}")

ctx.add_shutdown_callback(log_usage)

await session.start(
agent=TelnyxVoiceAgent(),
room=ctx.room,
room_options=room_io.RoomOptions(),
)


if __name__ == "__main__":
cli.run_app(server)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from .stt import STT
from .tts import TTS
from .version import __version__

__all__ = ["STT", "TTS", "__version__"]
Comment on lines +1 to +5
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check plugin __init__.py files from other plugins mentioned in learnings
echo "=== OpenAI Plugin ==="
cat livekit-plugins/livekit-plugins-openai/livekit/plugins/openai/__init__.py 2>/dev/null | head -30

echo -e "\n=== Deepgram Plugin ==="
cat livekit-plugins/livekit-plugins-deepgram/livekit/plugins/deepgram/__init__.py 2>/dev/null | head -30

echo -e "\n=== Ultravox Plugin ==="
cat livekit-plugins/livekit-plugins-ultravox/livekit/plugins/ultravox/__init__.py 2>/dev/null | head -30

echo -e "\n=== Personaplex Plugin (from learning) ==="
cat livekit-plugins/livekit-plugins-personaplex/livekit/plugins/personaplex/__init__.py 2>/dev/null | head -30

echo -e "\n=== Telnyx Plugin (current) ==="
cat livekit-plugins/livekit-plugins-telnyx/livekit/plugins/telnyx/__init__.py 2>/dev/null

Repository: livekit/agents

Length of output: 3507


🏁 Script executed:

#!/bin/bash
# Check the full Deepgram and Ultravox __init__.py files to see plugin registration
echo "=== Deepgram Plugin (full file) ==="
wc -l livekit-plugins/livekit-plugins-deepgram/livekit/plugins/deepgram/__init__.py

echo -e "\n=== Ultravox Plugin (full file) ==="
wc -l livekit-plugins/livekit-plugins-ultravox/livekit/plugins/ultravox/__init__.py

# Get full contents
echo -e "\n=== Deepgram Plugin Content ==="
cat livekit-plugins/livekit-plugins-deepgram/livekit/plugins/deepgram/__init__.py

echo -e "\n=== Ultravox Plugin Content ==="
cat livekit-plugins/livekit-plugins-ultravox/livekit/plugins/ultravox/__init__.py

Repository: livekit/agents

Length of output: 3205


🏁 Script executed:

#!/bin/bash
# Check the full telnyx __init__.py file
echo "=== Telnyx Plugin file size ==="
wc -l livekit-plugins/livekit-plugins-telnyx/livekit/plugins/telnyx/__init__.py

echo -e "\n=== Telnyx Plugin full content ==="
cat livekit-plugins/livekit-plugins-telnyx/livekit/plugins/telnyx/__init__.py

echo -e "\n=== Check if log.py exists in telnyx ==="
ls -la livekit-plugins/livekit-plugins-telnyx/livekit/plugins/telnyx/log.py 2>&1

Repository: livekit/agents

Length of output: 475


Add plugin registration following the established pattern.

The telnyx plugin is missing the standard plugin registration that all other plugins implement. Based on the consistent pattern in deepgram and ultravox, add the following after the __all__ definition:

Plugin registration pattern
from livekit.agents import Plugin

from .log import logger


class TelnyxPlugin(Plugin):
    def __init__(self) -> None:
        super().__init__(__name__, __version__, __package__, logger)


Plugin.register_plugin(TelnyxPlugin())

# 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

This ensures proper plugin registration with the LiveKit agents framework, consistent with the plugin system design.

🤖 Prompt for AI Agents
In `@livekit-plugins/livekit-plugins-telnyx/livekit/plugins/telnyx/__init__.py`
around lines 1 - 5, Add the standard plugin registration after the existing
__all__ by importing Plugin from livekit.agents and the module logger, define a
TelnyxPlugin class that calls super().__init__(__name__, __version__,
__package__, logger), register it with Plugin.register_plugin(TelnyxPlugin()),
and then build the NOT_IN_ALL list and __pdoc__ cleanup so unexported names
(beyond STT, TTS, __version__ in __all__) are hidden from docs; use the symbols
TelnyxPlugin, Plugin.register_plugin, logger, NOT_IN_ALL and __pdoc__ to locate
where to insert this code.



from livekit.agents import Plugin

from .log import logger


class TelnyxPlugin(Plugin):
def __init__(self) -> None:
super().__init__(__name__, __version__, __package__, logger)


Plugin.register_plugin(TelnyxPlugin())
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from __future__ import annotations

import os

import aiohttp

API_BASE_URL = "wss://api.telnyx.com/v2"
TTS_ENDPOINT = f"{API_BASE_URL}/text-to-speech/speech"
STT_ENDPOINT = f"{API_BASE_URL}/speech-to-text/transcription"

SAMPLE_RATE = 16000
NUM_CHANNELS = 1


def get_api_key(api_key: str | None = None) -> str:
resolved_key = api_key or os.environ.get("TELNYX_API_KEY")
if not resolved_key:
raise ValueError("Telnyx API key required. Set TELNYX_API_KEY or provide api_key.")
return resolved_key


class SessionManager:
def __init__(self, http_session: aiohttp.ClientSession | None = None) -> None:
self._session = http_session
self._owns_session = False

def ensure_session(self) -> aiohttp.ClientSession:
if not self._session:
self._session = aiohttp.ClientSession()
self._owns_session = True
return self._session

async def close(self) -> None:
if self._owns_session and self._session:
await self._session.close()
self._session = None
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import logging

logger = logging.getLogger("livekit.plugins.telnyx")
Empty file.
Loading