Hi, Context: I am designing a complex, stateful Pr...
# ask-ai
n
Hi, Context: I am designing a complex, stateful Procedural Voice Assistant using LiveKit. The goal is to build a high-performance system that provides step-by-step guidance while being integrated with external business tools (e.g., Ticketing systems, CRM APIs, or custom Databases). Key Requirements: 1. Procedural Interaction: The assistant must deliver instructions one step at a time, waiting for explicit user confirmation (e.g., "Step 1 completed", "Next") before moving to the next phase. 2. Context & Navigation: It must maintain a robust conversation state to handle "backtracking" (e.g., "Can you repeat that step?") or clarifying questions without losing its place in the procedure. 3. External Tool Integration (Function Calling): The agent needs to interact with external APIs during the flow. For example, it should be able to "Check ticket status", "Log a completed step in the system", or "Escalate to a human agent" based on the conversation. Please provide guidance on: • Orchestration Pattern: Within the LiveKit Agents Framework, what is the best way to manage this "Step-by-step + Tool Calling" logic? Should I use a dedicated state-management library or handle it within the LLM's system prompt and tool definitions? • Parallel Tool Execution: How does LiveKit handle scenarios where the agent needs to call an external tool (e.g., creating a ticket) while simultaneously keeping the user engaged via voice? • Memory Management: What is the recommended approach for keeping the "Procedural State" (which step we are on) and the "External Data" (ticket IDs, user info) synchronized throughout the session? • Example Architectures: Are there any reference projects or templates that demonstrate multi-tool integration combined with a procedural flow in a voice-first environment? I'm aiming for a low-latency, "human-in-the-loop" style automation for a professional use case.
t
Thanks for reaching out, Berfin. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — this is a great, clear brief. I’ll respond to each of your four areas (orchestration pattern, parallel tool execution, memory management, example architectures) and finish with a concise recommended implementation outline you can adapt. Acknowledgement: you want a low-latency, voice-first, step-by-step assistant that (1) pauses for explicit user confirmation, (2) supports backtracking/clarification without losing place, and (3) calls external tools (ticketing/CRM/etc.) during the flow. Below is a practical approach using LiveKit Agents. 1) Orchestration pattern — how to manage step-by-step + tool calling • Use AgentTask (and TaskGroups) as the primary control construct. ◦ Model each procedural step as an AgentTask so the task “owns” the session until that step is explicitly completed (e.g., user says “Step 1 completed” or triggers a confirm tool). ◦ Tasks give deterministic control flow—better than relying only on LLM chat memory for production workflows. • Keep the LLM responsible for natural language understanding and decision-making but not as the single source of truth for state. • Persist the authoritative procedural state in
session.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
Copy code
@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 AI
n
can I use VAD, noise cancellation, turn detection, sip trunking, web RTC features in self hosted version of LiveKit
t
Thanks — I see you’re asking whether VAD, noise cancellation, turn detection, SIP trunking, and WebRTC features are available in the self‑hosted LiveKit deployment and how they differ from the managed Cloud offering. Short answer: yes for WebRTC, SIP, VAD, and turn detection (with caveats); noise cancellation is available but only agent‑side in self‑hosted setups. Below is a concise breakdown and practical notes so you can plan your deployment. WebRTC • ✅ Supported in self‑hosted LiveKit. • All core WebRTC capabilities (audio/video streaming, SFU routing, simulcast/SVC, track subscriptions) work when you self‑host. • You are responsible for TURN/STUN, NAT traversal, and scaling your servers. SIP trunking • ✅ Supported, but requires setup. • SIP integration is handled via server components / SDKs. You must configure your SIP provider, codecs, RTP/DTMF handling, and any transcoding you need. • Expect to run SIP handling server‑side in your environment and configure dispatch/ingress rules appropriately. Voice Activity Detection (VAD) • ✅ Supported in self‑hosted LiveKit via plugins (e.g., Silero VAD). • VAD runs locally on the agent process (CPU-based). Install the VAD plugin and required model weights on your agent host. • Use VAD for turn detection, interruption handling, and stream control. Turn detection • ✅ Supported with options: ◦ VAD‑based turn detection (Silero) is fully supported. ◦ STT‑based endpointing (e.g., Deepgram/AssemblyAI) is supported. ◦ LiveKit transformer‑based Turn Detector is available but needs additional resources and model downloads (higher CPU/RAM). • Choose VAD for lightweight detection; choose STT/turn‑detector if you need context‑aware endpointing. Noise cancellation • ⚠️ Available in self‑hosted — but only agent‑side. ◦ Enhanced server‑side noise cancellation (Krisp/BVC) is a Cloud feature and is not available on the self‑hosted media server. ◦ You can run agent‑side noise cancellation plugins such as
noise_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 Documentation
n
I was wondering that I asked you how can I create agent with statful approach. My voice bot should answer step by step. for thise scenerio that I mentioned to you before can you provide me some code ideas. I have services stt-azure speech, llm-azure openai, tts-azure speech. I have a knowledge base in azure ai search and a rag system which can pull relevant information with the user query. How can I achieve this workflow: voice bot starts to speak, welcome the user, user asks his question: I have a problem with my vpn connection, bot start to investigate that wether it can solve this problem or not(it should decide this looking a the knowledge base and find enough information to solve the problem)(my rag function is ready so you don't have to think about it. We can assume that I am using semantic_search() pull the relevant information with the query), After it decides it can solve it create solutions steps from the data and starts to tell the steps to the user one by one. After every step it waits for a validation wether user could achieve that spesific step or not. If he can then other step comes if he can't then waits and repeat the same step. I want a natural flow in this part. If bot decides it can't solve this problem then should tell I can direct you a tecnichian, Okay for the first part create me a proper project, it is important that you should use latest livekit methods. I dont want to take any error.
t
Thanks — this is a great, specific brief. I’ll give you a clear, working Python project layout and runnable code ideas using LiveKit Agents (latest patterns), Azure Speech (STT/TTS), Azure OpenAI (LLM Realtime), and your existing RAG function (
semantic_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
Copy code
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.
Copy code
# 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)
Copy code
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)
Copy code
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 Documentation
n
this app should speak Turkish, so edit the instructions and descriptions according to this. besides that in my rag function it is not producing step by step solutions, it just takes the relevant information. So you have to write a structure for llm produce multi step answer. Edit the code
t
I don't have the answer you're looking for. You could also try asking your question: • in one of the other Slack channels or • to https://deepwiki.com/livekit/livekit_composite which is trained on all LiveKit source code If you find the answer, please post it here to help others!
n
you told me to use agenttask and taskgroup can you give me an example about how can I use this structure in my specific scenerio
you told me to use agenttask and taskgroup can you give me an example about how can I use this structure in my specific scenerio