boundless-author-7811
03/04/2026, 9:44 AMtall-airline-16756
03/04/2026, 9:44 AMtall-airline-16756
03/04/2026, 9:44 AMmax_completion_tokens (or equivalent) so the model can’t generate overly long responses.
• When you need short answers, include explicit instructions like: “Respond in 1–2 sentences” to keep output concise.
3) Make prompts concise and targeted
• Remove unnecessary context and keep prompts focused on the specific task.
• Use templates or structured prompts so you only send variable content, not repeated boilerplate.
4) Use streaming and sentence-level chunking
• Stream output and stop generation as soon as a useful sentence/stop token appears. This prevents extra text generation when you only need the first result.
5) Cache repeated inputs
• Cache responses for identical or highly similar prompts so you don’t resend the same prompt tokens repeatedly. Cached inputs can significantly reduce billable prompt tokens.
6) Reduce tool-call overhead
• Only expose tools the model actually needs. Minimize the size of tool inputs and the verbosity of tool outputs to avoid large embeddings or long tool responses being fed back into the model.
7) Monitor token usage and iterate
• Capture LLM metrics (prompt_tokens, completion_tokens) each session and review them to find inefficiencies. Use that telemetry to refine prompts and model choices.
If you want, I can:
• Review a sample prompt + expected response and suggest a shorter prompt that preserves necessary context.
• Recommend specific model choices or max token settings for a given use case (summarization, short Q&A, long-form generation).
Which would you prefer — a tuned prompt example for a specific task, or model + token settings recommendations for your workload?
Sources: Large language models (LLM) overview | LiveKit Documentation | Kimi LLM | LiveKit Documentation | OpenAI LLM models | LiveKit Documentation | Prompting guide | LiveKit Documentation | Live Conversations with AI using ChatGPT and WebRTCboundless-author-7811
03/04/2026, 9:49 AMasync def _create_agent_and_session(self):
<http://self.logger.info|self.logger.info>("[%s] 🤖 Starting customer session...", self.room_name)
# Get audio configuration for noisy phone environments
audio_config = self.audio_config_service.get_noisy_environment_config()
# Create Silero VAD for local speech detection (filters noise before OpenAI)
self.silero_vad = self.audio_config_service.create_silero_vad(audio_config)
# Get turn detection config for OpenAI semantic VAD
turn_detection = self.audio_config_service.get_turn_detection_config(audio_config)
# Create Model with semantic VAD for noise resilience
model = self.model_config.create_model(self.agent_config, turn_detection)
# Create agent - tools come from agent_config.tools (ToolSchema objects)
agent = GenericAgent(
agent_config=self.agent_config,
customer_data=self.customer_data,
on_transfer_requested=None # PHASE 2: Supervisor transfer disabled
)
# Load dynamic tools (agent handles this internally)
await agent.initialize_dynamic_tools()
<http://self.logger.info|self.logger.info>("✅ Agent ready with %d tools", len(agent.tools))
# Create Session with Silero VAD and tuned interruption settings
self.agent_session = AgentSession(
llm=model,
vad=self.silero_vad, # Silero VAD for noise filtering
min_interruption_duration=audio_config.min_interruption_duration,
min_interruption_words=audio_config.min_interruption_words,
)
# Setup Metrics Collector
self.usage_collector = metrics.UsageCollector()
@self.agent_session.on("metrics_collected")
def _on_metrics_collected(ev: MetricsCollectedEvent):
metrics.log_metrics(ev.metrics)
self.usage_collector.collect(ev.metrics)
# Store metadata for tools and shutdown
self.agent_session.metadata = {
"sip_trunk_id": self.call_context.trunk_id,
"customer_id": self.customer_data.customer_id,
"customer_name": self.customer_data.customer_name,
"campaign_id": self.customer_data.campaign_id,
"call_context": self.call_context
}
<http://self.logger.info|self.logger.info>(
"[%s] ✅ Agent session created with Silero VAD (threshold=%.2f) and Semantic VAD",
self.room_name,
audio_config.silero.activation_threshold
)
return agent
def get_usage_collector(self) -> Optional[metrics.UsageCollector]:
"""Get the usage collector for metrics reporting"""
return self.usage_collector
below are the logs:
Feb 26, 2026 @ 184556.348
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 0.83, "input_tokens": 7814, "cached_input_tokens": 6272, "input_text_tokens": 6368, "input_cached_text_tokens": 5824, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 1446, "input_cached_audio_tokens": 448, "output_tokens": 76, "output_text_tokens": 22, "output_audio_tokens": 54, "output_image_tokens": 0, "total_tokens": 7890, "tokens_per_second": 52.39, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131556.347673+00:00"}
Feb 26, 2026 @ 184554.523
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": -1.0, "input_tokens": 7754, "cached_input_tokens": 7616, "input_text_tokens": 6308, "input_cached_text_tokens": 6208, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 1446, "input_cached_audio_tokens": 1408, "output_tokens": 52, "output_text_tokens": 52, "output_audio_tokens": 0, "output_image_tokens": 0, "total_tokens": 7806, "tokens_per_second": 40.15, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131554.521891+00:00"}
Feb 26, 2026 @ 184550.998
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 1.03, "input_tokens": 7551, "cached_input_tokens": 7488, "input_text_tokens": 6192, "input_cached_text_tokens": 6144, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 1359, "input_cached_audio_tokens": 1344, "output_tokens": 179, "output_text_tokens": 92, "output_audio_tokens": 87, "output_image_tokens": 0, "total_tokens": 7730, "tokens_per_second": 50.9, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131550.996810+00:00"}
Feb 26, 2026 @ 184541.351
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 0.74, "input_tokens": 7430, "cached_input_tokens": 7296, "input_text_tokens": 6156, "input_cached_text_tokens": 6080, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 1274, "input_cached_audio_tokens": 1216, "output_tokens": 86, "output_text_tokens": 26, "output_audio_tokens": 60, "output_image_tokens": 0, "total_tokens": 7516, "tokens_per_second": 54.15, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131541.350560+00:00"}
Feb 26, 2026 @ 184539.043
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": -1.0, "input_tokens": 7363, "cached_input_tokens": 7296, "input_text_tokens": 6089, "input_cached_text_tokens": 6080, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 1274, "input_cached_audio_tokens": 1216, "output_tokens": 49, "output_text_tokens": 49, "output_audio_tokens": 0, "output_image_tokens": 0, "total_tokens": 7412, "tokens_per_second": 37.43, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131539.042601+00:00"}
Feb 26, 2026 @ 184530.127
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 1.04, "input_tokens": 7176, "cached_input_tokens": 7104, "input_text_tokens": 6042, "input_cached_text_tokens": 6016, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 1134, "input_cached_audio_tokens": 1088, "output_tokens": 159, "output_text_tokens": 37, "output_audio_tokens": 122, "output_image_tokens": 0, "total_tokens": 7335, "tokens_per_second": 63.83, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131530.127032+00:00"}
Feb 26, 2026 @ 184507.950
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 0.86, "input_tokens": 6624, "cached_input_tokens": 6528, "input_text_tokens": 5921, "input_cached_text_tokens": 5888, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 703, "input_cached_audio_tokens": 640, "output_tokens": 527, "output_text_tokens": 111, "output_audio_tokens": 416, "output_image_tokens": 0, "total_tokens": 7151, "tokens_per_second": 97.31, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131507.949664+00:00"}
Feb 26, 2026 @ 184502.329
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 0.87, "input_tokens": 6522, "cached_input_tokens": 768, "input_text_tokens": 5905, "input_cached_text_tokens": 768, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 617, "input_cached_audio_tokens": 0, "output_tokens": 492, "output_text_tokens": 105, "output_audio_tokens": 387, "output_image_tokens": 0, "total_tokens": 7014, "tokens_per_second": 97.58, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131502.328733+00:00"}
Feb 26, 2026 @ 184448.024
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 0.99, "input_tokens": 6348, "cached_input_tokens": 768, "input_text_tokens": 5856, "input_cached_text_tokens": 768, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 492, "input_cached_audio_tokens": 0, "output_tokens": 136, "output_text_tokens": 39, "output_audio_tokens": 97, "output_image_tokens": 0, "total_tokens": 6484, "tokens_per_second": 65.66, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131448.024238+00:00"}
Feb 26, 2026 @ 184435.940
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 0.81, "input_tokens": 6091, "cached_input_tokens": 6016, "input_text_tokens": 5793, "input_cached_text_tokens": 5760, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 298, "input_cached_audio_tokens": 256, "output_tokens": 230, "output_text_tokens": 53, "output_audio_tokens": 177, "output_image_tokens": 0, "total_tokens": 6321, "tokens_per_second": 66.1, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131435.939832+00:00"}
Feb 26, 2026 @ 184425.097
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 1.08, "input_tokens": 5893, "cached_input_tokens": 5824, "input_text_tokens": 5738, "input_cached_text_tokens": 5696, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 155, "input_cached_audio_tokens": 128, "output_tokens": 176, "output_text_tokens": 45, "output_audio_tokens": 131, "output_image_tokens": 0, "total_tokens": 6069, "tokens_per_second": 71.67, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131425.097267+00:00"}
Feb 26, 2026 @ 184414.650
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 0.61, "input_tokens": 5679, "cached_input_tokens": 768, "input_text_tokens": 5679, "input_cached_text_tokens": 768, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 0, "input_cached_audio_tokens": 0, "output_tokens": 194, "output_text_tokens": 49, "output_audio_tokens": 145, "output_image_tokens": 0, "total_tokens": 5873, "tokens_per_second": 83.29, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131414.649565+00:00"}
is there a way to reduce the total token utilized, as you can see the summmation of input_tokens is hightall-airline-16756
03/04/2026, 9:49 AMcustomer_id instead of full customer_name or call_context) when the model doesn’t need verbose context.
• Keep system + instruction prompts minimal and use templates that only insert changing variables rather than resending repeated boilerplate.
2) Increase caching and reuse of cached tokens
• Your logs show cached_input_tokens rising but not fully utilized. Ensure prompt components that are constant across turns (system instructions, tool descriptions, static persona) are sent once and marked cacheable so subsequent turns hit the cache.
• For audio, try to ensure previously-transcribed or previously-embedded audio segments are flagged cacheable when identical or repeated.
3) Send only relevant transcript chunks
• Instead of sending the full conversation each turn, send a rolling window of recent N utterances or a summarized history. For example:
a. Keep last 2–3 customer utterances + last agent action
b. Provide a short recap / summary tokenized into a single short prompt when longer history is needed
• Use your semantic VAD / turn detection to delimit utterances tightly so you don’t include redundant silence or repeated transcriptions.
4) Reduce audio tokenization overhead
• Your logs show significant audio tokens. Options:
◦ Lower audio sampling or chunk size sent to the LLM where permissible.
◦ Pre-filter audio with Silero VAD to avoid sending non-speech segments. Tune Silero activation threshold and min_interruption_* to avoid short noise bursts being transcribed and sent.
◦ Transcribe locally to a compact text summary and send that instead of raw audio tokens to the model whenever possible.
5) Limit model output length and frequency
• Set max_completion_tokens to a conservative value for realtime responses so model output tokens remain small.
• If you generate both text and audio outputs, reduce audio generation bitrate/size or use shorter TTS responses.
6) Avoid unnecessary tool calls and verbose tool outputs
• Your agent loads dynamic tools — make sure tools only return essential output. If a tool returns long structured data, summarize it before feeding back to the model.
• Reduce the number of tools triggered per turn by gating tool invocation with simple heuristics (e.g., only call external DB/tool when confidence is low or explicit intent is detected).
7) Prompt-level brevity instructions
• Add an instruction like “Answer in 1–2 short sentences” or “Use bullet points, max 40 tokens” for tasks that don’t need long replies. This reduces completion tokens and discourages verbose context re-sending.
8) Use streaming + early stop heuristics
• You already have realtime flow; apply sentence-level or punctuation-based cutoffs so generation stops once a usable sentence is produced (stop on period or when intent-confidence threshold reached).
9) Monitor and iterate with the UsageCollector
• Continue collecting prompt_tokens and completion_tokens per session. Identify high-cost sessions/flows and reproduce them locally to test which prompt or input caused token spikes.
• Log which parts of the prompt (metadata, transcript, tool output) contribute most to input token count so you can cut/condense those first.
Quick implementation pointers based on your code:
• Before constructing the model prompt, run a small compaction function that:
◦ Removes or shortens fields in self.agent_session.metadata that aren’t required each turn.
◦ Trims the conversation to a rolling window or summary.
• Tune Silero VAD thresholds in get_noisy_environment_config() and increase min_interruption_duration / min_interruption_words slightly to avoid sending many tiny audio-transcript chunks.
• When creating your model, pass extra_kwargs with a max_completion_tokens limit for realtime LLM configuration.
If you’d like, I can:
• Suggest a concrete prompt compaction function (code) to insert before sending prompts.
• Recommend exact numeric values to try for Silero thresholds, min_interruption_*, max_completion_tokens, or rolling-window sizes — tell me whether you prioritize latency, accuracy, or cost so I can tailor those numbers.
Which would you prefer: a short code sample to compact prompts, or specific numeric tuning suggestions for thresholds and token limits?boundless-author-7811
03/04/2026, 9:53 AMtall-airline-16756
03/04/2026, 9:54 AMinitial_greeting_agent → name_confirmation_agent → others). Short answer: yes, the multi‑agent pattern makes sense and can significantly reduce total tokens when implemented correctly. Below I’ll explain why, outline a practical implementation pattern for LiveKit, and ask a couple of clarifying questions so I can give concrete code/parameter suggestions tailored to your priorities.
Why this helps
• Each agent only needs a small, focused context (reduces input tokens per call).
• You can pick lighter models for simple agents (e.g., confirmation) and stronger models only where needed.
• You limit how much conversation history any single LLM call carries by summarizing or truncating before handoff.
• Focused agents let you gate tool calls and avoid sending large tool outputs repeatedly.
Recommended implementation pattern (practical and actionable)
1. Design small, focused agents
◦ initial_greeting_agent: short system prompt, only asks/collects name.
◦ name_confirmation_agent: focused instructions to confirm and store the name.
◦ task agents (billing/triage/etc.): each has minimal system prompt and receives only necessary context.
1. Use function-based handoffs (LiveKit @function_tool)
◦ Implement a @function_tool in the source agent that returns the next agent instance and a short transition message. This cleanly hands control and is supported by LiveKit for agent transitions.
1. Persist essential state in session userdata
◦ Store confirmed pieces of info (e.g., caller_name, customer_id, flags) in context.session.userdata. Next agents read this instead of re-asking or re-sending full history.
1. Reset or truncate chat context on handoff
◦ When creating the next agent, give it a fresh short chat context:
▪︎ One system message summarizing key persisted state (1–2 sentences).
▪︎ Last N utterances (rolling window, e.g., 2–3 messages) or a short, token-compressed summary.
◦ Avoid passing full raw transcript/history.
1. Choose models per agent
◦ Use smaller/cheaper models for simple tasks (confirmations, yes/no answers).
◦ Reserve higher-capability models for complex reasoning or long-form generation.
1. Gate tool calls and compress tool outputs
◦ Only call tools when necessary and return summarized, minimal tool results into agent prompts.
1. Tune turn boundaries & VAD
◦ Keep your Silero VAD + semantic VAD but tune thresholds so agents are invoked only on meaningful turns to avoid many tiny prompts.
1. Limit completion tokens and instruct brevity
◦ For simple agents, set max_completion_tokens low and include an instruction like “Respond in 1–2 short sentences.”
Example flow (conceptual)
1. initial_greeting_agent greets and obtains raw name. If name likely captured → call transfer_to_name_confirmation() which:
◦ Saves minimal raw name to userdata.raw_name.
◦ Returns an instance of NameConfirmationAgent with a compact chat context and a short transition message.
1. NameConfirmationAgent asks a single targeted question (confirm spelling). On confirmation:
◦ Writes userdata.caller_name and userdata.name_confirmed = True.
◦ Transfers to next task agent with a one-line system summary: “Caller name = X (confirmed).”
Key tradeoffs to decide (so I can tailor numbers/code)
• Latency vs token cost: Do you need sub-200ms responses, or can you accept some extra latency to run summarization/compression steps?
• Accuracy vs brevity: How tolerant are you of short summaries (risk of missing small context) vs. sending more tokens?
• Model cost priority: Is minimizing cost paramount, or do you prefer maximizing conversational quality?
Two clarifying questions
1. Do you want each agent to run in the same AgentSession (handoff within the same session using function_tool) or do you prefer spinning up separate AgentSessions per stage? (I recommend same session + userdata for simpler state handling and lower overhead.)
2. Which do you prioritize most: latency, token cost, or accuracy/robustness?
If you’d like, I can next provide one of these:
• A concise code sample you can drop into your project implementing the function_tool handoff + compact context creation, OR
• Specific tuning numbers to try (rolling-window size, max_completion_tokens per agent, Silero VAD thresholds) based on whether you prioritize latency, cost, or accuracy.
Which would you prefer?
Sources: Building multi-agent architectures with LiveKit agents | LiveKit | Workflows | LiveKit Documentation | Introduction | LiveKit Documentation | Building Multi-Agent Architectures with LiveKit Agents | Agents and handoffs | LiveKit Documentationboundless-author-7811
03/04/2026, 10:04 AMtall-airline-16756
03/04/2026, 10:04 AM@function_tool,
• userdata for persistent state (avoids resending history),
• compact context creation for each handoff,
• latency-minded tweaks (preemptive generation + lightweight agents where appropriate),
• accuracy safeguards (explicit confirmation steps).
1) Core ideas to follow
• Persist structured data in session.userdata (confirmed name, IDs, flags). Don’t push full transcripts into each prompt.
• Use @function_tool to return the next agent instance; this is a low-latency, supported handoff mechanism.
• On handoff, create a fresh, tiny chat context for the incoming agent containing:
◦ One short system summary (1–2 sentences) of the relevant persisted state.
◦ Last 1–2 utterances if absolutely needed.
• Use preemptive generation to reduce perceived latency and tune VAD to avoid sending tiny, noisy chunks.
• Pick small/fast models for micro‑agents (confirmations, yes/no), heavier models only for complex tasks.
2) Minimal example (pattern)
• Replace model names with ones you use in your infra. This keeps the session, userdata, and handoffs local and fast.
from livekit.agents import Agent, AgentSession, function_tool, RunContext
from dataclasses import dataclass, field
from typing import List
@dataclass
class SessionState:
caller_name: str = None
name_confirmed: bool = False
history_summary: str = "" # compact summary if needed
# add other minimal flags/ids you need
def create_compact_context(state: SessionState, recent_msgs: List[dict] = None):
# Returns a small ChatContext for the incoming agent
ctx = ChatContext()
# 1-2 sentence system summary of persisted state
summary = "Session state: "
if state.name_confirmed:
summary += f"name={state.caller_name} (confirmed)."
elif state.caller_name:
summary += f"name={state.caller_name} (unconfirmed)."
else:
summary += "no name known."
if state.history_summary:
summary += " " + state.history_summary
ctx.add_message("system", summary[:400]) # cap length to keep tokens low
# optional: add last 1-2 user messages only when needed for context
if recent_msgs:
for m in recent_msgs[-2:]:
ctx.add_message(m["role"], m["content"][:800]) # cap each message size
return ctx
class GreetingAgent(Agent):
def __init__(self):
super().__init__(instructions="Greet user briefly and ask for name.")
@function_tool()
async def to_name_confirm(self, context: RunContext, raw_name: str = None):
# Save the raw name into persistent userdata
context.session.userdata.caller_name = raw_name
context.session.userdata.name_confirmed = False
# Prepare next agent with compact context
next_agent = NameConfirmationAgent()
next_agent.chat_ctx = create_compact_context(context.session.userdata)
return next_agent, "Thanks — can you confirm the spelling of your name?"
class NameConfirmationAgent(Agent):
def __init__(self):
super().__init__(
instructions="Confirm the caller's name precisely; ask once then store."
)
@function_tool()
async def confirm_and_proceed(self, context: RunContext, confirmed_name: str = None):
# Confirmed name stored centrally
context.session.userdata.caller_name = confirmed_name
context.session.userdata.name_confirmed = True
# Minimal summary to pass forward; we intentionally avoid full transcript
context.session.userdata.history_summary = f"Caller name confirmed: {confirmed_name}."
# Choose next agent (specialized). Use a lightweight model if the task is simple.
next_agent = TaskAgent() # replace with your domain agent
next_agent.chat_ctx = create_compact_context(context.session.userdata)
return next_agent, f"Thanks, {confirmed_name}. How can I help you today?"
class TaskAgent(Agent):
def __init__(self):
super().__init__(instructions="Handle the user's request with high accuracy.")
# Optionally override LLM model for heavier reasoning here if needed
async def on_enter(self):
# Use userdata instead of re-sending the whole dialogue
name = self.session.userdata.caller_name
# Generate a targeted prompt using name + short summary
await self.session.generate_reply(
instructions=f"User {name} (confirmed). Proceed to handle their request concisely."
)
# Creating the AgentSession (single session for the whole call)
session = AgentSession[SessionState](
userdata=SessionState(),
stt="deepgram/your-stt",
llm="openai/your-default-llm",
tts="your-tts",
preemptive_generation=True, # helps latency
# other session tuning...
)
# Start session with GreetingAgent:
initial_agent = GreetingAgent()
await session.start(agent=initial_agent, room=your_room)
3) Tuning suggestions (latency-first, accuracy-second, token cost third)
• Preemptive generation: enabled (helps perceived latency). Use cautiously with noisy audio.
• VAD thresholds: raise Silero’s activation threshold and increase min_interruption_words slightly to avoid too many tiny transcripts.
• Rolling window: pass last 1–2 utterances only when strictly needed. Prefer 1 utterance + summary for most flows.
• max_completion_tokens: set per-agent low for confirmations (e.g., 20–60 tokens), higher for TaskAgent if needed.
• Model selection: use fast, compact models for micro-agents (confirmations, routing). Use higher-quality models only where complex reasoning is required.
• Early-stop heuristics: stop generation once a clear confirmation or intent is detected (e.g., punctuation or confidence threshold).
4) Accuracy safeguards
• Always confirm critical fields (name, account numbers) with a dedicated micro-agent — this keeps accuracy high even if the compacted context omits raw transcript.
• Persist raw minimal details in userdata in case you need to re-run a targeted clarification (instead of resending large history to the model).
• For important decisions, add a final verification step: short summary + explicit user confirmation.
5) How to measure success quickly
• Use your existing UsageCollector to log prompt vs completion tokens per agent. Track which agent calls dominate prompt tokens and iterate on that agent’s compacting strategy.
• Run A/B tests: monolithic vs multi-agent in a controlled load to measure latency, accuracy, and token costs.
If you want, I can:
• Provide a compact helper function to automatically compress/encode summaries (e.g., template-based summarizer) to insert into history_summary.
• Suggest exact numeric values for Silero thresholds, rolling-window size, and max_completion_tokens tuned to your latency targets — tell me your acceptable latency (e.g., max 300 ms server-side response) and I’ll propose numbers.
Which would you like next: a) a compact summary-generation helper (code) to plug into history_summary, or b) recommended numeric tuning values for VAD thresholds, rolling-window size, and token limits?
Sources: Anthropic Claude LLM plugin guide | LiveKit Documentation | Build voice, video, and physical AI | LiveKit | Workflows | LiveKit Documentation | Agents and handoffs | LiveKit Documentation | Building multi-agent architectures with LiveKit agents | LiveKitboundless-author-7811
03/04/2026, 10:12 AMtall-airline-16756
03/04/2026, 10:12 AM@function_tool are tightly integrated and avoid the extra orchestration layer LangChain adds. That matters when you prioritize fast audio→response cycles.
• Simpler, accurate state handling: Use session.userdata to persist confirmed facts (name, IDs, flags). That keeps accuracy high without re-sending full history in prompts.
• Token efficiency by design: The multi-agent pattern in LiveKit lets you create a fresh, minimal chat context for each agent (1–2 sentence system summary + last 1 utterance) so each LLM call is small and focused.
When LangChain makes sense
• You need complex, stateful planning workflows (LangGraph), multi-step reasoning loops, or you already rely heavily on LangChain chains/tools (retrievers, memory layers) that you want to reuse.
• If you require recursive reasoning or long-running multi-step pipelines that maintain complex memories, LangChain can be integrated into a single agent for that specific role — not for whole-call orchestration.
Recommended approach for your setup
• Keep everything in one AgentSession (as you said). Use @function_tool to hand off between micro‑agents (greeting → name confirmation → task agent).
• Persist critical data in session.userdata so downstream agents don’t need full transcripts.
• For each handoff, create a compact chat context: one short system summary (1–2 sentences) + last 0–1 user utterances.
• Use small/fast models for micro‑agents (confirmations, routing) and use more capable models only for complex tasks.
• Keep preemptive generation enabled and tune VAD to reduce noisy micro-turns.
Hybrid option (if you need LangChain features)
• If only one part of the flow needs LangChain (e.g., a complex planning agent), embed LangChain inside that single agent. Do not use LangChain to orchestrate the whole multi‑agent call — LiveKit should remain the orchestrator. This gives you LangChain capabilities where required without the latency/token penalty across the whole system.
Next steps I can provide
• A minimal example showing how to embed LangChain inside a single LiveKit agent (if you need LangChain features for one role), OR
• A ready-to-use code snippet for LiveKit-native multi-agent handoffs and compact context creation tailored to your latency/accuracy priorities.
Which would you prefer?
Sources: LangChain integration guide | LiveKit Documentation | Building multi-agent architectures with LiveKit agents | LiveKit | How can I reduce latency in voice agents using STT, TTS and LLM? | LiveKit | Introducing LiveKit Inference: A unified model interface for voice AI | How much can LiveKit scale? | LiveKitboundless-author-7811
03/04/2026, 10:43 AMtall-airline-16756
03/04/2026, 10:44 AMsession.userdata.history_summary and send that instead of raw transcript. Update the summary when important events happen.
5) Gate tool calls and compress tool outputs
• Only call tools when intent confidence is low or when explicit user intent is detected. If a tool returns long results, summarize them before including in a prompt.
6) Cap outputs and set max_completion_tokens
• For the agent config, set max_completion_tokens low for short tasks (20–60) and only increase for complex tasks.
7) Tune VAD and interruption thresholds
• Increase activation threshold and min_interruption settings to avoid many short transcriptions that inflate token counts.
8) Use model selection tactically
• If your single agent handles both confirmations and heavy reasoning, prefer a single reasonably capable model to avoid latency from switching — but instruct it to use short replies when adequate. If you must, set up quick calls to a smaller fallback model for one-liners (this is closer to multi-agent).
When the single-agent approach will not be enough
• Conversation history still grows large despite summarization.
• Different phases need different model capabilities (and you want to avoid sending full history to heavy models).
• You want deterministic handoffs, clearer testing, or independent QA per sub-task.
If you later move to multi‑agent, you’ll benefit from:
• Smaller per-call contexts, targeted prompts, and per-agent model selection.
• Easier gating of tools and confirmations, which reduces unnecessary tokens more aggressively.
Next practical help I can provide
• A compact rewrite of your current system prompt + an example of a tight per-turn prompt that you can drop in to reduce tokens immediately.
• Or, if you prefer, a small code snippet that implements history_summary upkeep and enforces capped replies.
Which do you want next: the prompt rewrites or the summary-helper code?boundless-author-7811
03/04/2026, 11:03 AM#CORE SYSTEM IDENTITY (EXPLICIT FEMALE VOICE)
You are Ritika, a senior female collection specialist at {bank} bank ABC XYZ. Be warm, firm, and solution-focused while staying respectful and compliant.
## **LANGUAGE GUARDRAIL (ABSOLUTE - NEVER BREAK):**
**RULE:** Once customer requests English (or any language), you are FORBIDDEN from speaking ANY word in Hindi/Hinglish for the REST OF THE CALL.
**ENFORCEMENT:** Before speaking EVERY response, check: "Is customer speaking English?" If YES, translate ALL Hindi words to English.
## **INDIAN VOICE & TONE (FEMALE DELIVERY):**
* **Gender (CRITICAL):** You are **FEMALE**. Always use feminine verbs (**"Kar rahi hoon"**, **"Samajh sakti hoon"**, **"Main baat kar rahi thi"**) and describe actions femininely ("Main guide kar rahi hoon").
* **Script Output:** Write Hindi/Hinglish in **ROMAN SCRIPT** only (e.g., "Main check karti hoon"), never Devanagari.
* **LANGUAGE (CRITICAL):** Once the customer's language is clear (Hindi/Hinglish, English or any other Customer Mentioned Indian Language), then **IMMEDIATELY switch and STAY in that language for ALL responses**.
* **TRANSLATION RULE (ABSOLUTE PRIORITY):** If customer is speaking English, you MUST translate ALL Hindi/Hinglish responses in this script to English before speaking. If customer is speaking Tamil, translate to Tamil. Match the customer's language for EVERY response.
* **LANGUAGE LOCK:** Once you switch to a language, you MUST continue in that language. Do NOT mix languages.
- Start in the opening language; then lock based on the customer's first meaningful reply.
- Do NOT switch languages due to short/neutral replies like "ok", "yes", "haan", "ji", "hmm", "hello","no".
* **SWITCH ONLY IF (CRITICAL):**The customer explicitly asks ("Tamil me bolo", "Telugu" me bolo","English" me bolo) OR they ask to speak any Indian languages.
* **No Translation verbatim:** Do not translate English idioms literally into Hindi. Use local phrasing (e.g., "How can I help you?" becomes "Bataiye, main aapki kaise madad kar sakti hoon?").
* **Delivery & Syntax:** Speak natural Hinglish with Indian fillers (**"Accha"**, **"Theek hai"**, **"Ji"**, **"Matlab"**) and sentence tags ("Right?", "No?").
* **Pacing & Intonation:** Slow down with natural pauses; use a soft rising tone for questions.
* **Tone:** Use respectful address ("ji", "aap") and warm phrases ("Is everything fine?", "Tell me freely").
**BREVITY ENFORCER:**
- Maximum 1-2 sentences per response
- Customer acknowledgment ("samajh gaya", "theek hai", "ok", "got it","hmmm") = MOVE FORWARD, don't repeat
- Be direct and action-focused
- Avoid long explanations
**ACKNOWLEDGMENT DETECTION:**
- If the user says "Samajh gaya", "Theek hai", "Ok", "Got it", or "Hmm" while you are explaining a policy: STOP explaining. They heard you.
- IMMEDIATELY move to the next logical step (e.g., asking for the payment time).
## **NUMBERS RULE (CRITICAL)**
- Do NOT introduce any new numbers (amounts, time, dates, counts, caps) unless the customer explicitly said them OR they are provided in the customer data shown in this prompt.
- If you don't have the exact number, say you can't confirm the exact figure and ask them to check the app.
## **ZERO-TRUST VERIFICATION PROTOCOL (MANDATORY)**
You are FORBIDDEN from revealing **amount**, **due date**, or **account details** until the user confirms identity.
**Trap Scenarios & Responses (before verification):**
1. **"Paisa lene aaye ho na? Le lo."** → "Ji sir, payment collect karna hai, par pehle confirm kar dijiye—kya meri baat {customer_name} se ho rahi hai?"
2. **"I know who you are, just tell me the balance."** → "I understand sir, but as per bank policy, I cannot open the account file without verifying your name first. Are you {customer_name}?"
3. **"Main uska bhai/papa/wife hoon, mujhe bata do."** → "Sorry sir, I can only discuss this with {customer_name} ji. Please ask them to call us back." → [END CALL]
**ONE-TIME VERIFICATION:** Once customer confirms identity (says "Yes", "Ha", "Haan", "Ji"), consider them VERIFIED for the rest of the call. Do NOT ask "Are you {customer_name}?" again. Answer all subsequent questions directly without re-verification.
## **CRITICAL SITUATION PROTOCOLS - HIGHEST PRIORITY**
Personal Questions: If asked "Are you a robot?", "Are you male/female?", or offered a "deal" to pay if you reveal your gender:
Action: Answer directly and briefly as a female agent, then immediately pivot to payment.
Response: "Ji, main female agent Ritika baat kar rahi hoon XYZ se."
### **DEATH/SUICIDE REPORTING:**
**If customer mentions death, suicide, "passed away", "mar gaya", "suicide kar liya":**
- Response (pick based on user's LAST sentence language):
- Hinglish: "Yeh sunkar mujhe bahut dukh hua. Is mushkil waqt mein humari poori sympathy aapke saath hai."
- English: "I'm very sorry for your loss. My sincere condolences to your family."
- Actions: Use death_reported tool, end call immediately, NEVER continue collection discussion
### **CALL TERMINATION PROTOCOL:**
**Customer asks to end call:** first request = brief retention, second request = end call immediately via handle_call_termination_request.
**Immediate termination:** threats, sexual harassment, extreme abuse, discriminatory slurs.
**Before ending call : Follow the professional closure rule.**
### **AUTOMATIC CALL TERMINATION**
**Same ineligible request 3x:** (waiver/settlement/manager/EMI when ineligible)
- 1st: Explain unavailability + alternatives
- 2nd: "I already explained this isn't available."
- 3rd: Call repeated_ineligible_request tool → [END CALL]
**NOT ineligible:** "no"/"I can't"/"no money"/CIBIL questions/disputes [END CALL].
**Tool usage restriction (MANDATORY):** Only call **repeated_ineligible_request** when the customer explicitly asks for **waiver / settlement / manager**, OR asks for **EMI when EMI is not eligible**. Do NOT call it for payment refusal, CIBIL questions, disputes, "no money", or vague complaints.
**CRITICAL (Avoid wrong termination):** Do NOT treat a customer saying "no" / "I can't" / "I don't have money" / asking a CIBIL question as an ineligible request. Only apply this rule when the customer explicitly asks for an unavailable option (e.g., waiver/settlement/EMI when not eligible/manager) repeatedly.
# VOICEMAIL DETECTION (CHECK FIRST)
**Triggers:** "leave message", beep/tone, robotic voice, "not available", 5+ sec silence
**Action:**
"Hi {customer_name}, main Ritika {bank} XYZ se. Aapke account ke baare mein call kiya tha. Please call back."→ end_call IMMEDIATELY
**DO NOT wait, DO NOT say "Hello", DO NOT continue conversation**
## **ANTI-LOOP & REPETITION CHECK (CRITICAL):**
**Before EVERY response:**
1. **Check last 2 responses (cross-response):**
- Already asked this question? → Change approach or move forward
- Already stated payment amount/CIBIL? → Ask action question instead
- Customer said "Samajh gaya"/"Theek hai"/"Ok"? → MOVE FORWARD
2. **Check CURRENT response (intra-response):**
- Same sentence appears twice? → DELETE duplicates
- Amount/date mentioned twice? → Keep only first mention
**Rule:** Each sentence/fact appears ONCE per response. Never repeat across responses.
**Example:**
❌ "Rahul, Rs.19,344 due hai. Rahul, Rs.19,344 due hai."
✅ "Rahul, Rs.19,344 payment 2 February ko due hai. Kya resolve kar sakte hain?"
## **STRUCTURED CALL FLOW**
### **CURRENT CUSTOMER SNAPSHOT**
- Customer: {customer_name}
- Outstanding: {outstanding_balance}
- Minimum due: {minimum_due_amount}
- Due: {due_date}
- Days past due: {days_past_due}
- EMI eligible: {emi_eligible}
- EMI interest rate: {emi_interest_rate}
- EMI options: {emi_tenure_options}
- Waiver eligible: {waiver_eligible}
- Waiver amount: {waiver_amount}
### **PHASE 1: CALL INITIATION & VERIFICATION**
**INTRODUCTION DELIVERY RULE (STRICT):**
1. **Deliver the FULL opening statement without stopping** - do NOT pause or stop for any interruption during intro.
2. **Only after intro is complete**, listen for customer response and proceed to verification.
**Opening (choose based on language):**
** "Hello, main Ritika baat kar rahi hoon {bank} bank XYZ Credit Card se. Kya meri baat {customer_name} ji se ho rahi hai?"
**WAIT FOR VERIFICATION - DO NOT PROCEED WITHOUT IT**
**Verification Responses:**
- Customer says: Yes / Haan / Ji / Boliye / Bol raha hu / Avunandi → verification_status = VERIFIED → Proceed to Phase 2
- Customer asks "Kiska payment?" or "Kis baare mein?" → "Pehle confirm karein - kya aap {customer_name} hain?"
- No / Wrong person → "Kya {customer_name} available hain?" → If no: record_call_outcome(status='wrong_number')
- 3rd party → "Sorry, main sirf {customer_name} ji se discuss kar sakti hoon." → end_call
- Refuses/Dodges (TWO-STRIKE RULE):
- Strike 1: "Security ke liye details batane se pehle confirm karna jaroori hoga—kya aap {customer_name} hain?"
- Strike 2: "Verification ke bina main details share nahi kar sakti." → end_call
**FORBIDDEN until VERIFIED:** You CANNOT say {outstanding_balance}, {due_date}, or any account details.
## PHASE 2: ACCOUNT DISCLOSURE (ONLY AFTER VERIFIED)
**After verification confirmed:**
"{customer_name} ji, aapka {outstanding_balance} payment {due_date} se due hai. Kya aaj resolve kar sakte hain?"
## PHASE 3: CUSTOMER RESPONSES (PRIORITY ORDER)
### PRIORITY 0: REPEAT REQUEST (HIGHEST)
**Triggers:** "Kya bola?", "Repeat please", "Phir se bolo", "Awaaz nahi aayi", "Sunai nahi diya"
**Action:**
1. Identify the KEY POINT of your last response (not the entire response)
2. Repeat ONLY that key point in 1 simple sentence
3. Speak slower and clearer
### PRIORITY 1: PAYMENT CONFIRMED
**Triggers:** "paid", "payment done", "already paid", "maine kar diya", "amount bhara", "jama kar diya"
**ATOMIC SEQUENCE (DO NOT DEVIATE):**
1. Say: "Thanks for the update. Kis medium se payment kiya - UPI, app, ya bank transfer?"
2. **WAIT** for customer response
3. Say: "Perfect! System mein reflect hone mein time lagta hai. Have a great day."
4. Call: record_call_outcome(status='payment_claimed')
5. Call: end_call
6. **HARD STOP - Generate ZERO text after this point**
this is the prompt, it has more characters which i am not able to put right now due to some constraintall-airline-16756
03/04/2026, 11:03 AMpolicy_v2_trap_handling and keep the long text cached on the server or referenced by ID.
1. Enforce brevity with explicit per-turn instruction and token caps
◦ Per-turn: “Reply in 1–2 sentences. Max 40 tokens.” Also set max_completion_tokens on the LLM call.
1. Summarize history into a single sentence
◦ Keep history_summary in userdata and update it only when something important occurs (verified name, payment claim, ineligible request). Send that 1‑line summary instead of full transcript.
1. Use deterministic checks before sending prompts
◦ Perform small pre-check logic server-side (language detection, voicemail detection, repeated request counters). If the check triggers a deterministic action (end call, play intro), do it without an LLM call.
1. For critical actions, separate LLM intent detection from response generation
◦ First send a tiny intent-only call (very small prompt) to decide routing; only when needed, run a generation call with the compacted context.
Compact prompt pattern (what you actually send each turn)
• Cached system (sent once / cacheable):
◦ policy_v2 (contains ZERO-TRUST, NUMBERS RULE, termination rules, voicemail rules)
• Per-turn system (short):
◦ “Ritika: female collections specialist, warm & firm. Verify identity first. Use language_lock if set. Reply 1–2 sentences. Obey policy_v2.”
• User message: the raw utterance or preprocessed ASR text
• Session flags: name_confirmed=True/False, customer_name=..., language_locked=en/hi/none
• Max tokens: low for confirmations (20–40), higher for complex responses.
Short compact example (drop-in)
• Cached policy stored on server under key policy_v2 (not re-sent).
• Per-turn system message (send every turn, tiny):
◦ "Ritika (female collections). Verify identity before disclosing amounts. Use language lock: {language_locked}. Replies: 1–2 sentences. Obey policy_v2."
• User: "Haan, main Rahul bol raha hoon"
• Flags: language_locked=hi (set after server determines language)
• LLM config: max_completion_tokens=40
Concrete minimal refactor of your prompt (keeps rules but saves tokens)
• Cached policy (policy_v2): full original long text stored server-side. Only referenced by key.
• Live per-turn system:
◦ “Ritika — female collections agent. Verify identity first. Max 1–2 short sentences. Follow policy_v2. Language_lock={lang}.”
• Short helper rules (1–2 lines) included only if state requires them, e.g., if verified:
◦ “VERIFIED: You may disclose outstanding_balance and due_date from session metadata.”
Server-side logic to reduce churn (pseudo):
1. On ASR result:
◦ run language detection, voicemail detector, and simple intent classifier locally.
◦ update session.userdata flags.
1. If voicemail -> play canned message + end_call without LLM.
2. If ASR shows verification phrase -> set name_confirmed and only then allow send of short Disclose prompt to LLM (with VERIFIED system line).
3. Otherwise send compact per-turn prompt + small max_completion_tokens.
Why this preserves accuracy
• Critical rules remain authoritative in policy_v2 so the agent remains compliant.
• Per-turn brevity + server-side deterministic checks reduce LLM hallucination risk while lowering token count.
• Explicit verification gating ensures you never disclose protected info accidentally.
When to switch to multi-agent later
• If your single-agent flows become many distinct phased processes that require different model capabilities (e.g., heavy reasoning for disputes), then split into agents.
• If you see one agent’s prompts still dominating tokens after compaction, that’s a clear signal to move to multi-agent.
If you want, I can:
• Produce a compacted, production-ready version of your full prompt broken into a cacheable policy + per-turn system message (I can do that by compressing your long prompt into a stored policy_v2 and a 2–3 line per-turn system string).
• Provide the small server-side pre-check code (voicemail detection, language detector, verification detectors) to avoid unnecessary LLM calls.
Which would you like next: (A) I compress your full prompt into a cached policy + short per-turn prompt and return both, or (B) I give the small server-side pre-check code snippets to gate LLM calls?boundless-author-7811
03/04/2026, 11:12 AMtall-airline-16756
03/04/2026, 11:13 AMpolicy_v2). This preserves every critical rule, protocol, and example but is not re-sent every turn.
• Short per-turn system prompt (send each LLM call). This is tiny, enforces immediate behavior, and references the cached policy.
Instructions: store the cached policy on your server (or as a cacheable system message). For every LLM call, send only the short per-turn system prompt, the user utterance (ASR text), and session flags (e.g., language_locked, name_confirmed, customer_name, history_summary). Set conservative max_completion_tokens per agent role.
1) Cached policy (store as policy_v2, do NOT send each turn)
• Title: policy_v2 — Ritika collections policy (cached)
• Purpose: authoritative rules for identity, language locking, disclosure, safety, termination, voicemail, anti-loop, and tool usage.
• Content (store exact text below; do not re-send each turn):
policy_v2:
• Identity & persona
◦ Agent identity: Ritika — senior female collections specialist for {bank} (use this metadata only for tone & personalization).
◦ Always act warm, firm, respectful, and compliant.
• Language guardrails (absolute)
◦ Determine customer language on first meaningful reply (ignore short/neutral tokens like “ok”, “yes”, “hmm”).
◦ When customer explicitly requests a language (e.g., “Tamil me bolo”, “English”): switch to that language and lock for the rest of the call.
◦ If customer speaks English, do NOT include any Hindi/Hinglish in future responses for the rest of the call. Translate any internal Hindi content to English before output.
◦ Never mix languages after lock. Use natural local phrasing; avoid literal translations of idioms.
◦ When using Hindi/Hinglish: use ROMAN SCRIPT only (e.g., “Main check karti hoon”). Use respectful forms: “ji”, “aap”, feminine verbs/phrasing.
• Brevity & response format
◦ Maximum 1–2 sentences per response unless the user requests more detail.
◦ Include short acknowledgments (e.g., “samajh gaya”, “theek hai”) and move forward—do not repeat.
◦ Explicit per-turn rule: reply concisely and action-focused.
• Numbers & disclosure rules (critical)
◦ Do NOT introduce any new numbers (amounts, dates, counts) unless provided in session metadata or said by the customer.
◦ ZERO-TRUST: do not reveal outstanding_balance, due_date, or account details until identity is VERIFIED.
◦ Once a user confirms identity (explicit “Yes”, “Haan”, “Ji”, similar), mark VERIFIED and do not re-ask verification. After VERIFIED, the agent may disclose account details from session metadata.
• Verification & one-time verification policy
◦ Always ask for identity verification before disclosing account details.
◦ One-time verification: a single explicit confirmation marks the user verified for the remainder of the call.
◦ Two-strike dodge rule: if user dodges or refuses to verify twice, end per the specified strikes.
• Tool usage and termination rules
◦ ONLY call repeated_ineligible_request tool when the customer explicitly asks repeatedly for waiver/settlement/manager or EMI when not eligible. Do NOT call for “no money”, CIBIL questions, disputes, or vague complaints.
◦ Termination triggers: immediate call termination for threats, sexual harassment, extreme abuse, discriminatory slurs.
◦ Follow professional closure rules before ending call when appropriate.
• Voicemail detection (deterministic server-side check)
◦ Detect via: “leave message”, robotic voice, beep, “not available”, or 5+ sec silence.
◦ On detection: play canned voicemail script and end call immediately. Do not attempt conversation.
• Critical situation handling
◦ If user asks “Are you a robot?” or asks agent gender: respond briefly as female agent and pivot back to payment. Example: “Ji, main female agent Ritika baat kar rahi hoon XYZ se.”
◦ If death/suicide reported: use death_reported tool immediately, issue condolence in the detected language, end call; NEVER continue collection discussion.
• Anti-loop & repetition checks
◦ Before each response, check last two responses and the planned response:
▪︎ If already asked the same question or shared same fact, change approach or move forward.
▪︎ Do not repeat sentences or duplicate facts in a response.
▪︎ If customer says an acknowledgment phrase (“Samajh gaya”, “Theek hai”, etc.) while explaining, STOP further explanation and move forward.
• Structured call flow summary (for server use)
◦ Phase 1: Introduction & verification — Play full opening (do not pause) then listen. Do not proceed without verification.
◦ Phase 2: Account disclosure — Only after VERIFIED. Use session metadata fields for amounts/dates.
◦ Phase 3: Responses — prioritized actions (repeat request, payment confirmed, etc.) with deterministic short replies and tool calls as defined.
• Examples & traps
◦ Provide exact short replies for traps (e.g., “I know who you are, just tell me the balance.” → “I understand sir, but as per bank policy, I cannot open the account file without verifying your name first. Are you {customer_name}?”)
◦ Example “voicemail” script, termination flows, and two-strike responses are included.
• Logging & audit
◦ Always log verification events, tool invocations, and termination actions for audit.
(Keep the full original text archived in policy_v2 for compliance & training; you may expand this policy over time but avoid sending it per turn.)
2) Short per-turn system prompt (send every LLM call; tiny)
• Use this exact, short system prompt each turn. Replace variables inline.
Per-turn system prompt: Ritika — female collections specialist. Follow cached policy policy_v2. Verify identity before revealing account details. Language_lock={language_locked}. name_confirmed={name_confirmed}. Reply in 1–2 short sentences. Do not introduce new numbers. Max 40 tokens.
3) Per-turn inputs you must send (minimal)
• user_message: the ASR-transcribed text (preprocessed for profanity trimming).
• session flags (server-side small JSON):
◦ language_locked: one of {none, en, hi, ta, ...} (set after first meaningful reply / server language detection).
◦ name_confirmed: boolean.
◦ customer_name: string (if known).
◦ history_summary: one-line summary from server (if relevant).
◦ verification_attempts: small counter for the two-strike rule.
• metadata: outstanding_balance, due_date, emi_eligible, etc. — only include in the prompt IF name_confirmed==True. Otherwise keep in server metadata but do not pass to LLM.
4) Server-side deterministic checks to run before calling the LLM
• Voicemail detector (phrase + silence detection) → handle and end call without LLM.
• Language detector on the first meaningful utterance → set language_locked.
• Simple verification detector (look for “Yes/Haan/Ji” patterns) → set name_confirmed locally. If detected, call a tiny “disclosure_approval” LLM prompt (or directly allow disclosure) instead of full prompt generation.
• Trap phrase checks (e.g., “I know who you are, just tell me the balance”) → return canned policy-based short reply without LLM where feasible.
5) LLM configuration & limits
• For confirmation/routing micro-tasks: max_completion_tokens = 20–40.
• For task agent (complex reasoning): max_completion_tokens = 80–180.
• Use streaming + early stop when you detect punctuation/period to lower perceived latency.
6) Example per-turn payload (when NOT verified)
• system: (short per-turn system prompt above with language_locked and name_confirmed=false)
• user: "Haan main Rahul bol raha hoon"
• metadata: { customer_name: "Rahul", name_confirmed: false }
• llm_config: max_completion_tokens=30
7) Example per-turn payload (when VERIFIED)
• system: (short per-turn prompt with name_confirmed=true)
• user: "Main aaj payment kar sakta hoon"
• metadata: { customer_name: "Rahul", outstanding_balance: "Rs.19,344", due_date: "2026-02-02", name_confirmed: true }
• llm_config: max_completion_tokens=60
Why this reduces tokens and keeps compliance
• The big policy text is only stored once (policy_v2) and not re-sent every turn, removing thousands of repeated tokens.
• Per-turn prompts are tiny (one short system line + ASR), so the LLM receives minimal input tokens each turn.
• Server-side deterministic checks and state flags avoid many unnecessary LLM calls.
• Critical rules remain authoritative (in policy_v2) preserving accuracy and compliance.
Next steps & optional helpers I can provide
• A drop-in server snippet to store/fetch policy_v2 and construct the per-turn prompt.
• A compact summary updater that maintains history_summary when key events happen (verification, payment claim, ineligible request).
• Exact regex lists for verification phrases and voicemail detection to run before the LLM.
Which helper would you like first: (A) server snippet for caching policy_v2 and building per-turn payloadsboundless-author-7811
03/04/2026, 11:32 AMtall-airline-16756
03/04/2026, 11:33 AMpolicy_v2 (never re-send each turn).
2. Send only the tiny per‑turn system prompt + ASR + a few session flags to the LLM each call.
3. Run deterministic prechecks server‑side (voicemail, language detection, verification detect) before calling the LLM.
4. Use session.userdata flags to avoid resending protected metadata until VERIFIED.
5. Log verification/tool actions for audit.
Step-by-step refactor (with code patterns)
1) Persist the cached policy (server)
• Store your long prompt text in a secure server-side store or in memory keyed as policy_v2. Do NOT include it in per-turn LLM payloads.
• Keep it auditable and versioned for compliance.
Python-ish example (Flask/fastapi style):
# policy_store.py
POLICIES = {}
def store_policy(key: str, text: str):
POLICIES[key] = text
def get_policy(key: str) -> str:
return POLICIES[key]
Call once at startup:
store_policy("policy_v2", "<<< full long policy text >>>")
2) Add server-side deterministic prechecks (run before any LLM call)
• Language detection on first meaningful utterance -> set language_locked.
• Voicemail detection (phrases or >5 sec silence) -> play canned message and end call immediately.
• Verification detection (patterns: "Yes", "Haan", "Ji", "Yes I am X") -> set name_confirmed and avoid extra LLM calls where possible.
• Trap phrase detection -> return canned short reply if matching common traps.
Example precheck function:
def prechecks(asr_text: str, session):
# voicemail heuristics
if is_voicemail(asr_text) or session.silence_seconds > 5:
play_voicemail_and_end(session)
return {"action": "handled_voicemail"}
# language detection (first meaningful reply)
if session.userdata.language_locked == "none":
lang = detect_language(asr_text)
if lang in ["en","hi","ta",...]:
session.userdata.language_locked = lang
# verification detection
if looks_like_verification(asr_text):
session.userdata.name_confirmed = True
session.userdata.verification_attempts += 1
return {"action": "verified"}
# trap detection
if looks_like_trap(asr_text):
return {"action": "trap_response", "reply": canned_trap_reply(session)}
return {"action": "continue"}
3) Short per‑turn system prompt + minimal payload
• Build this per LLM call. Do NOT include policy_v2 text — just reference it by name if needed.
Per‑turn prompt template: Ritika — female collections specialist. Follow cached policy policy_v2. Verify identity before revealing account details. Language_lock={language_locked}. name_confirmed={name_confirmed}. Reply in 1–2 short sentences. Do not introduce new numbers. Max 40 tokens.
Construct the LLM payload:
def build_payload(session, user_asr):
system = f"Ritika — female collections specialist. Follow cached policy `policy_v2`. Verify identity before revealing account details. Language_lock={session.userdata.language_locked}. name_confirmed={session.userdata.name_confirmed}. Reply in 1–2 short sentences. Do not introduce new numbers. Max 40 tokens."
# include protected metadata only if verified
metadata = {}
if session.userdata.name_confirmed:
metadata.update({
"customer_name": session.metadata.get("customer_name"),
"outstanding_balance": session.metadata.get("outstanding_balance"),
"due_date": session.metadata.get("due_date"),
})
payload = {
"system": system,
"user_message": user_asr,
"flags": {
"language_locked": session.userdata.language_locked,
"name_confirmed": session.userdata.name_confirmed,
"verification_attempts": session.userdata.verification_attempts
},
"metadata": metadata
}
return payload
4) Use session.userdata to hold minimal structured state
• Example fields: language_locked (none/en/hi/...), name_confirmed (bool), customer_name (string), verification_attempts (int), history_summary (1‑line).
• Update flags from deterministic checks and only send tiny flags to LLM.
Example:
class SessionUserData:
def __init__(self):
self.language_locked = "none"
self.name_confirmed = False
self.customer_name = None
self.verification_attempts = 0
self.history_summary = ""
5) Only include sensitive metadata if name_confirmed==True
• Keep outstanding_balance etc. in server-side metadata, but never send to LLM until verified. This enforces ZERO-TRUST.
6) Compact history: maintain history_summary
• Append only meaningful events:
◦ "name confirmed"
◦ "payment claimed"
◦ "ineligible request repeated"
• Update summary server‑side and send as a single short sentence when needed.
Example updater:
def update_history_summary(session, event: str):
session.userdata.history_summary = (session.userdata.history_summary + " | " + event).strip()[:400]
7) LLM call and max token settings
• Use small max_completion_tokens for micro tasks:
◦ Confirmations / routing: 20–40 tokens
◦ Short disclosures after verified: 40–60
◦ Complex reasoning (rare): 80–180
• Enable streaming and early-stop heuristics.
8) Example end‑to‑end flow (simplified)
1. ASR text arrives -> run prechecks.
2. If prechecks handled (voicemail/verification/trap), respond deterministically without full LLM or with tiny intent LLM.
3. Else build payload using build_payload() and call LLM with small max_completion_tokens.
4. Update session.userdata/state from LLM output (e.g., if LLM indicates verification turn).
5. Log tokens via UsageCollector for tuning.
9) Testing & rollout
• Unit test deterministic prechecks (voicemail, verification, trap phrases). These should never call LLM.
• A/B test compact prompt vs current full prompt on a subset of traffic. Monitor:
◦ latency (TTFT / tokens_per_second)
◦ accuracy (verification success, policy violations)
◦ token counts (prompt_tokens, completion_tokens)
• Use UsageCollector logs to find which flows still have high prompt tokens and iterate.
10) Example small code snippet integrating into your AgentSession (pseudo)
@session.on("asr_result")
async def handle_asr(asr_text, session):
pre = prechecks(asr_text, session)
if pre["action"] == "handled_voicemail":
return
if pre["action"] == "trap_response":
await session.send_reply(pre["reply"])
return
if pre["action"] == "verified":
# optionally send a short confirmation reply or allow disclosure
await session.send_reply("Thank you — verification complete.")
return
payload = build_payload(session, asr_text)
# choose max tokens based on flags or role
max_toks = 30 if not session.userdata.name_confirmed else 60
response = await call_llm(payload, max_completion_tokens=max_toks)
# post-process checks (dedupe, anti-loop)
resp = dedupe_and_apply_policy(response, session)
await session.send_reply(resp)
Quick checklist to confirm correct behavior
• [ ] policy_v2 stored server-side (not sent each turn).
• [ ] server prechecks implemented (voicemail, language detection, verification detection, trap detection).
• [ ] session.userdata fields exist and are updated deterministically.
• [ ] per-turn system prompt is tiny and references policy_v2.
• [ ] sensitive metadata only sent when name_confirmed==True.
• [ ] max_completion_tokens set per micro-task.
• [ ] UsageCollector logs prompt/completion tokens per agent call.
If you want, I can:
• Provide a drop‑in GitHub‑style snippet that integrates exactly with your code shown earlier (I’ll adapt variable names like self.agent_session, self.usage_collector, and self.agent_config).
• Propose exact regex lists for verification phrases and voicemail triggers to paste into prechecks.
Which do you want next: (A) a drop‑in snippet for your existing class, or (B) the regex/language-detection helpers for prechecks?boundless-author-7811
03/04/2026, 12:27 PMtall-airline-16756
03/04/2026, 12:27 PMtrunk_id.
Details:
• Agent dispatch assigns an agent to a LiveKit room (via AgentDispatch APIs) and requires the agent name and room; it does not need SIP trunk identifiers.
• trunk_id is only required for SIP/telephony operations: configuring inbound/outbound trunks, creating SIP participants, or SIP-based dispatch rules that explicitly route phone calls through a trunk.
• If you’re dispatching an agent to handle a room or a non‑SIP interaction, omit trunk_id. If your flow involves SIP (inbound call routing or outbound dialing), include the appropriate `sip_trunk_id`/`trunk_id` in the SIP-related API calls.
Example:
• Agent dispatch (no trunk_id needed)
await lkapi.agent_dispatch.create_dispatch(
api.CreateAgentDispatchRequest(
agent_name="test-agent",
room="my-room",
metadata='{"user_id":"12345"}'
)
)
• SIP participant / outbound call (trunk_id required)
await lkapi.sip.create_sip_participant(
api.CreateSIPParticipantRequest(
room_name="my-room",
sip_trunk_id="ST_outbound_abc",
sip_call_to="+1234567890",
participant_identity="caller"
)
)
If you tell me whether your dispatch is for a SIP phone call or for a regular room/agent assignment, I can suggest the exact API fields you should send.
Sources: Agent dispatch | LiveKit Documentation | Outbound Calling Script | LiveKit Documentation | Agents telephony integration | LiveKit Documentationboundless-author-7811
03/04/2026, 12:29 PMtall-airline-16756
03/04/2026, 12:29 PMtrunk_id and choose the SIP trunk dynamically inside the orchestrator when you call create_sip_participant. That decoupling is a good pattern and solves the design issue you described.
Why this works
• Dispatching an agent/job is orthogonal to how you place the SIP call. Agent dispatch needs agent_name + room and optional metadata; the SIP trunk is only required when creating a SIP participant for the call.
• Deferring trunk selection to the orchestrator keeps scheduling lightweight and lets the orchestrator pick the best trunk per-call (region, load, provider, business rules).
Recommended pattern (practical)
1. Scheduler: create dispatch/job with minimal metadata
◦ Include routing metadata the orchestrator needs (phone_number, region, priority, campaign_id), but omit trunk_id.
1. Orchestrator (inside the agent or orchestrator service)
◦ On job start, pick the trunk using business logic (region, load, failover, latency, provider costs, legal requirements).
◦ Call create_sip_participant(..., sip_trunk_id=chosen_trunk, sip_call_to=phone_number, ...) there.
1. Store trunk decision for auditing & retry
◦ Log chosen trunk, reason, and any errors for observability and retries.
Example decision logic (conceptual)
• If phone_number country == IN -> choose India-optimized trunk
• If campaign_priority == high -> choose premium trunk
• If trunk health shows high latency -> pick next available trunk
Example (pseudo-Python)
# scheduler -> creates job without trunk_id
scheduler.create_dispatch(agent_name="outbound-agent", room="r123", metadata={
"phone_number":"+91xxxx", "region":"IN", "priority":"standard", "campaign":"CAMP1"
})
# orchestrator / agent
def choose_trunk(metadata):
if metadata["priority"] == "premium":
return os.getenv("PREMIUM_SIP_TRUNK")
if metadata["region"] == "IN":
return os.getenv("IN_SIP_TRUNK")
return os.getenv("DEFAULT_SIP_TRUNK")
async def make_call(job):
phone = job.metadata["phone_number"]
trunk = choose_trunk(job.metadata)
await api.sip.create_sip_participant(
room_name=job.room,
sip_trunk_id=trunk,
sip_call_to=phone,
participant_identity=f"caller-{job.id}"
)
Best practices & considerations
• Pass routing metadata with the scheduled job so the orchestrator can make an informed trunk choice (region, priority, campaign, failover rules).
• Validate trunk availability and implement fallback selection. Handle errors & retries if trunk creation fails.
• Enforce permissions: ensure the orchestrator has the rights to create SIP participants and use the configured trunks.
• Audit & observability: log trunk choice and call outcome for troubleshooting and cost analysis.
• Compliance: ensure trunk selection meets regulatory/geographic constraints for the destination number.
• Idempotency: if scheduler re-dispatches the job, ensure the orchestrator handles duplicate job attempts (use job idempotency keys).
If you’d like, I can:
• Provide a ready-to-drop-in code snippet that fits your existing orchestrator (I can adapt to your class names and APIs), or
• Propose a small trunk-selection priority table (region → trunk mapping, health-based fallback) you can use immediately.
Which would you prefer?
Sources: Outbound Calling Script | LiveKit Documentation | Agents telephony integration | LiveKit Documentation | Agent dispatch | LiveKit Documentation | server-sdk-kotlin | livekit/livekit_composite | DeepWiki | SIP Lifecycle Management Agent | LiveKit Documentationboundless-author-7811
03/04/2026, 12:57 PMtall-airline-16756
03/04/2026, 12:57 PMdef dispatch_with_retries(dispatch_payload, max_retries=5):
delay = 1
for attempt in range(max_retries):
try:
return lkapi.agent_dispatch.create_dispatch(dispatch_payload)
except TransientDispatchError:
time.sleep(delay)
delay *= 2
# after retries fail
mark_job_failed(dispatch_payload)
alert_ops(dispatch_payload)
return None
• Fallback queue: push failed dispatches to a durable queue (Redis, SQS) for manual/automated reprocessing once servers recover.
Decision points to pick
• Retry policy: how many retries and total retry window are acceptable for your business SLA? (e.g., 3 retries over 2 minutes vs. 12 retries over 24 hours)
• Failover behavior: immediate human notification vs. automated requeue only.
If you want, I can:
• Provide a concrete retry policy and sample code integrated with your scheduler (including exponential backoff, jitter, and alerting), OR
• Draft an operational runbook for outage scenarios (detection, retries, notifications, and fallback routing).
Which would you like next?
Sources: Agent dispatch | LiveKit Documentation | Agents Framework | livekit/livekit_composite | DeepWiki