how to track if customer picked upd the outbound c...
# ask-ai
p
how to track if customer picked upd the outbound call or not async def make_single_call(phone: str, name: str) -> dict: lk: lkapi.LiveKitAPI = get_livekit_client() room_name = f"call-{uuid.uuid4().hex[:10]}" try: await lk.agent_dispatch.create_dispatch( lkapi.CreateAgentDispatchRequest( agent_name="excelr-bot", room=room_name, metadata=json.dumps({ "phone": phone, "name": name, }), ) ) logger.info(f"Agent dispatched → room: {room_name}") sip = await lk.sip.create_sip_participant( lkapi.CreateSIPParticipantRequest( sip_trunk_id=SIP_OUTBOUND_TRUNK_ID, sip_call_to=phone, room_name=room_name, participant_identity=phone, participant_name=name, play_dialtone=True, wait_until_answered=True, play_ringtone=True, participant_metadata=json.dumps({ "phone": phone, "name": name, "room": room_name, }) ) ) logger.info(f"SIP participant created → {phone}, status: {sip.s}") print(sip) return { "room": room_name, "phone": phone, "name": name, "status": sip, } finally: await lk.aclose()
✅ 1
t
Thanks for reaching out, arpan. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — you're asking how to detect whether the outbound SIP call was picked up. I looked at your code and the LiveKit SIP behavior; here are practical options you can use. Short answer • With
wait_until_answered=True
, a successful
create_sip_participant
response means the call was answered; if the API call errors or times out, the call was not answered. • If you need non-blocking tracking, use SIP participant status/events or handle the response object's status field to determine answer state. What to do in your code 1. Keep
wait_until_answered=True
if you want the call creation to only succeed when answered. Wrap the call in try/except and treat exceptions/timeouts as "not answered". 2. Inspect the SIP response object for status/state fields (the SDK returns status info on the created SIP participant). Log or return that field instead of printing
sip.s
. 3. For async/non-blocking flows, set
wait_until_answered=False
and: ◦ Subscribe to SIP/participant events or poll the participant state to detect when it transitions to answered/connected. ◦ Or add logic to detect silence/voicemail (e.g., simple speech detection timeout) if you need to distinguish voicemail vs answered. Example (minimal change to your function) 1. Use try/except to capture a failed/timeout create:
Copy code
try:
    sip = await lk.sip.create_sip_participant(..., wait_until_answered=True, ...)
    # success → answered
except Exception as e:
    # failure/timeout → not answered
1. Or for non-blocking:
Copy code
sip = await lk.sip.create_sip_participant(..., wait_until_answered=False, ...)
# then check sip.status or listen for participant state events to see when/if it becomes 'answered' or 'connected'
Questions to narrow this down • Do you want the call flow to block until answered (current
wait_until_answered=True
) or remain async and notify later? • Do you have access to participant state/events in your runtime (so I can show a sample using event callbacks or polling)? Tell me which approach you want and I’ll provide a concrete code sample. Sources: Preserving Caller ID During SIP Transfers in LiveKit Agents | Accepting calls overview | LiveKit Documentation | Making calls overview | LiveKit Documentation, with interactive elements
p
i have a class fro semantic cahe but how to implement in llm_node
t
Thanks — you're asking how to plug your semantic cache class into the agent's
llm_node
. I can show a concise pattern and a short code example, but first a quick clarification so I give the right sample: • Which language/runtime are you using (Python or TS)? • Does your semantic cache expose async methods like
lookup(query) -> Optional[str]
and
store(query, response)
? If not, what are the method names/signatures? If you're using Python and your cache has async `lookup`/`store`, here's the minimal pattern to override
llm_node
and use the cache:
Copy code
async def llm_node(self, chat_ctx: llm.ChatContext, tools: list, model_settings) -> AsyncIterable[llm.ChatChunk]:
    # 1) Build a cache key from the incoming context (message text, metadata, etc.)
    cache_key = self.semantic_cache.build_key(chat_ctx)

    # 2) Try cache hit
    cached = await self.semantic_cache.lookup(cache_key)
    if cached:
        # Return cached text as a single ChatChunk (or stream if you prefer)
        yield llm.ChatChunk(text=cached)
        return

    # 3) No hit — call default LLM behavior and stream results, while capturing them
    collected = []
    async for chunk in Agent.default.llm_node(self, chat_ctx, tools, model_settings):
        collected.append(chunk.text)
        yield chunk

    # 4) Store combined response in cache
    full_response = "".join(collected)
    await self.semantic_cache.store(cache_key, full_response)
What I can provide next • A TypeScript equivalent if you use TS. • A version that returns chunked streaming cached responses. • Help adapting this to your actual cache method names or key schema. Tell me runtime and your cache method signatures and I’ll adapt the snippet. Sources: Pipeline nodes and hooks | LiveKit Documentation, with interactive elements
p
one python example with cahe function
t
Thanks — below is a concise, usable Python example that shows a simple async semantic cache class and how to integrate it into an Agent by overriding
llm_node
. The example assumes your cache exposes async
lookup(key)
and
store(key, value)
methods and that
chat_ctx.messages[-1].content
gives the user prompt text. Adapt keys and message extraction to your schema.
Copy code
import asyncio
from typing import AsyncIterable
from livekit import llm  # adjust import to your SDK
from livekit.agents import Agent  # adjust import path
from livekit.agents import llm as llm_types  # for ChatChunk type if needed

# Simple in-memory semantic cache (replace with vector DB or Redis in prod)
class SemanticCache:
    def __init__(self):
        self._store: dict[str, str] = {}

    async def build_key(self, chat_ctx) -> str:
        # Example: use last user message as key (customize with embeddings/metadata)
        return chat_ctx.messages[-1].content.strip()

    async def lookup(self, key: str) -> str | None:
        # Real semantic cache would do similarity search; this is exact match
        await asyncio.sleep(0)  # simulate async I/O
        return self._store.get(key)

    async def store(self, key: str, value: str):
        await asyncio.sleep(0)  # simulate async I/O
        self._store[key] = value

# Example Agent subclass with llm_node override
class CachingAgent(Agent):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.semantic_cache = SemanticCache()

    async def llm_node(
        self,
        chat_ctx: llm.ChatContext,
        tools: list,
        model_settings
    ) -> AsyncIterable[llm.ChatChunk]:
        # 1) Build cache key
        cache_key = await self.semantic_cache.build_key(chat_ctx)

        # 2) Try cache hit
        cached = await self.semantic_cache.lookup(cache_key)
        if cached:
            # Return cached response as a single ChatChunk
            yield llm.ChatChunk(text=cached)
            return

        # 3) No cache hit — call default LLM and stream results while collecting
        collected_chunks: list[str] = []
        async for chunk in Agent.default.llm_node(self, chat_ctx, tools, model_settings):
            # chunk may be str or ChatChunk depending on SDK; handle both
            text = chunk.text if hasattr(chunk, "text") else str(chunk)
            collected_chunks.append(text)
            yield chunk

        # 4) Store the combined response in cache
        full_response = "".join(collected_chunks)
        await self.semantic_cache.store(cache_key, full_response)
If you want: • A version that uses embeddings & similarity search for fuzzy cache hits. • Streaming cached responses (yielding multiple small chunks). • Integration with a real vector DB (e.g., Pinecone, Weaviate, RedisVector). Tell me which and I’ll extend the snippet. , with interactive elements
p
i have new idea i want to use background planner agent that have research tools , summarize tools this work with separate llm not main llm can i do that
t
Yes — you can. A background planner/observer that uses its own LLM and tools (research, summarization) while the main agent uses a different LLM is a supported and recommended pattern. Quick overview of how to set it up • Run the planner as a separate async component (not blocking the main agent). • Give the planner its own LLM client and tool set (web search, summarizer, etc.). • Listen to conversation events (e.g.,
conversation_item_added
) or poll the chat state. • Use the planner to research/summarize, then inject results into the main agent via
update_chat_ctx()
so the main agent can use them on the next turn. Minimal Python pattern 1. BackgroundPlanner: holds its own LLM and tools, subscribes to events, runs async tasks for research/summaries. 2. On new message: planner runs research, generates summary, then calls main_agent_session.update_chat_ctx(...) or creates a system message with findings. 3. Main agent remains fast; it sees injected context on next inference. If you'd like, I can: • Provide a concise Python example showing a BackgroundPlanner class that listens for
conversation_item_added
, calls a web-search tool, summarizes with another LLM, and injects the summary. • Or tailor the example to specific tools/models you plan to use. Which do you want—full example or a sketch tailored to your tools/models? Sources: How to Build a Background Observer for Voice AI Guardrails | LiveKit | Tool definition and use | LiveKit Documentation, with interactive elements
p
give one example
t
Below is a concise, runnable Python example of a background planner that uses its own LLM and tools (web search + summarizer), listens for new user messages, performs research/summarization, and injects results into the main agent’s chat context.
Copy code
import asyncio
from livekit.agents import AgentSession, JobContext, llm, inference
from livekit.agents.events import ChatContextEvent

# --- Simple planner tools (replace with real implementations) ---
class WebSearchTool:
    async def search(self, query: str) -> str:
        await asyncio.sleep(0)  # simulate async I/O
        return f"Search results for: {query}"

class SummarizerTool:
    async def summarize(self, text: str) -> str:
        await asyncio.sleep(0)
        return f"Summary: {text[:200]}"

# --- Background planner ---
class BackgroundPlanner:
    def __init__(self, session: AgentSession, planner_llm: inference.LLM):
        self.session = session
        self.planner_llm = planner_llm
        self.search_tool = WebSearchTool()
        self.summary_tool = SummarizerTool()
        # subscribe to chat events
        self.session.event_queue.subscribe(ChatContextEvent)
        self._task = asyncio.create_task(self._loop())

    async def _loop(self):
        async for event in self.session.event_queue.disposable_iter():
            if not isinstance(event, ChatContextEvent) or event.added_item is None:
                continue
            item = event.added_item
            # only react to user messages
            if getattr(item, "role", "") == "user":
                text = item.content
                # run research/summarization in background
                asyncio.create_task(self._research_and_inject(text))

    async def _research_and_inject(self, user_text: str):
        # 1) Research
        results = await self.search_tool.search(user_text)

        # 2) Summarize results using planner LLM (or summarizer tool)
        # Example uses planner_llm.chat() — adapt to your SDK's API
        chat_ctx = llm.ChatContext()
        chat_ctx.add_message(role="system", content="You are a research assistant.")
        chat_ctx.add_message(role="user", content=f"Please summarize these findings:\n\n{results}")

        completion = await self.planner_llm.chat(chat_ctx=chat_ctx)
        planner_summary = completion.message.content if completion and completion.message else await self.summary_tool.summarize(results)

        # 3) Inject the summary into the main agent's chat context
        new_ctx = self.session.chat_ctx.copy()
        new_ctx.add_message(role="system", content=f"[PLANNER UPDATE]: {planner_summary}")
        await self.session.update_chat_ctx(new_ctx)

# --- Example main session setup ---
async def start_session(ctx: JobContext):
    session = AgentSession(
        stt=inference.STT(model="some-stt"),
        llm=inference.LLM(model="gpt-4o-mini"),  # main agent LLM (fast)
        tts=inference.TTS(model="some-tts"),
    )

    # planner uses a separate, stronger model
    planner_llm = inference.LLM(model="gpt-4.1")
    BackgroundPlanner(session=session, planner_llm=planner_llm)

    await session.start(agent=YourMainAgent(), room=ctx.room)
Would you like this adapted to: • use a real web-search tool (e.g., Google / Bing wrapper), or • stream planner updates as multiple messages instead of a single system message? Sources: How to Build a Background Observer for Voice AI Guardrails | LiveKit | Tool definition and use | LiveKit Documentation, with interactive elements
p
i want as tool when need it work only then
t
Got it — you want the planner/tools to run only on demand. Quick clarification so I give the right pattern: • How do you want to trigger the planner/tools? (examples: a special user command like "research:", a system signal, a specific intent detected by the main agent, or a timeout/threshold) • Do you want the main agent to wait for the result (sync) or have the planner run async and inject results later (async)? If you confirm the trigger and sync vs async preference, I’ll provide a short code example showing conditional activation (e.g., intent check or command-based trigger) and the minimal plumbing to call the planner tools only when needed. , with interactive elements
p
async tool
t
Below is a minimal Python example showing an async tool (ResearchTool) that runs only when the LLM invokes it. It uses an AsyncToolset, registers the tool on the AgentSession (so it persists across handoffs), and demonstrates how the tool can run in background while the agent continues. Adapt the tool body (web search, summarizer) to your implementation.
Copy code
import asyncio
from typing import Any
from livekit.agents import Agent, AgentSession, function_tool
from livekit.agents.llm.async_toolset import AsyncToolset, AsyncRunContext
from livekit.agents import inference, llm

# --- Async toolset ---
class ResearchToolset(AsyncToolset):
    def __init__(self):
        super().__init__(id="research")

    @function_tool()
    async def perform_research(self, ctx: AsyncRunContext, query: str) -> str:
        # progress update visible in chat context while running
        await ctx.update(f"Starting research for: {query}")

        # simulate long-running research (replace with real web search / API calls)
        await asyncio.sleep(2)
        results = f"Fake search results for: {query}"

        await ctx.update("Research complete, summarizing...")
        # simulate summarization
        await asyncio.sleep(1)
        summary = f"Summary of '{query}': {results[:200]}"

        # final return becomes the tool result
        return summary

# --- Main agent (no changes needed for tool calling) ---
class MyAgent(Agent):
    def __init__(self):
        super().__init__(instructions="You are a helpful assistant.")
        # Agent-level tools are cancelled on handoff; prefer session-level tools if persistence needed

# --- Start session with async tool registered on the session ---
async def start_session():
    session = AgentSession(
        stt=inference.STT(model="some-stt"),
        llm=inference.LLM(model="gpt-4o-mini"),  # main agent LLM
        tts=inference.TTS(model="some-tts"),
        tools=[ResearchToolset()],  # register async tools on session (persist across handoffs)
    )

    # Start the agent session (replace with your actual session.start usage)
    await session.start(agent=MyAgent(), room="room-name")

# --- How it works ---
# - The LLM (main agent) will only call `perform_research` when it decides the tool is needed
#   (based on prompt/context or explicit user command).
# - The tool runs asynchronously; use `await ctx.update(...)` to send progress updates into chat.
# - You can rely on the LLM to invoke the tool via ReAct-style reasoning, or prompt it explicitly:
#     User: "Research: best restaurants in Seattle"
#   The agent will decide to call the async tool and return the tool result when ready.

# Run example (in real code this is driven by your server framework)
# asyncio.run(start_session())
Notes: • Tools defined with
AsyncToolset
are only available in Python. • Register the tool on
AgentSession
(not Agent) if you want it to persist across agent handoffs. • The LLM decides when to call the tool; you can encourage on-demand use by prompting (e.g., "Research: ...") or by detecting an intent and letting the model call the tool. • Use
ctx.update()
to push progress messages into the chat without blocking the agent. Want a version that: • Shows how to explicitly trigger the tool by intent detection in code, or • Demonstrates returning intermediate streaming updates to the user? Which would help most? Sources: Async tools | LiveKit Documentation | The ReAct Pattern for Voice Agents and How AI Agents Think, Act, and Respon..., with interactive elements
p
Copy code
ResearchToolset
i want seaprate llm work as sub agent
t
Nice — below is a concise Python example that shows: • A main agent using a fast LLM. • A separate sub-agent (BackgroundPlanner) with its own LLM that acts as a sub-agent. • An async ResearchToolset invoked only when the sub-agent needs it. • The sub-agent runs on demand (triggered by a special system message or explicit call) and injects results back into the main session. Adapt models, tool implementations, and session wiring to your environment.
Copy code
import asyncio
from livekit.agents import Agent, AgentSession, function_tool, llm, inference
from livekit.agents.llm.async_toolset import AsyncToolset, AsyncRunContext
from livekit.agents.events import ChatContextEvent

# --- Async toolset used by the sub-agent ---
class ResearchToolset(AsyncToolset):
    def __init__(self):
        super().__init__(id="research")

    @function_tool()
    async def perform_research(self, ctx: AsyncRunContext, query: str) -> str:
        await ctx.update(f"Planner: starting research for '{query}'")
        await asyncio.sleep(2)  # replace with real web search
        results = f"Fake search results for: {query}"
        await ctx.update("Planner: summarizing results...")
        await asyncio.sleep(1)
        return f"Summary of '{query}': {results[:200]}"

# --- Main agent (fast, responsive) ---
class MainAgent(Agent):
    def __init__(self):
        super().__init__(instructions="You are the primary assistant.")

# --- Sub-agent / Background planner that uses its own LLM and tools ---
class BackgroundPlanner:
    def __init__(self, session: AgentSession, planner_llm: inference.LLM):
        self.session = session
        self.planner_llm = planner_llm
        # Register tools on the session so they persist across handoffs
        # (Session already created with tools=[ResearchToolset()], see start_session)
        self.session.event_queue.subscribe(ChatContextEvent)
        self._task = asyncio.create_task(self._loop())

    async def _loop(self):
        async for ev in self.session.event_queue.disposable_iter():
            if not isinstance(ev, ChatContextEvent) or ev.added_item is None:
                continue
            item = ev.added_item
            # Trigger policy: sub-agent acts only on explicit "PLAN:" commands
            if getattr(item, "role", "") == "user" and item.content.startswith("PLAN:"):
                query = item.content[len("PLAN:"):].strip()
                asyncio.create_task(self._run_planner(query))

    async def _run_planner(self, query: str):
        # Build a small chat context for the planner
        chat_ctx = llm.ChatContext()
        chat_ctx.add_message(role="system", content="You are a planning assistant.")
        chat_ctx.add_message(role="user", content=f"Research: {query}")

        # Call planner's LLM; allow it to call async tools (ResearchToolset)
        # The planner_llm should be configured to allow tool use (handled by session tools)
        completion = await self.planner_llm.chat(chat_ctx=chat_ctx)

        # If planner's LLM directly returns result, inject it; otherwise the async tool's
        # progress/return will already be added to the session via ctx.update() from the tool.
        planner_out = completion.message.content if completion and completion.message else None
        if planner_out:
            new_ctx = self.session.chat_ctx.copy()
            new_ctx.add_message(role="system", content=f"[PLANNER RESULT]: {planner_out}")
            await self.session.update_chat_ctx(new_ctx)

# --- Start session wiring: register async tool on session and start both agents ---
async def start_session():
    session = AgentSession(
        stt=inference.STT(model="some-stt"),
        llm=inference.LLM(model="gpt-4o-mini"),        # main agent LLM (fast)
        tts=inference.TTS(model="some-tts"),
        tools=[ResearchToolset()],                     # session-level async tools
    )

    # Planner uses a separate, more capable LLM
    planner_llm = inference.LLM(model="gpt-4.1")       # planner/sub-agent LLM

    # Attach the background planner (sub-agent)
    BackgroundPlanner(session=session, planner_llm=planner_llm)

    # Start main agent
    await session.start(agent=MainAgent(), room="room-name")

# Example trigger:
# - User sends: "PLAN: best electric cars 2026"
# - Planner sees "PLAN:" command, runs research tool (async), and injects summary when ready.
Notes: • The planner uses its own LLM instance (
planner_llm
) separate from the main agent LLM. • The async tool (ResearchToolset) is registered on the AgentSession so the planner can call it; it runs only when invoked by the planner (or when the LLM decides to). • Triggering is demonstrated with a "PLAN:" user prefix to run the planner on demand; you can replace this with intent detection, a special system message, or an explicit call from the main agent. • Progress updates from the async tool use
ctx.update()
and appear in the chat context while the tool runs. • If you want the main agent to wait for the planner result synchronously, have the main agent detect the intent and either call the planner LLM directly or wait for injected results before responding. Want this adapted to intent-based triggers (no special prefix) or to stream planner updates as separate assistant messages? , with interactive elements
p
what is toolset and benifit
t
A Toolset is a grouped collection of related tools you can attach to an agent or session. It bundles multiple function-callable tools under one ID, exposes the individual tools to the LLM, and manages setup/teardown as a unit. Key benefits • Modular organization: encapsulate domain-specific capabilities (e.g., search, bookings, billing) so code stays clean. • Dynamic management: add/remove whole tool groups at runtime to change agent behavior or conserve resources. • Persistence control: register toolsets on AgentSession (persist across handoffs) or on Agent (cancelled on handoff) depending on lifecycle needs. • Async support: use AsyncToolset for long-running background tasks with progress updates via
ctx.update()
. • On-demand loading patterns: enable lazy tool discovery or proxies to avoid large tool lists hurting performance. • Resource lifecycle: override setup/aclose for connections or cleanup when a toolset is activated/deactivated. When to use • Group related APIs or operations you want the LLM to call via function tools. • Provide background/long-running actions (use AsyncToolset). • Keep tool availability flexible across sessions and agent handoffs. If you want, I can: • Show a minimal Toolset vs AsyncToolset example, or • Show how to register a Toolset only when the planner/sub-agent is invoked. Which do you prefer? Sources: Toolsets | LiveKit Documentation | The ReAct Pattern for Voice Agents and How AI Agents Think, Act, and Respon..., with interactive elements