here is my python agent code that’s expected to join the room automatically but it’s not joinning
#!/usr/bin/env python3
“”"
Production-leaning LiveKit Voice Agent (Python)
- Env-only secrets (no in-file os.environ mutation)
- Room dispatch filtering by prefix
- Prewarm Silero VAD & turn detector
- AgentSession + RoomInputOptions start
“”"
import asyncio
import logging
import os
from dotenv
import load_dotenv
# LiveKit Agents (Python)
from livekit
import agents
from livekit.agents
import (
JobContext,
WorkerOptions,
cli,
AgentSession,
Agent,
RoomInputOptions,
AutoSubscribe,
)
# Plugins
from livekit.plugins
import openai, silero
from livekit.plugins.turn_detector.english
import EnglishModel
# -----------------------------------------------------------------------------
# Configuration
# -----------------------------------------------------------------------------
load_dotenv()
LIVEKIT_URL = os.getenv(“LIVEKIT_URL”, “”)
LIVEKIT_API_KEY = os.getenv(“LIVEKIT_API_KEY”, “”)
LIVEKIT_API_SECRET = os.getenv(“LIVEKIT_API_SECRET”, “”)
OPENAI_API_KEY = os.getenv(“OPENAI_API_KEY”, “”)
OPENAI_LLM_MODEL = os.getenv(“OPENAI_LLM_MODEL”, “gpt-4o-mini”)
OPENAI_STT_MODEL = os.getenv(“OPENAI_STT_MODEL”, “gpt-4o-mini-transcribe”)
OPENAI_TTS_MODEL = os.getenv(“OPENAI_TTS_MODEL”, “tts-1")
OPENAI_TTS_VOICE = os.getenv(“OPENAI_TTS_VOICE”, “alloy”)
AGENT_INSTRUCTIONS = os.getenv(
“AGENT_INSTRUCTIONS”,
“You are a friendly ESL tutor. Be concise, natural, and helpful.“,
)
ROOM_PREFIX = os.getenv(“ROOM_PREFIX”, “voice-chat-“)
AGENT_NAME = os.getenv(“AGENT_NAME”, “nuva-esl”)
NUM_IDLE_PROCESSES = int(os.getenv(“NUM_IDLE_PROCESSES”, “1"))
# prewarmed workers
LOG_LEVEL = os.getenv(“LOG_LEVEL”, “INFO”).upper()
logging.basicConfig(
level=getattr(logging, LOG_LEVEL,
logging.INFO),
format=“%(asctime)s %(levelname)s %(message)s”,
)
logger = logging.getLogger(AGENT_NAME)
def _require_env(_name_: str) -> str:
v = os.getenv(name)
if not v:
raise RuntimeError(f”Missing required env: {name}“)
return v
def _validate_required_env() -> None:
_require_env(“LIVEKIT_URL”)
_require_env(“LIVEKIT_API_KEY”)
_require_env(“LIVEKIT_API_SECRET”)
_require_env(“OPENAI_API_KEY”)
# -----------------------------------------------------------------------------
# Diagnostics: log effective (masked) configuration
# -----------------------------------------------------------------------------
def _mask_secret(_value_: str) -> str:
if not value:
return “NOT_SET”
if len(value) <= 6:
return “***”
return f”{value[:3]}***{len(value)}”
def _log_effective_config() -> None:
_try_:
logger.info(
“Config: LIVEKIT_URL=%s, AGENT_NAME=%s, ROOM_PREFIX=%s, NUM_IDLE_PROCESSES=%s, LOG_LEVEL=%s”,
LIVEKIT_URL,
AGENT_NAME,
ROOM_PREFIX,
NUM_IDLE_PROCESSES,
LOG_LEVEL,
)
logger.info(
“Keys: LIVEKIT_API_KEY=%s, LIVEKIT_API_SECRET=%s, OPENAI_API_KEY=%s”,
_mask_secret(LIVEKIT_API_KEY),
_mask_secret(LIVEKIT_API_SECRET),
_mask_secret(OPENAI_API_KEY),
)
except Exception
as e:
logger.warning(“Env logging failed: %s”, e)
# -----------------------------------------------------------------------------
# Prewarm: load heavy models once per process
# -----------------------------------------------------------------------------
def prewarm_fnc(_proc_: agents.JobProcess) -> None:
“”"
Called for each idle worker process. We load models here so first call
has no cold start penalty. Must not raise.
“”"
_try_:
# Best-effort process label (API differs by version)
pid = getattr(proc, “pid”, None)
if pid is None:
process_obj = getattr(proc, “process”, None)
pid = getattr(process_obj, “pid”, None)
label = pid
if pid is not None
else “unknown”
logger.info(“🔧 Prewarming process %s ...“, label)
# Silero VAD (you can tune thresholds if desired)
proc.userdata[“vad”] = silero.VAD.load(
# Common, conservative defaults; tweak if you want snappier cuts:
_# min_speech_duration=0.05,_
_# min_silence_duration=0.55,_
_# activation_threshold=0.3,_
_# prefix_padding_duration=0.5,_
)
# English semantic end-of-utterance model
proc.userdata[“turn_detection”] = EnglishModel()
logger.info(“✅ Prewarm complete for process %s”, label)
except Exception
as e:
# Never let prewarm kill the worker; log and continue
logger.error(“Prewarm error (ignored): %s”, e)
# -----------------------------------------------------------------------------
# Request filtering: only accept intended rooms
# -----------------------------------------------------------------------------
async def request_fnc(_req_: agents.JobRequest):
“”"
Only accept jobs for rooms that start with ROOM_PREFIX.
This avoids accidental joins and lets you run multiple agents safely.
“”"
room_name = req.room.name or “”
_try_:
if room_name.startswith(ROOM_PREFIX):
logger.info(“🛎️ JobRequest received: accepting room=%s agent=%s”, room_name, AGENT_NAME)
return await req.accept(
attributes={“agent”: AGENT_NAME})
_else_:
reason = f”Room ‘{room_name}’ does not match prefix ’{ROOM_PREFIX}’”
logger.info(“🛎️ JobRequest received: rejecting room=%s reason=%s”, room_name, reason)
return await req.reject(
reason=reason)
except Exception
as e:
logger.error(“JobRequest handler error: %s”, e)
# Best-effort reject on error to avoid stuck jobs
return await req.reject(
reason=str(e))
# -----------------------------------------------------------------------------
# Job entrypoint
# -----------------------------------------------------------------------------
async def entrypoint(_ctx_: JobContext):
“”"
Main job for each dispatched room. Builds the voice pipeline and starts the agent.
“”"
_validate_required_env()
# Helpful context in logs
ctx.log_context_fields = {“room”: ctx.room.name, “job_id”: ctx.job_id, “agent”: AGENT_NAME}
logger.info(“🚀 Starting agent for room: %s (job: %s)“, ctx.room.name, ctx.job_id)
_# Connect as a participant. AUDIO_ONLY avoids needless video subscription._
await ctx.connect(_auto_subscribe_=AutoSubscribe.AUDIO_ONLY)
# docs show this enum
# Build plugins (use your provider keys)
stt = openai.STT(_api_key_=OPENAI_API_KEY,
model=OPENAI_STT_MODEL,
language=“en”)
llm = openai.LLM(_api_key_=OPENAI_API_KEY,
model=OPENAI_LLM_MODEL, _system_prompt_=AGENT_INSTRUCTIONS)
tts = openai.TTS(_api_key_=OPENAI_API_KEY,
model=OPENAI_TTS_MODEL,
voice=OPENAI_TTS_VOICE)
# Pull prewarmed models or load on the fly as a fallback
vad = (getattr(ctx, “proc”, None) and ctx.proc.userdata.get(“vad”)) or silero.VAD.load()
turn_detection = (getattr(ctx, “proc”, None) and ctx.proc.userdata.get(“turn_detection”)) or EnglishModel()
# Define agent instructions
agent = Agent(
instructions=AGENT_INSTRUCTIONS,
)
# Compose voice session
session = AgentSession(
stt=stt,
llm=llm,
tts=tts,
vad=vad,
_turn_detection_=turn_detection,
_allow_interruptions_=True,
# natural barge-in
)
# Start the session bound to this room
await session.start(
room=ctx.room,
agent=agent,
_room_input_options_=RoomInputOptions(
_close_on_disconnect_=False,
# keep session alive if the front-end reconnects
_# noise_cancellation=None, # set if you want server-side NC_
),
)
# Optional: initial greeting (await ensures audio actually plays before we idle)
await session.say(“Hi! I’m ready. Talk to me anytime—no need to press a button.“)
logger.info(“🎧 Agent is live in room: %s”, ctx.room.name)
# Idle the job until shutdown. Framework will call shutdown hooks on exit.
await asyncio.Future()
# -----------------------------------------------------------------------------
# Main (Worker)
# -----------------------------------------------------------------------------
if name == “__main__“:
_# NOTE: Worker uses LIVEKIT_URL / LIVEKIT_API_KEY / LIVEKIT_API_SECRET from env_
# to register for jobs. Do NOT embed secrets in code.
_log_effective_config()
cli.run_app(
WorkerOptions(
_entrypoint_fnc_=entrypoint,
_request_fnc_=request_fnc,
_prewarm_fnc_=prewarm_fnc,
_# Do not set agent_name to enable automatic dispatch_
_num_idle_processes_=NUM_IDLE_PROCESSES,
_shutdown_process_timeout_=30.0,
)
)