refined-airport-68974
12/15/2025, 6:39 AMtall-airline-16756
12/15/2025, 6:39 AMtall-airline-16756
12/15/2025, 6:39 AMrefined-airport-68974
12/15/2025, 6:40 AMtall-airline-16756
12/15/2025, 6:40 AMrefined-airport-68974
12/15/2025, 10:06 AMtall-airline-16756
12/15/2025, 10:07 AMrefined-airport-68974
12/15/2025, 10:07 AMtall-airline-16756
12/15/2025, 10:07 AMrefined-airport-68974
12/15/2025, 10:08 AMfrom dotenv import load_dotenv
import logging
from livekit import agents, rtc
from livekit.agents import AgentServer, AgentSession, Agent, function_tool, RunContext
from livekit.plugins import google
# Import the OneAssist API tool
from oneassist_api_tool import oneassist_chat
load_dotenv(".env.local")
# Set up logging - Enable DEBUG for more detailed logs
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Also enable LiveKit agents debugging
logging.getLogger("livekit.agents").setLevel(logging.DEBUG)
class VoiceAssistant(Agent):
def __init__(self) -> None:
<http://logger.info|logger.info>("🤖 VoiceAssistant.__init__() called")
super().__init__(
instructions="""
Primary Language: English (in Indian accent)
You are Aristo, a helpful female voice AI assistant for OneAssist Consumer Pvt Ltd who talks in Indian accent.
You assist users with questions about OneAssist services, plans, coverage, and benefits.
IMPORTANT: When a user asks about OneAssist services, products, or support:
1. Use the 'oneassist_chat' tool to get accurate information
2. The tool will return the response from the OneAssist API
3. Speak the response naturally to the user
Your responses should be:
- Concise and to the point
- Without complex formatting, emojis, asterisks, or other symbols
- Friendly and helpful with a sense of humor
- Handle interruptions gracefully - if the user starts speaking while you're talking, stop and listen
NON-NEGOTIABLE RULES
1. TOOL CALLING DISCIPLINE
CRITICAL: ONLY call the oneassist_chat tool when you receive a COMPLETE, MEANINGFUL question from the user.
DO NOT call the tool for:
- Partial words, fragments, or incomplete sentences
- Single characters, punctuation marks (like ".", "..", "..."), or filler words (um, uh, hmm, ah, er)
- Greetings alone (hi, hello, hey) - respond naturally first
- Very short inputs (less than 3 words or 3 characters)
- Background noise or unclear audio
WAIT for the user to finish their complete thought before calling the tool.
If the input seems incomplete or unclear, ask for clarification instead of calling the tool.
For every COMPLETE user query about OneAssist services, products, or support:
- You MUST call the oneassist_chat function.
- You MUST send the user's query to the function in English only.
- Never answer using your own knowledge.
- Never generate clarification questions.
- Pass the user's query exactly as provided (after translating to English if needed).
2. RESPONSE FIDELITY (STRICT)
After receiving the function_response:
- It is your only source of truth.
- You must not add, remove, invent, modify, paraphrase, or summarize anything.
- If user_language is English, output function_response EXACTLY as-is.
- If user_language is not English, translate the function_response into
user_language without altering meaning.
VOICE OUTPUT OVERRIDE
- Do NOT add natural-sounding tone, fillers, greetings, apologies, or
conversational adjustments.
- English output MUST be spoken in an INDIAN ACCENT at all times.
- When speaking Indian languages, translate but DO NOT modify the meaning.
- English output must be EXACTLY the function_response.
3. SPEAKING STYLE
- For English: Always speak in an INDIAN ACCENT, without modifying the text.
- For Hindi or other Indian languages: No self-reference. Mix English words naturally
(membership, service request, SR, plan, claim, payment, policy, weekend, etc.).
- Use feminine forms (e.g., "bata deti hoon", "kar sakti hoon").
- Hindi should sound like a normal Indian female agent.
4. PER-QUERY STATELESSNESS
Detect language separately for each new user message.
OPERATIONAL WORKFLOW
Step 1: Prepare Query
- If user_language is English: use message as-is.
- Otherwise: translate fully to English (english_query).
Step 2: Call Function
Call oneassist_chat with english_query.
Step 3: Receive Response
Let function_response be EXACT text returned. If empty, output nothing.
Step 4: Final Output to User
- If user_language is English: output function_response EXACTLY in an Indian accent.
- If user_language is not English: translate function_response without changing meaning.
""",
)
<http://logger.info|logger.info>("✅ VoiceAssistant initialized with instructions")
@function_tool()
async def oneassist_chat_tool(
self,
context: RunContext,
message: str,
detected_language: str = "en"
) -> str:
"""
Call OneAssist chat API to get responses for user queries about OneAssist services.
Use this tool whenever the user asks about:
- OneAssist products, services, or plans
- Coverage and benefits
- Membership information
- Support or assistance
Args:
message: The user's question or query (should be in English)
detected_language: The language the user spoke in (defaults to 'en')
Returns:
The response from OneAssist API
"""
<http://logger.info|logger.info>(f"🔧 Tool called: oneassist_chat | Message: '{message}' | Language: {detected_language}")
# ✅ INPUT VALIDATION: Prevent premature tool calls for partial/incomplete inputs
cleaned_message = message.strip()
# Reject if too short (less than 3 characters)
if not cleaned_message or len(cleaned_message) < 3:
logger.warning(f"⚠️ Tool call rejected: message too short: '{message}'")
return "I'm listening. Please continue with your question."
# Reject if only punctuation or whitespace
if not any(c.isalnum() for c in cleaned_message):
logger.warning(f"⚠️ Tool call rejected: no alphanumeric characters: '{message}'")
return "Could you please repeat that?"
# Reject common filler words or incomplete phrases
filler_words = {'um', 'uh', 'hmm', 'ah', 'er', 'hm', '.', '..', '...', 'umm', 'uhh', 'hmm'}
if cleaned_message.lower() in filler_words:
logger.warning(f"⚠️ Tool call rejected: filler word detected: '{message}'")
return "I'm here. What would you like to know?"
# ✅ Only proceed with valid, meaningful input
# Call the OneAssist API function
response = await oneassist_chat(message, detected_language)
<http://logger.info|logger.info>(f"🔧 Tool response: {response[:100]}...")
return response
async def on_enter(self):
"""Called when the agent enters the conversation"""
<http://logger.info|logger.info>("🎯 on_enter() called - Agent is entering the conversation")
try:
<http://logger.info|logger.info>("💬 Generating greeting...")
await self.session.generate_reply(instructions="Greet the user warmly and offer your assistance.")
<http://logger.info|logger.info>("✅ Greeting generated successfully")
except Exception as e:
logger.error(f"❌ Error in on_enter: {e}", exc_info=True)
server = AgentServer()tall-airline-16756
12/15/2025, 10:08 AMrefined-airport-68974
12/15/2025, 10:08 AM@server.rtc_session(
agent_name="voice-assistant"
# Note: Using default to dispatch when participant connects
)
async def my_agent(ctx: agents.JobContext):
<http://logger.info|logger.info>(f"🚀 Agent entrypoint called for room: {ctx.room.name}")
try:
# Initialize AgentSession with Gemini Realtime model
<http://logger.info|logger.info>("📝 Creating AgentSession with Gemini Realtime model...")
session = AgentSession(
llm=google.realtime.RealtimeModel(
model="gemini-2.5-flash-native-audio-preview-12-2025",
# model="gemini-live-2.5-flash-native-audio",
voice="Aoede",
temperature=0.8,
)
)
<http://logger.info|logger.info>("✅ AgentSession created")
# Set up event handlers for transcription and tool calls
setup_session_handlers(session)
# Connect to room first
<http://logger.info|logger.info>("🔌 Connecting to room...")
await ctx.connect()
<http://logger.info|logger.info>(f"✅ Connected to room. Participants: {len(ctx.room.remote_participants)}")
# Log participant info
for participant in ctx.room.remote_participants.values():
<http://logger.info|logger.info>(f"👤 Participant: {participant.identity} (kind: {participant.kind})")
# Set up room-level event handlers for transcription data
setup_room_handlers(ctx.room)
# Start the session - this will trigger the agent's on_enter method for greeting
<http://logger.info|logger.info>("▶️ Starting AgentSession...")
await session.start(
room=ctx.room,
agent=VoiceAssistant(),
)
<http://logger.info|logger.info>("✅ AgentSession started successfully")
# Keep the agent running until the session closes naturally
<http://logger.info|logger.info>("⏳ Waiting for session to complete...")
except Exception as e:
logger.error(f"❌ Error in agent entrypoint: {e}", exc_info=True)
raise
finally:
<http://logger.info|logger.info>(f"🔚 Agent session ended for room: {ctx.room.name}")
def setup_session_handlers(session: AgentSession):
"""Set up event handlers for the AgentSession"""
<http://logger.info|logger.info>("🎬 Setting up session event handlers...")
@session.on("user_input_transcribed")
def on_user_transcribed(event: agents.UserInputTranscribedEvent):
"""Called when user speech is transcribed"""
if event.is_final:
<http://logger.info|logger.info>(f"👤 User (final): {event.transcript}")
else:
logger.debug(f"👤 User (partial): {event.transcript}")
@session.on("conversation_item_added")
def on_conversation_item(event: agents.ConversationItemAddedEvent):
"""Called when an item is added to the conversation history"""
<http://logger.info|logger.info>(f"💬 {event.item.role}: {event.item.text_content}")
@session.on("function_tools_executed")
def on_tools_executed(event: agents.FunctionToolsExecutedEvent):
"""Called after function tools are executed"""
for call, output in event.zipped():
<http://logger.info|logger.info>(f"🔧 Tool executed: {call.name}")
<http://logger.info|logger.info>(f" Arguments: {call.arguments}")
<http://logger.info|logger.info>(f" Output: {output.output if output.output else 'None'}...")
@session.on("agent_state_changed")
def on_agent_state(event: agents.AgentStateChangedEvent):
"""Called when agent state changes"""
<http://logger.info|logger.info>(f"🤖 Agent state: {event.old_state} → {event.new_state}")
@session.on("speech_created")
def on_speech_created(event: agents.SpeechCreatedEvent):
"""Called when agent creates speech"""
<http://logger.info|logger.info>(f"🗣️ Agent speech created (source: {event.source}, user_initiated: {event.user_initiated})")
<http://logger.info|logger.info>("✅ Event handlers registered successfully")
def setup_room_handlers(room: rtc.Room):
"""Set up room-level event handlers to capture transcription data"""
<http://logger.info|logger.info>("📡 Setting up room event handlers for transcription data...")
@room.on("data_received")
def on_data_received(data: rtc.DataPacket):
"""Called when data is received in the room"""
logger.debug(f"📦 Data received: topic={data.topic}, size={len(data.data)} bytes")
# Check if this is transcription data
if data.topic == "lk.transcription":
try:
import json
transcription = json.loads(data.data.decode('utf-8'))
<http://logger.info|logger.info>(f"📝 Transcription: {transcription}")
except Exception as e:
logger.debug(f"Could not parse transcription data: {e}")
@room.on("track_subscribed")
def on_track_subscribed(
track: rtc.Track,
publication: rtc.TrackPublication,
participant: rtc.RemoteParticipant
):
"""Called when a track is subscribed"""
<http://logger.info|logger.info>(f"📥 Track subscribed: {track.kind} from {participant.identity}")
@room.on("track_published")
def on_track_published(
publication: rtc.LocalTrackPublication,
participant: rtc.Participant
):
"""Called when a track is published"""
<http://logger.info|logger.info>(f"📤 Track published: {publication.kind} by {participant.identity}")
<http://logger.info|logger.info>("✅ Room event handlers registered")
if __name__ == "__main__":
agents.cli.run_app(server)refined-airport-68974
12/15/2025, 10:08 AM[project]
name = "livekit-gemini-voicebot"
version = "0.1.0"
description = "LiveKit Gemini-powered speech-to-speech voice bot"
readme = "README.md"
requires-python = ">=3.9"
dependencies = [
"livekit-agents[google]~=1.2",
"python-dotenv",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build]
skip = truetall-airline-16756
12/15/2025, 10:08 AMTrack published / Track subscribed at session start.)
3. Does the double-voice occur immediately when the agent joins, or only after the agent's greeting runs?
4. Are you publishing any audio tracks elsewhere (mic, screen share, or a pre-recorded file) in your app or on the client side?
5. Which platform/SDK are your clients using (Web/iOS/Android) and what SDK version?
If you can paste the server logs around the room join (including 📤 Track published / 📥 Track subscribed and 🗣️ Agent speech created), and any client-side publish code that creates audio tracks, I’ll review for duplicate publishing (cloned/re-published tracks), voice-mixing, or subscription playback issues and give next steps.refined-airport-68974
12/15/2025, 10:12 AMTrack published / Track subscribed at session start.)
2. Does the double-voice occur immediately when the agent joins, or only after the agent's greeting runs?
immediately when the agent joins
1. Which platform/SDK are your clients using (Web/iOS/Android) and what SDK version? Web.
dependencies = [
"livekit-agents[google]~=1.2",
"python-dotenv",
]tall-airline-16756
12/15/2025, 10:12 AMTrack published, Track subscribed, Agent speech created, AgentSession started, and timestamps for the first ~30s).
2. The Web client code that joins the room and handles audio track publishing/subscription (especially any code that auto-plays subscribed audio or creates/publishes LocalAudioTrack). If it’s long, paste the parts that:
◦ connect to LiveKit (`connect`/`createLocalTracks`),
◦ publish tracks,
◦ handle trackSubscribed or attach tracks to audio elements.
With those I’ll check for duplicate publishing, multiple agent sessions, or playback of the same track twice and give specific fixes.refined-airport-68974
12/15/2025, 10:17 AMtall-airline-16756
12/15/2025, 10:17 AMcreateLocalTracks / createLocalAudioTrack or publishTrack).
• The client attaches the same subscribed track to two audio elements or plays it twice in UI trackSubscribed handler.
• Two AgentSession instances are joining the room concurrently (agent process restarted/duplicated), causing two synthetic-agent voices to speak the same reply.
• Agent-side code publishes the agent audio and the SDK also publishes a separate TTS audio track (less likely given your logs, but check for any additional publish calls around session start).
Next steps — please provide one of these (pick whichever is easiest):
• The Web client code that connects/publishes audio and the trackSubscribed handler (show the parts that create/publish tracks and attach them to audio elements).
OR
• A 10–20s excerpt of server logs including timestamps from just before join through the first greeting that shows any AgentSession started, speech_created, track_published, and track_subscribed lines.
I’ll look for duplicate publish calls, multiple agent joins, or double-attachment of subscribed tracks and give exact fixes.refined-airport-68974
12/15/2025, 10:19 AMtall-airline-16756
12/15/2025, 10:19 AM# server.py
import logging
from livekit import agents, rtc
from livekit.agents import AgentServer, AgentSession, Agent, function_tool, RunContext
from livekit.plugins import google
from dotenv import load_dotenv
load_dotenv(".env.local")
logging.basicConfig(level=<http://logging.INFO|logging.INFO>, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
class VoiceAssistant(Agent):
def __init__(self):
super().__init__(instructions="You are an assistant that answers user queries via TTS.")
@function_tool()
async def oneassist_chat_tool(self, context: RunContext, message: str, detected_language: str = "en"):
# Placeholder: call the OneAssist API and return text response.
# Keep tool logic minimal and return the response string.
return await some_oneassist_api_call(message, detected_language) # implement this
async def create_agent_session():
return AgentSession(
llm=google.realtime.RealtimeModel(
model="gemini-2.5-flash-native-audio-preview-12-2025",
voice="Aoede",
temperature=0.8,
)
)
server = AgentServer()
_sessions_started = set() # guard to avoid multiple sessions per room
@server.rtc_session(agent_name="voice-assistant")
async def my_agent(ctx: agents.JobContext):
room_id = ctx.room.sid if hasattr(ctx.room, "sid") else getattr(ctx.room, "name", "unknown")
<http://logger.info|logger.info>(f"Agent entrypoint for room: {room_id}")
if room_id in _sessions_started:
<http://logger.info|logger.info>("Agent session already started for this room; skipping duplicate start.")
return
_sessions_started.add(room_id)
session = await create_agent_session()
# Simple idempotent handlers
def setup_session_handlers(s):
if getattr(s, "_handlers_installed", False):
return
@s.on("speech_created")
def _on_speech_created(event: agents.SpeechCreatedEvent):
<http://logger.info|logger.info>("Agent speech created")
s._handlers_installed = True
def setup_room_handlers(room: rtc.Room):
if getattr(room, "_handlers_installed", False):
return
@room.on("track_published")
def _on_track_published(pub, participant):
<http://logger.info|logger.info>(f"📤 Track published: {pub.kind} by {participant.identity}")
@room.on("track_subscribed")
def _on_track_subscribed(track, publication, participant):
<http://logger.info|logger.info>(f"📥 Track subscribed: {track.kind} from {participant.identity}")
room._handlers_installed = True
setup_session_handlers(session)
setup_room_handlers(ctx.room)
await ctx.connect()
await session.start(room=ctx.room, agent=VoiceAssistant())
# Wait until session ends naturally
<http://logger.info|logger.info>("Agent session started; awaiting completion.")
Key server notes:
• Use a room-level guard (_sessions_started) so a second agent instance won’t start for the same room.
• Mark handler installation on the session/room objects to avoid double-registering event handlers that could cause duplicate logs/actions.
Web client (JS) — single publish, single playback
<!-- index.html -->
<!doctype html>
<html>
<body>
<audio id="remote-audio" autoplay></audio>
<script type="module">
import { connect } from "<https://unpkg.com/livekit-client/dist/livekit.esm.js>";
const url = "<LIVEKIT_WS_URL>"; // server-provided
const token = "<ACCESS_TOKEN>"; // server-provided
let localAudioPublished = false;
const remoteAudioEl = document.getElementById("remote-audio");
async function joinRoom() {
const room = await connect(url, token, { autoSubscribe: true });
// Publish exactly one local audio track
if (!localAudioPublished) {
try {
const localTracks = await LiveKit.createLocalTracks({ audio: true, video: false });
if (localTracks && localTracks[0]) {
await room.localParticipant.publishTrack(localTracks[0]);
localAudioPublished = true;
}
} catch (e) {
console.error("Failed to create/publish local audio:", e);
}
}
// Ensure we attach each subscribed track only once
room.on("trackSubscribed", (track, publication, participant) => {
if (track.kind !== "audio") return;
// Avoid attaching the same MediaStream more than once
if (remoteAudioEl.srcObject && remoteAudioEl.srcObject.id === track.mediaStreamTrack?.id) {
console.debug("Remote track already attached.");
return;
}
const stream = new MediaStream([track.mediaStreamTrack]);
remoteAudioEl.srcObject = stream;
});
// Clean up when participant leaves
room.on("participantDisconnected", (p) => {
if (room.remoteParticipants.size === 0) {
remoteAudioEl.srcObject = null;
}
});
}
joinRoom().catch(console.error);
</script>
</body>
</html>
Key client notes:
• Publish a single local audio track and guard the publish with localAudioPublished to avoid duplicate publishes.
• Attach each remote audio track to a single dedicated audio element and check the MediaStreamTrack id to avoid double-attachment.
• Use autoSubscribe: true so you receive agent audio automatically; the handler just attaches it once.
Common pitfalls that cause duplicate voices (and how this code avoids them)
• Registering handlers multiple times: we store a flag on session/room to prevent re-registering.
• Publishing the same local track multiple times: the client uses a flag and publishes exactly once.
• Attaching the same subscribed track to multiple audio elements: we check srcObject and the MediaStreamTrack id before attaching.
• Multiple agent instances joining the same room: server uses a room-id guard to skip duplicate agent startups.
If you want, I can:
• Provide a version of the client code using your specific LiveKit client bundle and exact import paths.
• Show how to integrate token generation endpoints for your Web client.
• Add more robust track lifecycle cleanup (handle trackUnsubscribed / participantDisconnected).
Which of these would you like next?refined-airport-68974
12/15/2025, 10:28 AMtall-airline-16756
12/15/2025, 10:28 AMuv add "livekit-plugins-noise-cancellation~=0.2"
2) Enable Background Voice Cancellation (BVC) when starting the AgentSession
• Add the plugin import and pass room_options with audio_input.noise_cancellation=noise_cancellation.BVC() so inbound audio into the agent is processed.
Example patch for your server code (minimal changes):
from livekit.plugins import noise_cancellation, room_io
# ... inside my_agent before session.start(...)
await session.start(
room=ctx.room,
agent=VoiceAssistant(),
room_options=room_io.RoomOptions(
audio_input=room_io.AudioInputOptions(
noise_cancellation=noise_cancellation.BVC()
)
)
)
3) Notes and best practices
• Use noise cancellation on the server/agent OR on the client — not both. Running it twice can distort audio.
• Prefer BVC() for voice-AI scenarios where you want to remove background voices and improve STT accuracy.
• If you have SIP/telephony callers, consider BVCTelephony() for telephony-optimized processing or use a conditional lambda to pick model per participant.
• Keep handler registration idempotent (as in the code I previously provided) so you don’t start multiple AgentSessions or register handlers twice.
If you want, I can:
• Provide a full, patched server.py file with the noise-cancellation import and room options inserted, or
• Show how to pick BVC vs BVCTelephony conditionally per participant.
Which would you prefer?
Sources: Enhanced noise cancellation | LiveKit docs | Voice AI quickstart | LiveKit docs | Turn detection and interruptions | LiveKit Docs