nutritious-businessperson-79241
02/19/2026, 7:09 AMtall-airline-16756
02/19/2026, 7:09 AMtall-airline-16756
02/19/2026, 7:09 AMsession.userdata (see Memory below). Use the LLM’s context for transient conversational reasoning and for generating user prompts.
• Use function tools (annotated tool functions) for external actions; have tasks call those tools when appropriate. This keeps the procedural flow separate from the implementation of tool calls.
When to ask a clarifying question first:
• If there are multiple implementation routes (e.g., server-side orchestrator vs. per-agent state machine), tell me which one you prefer (fully hosted orchestration or embedded per-agent state) and I’ll give more specific code patterns. Right now the Task + session.userdata hybrid is recommended.
2) Parallel tool execution — how to call tools while keeping the user engaged
• Run long-running external calls asynchronously so you don’t block speech/TTS generation.
◦ Start the external API call in a background async task (e.g., asyncio.create_task), and immediately confirm to the user with a short acknowledgement: “I’m creating that ticket now — I’ll confirm when it’s done.”
◦ Provide interim feedback (short voice messages, “thinking” sounds, or progress updates) while the background work completes.
• Use streaming patterns already present in LiveKit: the pipeline (STT → LLM → TTS) continues producing audio while backend tasks run.
• If the tool result will affect the immediate next step, design the flow to:
a. Acknowledge to the user that the action started,
b. Continue with non-dependent dialog (or ask the next confirmation),
c. Once the async tool returns, update session.userdata, and push a follow-up utterance with the result (“Ticket 123 created”).
• For errors or escalations, provide immediate graceful fallback (e.g., “I’m unable to create that ticket right now — would you like me to try again or escalate?”) while the session state retains where you were in the procedure.
3) Memory management — keeping procedural state and external data synchronized Use a hybrid memory model with clear separation of responsibilities:
• session.userdata (authoritative procedural state)
◦ Store deterministic items: current_step, completed_steps, step_deadlines, ticket_id, escalation_status, any external IDs and flags.
◦ This is your source of truth for “where are we in the procedure” and must be updated atomically when tasks complete or tools return.
• agent.chat_ctx (short-lived agent reasoning context)
◦ Use for the LLM’s immediate reasoning and to hold the truncated context necessary for on-the-fly decisions.
• session.history (full conversational log)
◦ Use this to reconstruct prior turns for “repeat” or “what did you just say?” requests.
◦ When user asks to repeat/backtrack, consult session.userdata for the step index and session.history to recover the exact phrasing if you want to replay the earlier instruction.
• External store (optional for multi-session resilience)
◦ If sessions may reconnect or survive beyond a single connection, persist session.userdata (or a normalized projection of it) in a fast datastore (Redis/DB).
◦ Keep a compact representation in session.userdata and write-through to the external store on step transitions.
• Synchronization rules
◦ Update session.userdata immediately after a tool returns (success or fail).
◦ When multiple async operations might write the same fields, use small atomic update functions or a Task-coordinator that sequences updates to avoid races.
◦ Make LLM outputs idempotent or checkpoint each step so replays/retries do not corrupt state.
4) Example architectures / reference projects to follow
• Drive-thru ordering example: demonstrates multi-step workflows, structured function calling, stateful order management — suitable as a template for step-by-step flows and for extracting structured data from user speech.
• Multi-agent handoff pattern: demonstrates handing off state and context between agents (intake → specialist) and shows how to preserve session.userdata.
• Agent Playground / multimodal prototype: good for validating voice-first UX and low-latency interactions before production hardening.
If you want, I can point you to relevant example code or walk through one example flow mapped to your ticketing/CRM use case.
Recommended implementation outline (practical skeleton)
1. Define a ProcedureState dataclass and attach one instance to context.session.userdata.
◦ Fields: current_step, completed_steps, ticket_id, last_prompt_id, in_progress_tasks, etc.
1. Implement each procedural step as an AgentTask:
◦ Task responsibilities: prompt the user, validate confirmation, call necessary tools, update session.userdata, complete or yield to next Task.
1. Expose external APIs as @function_tool() functions:
◦ Tools run asynchronously. For long calls, spawn an async worker and immediately return an acknowledgement to user.
◦ On completion, the tool updates session.userdata and emits a follow-up reply into the session.
1. Manage backtracking & repetition:
◦ Use session.userdata.current_step to identify which step to replay.
◦ Pull the original instruction from session.history or regenerate the step instruction from the stored step metadata and replay it.
1. Multi-agent handoff (if needed):
◦ Transfer session.userdata to the receiving agent and keep a small transfer summary in session.history.
1. Persistence for reconnection:
◦ Optionally persist session.userdata to a fast store on each step transition; restore it on reconnect.
Short code sketch
@dataclass
class ProcedureState:
current_step: int = 1
completed_steps: List[int] = field(default_factory=list)
ticket_id: Optional[str] = None
in_progress: bool = False
class ConfirmStepTask(AgentTask[bool]):
async def run(self, context: RunContext):
state: ProcedureState = context.session.userdata
# Prompt user to confirm the current step
await context.session.generate_reply(f"Please confirm completion of step {state.current_step}.")
# Wait for a confirmation signal (turn detection + intent)
# On positive confirmation:
state.completed_steps.append(state.current_step)
self.complete(True)
Follow-ups I can help with
• Map a specific 5–10 step procedure (you provide steps) into AgentTasks and session.userdata layout.
• Example code showing async ticket creation with interim voice feedback and final confirmation.
• Patterns for multi-agent escalation (state transfer, human-in-loop handoff).
• Data model suggestions for session.userdata and persistence to Redis or DB for resilience.
If you want the next step, tell me:
• Do you prefer a single-agent implementation that orchestrates all steps, or a multi-agent system (intake agent → specialist agents)?
• Do you need sample code for a specific external API (a ticketing system or CRM) to demonstrate the async tool pattern?
I’ll produce targeted code examples and a sequence diagram for whichever option you pick.
Sources: Building multi-agent architectures with LiveKit agents | LiveKit | Tasks and task groups | LiveKit Documentation | External data and RAG | LiveKit Documentation | examples/drive-thru | livekit/livekit_composite | DeepWiki | An open source stack for real-time multimodal AInutritious-businessperson-79241
02/19/2026, 11:56 AMtall-airline-16756
02/19/2026, 11:56 AMnoise_cancellation.BVC() or noise_cancellation.BVCTelephony() on your agent hosts. This performs NC on the agent CPU before sending/after receiving audio.
• Don’t try to enable both server‑side and agent‑side NC simultaneously — that causes audio degradation.
Practical deployment notes and best practices
• Resource sizing: agents running VAD, turn detection, or agent‑side noise cancellation need CPU and memory; allocate accordingly (examples: multi‑vCPU and several GB RAM depending on model choice).
• Async tooling and UX: run long audio processing or SIP calls asynchronously so speech/TTS remains low‑latency and the user stays engaged (start the API call in background and provide interim feedback).
• Persistence: if you need resilience across reconnects, persist small state/proc flags externally (Redis/DB) — session.userdata is authoritative in‑session but backing it up helps multi‑host or reconnect scenarios.
• Testing: validate on representative hardware; transformer‑based turn detectors and some NC models can be memory/CPU heavy.
If you’d like, I can:
• Map these features to a recommended self‑hosted architecture (agent sizing, plugin placement, network components).
• Provide a short checklist for installing and configuring VAD, noise cancellation plugins, and SIP trunks in a self‑hosted LiveKit.
• Show a minimal agent example that uses Silero VAD + async SIP call + agent‑side noise cancellation.
Which of those would be most useful next?
Sources: Optimizing Video Calls for Low-Powered Devices | LiveKit | Creating SIP Inbound Trunks and Dispatch Rules with Python SDK | sip | livekit/livekit_composite | DeepWiki | Silero VAD plugin | LiveKit Documentation | SIP Lifecycle Management Agent | LiveKit Documentationnutritious-businessperson-79241
02/19/2026, 12:13 PMtall-airline-16756
02/19/2026, 12:13 PMsemantic_search). This will implement the exact workflow you described: greet → user problem → RAG check → create ordered steps → speak steps one-by-one → wait for confirmation/repeat → escalate if unsolvable.
I’ll keep this concise and focused on a minimal, production-ready pattern you can extend.
What I’ll provide
• Project layout and dependencies
• A robust agent implementation using session userdata for deterministic state
• Async tool patterns for RAG lookup and step actions
• VAD/turn-detection hints and where to hook STT/TTS
• Notes on persistence, error handling, and how to test
If you want me to generate a deployable repo, tell me whether you prefer:
• single-agent design (one agent manages the whole flow) — simpler and recommended for now, or
• multi-agent design (intake agent → specialist) — useful if you need separate capabilities/roles.
I’ll assume single-agent unless you ask otherwise.
Project structure
• stateful-troubleshooter/
◦ agent.py # main agent implementation
◦ rag.py # wrapper around your semantic_search (calls Azure AI Search)
◦ requirements.txt
◦ .env # secrets for local dev
◦ run.sh # run/dev helper
Key design decisions (short)
• Authoritative state: store procedure state and external IDs in session.userdata (TroubleshootingState). Don’t rely solely on LLM context.
• Steps are represented as a list in state and delivered one at a time by a Task/Tool that waits for explicit confirmation.
• Use function_tool for RAG lookup & ticket creation; run long external calls in background tasks and give voice feedback immediately.
• Use VAD/turn detection to mark end-of-turn; Azure STT provides stable interim/final results (hook STT plugin or stream).
• Persist session state to a DB (Redis) if you need reconnection/resilience; otherwise session.userdata is enough for single-session flows.
requirements.txt (minimal)
• livekit-agents[azure,openai]
• python-dotenv
• aiohttp
• (add Azure SDKs used by your existing RAG function if needed)
.env (local)
• LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET
• AZURE_SPEECH_KEY, AZURE_SPEECH_REGION
• AZURE_OPENAI_API_KEY, AZURE_OPENAI_ENDPOINT
• (Azure Search keys if rag.py uses them)
High-level code sketch (single-file overview)
• I’ll present the important parts you’ll need to adapt. This uses modern LiveKit Agents constructs, session.userdata, @function_tool, and async background tasks.
1) Data model for session state
from dataclasses import dataclass, field
from typing import List, Optional
@dataclass
class TroubleshootingState:
phase: str = "greeting" # greeting | investigating | resolving | escalating | resolved
problem: str = ""
steps: List[str] = field(default_factory=list)
current_step_index: int = 0
ticket_id: Optional[str] = None
escalation_offered: bool = False
2) RAG wrapper (rag.py)
• You said your semantic_search() exists. Expose it as an async function that returns ordered steps/evidence.
# rag.py
async def semantic_search(query: str) -> List[str]:
# call your Azure AI Search RAG pipeline and return ordered "solution steps" list
...
3) Agent implementation (agent.py)
• Core behaviors:
◦ on_enter: greet user
◦ @function_tool lookup_solution(problem): call semantic_search(), set state.steps and phase
◦ @function_tool offer_next_step(): speak the next step, ask user to confirm
◦ handle confirmations via intent detection (simple keyword or LLM intent)
◦ repeat/wait logic if user says they couldn't complete a step
◦ escalate_to_technician()
Important patterns (code excerpts)
from livekit.agents import Agent, function_tool
from livekit.plugins import azure as azure_plugins # STT/TTS plugin wrappers
import asyncio
class TroubleshooterAgent(Agent):
def __init__(self, ...):
super().__init__(
name="Troubleshooter",
instructions="You are a voice troubleshooting assistant. Speak one step at a time and wait for user confirmation.",
stt=azure_plugins.STT(...),
tts=azure_plugins.TTS(...),
llm=azure_plugins.RealtimeLLM(...), # Azure OpenAI Realtime
# optionally load VAD plugin if using Silero
)
async def on_enter(self):
# Attach authoritative state object to session.userdata if not present
if not hasattr(self.session, "userdata") or self.session.userdata is None:
self.session.userdata = TroubleshootingState()
await self.session.say("Hello! I am your support assistant. What problem are you having?")
@function_tool()
async def lookup_solution(self, context, problem: str):
state: TroubleshootingState = self.session.userdata
state.problem = problem
state.phase = "investigating"
await self.session.say(f"Let me check the knowledge base for: {problem}")
# call your RAG (async)
steps = await semantic_search(problem)
if steps and len(steps) >= 1:
# keep only actionable steps (your RAG can return ordered steps)
state.steps = steps
state.current_step_index = 0
state.phase = "resolving"
return f"Found {len(steps)} steps. I'll guide you through them one at a time."
else:
state.phase = "escalating"
return "I couldn't find enough info to fix this. I can connect you to a technician."
@function_tool()
async def offer_next_step(self, context):
state: TroubleshootingState = self.session.userdata
if state.phase != "resolving":
return
idx = state.current_step_index
if idx >= len(state.steps):
state.phase = "resolved"
await self.session.say("All steps completed. Your issue should be resolved. Anything else?")
return
step_text = state.steps[idx]
# Do not increment current_step_index until user confirms success
await self.session.say(f"Step {idx+1}: {step_text}")
await self.session.say("Say 'done' when you've completed this step, or say 'help' if you need assistance.")
# wait for user confirmation via STT/intent handling
# implement a simple waiter that listens for confirmation events or intents
4) Waiting for confirmation — two approaches
• Lightweight: keyword detection on STT final result (“done”, “yes”, “next”, “no”, “repeat”).
◦ Use a loop with a small timeout; leverage VAD/turn detection to know when user finished speaking.
• LLM intent: send the short user utterance to LLM with an intent prompt to classify confirm/need_help/repeat/escalate.
◦ More robust for language variability.
Example confirmation loop (simplified)
async def wait_for_confirmation(self, timeout=30):
# Wait for next user utterance from session events (you'll hook to STT final transcripts).
# This is a schematic approach—adapt to your event API for STT results.
while True:
result = await self.session.next_user_utterance(timeout=timeout)
if result is None:
await self.session.say("I didn't hear anything. Do you need more time?")
continue
text = result.text.lower().strip()
if text in ("done","yes","i did","completed","done it"):
return "confirmed"
if text in ("no","not yet","i can't","help"):
return "need_help"
if "repeat" in text or "say again" in text:
return "repeat"
# fallback: call the LLM to classify
intent = await self.classify_intent_with_llm(text)
if intent in ("confirm","repeat","help"):
return intent
5) Handling user responses
• On "confirmed": increment state.current_step_index and call offer_next_step()
• On "need_help" or "repeat": re-say the same step with extra clarifying sub-questions (you can use LLM to generate clarifying probes)
• Persist steps/status to Redis/DB if you want resilience across reconnects
6) Escalation
• If RAG returns no usable steps, run escalate_to_technician() function_tool:
◦ create ticket via your ticket API (async background task)
◦ optionally create a SIP call to a human agent
◦ update state.ticket_id and set escalation_offered=True
◦ inform user: “I couldn’t find a fix. I’ll connect you to a technician. One moment please.”
7) VAD, turn detection, and STT notes
• Use VAD to reduce latency in detecting end-of-turn; Azure STT offers interim/final results—use final to drive intent detection.
• If you have Silero VAD plugin available, use it in the agent to do short-turn decisions and avoid waiting for long silence.
• Ensure the STT plugin is configured to emit final transcripts promptly.
8) Async tool pattern for long calls (ticket creation)
```
``` Sources: Azure OpenAI Realtime API and LiveKit | LiveKit Documentation | Introduction | LiveKit Documentation | Tool definition and use | LiveKit Documentation | Voice AI quickstart | LiveKit Documentation | Modifying LLM output before TTS | LiveKit Documentationnutritious-businessperson-79241
02/19/2026, 12:20 PMtall-airline-16756
02/19/2026, 12:21 PMnutritious-businessperson-79241
02/19/2026, 12:32 PMnutritious-businessperson-79241
02/19/2026, 12:48 PM