Workshop logo

Building Production-Ready Voice Agents with LiveKit

Hands-on workshop for real-time voice AI systems

01

Hours

Consent and Escalations

Operational workflows for real deployments

Production deployments must respect compliance requirements and hand off gracefully when the caller needs a human. We will add:

  1. A task that collects recording consent
  2. A manager agent and a function tool that escalates the call

Consent workflows capture a yes/no answer before the main conversation begins.

Imports

Add at the top of agent.py:

from livekit.agents import AgentTask

Reuse function_tool from the previous lesson. No new imports required!

Task implementation

Define the task class below your Assistant declaration:

from livekit.agents import AgentTask, function_tool

class CollectConsent(AgentTask[bool]):
    def __init__(self, chat_ctx=None):
        super().__init__(
            instructions="""
            Ask for recording consent and get a clear yes or no answer.
            Be polite and professional.
            """,
            chat_ctx=chat_ctx,
        )

    async def on_enter(self) -> None:
        await self.session.generate_reply(
            instructions="""
            Briefly introduce yourself, then ask for permission to record the call for quality assurance and training purposes.
            Make it clear that they can decline.
            """
        )

    @function_tool
    async def consent_given(self) -> None:
        """Use this when the user gives consent to record."""
        self.complete(True)

    @function_tool
    async def consent_denied(self) -> None:
        """Use this when the user denies consent to record."""
        self.complete(False)

Kick off the task

Extend Assistant.on_enter to run the task before normal assistance:

class Assistant(Agent):
    # __init__ ...

    async def on_enter(self) -> None:
        if await CollectConsent(chat_ctx=self.chat_ctx):
            await self.session.generate_reply(instructions="Offer your assistance to the user.")
        else:
            await self.session.generate_reply(instructions="Inform the user that you are unable to proceed and will end the call.")
            job_ctx = get_job_context()

Notice how we start the task with run(self.session), which keeps the conversational context consistent.

Escalating to a manager

Add a dedicated manager agent:

class Manager(Agent):
    def __init__(self, chat_ctx=None) -> None:
        super().__init__(
            instructions=(
                "You are a manager for a team of helpful voice AI assistants. "
                "Handle escalations professionally."
            ),
            tts="inworld/inworld-tts-1:ashley",
            chat_ctx=chat_ctx,
        )

Then register a function tool on Assistant that returns the manager:

@function_tool
async def escalate_to_manager(self, context: RunContext):
    """Escalate the call to a manager on user request."""
    return Manager(chat_ctx=self.chat_ctx), "Escalating you to my manager now."

The LiveKit agent runtime handles the handoff automatically once the tool returns the new agent instance.

Keep the consent flow crisp, explicit, and region-appropriate:

  • State purpose plainly: recording for quality and training; that participation is optional.
  • Ask a yes/no question; avoid compound prompts that bury the ask.
  • Confirm the outcome back to the caller in your own words.

Latency and UX tips

  • Don’t block on long tools during consent; keep it self-contained.
  • Use preemptive generation (Lesson 4) so acknowledgments start speaking quickly.
  • Keep voices distinct (Lesson 3): one for assistant, one for manager, to make role changes obvious.