This message was deleted.
# ask-ai
s
This message was deleted.
✅ 1
t
Thanks for reaching out, Ananya. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I understand you want a LiveKit agent that only listens to PSTN callers in a LiveKit room and produces transcriptions. I’ll walk you through the approach and the key configuration steps so you can implement an STT-only agent that transcribes PSTN (SIP) callers. Quick summary of the plan • Use LiveKit SIP integration so PSTN callers join the LiveKit room as SIP participants. • Start an agent session with audio input enabled and audio output disabled so the agent does not publish audio (text-only agent). • Configure the agent to listen specifically to the SIP participant (PSTN caller) using RoomIO or a specified participant identity. • Receive the text transcription via the lk.transcription text stream and/or the agent’s
user_input_transcribed
events. What to configure and why • SIP / PSTN inbound ◦ Ensure your SIP trunk / dispatch rules route inbound calls to create or join a LiveKit room. PSTN callers will appear as SIP participants in that room. ◦ PSTN callers are treated like any other participant for audio input; the agent can listen to them by default. • Agent session (STT-only) ◦ Start the agent session with audio input enabled and audio output disabled. ▪︎ This results in the agent not publishing an audio track and producing text-only output. ◦ When audio output is disabled, agent text responses are published to the
lk.transcription
text stream (they will not have
lk.transcribed_track_id
since no audio track exists). ◦ Use the STT provider you prefer (example: Deepgram) in your agent config so incoming audio is transcribed. • Targeting the PSTN caller only ◦ By default agents listen to standard participants and SIP callers but not to other agents. ◦ To ensure the agent only processes audio from the PSTN caller (if there are other participants), configure RoomIO with the SIP participant identity or explicitly set the participant identity the agent should process. ◦ This ensures the agent ignores other room audio and transcribes only the selected SIP participant. How to receive and display transcriptions • Listen for the
lk.transcription
text stream topic on your frontend client to receive text segments published by the agent. • Alternatively, handle the
user_input_transcribed
events in the agent runtime if you need server-side handling of final transcripts. • Note: The built-in playground UI uses legacy transcription events and may not show text-only agent responses; implement the text-stream handler in your frontend to view these transcriptions. Implementation pointers (high level) 1. Create or configure your SIP trunk/dispatch so inbound PSTN calls join the intended LiveKit room. 2. Start the agent session with: ◦ audio input enabled ◦ audio output disabled (so the agent is STT-only) ◦ configure the STT provider and transcription settings 1. Use RoomIO or the room participant identity to point the agent’s audio input at the SIP participant only. 2. On the client side, register a handler for
lk.transcription
to receive and display transcription segments. Check for the absence of
lk.transcribed_track_id
to identify text-only responses. If you’d like, I can: • Provide a minimal example (Node.js or Python) for starting the agent with audio_output disabled and receiving lk.transcription events. • Show an example of how to configure RoomIO to target a specific participant identity (SIP caller). • Walk through a sample SIP dispatch configuration to ensure calls arrive in the right room. Which of these would help you next — a code example for starting the STT-only agent (Node or Python), or the RoomIO snippet to target the SIP participant? Sources: How to build an agent with speech-to-text input and text-only output | Live... | sip | livekit/livekit_composite | DeepWiki | Transcriber | LiveKit Documentation, with interactive elements
m
RoomIO snippet to target the SIP participant?
t
Thanks — I’ll show you RoomIO snippets that target a specific SIP participant so the agent listens only to that PSTN caller for transcription. Below are concise examples for Node.js and Python and a short note on how to obtain the SIP participant identity. How it works (brief) • You configure RoomIO with a participant identity (the SIP participant) so the agent processes audio from that participant only. • The agent session still has audio input enabled and audio output disabled for STT-only behavior. Node.js RoomIO snippet
Copy code
// Example Node.js snippet showing RoomIO targeting a SIP participant identity
const { RoomIO } = require('livekit-agent-sdk'); // illustrative import

async function startAgentForSipParticipant(roomClient, sipParticipantIdentity) {
  const roomIO = new RoomIO({
    // Specify the participant identity to listen to (SIP caller)
    participantIdentity: sipParticipantIdentity,
    // Keep audio input enabled so the agent hears the participant
    audioInput: true,
    // Disable agent audio output to make the agent STT-only
    audioOutput: false,
    // (Optional) STT provider config goes here
    stt: {
      provider: 'deepgram',
      apiKey: process.env.DG_API_KEY,
    },
  });

  // Connect RoomIO to the live room client / agent runtime
  await roomClient.attachRoomIO(roomIO);

  // Now the agent will process audio only from the participant identified by sipParticipantIdentity
}
Python RoomIO snippet
Copy code
# Example Python snippet showing RoomIO targeting a SIP participant identity
from livekit_agent_sdk import RoomIO  # illustrative import

def start_agent_for_sip_participant(room_client, sip_participant_identity):
    room_io = RoomIO(
        participant_identity=sip_participant_identity,
        audio_input=True,   # listen for audio
        audio_output=False, # STT-only: no audio published by agent
        stt={
            "provider": "deepgram",
            "api_key": os.environ.get("DG_API_KEY"),
        },
    )

    # Attach RoomIO to the room client / agent runtime
    room_client.attach_room_io(room_io)

    # Agent now listens only to the specified SIP participant
How to get the SIP participant identity • When a PSTN caller joins via SIP, they appear in the LiveKit room as a participant with an identity you can read from the room’s participant list (
participant.identity
). • You can: ◦ Inspect the participant list when the call arrives and capture the SIP participant identity. ◦ Use your SIP dispatch rules to set or predict the identity (for example, include a caller-specific identity when creating/joining the room). Notes and tips • Agents by default do not listen to other agents, but they do listen to SIP participants—explicitly targeting the SIP participant ensures other participants’ audio is ignored. • Keep agent audio output disabled to publish text-only transcriptions to
lk.transcription
. • Ensure your STT provider settings are configured in the agent so incoming audio is transcribed. If you want, I can now: • Provide a concrete example showing how to detect the SIP participant identity from the room participant list and pass it into RoomIO. • Convert the snippets to match your exact agent SDK import names if you share which SDK (package) and language you’re using. Which would you prefer?, with interactive elements
m
i want to use webhookhandlr to see if the new participant joins and then send that to agent and make sure that the agent strat listening to that new sip participant also along with the callers it was already listening to
t
Thanks — I’ve got what you want: detect new PSTN (SIP) participants via a webhook, then tell the agent to start listening and transcribing only that participant. Below I’ll give a concise, practical flow and example snippets you can adapt (webhook handler -> extract SIP participant identity -> notify agent -> agent attaches RoomIO for that identity). I’ll include Node.js and Python examples and highlight important checks (webhook verification, idempotency). Summary of the flow 1. Webhook receives a participant_joined event from LiveKit. 2. Verify the webhook signature and quickly ACK (2xx). 3. From the event payload, detect if the participant is a SIP/PSTN caller and extract their participant identity. 4. Send a command to your agent runtime (for example via an internal API call, data message, or agent control endpoint) containing the room and participant identity. 5. Agent attaches/updates RoomIO with that participantIdentity, with audioInput enabled and audioOutput disabled so it becomes an STT-only listener for that SIP participant. 6. Agent publishes transcriptions to
lk.transcription
(and/or emits
user_input_transcribed
server-side). Important notes • ACK webhooks quickly and process events asynchronously. Deduplicate using the webhook event
id
to avoid repeated work. • Verify webhook authenticity using the WebhookReceiver / signature JWT in Authorization header. • Agents by default don’t listen to other agents; targeting the SIP participant identity ensures the agent ignores unrelated audio. • When audioOutput is disabled, transcriptions appear on
lk.transcription
and won’t include
lk.transcribed_track_id
. Example: Node.js webhook handler (Express) 1. Handle and verify webhook, extract participant identity, then call your agent control endpoint.
Copy code
// Express route (illustrative)
app.post('/webhook', async (req, res) => {
  // 1) Verify webhook signature (use your WebhookReceiver equivalent)
  // Quick ACK
  res.sendStatus(200);

  // 2) Process async
  (async () => {
    const event = req.body;
    if (event.type !== 'participant_joined') return;

    // Extract participant info
    const participant = event.participant;
    // Check for SIP-specific attributes (depends on payload shape)
    if (!participant || !participant.identity) return;

    // Optionally check participant.kind or sip fields if present
    const isSip = participant.kind === 'sip' || (participant?.sip != null);
    if (!isSip) return;

    const sipIdentity = participant.identity;
    const room = event.room;
    // 3) Notify your agent runtime (internal API call)
    await fetch(`${AGENT_CONTROL_URL}/start-listen`, {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({ room, sipIdentity }),
    });
  })().catch(console.error);
});
Example: Python webhook handler (Flask)
Copy code
from flask import Flask, request, jsonify
import requests
import threading

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def webhook():
    # Verify webhook signature here (WebhookReceiver.verify)
    # Quick ACK
    data = request.get_json()
    threading.Thread(target=process_event, args=(data,)).start()
    return ('', 200)

def process_event(event):
    if event.get('type') != 'participant_joined':
        return
    p = event.get('participant', {})
    # Detect SIP participant (payload may include p.get('sip') or kind)
    if not p.get('identity') or not p.get('sip'):
        return
    sip_identity = p['identity']
    room = event.get('room')
    # Notify agent runtime
    requests.post(f'{AGENT_CONTROL_URL}/start-listen', json={'room': room, 'sipIdentity': sip_identity})
Agent control endpoint (example) • This is your backend API that the webhook calls. It tells the agent runtime to start listening to the SIP participant. Below is a minimal illustrative handler the agent runtime could expose. Node.js agent runtime snippet (receives room + sipIdentity)
Copy code
// POST /start-listen { room, sipIdentity }
app.post('/start-listen', async (req, res) => {
  const { room, sipIdentity } = req.body;
  // Start or update RoomIO for the running agent session in that room
  // Example call into agent runtime:
  await agentRuntime.attachRoomIO({
    participantIdentity: sipIdentity,
    audioInput: true,
    audioOutput: false,
    stt: { provider: 'deepgram', apiKey: process.env.DG_API_KEY },
  });
  res.json({ ok: true });
});
Python agent runtime snippet
Copy code
# Flask handler inside your agent process
@app.route('/start-listen', methods=['POST'])
def start_listen():
    payload = request.get_json()
    sip_identity = payload['sipIdentity']
    # Attach or update RoomIO in the agent runtime:
    room_io = RoomIO(participant_identity=sip_identity,
                     audio_input=True,
                     audio_output=False,
                     stt={'provider':'deepgram', 'api_key':DG_API_KEY})
    agent_runtime.attach_room_io(room_io)
    return jsonify(ok=True)
How RoomIO targets the participant • Setting
participant_identity
in RoomIO makes the agent subscribe to that participant’s audio only. • Keep `audioOutput`/`audio_output` disabled so the agent will be STT-only and publish text to
lk.transcription
. Detecting the participant identity reliably • Participant identity is in the participant_joined webhook payload; you can also inspect the room participant list via the server SDK if needed. • If your SIP dispatch can set a predictable identity or add metadata, use that to make matching simpler. Edge cases & best practices • If multiple SIP callers join, ensure your agent runtime can handle switching or multiple RoomIOs depending on the desired behavior (single-call focus vs. multi-call). • Deduplicate webhook events by event id to avoid re-starting the same RoomIO repeatedly. • If your agent process may not yet be in the room when the webhook fires, queue the instruction and attach RoomIO once the agent joins the room. • If you need server-side final transcripts, listen for
user_input_transcribed
events in the agent runtime or consume
lk.transcription
text stream on the frontend. If you’d like, I can now: • Provide a concrete example that reads the actual participant_joined webhook payload schema and extracts the SIP fields (I can tailor this if you share a sample webhook payload). • Show the exact RoomIO call in the SDK/language you use (if you tell me which agent SDK/package and language you’re running). • Show how to handle multiple simultaneous SIP participants (queueing, switching, or multi-listen patterns). Which would you prefer next — a payload-specific extraction example or a language-specific RoomIO call tailored to your agent SDK? Sources: Best practices for managing webhook event streams | LiveKit | server-sdk-ruby | livekit/livekit_composite | DeepWiki | server-sdk-js | livekit/livekit_composite | DeepWiki | SIP Lifecycle Management Agent | LiveKit Documentation | Agents telephony integration | LiveKit Documentation, with interactive elements
m
i want to use webhookhandlr to see if the new participant joins and then send that to agent and make sure that the agent strat listening to that new sip participant also along with the callers it was already listening to
t
Thanks — this is clear. You want your webhook handler to detect new SIP/PSTN participants and tell the agent to start listening to the new SIP participant while the agent continues to listen to any callers it already handles. Below is a concise, practical plan plus working examples you can adapt (webhook handler → notify agent control endpoint → agent attaches/updates RoomIO). I’ll also include notes on defaults so you know when you don’t need extra work. Quick summary • LiveKit sends a participant_joined webhook when a participant (including SIP callers) joins a room. • You should verify the webhook, ACK quickly, then process asynchronously. • Extract the participant identity from the webhook, check it’s a SIP participant, then notify your agent runtime (via internal API call or data message). • The agent can subscribe to multiple participants simultaneously. You can either: ◦ Let the agent’s default behavior process all SIP participants (no extra steps), or ◦ Explicitly attach/update RoomIO with the new participantIdentity so the agent is guaranteed to listen to that specific participant. • Keep agent audio output disabled to make the agent STT-only; transcriptions arrive on the lk.transcription text stream and via agent-side transcription events. Recommended flow (safe, reliable) 1. Webhook endpoint receives participant_joined. 2. Verify signature and immediately return 2xx. 3. Asynchronously: ◦ Check event type and that participant is SIP (inspect
participant.kind
or
participant.sip
fields). ◦ Extract
participant.identity
and
room
. ◦ POST to your agent control endpoint: { room, sipIdentity }. ◦ Deduplicate by webhook event
id
to avoid repeated processing. 1. Agent control endpoint attaches/updates RoomIO (or ensures agent will listen to matching participant kinds). Use audioInput=true and audioOutput=false for STT-only. 2. Agent publishes transcriptions to
lk.transcription
(frontend) and/or emits
user_input_transcribed
(server-side). Concise examples 1) Webhook handler (Node.js / Express — quick ACK, process async)
Copy code
app.post('/webhook', (req, res) => {
  // 1) Verify webhook signature (WebhookReceiver) -- omitted for brevity
  res.sendStatus(200); // ACK quickly

  // 2) Process asynchronously
  (async () => {
    const event = req.body;
    if (event.type !== 'participant_joined') return;

    const p = event.participant || {};
    // Detect SIP participant (payload may include p.sip or p.kind)
    const isSip = p.kind === 'sip' || !!p.sip;
    if (!isSip || !p.identity) return;

    const payload = { room: event.room, sipIdentity: p.identity, eventId: event.id };
    // POST to your agent control endpoint
    await fetch(`${AGENT_CONTROL_URL}/start-listen`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });
  })().catch(console.error);
});
2) Webhook handler (Python / Flask — quick ACK, background thread)
Copy code
from threading import Thread
from flask import Flask, request
import requests

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def webhook():
    # Verify signature here (WebhookReceiver) - omitted
    data = request.get_json()
    Thread(target=process_event, args=(data,)).start()
    return ('', 200)  # ACK quickly

def process_event(event):
    if event.get('type') != 'participant_joined': return
    p = event.get('participant', {})
    is_sip = p.get('kind') == 'sip' or bool(p.get('sip'))
    if not is_sip or not p.get('identity'): return
    payload = {'room': event.get('room'), 'sipIdentity': p['identity'], 'eventId': event.get('id')}
    requests.post(f'{AGENT_CONTROL_URL}/start-listen', json=payload)
3) Agent control endpoint (Node.js) — attach/update RoomIO for new sipIdentity
Copy code
// POST /start-listen { room, sipIdentity }
app.post('/start-listen', async (req, res) => {
  const { room, sipIdentity, eventId } = req.body;

  // Optional: dedupe by eventId to avoid reprocessing
  // Ensure agent runtime for `room` exists/connected
  await agentRuntime.attachRoomIO({
    participantIdentity: sipIdentity,
    audioInput: true,
    audioOutput: false,   // STT-only
    stt: { provider: 'deepgram', apiKey: process.env.DG_API_KEY },
  });

  res.json({ ok: true });
});
4) Agent control endpoint (Python) — same idea with RoomIO
Copy code
@app.route('/start-listen', methods=['POST'])
def start_listen():
    payload = request.get_json()
    sip_identity = payload['sipIdentity']
    # dedupe if needed using payload.get('eventId')
    room_io = RoomIO(
        participant_identity=sip_identity,
        audio_input=True,
        audio_output=False,
        stt={'provider': 'deepgram', 'api_key': DG_API_KEY},
    )
    agent_runtime.attach_room_io(room_io)
    return jsonify(ok=True)
Notes on multiple participants and defaults • Agents already can process audio from multiple participants that match the configured participant_kinds. If your agent is configured to include SIP participants, new SIP participants may be processed automatically without any RoomIO changes. • Explicitly attaching RoomIO with a
participantIdentity
guarantees the agent subscribes to that specific participant’s audio if you want strict control. • If you want the agent to continue listening to existing callers plus new ones, use attach/update behavior (do not replace existing RoomIOs) or rely on default multi-participant subscription. • Ensure you deduplicate events and handle the case where the agent isn’t yet connected to the room (queue the start-listen request or retry once agent joins). Where I can help next • I can produce a compact example that reads a real participant_joined payload you’ve received and extracts the SIP fields exactly. • I can tailor the agent control call to your agent SDK/language (if you tell me which SDK and runtime you’re using). • I can show patterns for handling multiple SIP callers concurrently (one RoomIO per caller vs letting the agent handle all matching participants). Which would you like next — sample payload extraction or a language-specific RoomIO implementation matching your agent SDK? Sources: SIP Lifecycle Management Agent | LiveKit Documentation | sip | livekit/livekit_composite | DeepWiki | Server-side | livekit/livekit_composite | DeepWiki | Best practices for managing webhook event streams | LiveKit | server-sdk-ruby | livekit/livekit_composite | DeepWiki, with interactive elements