late-glass-71191
12/26/2025, 7:44 PMtall-airline-16756
12/26/2025, 7:44 PMtall-airline-16756
12/26/2025, 7:44 PMlate-glass-71191
12/26/2025, 7:56 PMtall-airline-16756
12/26/2025, 7:56 PMpdf-parse or pdf-lib) into text, index them into a vector DB (Pinecone, Weaviate, etc.), and include retrieved content in the agent’s prompt context when generating replies.
• On-demand ingestion via frontend byte streams: accept file uploads, send bytes to your backend, parse and store them, then run retrieval at each user turn.
• Use lifecycle hooks (for example on_user_turn_completed) to query your knowledge store and inject relevant passages into the LLM context before the agent responds.
• Combine with RAG (retrieval-augmented generation) so the agent returns grounded answers and can cite or summarize PDF content.
I can:
• Share a concise Node.js example for parsing a PDF and storing embeddings, or
• Outline how to implement RAG using a specific vector DB and LLM provider.
Which would you like me to provide next?
Sources: Pipeline nodes and hooks | LiveKit docs | Vision | LiveKit docslate-glass-71191
12/26/2025, 8:53 PMParticipant joined: ${participant.identity});
let currentCallId: string | null = null;
const callerNumber = participant.identity || 'unknown';
try {
const call = await prisma.call.create({
data: {
agent_id: agentId,
callerNumber,
status: 'INITIATED',
startTime: new Date(),
endTime: new Date(),
duration: 0,
},
});
currentCallId = call.id;
console.log(Call record created: ${currentCallId});
} catch (error) {
console.error('Failed to create call record:', error);
}
const transcriptBuffer: Array<{
role: 'user' | 'agent';
content: string;
timestamp: string;
}> = [];
const usageCollector = new metrics.UsageCollector();
const agent = new voice.Agent({
instructions: '{{INSTRUCTIONS}}',
tools: {
getCurrentTime,
},
});
// Preloaded VAD
const vad = ctx.proc.userData.vad as silero.VAD;
const infomaniakSTT = new InfomaniakSTT({
apiToken: process.env.INFOMANIAK_API_TOKEN!,
productId: process.env.INFOMANIAK_PRODUCT_ID!,
pollingInterval: 500,
maxPollingAttempts: 60,
});
// This is required for non streaming STT like Infomaniak or OpenAI Whisper
const streamAdapter = new stt.StreamAdapter(infomaniakSTT, vad);
const session = new voice.AgentSession({
stt: streamAdapter,
llm: new inference.LLM({
model: '{{LLM_MODEL}}',
}),
tts: new inference.TTS({
model: '{{TTS_MODEL}}',
voice: '{{TTS_VOICE}}',
}),
vad: vad,
});
session.on(voice.AgentSessionEventTypes.MetricsCollected, async(ev) => {
metrics.logMetrics(ev.metrics);
usageCollector.collect(ev.metrics);
const metricsData = {
type: ev.metrics.type,
timestamp: new Date().toISOString(),
data: ev.metrics,
};
await saveMetricsToDatabase(agentId, metricsData);
});
session.on(
voice.AgentSessionEventTypes.UserInputTranscribed,
(ev) => {
if (ev.isFinal) {
transcriptBuffer.push({ role: 'user', content: ev.transcript, timestamp: new Date().toISOString() });
}
}
);
const logUsage = async () => {
const summary = usageCollector.getSummary();
console.log(Usage: ${JSON.stringify(summary)});
};
ctx.addShutdownCallback(logUsage);
await session.start({
agent,
room: ctx.room,
});
console.log('AgentSession started');
if (currentCallId) {
try {
await prisma.call.update({
where: { id: currentCallId },
data: { status: 'IN_PROGRESS' },
});
console.log('Call status updated to IN_PROGRESS');
} catch (error) {
console.error('Failed to update call status:', error);
}
}
await session.generateReply({
instructions: '{{GREETING_INSTRUCTIONS}}',
});
session.on(
voice.AgentSessionEventTypes.Close,
async () => {
if (!currentCallId) {
console.warn('No call ID available, skipping save');
return;
}
try {
const messages = (session.chatCtx?.items ?? session.history ?? []) as any[];
const agentReplies = messages
.filter(item => item.role === 'assistant' && item.type === 'message')
.map(item => ({
role: 'agent',
content: Array.isArray(item.content) ? item.content.join(' ') : String(item.content),
timestamp: (item.created_at ?? new Date().toISOString()) as string,
}));
const allTranscripts = [...transcriptBuffer, ...agentReplies].sort(
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
);
if (allTranscripts.length === 0) {
console.log('No transcripts to save');
return;
}
const endTime = new Date();
const call = await prisma.call.findUnique({
where: { id: currentCallId },
});
const duration = call
? (endTime.getTime() - call.startTime.getTime()) / 1000
: 0;
await prisma.call.update({
where: { id: currentCallId },
data: {
status: 'COMPLETED',
endTime,
duration,
},
});
await prisma.callSummary.create({
data: {
call_id: currentCallId,
extractedData: {
transcripts: allTranscripts,
totalTurns: allTranscripts.length,
userTurns: transcriptBuffer.length,
agentTurns: agentReplies.length,
sessionMetadata: {
agentId,
agentName,
roomName: ctx.room.name,
participantIdentity: participant.identity,
duration,
},
} as any,
},
});
console.log('Session data saved successfully');
} catch (error) {
console.error('Failed to save session data on Close:', error);
}
}
);
},
});
based on my code show me sample code in node js to integrate pdf informationtall-airline-16756
12/26/2025, 8:54 PMnpm install pdf-parse @pinecone-database/pinecone node-fetch cosine-similarity
# or replace Pinecone with your vector DB client and cosine-similarity with your chosen utils
2) Example code (integrate into your agent file)
import fs from 'fs';
import pdf from 'pdf-parse';
import fetch from 'node-fetch';
import { PineconeClient } from '@pinecone-database/pinecone';
import cosine from 'cosine-similarity';
// --- Helpers: embedding (replace with your provider) ---
async function embedTexts(texts: string[]): Promise<number[][]> {
// Example using an external embedding API; replace with your provider call
const resp = await fetch(process.env.EMBEDDING_API_URL!, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.EMBEDDING_API_KEY}` },
body: JSON.stringify({ inputs: texts }),
});
const data = await resp.json();
return data.embeddings; // expect [[...], [...]]
}
// --- Helpers: simple local similarity search against Pinecone or in-memory ---
async function upsertToPinecone(indexName: string, vectors: { id: string; values: number[]; metadata: any }[]) {
const pc = new PineconeClient();
await pc.init({ apiKey: process.env.PINECONE_API_KEY!, environment: process.env.PINECONE_ENV });
const index = pc.Index(indexName);
await index.upsert({ upsertRequest: { vectors } });
}
async function queryPinecone(indexName: string, queryVector: number[], topK = 4) {
const pc = new PineconeClient();
await pc.init({ apiKey: process.env.PINECONE_API_KEY!, environment: process.env.PINECONE_ENV });
const index = pc.Index(indexName);
const q = await index.query({ queryRequest: { topK, vector: queryVector, includeMetadata: true } });
return q.matches ?? [];
}
// --- PDF parsing & chunking ---
async function parsePdfBuffer(buffer: Buffer) {
const data = await pdf(buffer);
// simple split by paragraph (tune as needed)
const paragraphs = data.text.split(/\n{2,}/).map(p => p.trim()).filter(Boolean);
return paragraphs;
}
function chunkText(text: string, maxLen = 800) {
const words = text.split(/\s+/);
const chunks: string[] = [];
let cur = [];
let curLen = 0;
for (const w of words) {
cur.push(w);
curLen += w.length + 1;
if (curLen > maxLen) {
chunks.push(cur.join(' '));
cur = [];
curLen = 0;
}
}
if (cur.length) chunks.push(cur.join(' '));
return chunks;
}
// --- Prewarm: load PDFs, embed, and index ---
export default defineAgent({
prewarm: async (proc: JobProcess) => {
console.log('Prewarming agent and indexing PDFs...');
const indexName = process.env.PINECONE_INDEX || 'agent-knowledge';
// Example: load all PDFs from a folder
const pdfFiles = fs.readdirSync('./pdfs').filter(f => f.endsWith('.pdf'));
const allTexts: string[] = [];
const metadataList: any[] = [];
for (const file of pdfFiles) {
const buffer = fs.readFileSync(`./pdfs/${file}`);
const paragraphs = await parsePdfBuffer(buffer);
for (const para of paragraphs) {
const chunks = chunkText(para, 800);
for (const chunk of chunks) {
allTexts.push(chunk);
metadataList.push({ source: file });
}
}
}
if (allTexts.length === 0) {
console.log('No PDF content found for indexing');
proc.userData.knowledgeIndexed = false;
return;
}
// get embeddings in batches
const batchSize = 16;
const vectors: { id: string; values: number[]; metadata: any }[] = [];
for (let i = 0; i < allTexts.length; i += batchSize) {
const batch = allTexts.slice(i, i + batchSize);
const embeddings = await embedTexts(batch);
for (let j = 0; j < embeddings.length; j++) {
vectors.push({
id: `pdf_${i + j}`,
values: embeddings[j],
metadata: { text: batch[j], ...metadataList[i + j] },
});
}
}
await upsertToPinecone(indexName, vectors);
proc.userData.knowledgeIndexed = true;
proc.userData.knowledgeIndexName = indexName;
console.log(`Indexed ${vectors.length} chunks into ${indexName}`);
},
entry: async (ctx: JobContext) => {
await ctx.connect();
// ... your existing flow ...
const vad = ctx.proc.userData.vad as silero.VAD;
// create session similar to your code
const session = new voice.AgentSession({ /* ... */ });
// Helper: retrieve relevant docs for a query
async function retrieveRelevant(query: string, topK = 4) {
if (!ctx.proc.userData.knowledgeIndexed) return [];
const qVec = (await embedTexts([query]))[0];
const matches = await queryPinecone(ctx.proc.userData.knowledgeIndexName, qVec, topK);
return (matches || []).map(m => ({ score: m.score, text: m.metadata.text, source: m.metadata.source }));
}
// Example: on each user final transcription, run retrieval and inject into LLM prompt
session.on(voice.AgentSessionEventTypes.UserInputTranscribed, async (ev) => {
if (!ev.isFinal) return;
const userText = ev.transcript;
// retrieve
const docs = await retrieveRelevant(userText, 3);
if (docs.length === 0) return;
// Build a short context block
const contextBlock = docs.map((d, i) => `Source ${i+1} (${d.source}): ${d.text}`).join('\n\n');
// Inject context before next reply
await session.generateReply({
instructions: `Use the following documents as reference when answering. If the answer isn't in the documents, say so.\n\n${contextBlock}\n\nUser question: ${userText}`,
});
});
// start session
await session.start({ agent: new voice.Agent({ instructions: '{{INSTRUCTIONS}}' }), room: ctx.room });
// ... rest of your flow ...
},
});
Notes and adjustments
• Embeddings/LLM: Replace embedTexts and LLM calls with your provider (OpenAI, Anthropic, etc.). Ensure embedding dimensions match your vector DB.
• Vector DB: I used Pinecone as an example; swap for Weaviate, Milvus, or an in-memory store for small datasets.
• Security: Sanitize PDFs and limit indexing to allowed files.
• Latency: Pre-index at prewarm to avoid runtime delays. For uploaded PDFs at runtime, parse, embed, and upsert then use retrieval on subsequent turns.
• Prompt design: Keep retrieved passages concise; use attribution lines (source) to help the agent cite correctly.
If you want, I can:
• Provide a sample for handling runtime user PDF uploads (frontend byte stream → backend parse → upsert → immediate retrieval), or
• Swap the Pinecone example for Weaviate or an in-memory cosine search snippet. Which do you prefer?late-glass-71191
12/27/2025, 3:27 PMtall-airline-16756
12/27/2025, 3:28 PM# python_dialer.py (very small Flask example)
from flask import Flask, request, jsonify
from livekit_sip_client import create_sip_participant # pseudocode, use LiveKit Python SIP API
app = Flask(__name__)
@app.route('/dial', methods=['POST'])
def dial():
body = request.json
to_number = body['to']
room = body['room']
# create SIP participant via Python SDK (example shape)
sip_participant = create_sip_participant(room=room, to=to_number, some_meta=body.get('meta', {}))
# respond with IDs and room info
return jsonify({
'callId': sip_participant.call_id,
'sipParticipantId': sip_participant.id,
'room': room
})
2) Node.js initiator call (request Python dialer)
// callInitiator.js
import fetch from 'node-fetch';
export async function requestDial(toNumber, roomName, metadata = {}) {
const resp = await fetch(process.env.PY_DIALER_URL + '/dial', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ to: toNumber, room: roomName, meta: metadata }),
});
const data = await resp.json();
return data; // { callId, sipParticipantId, room }
}
3) Node.js agent: wait for call and start session (integrate into your existing entry)
// inside your entry: after ctx.connect()
const dialResult = await requestDial('+123456789', ctx.room.name);
console.log('Dial result', dialResult);
// Wait for participant to join (SIP participant should join the room)
const participant = await ctx.waitForParticipant({ identity: dialResult.sipParticipantId });
// then start session as you already do
await session.start({ agent, room: ctx.room });
4) Notification/webhook pattern (optional)
• Python can POST to Node.js /webhook/call-updated with { callId, state: 'ANSWERED' } to avoid polling.
• Node.js webhook handler updates DB and triggers agent actions (start/stop).
Practical tips and pitfalls
• Synchronize identities: have Python set participant identity or metadata so Node.js can waitForParticipant by identity.
• Room lifecycle: decide whether Python creates the room or Node.js; both must use the same room name and LiveKit API keys scoped appropriately.
• Security: authenticate calls between services (API keys, mutual TLS, signed JWT). Validate webhooks.
• Race conditions: If Node.js starts waiting before SIP participant arrives, use waitForParticipant with timeout and handle retries.
• Media bridging: ensure Python SIP creation is configured to bridge audio into the same LiveKit room so Node.js hears the call.
• Call teardown: ensure Python notifies Node.js on call end so Node.js can close AgentSession and persist final transcripts (your Close handler already handles saving).
Next steps I can provide
• A runnable Python example using the exact LiveKit Python SIP API snippet for create_sip_participant.
• A webhook handler example in your Node.js agent to receive call state updates.
• Example for secure service-to-service auth (JWT signing example).
Which of those would help you next?late-glass-71191
12/27/2025, 4:12 PMtall-airline-16756
12/27/2025, 4:12 PMcreate_sip_participant. It shows initialization, the API call, and minimal error handling. Set the required environment variables before running.
Save as create_sip_call.py:
import asyncio
import os
from livekit import api, models
async def create_sip_call():
lkapi = api.LiveKitAPI(
url=os.getenv("LIVEKIT_URL"),
api_key=os.getenv("LIVEKIT_API_KEY"),
api_secret=os.getenv("LIVEKIT_API_SECRET")
)
try:
req = models.CreateSIPParticipantRequest(
room_name="my-room", # room to bridge the SIP call into
sip_trunk_id=os.getenv("SIP_OUTBOUND_TRUNK_ID"),# your configured outbound trunk ID
sip_call_to="+1234567890", # destination phone number (E.164)
participant_identity="sip-caller-123", # identity used for the SIP participant in the room
wait_until_answered=True # wait until the call is answered before returning
)
sip_participant = await lkapi.sip.create_sip_participant(req)
print("SIP participant created:")
print(sip_participant)
except Exception as e:
print("Error creating SIP participant:", e)
finally:
await lkapi.aclose()
if __name__ == "__main__":
asyncio.run(create_sip_call())
Environment variables to set:
• LIVEKIT_URL — your LiveKit URL (e.g., https://your-livekit.example)
• LIVEKIT_API_KEY — API key with appropriate permissions
• LIVEKIT_API_SECRET — API secret
• SIP_OUTBOUND_TRUNK_ID — the outbound SIP trunk ID configured for your LiveKit project
Notes:
• sip_trunk_id must reference a valid outbound trunk in your project.
• Phone numbers must be E.164 formatted (e.g., +1234567890).
• wait_until_answered=True causes the call to wait until answered; set to False if you prefer async dialing.
• Ensure your SIP trunk/provider allows outbound calls and any required SIP features (e.g., REFER) are enabled.
If you want, I can:
• Provide a Flask/FastAPI wrapper around this snippet so your Node.js service can POST to it, or
• Show how to return structured JSON and send a webhook to your Node.js agent when the call state changes. Which would you like? Sources: Agents telephony integration | LiveKit docs | sip | livekit/livekit_composite | DeepWikilate-glass-71191
12/27/2025, 4:20 PMtall-airline-16756
12/27/2025, 4:20 PMimport os
import asyncio
from flask import Flask, request, jsonify
from livekit import api, models
app = Flask(__name__)
async def create_sip_part(room, to, identity, sip_trunk_id, wait):
lkapi = api.LiveKitAPI(
url=os.getenv("LIVEKIT_URL"),
api_key=os.getenv("LIVEKIT_API_KEY"),
api_secret=os.getenv("LIVEKIT_API_SECRET"),
)
try:
req = models.CreateSIPParticipantRequest(
room_name=room,
sip_trunk_id=sip_trunk_id,
sip_call_to=to,
participant_identity=identity,
wait_until_answered=wait,
)
resp = await lkapi.sip.create_sip_participant(req)
return resp
finally:
await lkapi.aclose()
@app.route("/dial", methods=["POST"])
def dial():
body = request.get_json() or {}
to = body.get("to")
room = body.get("room", "default-room")
identity = body.get("identity", f"sip-{int(os.times()[4])}")
wait = bool(body.get("wait", True))
sip_trunk_id = os.getenv("SIP_OUTBOUND_TRUNK_ID")
if not to:
return jsonify({"error": "missing 'to' phone number"}), 400
if not sip_trunk_id:
return jsonify({"error": "SIP_OUTBOUND_TRUNK_ID not configured"}), 500
try:
resp = asyncio.run(create_sip_part(room, to, identity, sip_trunk_id, wait))
# Convert response to JSON-serializable form if needed
return jsonify({"success": True, "participant": resp}), 200
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
if __name__ == "__main__":
app.run(host="0.0.0.0", port=int(os.getenv("PORT", 8080)))
FastAPI example (save as dialer_fastapi.py):
import os
import asyncio
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from livekit import api, models
import uvicorn
app = FastAPI()
class DialRequest(BaseModel):
to: str
room: str = "default-room"
identity: str | None = None
wait: bool = True
async def create_sip_part(room, to, identity, sip_trunk_id, wait):
lkapi = api.LiveKitAPI(
url=os.getenv("LIVEKIT_URL"),
api_key=os.getenv("LIVEKIT_API_KEY"),
api_secret=os.getenv("LIVEKIT_API_SECRET"),
)
try:
req = models.CreateSIPParticipantRequest(
room_name=room,
sip_trunk_id=sip_trunk_id,
sip_call_to=to,
participant_identity=identity,
wait_until_answered=wait,
)
resp = await lkapi.sip.create_sip_participant(req)
return resp
finally:
await lkapi.aclose()
@app.post("/dial")
async def dial(req: DialRequest):
sip_trunk_id = os.getenv("SIP_OUTBOUND_TRUNK_ID")
if not sip_trunk_id:
raise HTTPException(status_code=500, detail="SIP_OUTBOUND_TRUNK_ID not configured")
identity = req.identity or f"sip-{int(os.times()[4])}"
try:
resp = await create_sip_part(req.room, <http://req.to|req.to>, identity, sip_trunk_id, req.wait)
return {"success": True, "participant": resp}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
if __name__ == "__main__":
uvicorn.run("dialer_fastapi:app", host="0.0.0.0", port=int(os.getenv("PORT", 8080)), reload=False)
Environment variables required:
• LIVEKIT_URL
• LIVEKIT_API_KEY
• LIVEKIT_API_SECRET
• SIP_OUTBOUND_TRUNK_ID
Node.js caller example:
// callDialer.js
import fetch from 'node-fetch';
export async function requestDial(to, room, identity, wait = true) {
const resp = await fetch(process.env.PY_DIALER_URL + '/dial', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ to, room, identity, wait }),
});
return resp.json();
}
Notes and recommendations:
• Authenticate requests between Node.js and Python (API key, JWT) in production.
• Ensure room name/participant identity is agreed so Node.js can waitForParticipant in the LiveKit room.
• Use webhooks or POST back from Python to Node.js for call state updates (answered/ended) to avoid polling.
• For large-scale use, run the Python dialer as an async service (FastAPI + Uvicorn) for concurrency.
Would you like the FastAPI example extended to send a webhook to your Node.js service when the call is answered/ended?late-glass-71191
12/27/2025, 7:34 PMtall-airline-16756
12/27/2025, 7:35 PMsip_trunk_id used when creating SIP participants so LiveKit knows which trunk to use for dialing.
• Caller ID / From number (optional): The originating phone number presented to recipients if the provider supports overriding the From header.
• Region/geographic options (optional): Which region or egress point to use so calls originate from a desired locale or comply with local rules.
• Call behavior features (optional): Per-trunk capabilities or defaults such as codecs, DTMF behavior, or audio-processing flags (e.g., noise-cancel settings applied to calls using that trunk).
Practical notes:
• When placing an outbound call you pass the trunk’s sip_trunk_id to the create_sip_participant call so LiveKit routes the call through that provider.
• Ensure the trunk is configured with a provider that allows outbound dialing and the required SIP features (caller ID override, REFER, etc.).
• You can list configured outbound trunks with the CLI (lk sip outbound list) to find the correct sip_trunk_id.
If you want, I can show an example curl/CLI command or the exact Python snippet that references a sip_trunk_id. Which would you prefer?
Sources: sip | livekit/livekit_composite | DeepWiki | Outbound Calling Script | LiveKit docs | SIP Lifecycle Management Agent | LiveKit docs | Agents telephony integration | LiveKit docslate-glass-71191
12/27/2025, 7:36 PMtall-airline-16756
12/27/2025, 7:36 PMlate-glass-71191
12/27/2025, 8:15 PMtall-airline-16756
12/27/2025, 8:15 PMlate-glass-71191
12/27/2025, 9:48 PM<http://router.post|router.post>('/calls/initiate', async (req, res) => {
try {
const {
agentId,
trunkId,
fromNumber,
toNumber,
roomName,
displayName,
dtmf,
playDialtone,
waitUntilAnswered,
krispEnabled,
participantMetadata,
} = req.body;
// Validation
if (!agentId || !trunkId || !fromNumber || !toNumber) {
return res.status(400).json({
success: false,
message: 'Missing required fields: agentId, trunkId, fromNumber, toNumber',
});
}
// Validate phone number format (E.164)
const phoneRegex = /^\+\d{10,15}$/;
if (!phoneRegex.test(toNumber)) {
return res.status(400).json({
success: false,
message: 'Invalid phone number format. Use E.164 format (e.g., <tel:+12135550100|+12135550100>)',
});
}
// Validate DTMF if provided
if (dtmf && !/^[0-9*#wW]+$/.test(dtmf)) {
return res.status(400).json({
success: false,
message: 'Invalid DTMF format. Use digits 0-9, *, #, and w for delays',
});
}
const result = await outboundCallService.initiateCall(
agentId,
trunkId,
fromNumber,
toNumber,
{
roomName,
displayName,
dtmf,
playDialtone: playDialtone ?? false,
waitUntilAnswered: waitUntilAnswered ?? true,
krispEnabled: krispEnabled ?? true,
participantMetadata,
}
);
res.json({
success: true,
message: result.message,
data: result,
});
} catch (error: any) {
console.error('Failed to initiate call:', error);
// Check for SIP-specific errors
if (error.message.includes('SIP Error')) {
return res.status(400).json({
success: false,
message: 'Call failed',
error: error.message,
});
}
res.status(500).json({
success: false,
message: 'Failed to initiate call',
error: error.message,
});
}
});
what does teh above api call dotall-airline-16756
12/27/2025, 9:48 PMoutboundCallService.initiateCall(...) to perform the actual dialing/bridging work.
• Returns success JSON with the service result, or an error JSON if initiation fails.
Input it expects (from req.body)
• Required: agentId, trunkId, fromNumber, toNumber
• Optional: roomName, displayName, dtmf, playDialtone, waitUntilAnswered, krispEnabled, participantMetadata
Validation performed
• Ensures required fields are present; returns 400 if missing.
• Validates toNumber is E.164 (+ followed by 10–15 digits); returns 400 if invalid.
• Validates dtmf contains only digits, *, #, or w (for delays); returns 400 if invalid.
Call flow
1. After validation, it calls outboundCallService.initiateCall(agentId, trunkId, fromNumber, toNumber, options) where options contains the optional parameters (with defaults for playDialtone, waitUntilAnswered, krispEnabled).
2. outboundCallService is responsible for creating the SIP participant / dialing via your telephony/trunk integration (not shown in this snippet).
3. On success, the endpoint responds with { success: true, message, data } where data is the service result (call IDs, participant info, etc.).
Error handling
• Catches exceptions and logs them.
• If the error message includes "SIP Error", returns 400 with the SIP-specific error.
• Otherwise returns 500 with a generic failure message and the error text.
Practical notes
• trunkId selects which outbound trunk/provider to use (routing, credentials, transport).
• waitUntilAnswered: true typically blocks until the call is answered; set false for async dialing.
• Ensure outboundCallService.initiateCall implements trunk usage, room/identity mapping, and webhook/notification callbacks so your Node.js agent can detect participant join/leave.
If you want, I can:
• Show a sample implementation of outboundCallService.initiateCall (Node.js calling the Python dialer or directly calling LiveKit if available), or
• Suggest request/response shapes to return from the service for easier integration. Which would you prefer?late-glass-71191
12/27/2025, 9:51 PMtall-airline-16756
12/27/2025, 9:51 PMcreate_sip_participant. That API tells LiveKit to create a SIP participant and instruct the configured outbound trunk to place the outbound call to the specified sip_call_to number.
Key details:
• The outbound dialing is triggered by the create_sip_participant request.
• If you set wait_until_answered=true, the API call will wait (block) until the remote party answers (or a timeout/failure). If wait_until_answered=false, the API will return immediately while dialing continues asynchronously.
• Trunk-level settings (transport, credentials, caller ID, etc.) determine how the SIP INVITE is sent to the provider; the API simply requests the trunk to place the call.
• You should monitor participant/room events (participant joined, answered, disconnected) or use webhooks from your dialing service to detect when the call is answered, in-progress, or ended.
• Handle errors from the API (SIP failure, trunk misconfiguration) — these can surface synchronously if wait_until_answered is true, or via events/webhooks if false.
If you want, I can show:
• A short Python example of create_sip_participant with wait_until_answered both true and false, or
• How to wire a webhook / event listener so your Node.js agent is notified when the SIP participant joins/answers. Which would you like?late-glass-71191
12/27/2025, 9:53 PMtall-airline-16756
12/27/2025, 9:53 PMsession.generateReply() or your session’s say() method to produce the greeting/response.
Typical flow
1. Create SIP participant (e.g., create_sip_participant(...)). This starts dialing.
2. SIP provider answers → LiveKit bridges the SIP leg into the room.
3. LiveKit emits participant connected / joined event.
4. Your agent code listens for that event (or awaited participant) and then calls session.generateReply() (or session.say(...)) to speak.
5. Continue normal STT → LLM → TTS cycle while participant remains in room.
Notes and tips
• wait_until_answered=true makes the create call block until the call is answered, but it does not make the agent speak automatically — you still need to start the reply after detection.
• Use participant identity or metadata (set when creating the SIP participant) so your agent can reliably match the joined participant.
• Optionally detect human vs. voicemail: inspect early audio (VAD) or wait for a brief human-like response before sending the greeting.
• If you prefer async flows, have the dialing service send a webhook to your Node.js agent when the call is answered so the agent can start the session/greeting without polling.
• Ensure your agent subscribes to audio or auto-subscribes appropriately so it can hear the callee and send TTS.
Want a short code snippet showing the event handler that waits for the SIP participant and triggers generateReply()?
Sources: Agents telephony integration | LiveKit docs | SIP Lifecycle Management Agent | LiveKit docslate-glass-71191
12/27/2025, 10:14 PMtall-airline-16756
12/27/2025, 10:14 PMcall-{uuid} or outbound-{timestamp}-{uuid}).
• Ensure your dispatch rule or dialing flow returns that room name to both the dialer and the agent so both attach to the same room.
• Include participant identity/metadata (trunkId, callId) so the agent can match the SIP participant when it joins.
• Use webhooks or a small control API to notify the Node.js agent when the outbound call is created/answered so it can waitForParticipant or listen for participant_connected.
• Clean up rooms and participants on call end to avoid stale resources.
If you want, I can provide:
• A snippet showing how to generate and pass a unique room name between your Python dialer and Node.js agent, or
• A sample dispatch rule pattern (room_prefix + UUID) and webhook flow. Which would you like? Sources: Agents telephony integration | LiveKit docs | sip | livekit/livekit_composite | DeepWiki | Outbound Calling Script | LiveKit docs | Avoiding Room ID Reuse When Re-Creating Rooms With the Same Namelate-glass-71191
12/27/2025, 10:15 PMtall-airline-16756
12/27/2025, 10:15 PMcreateSipParticipant. You must explicitly connect the agent to that same room and then detect the SIP participant joining.
What you should do
• Create/pass a unique room name when dialing (you already use callRoomName).
• Have your agent call ctx.connect() / create a LiveKit client and join that same callRoomName.
• After joining, wait for the SIP participant (by identity or via waitForParticipant() / participant_connected event).
• When the SIP participant is observed, trigger the agent to speak (e.g., session.generateReply() or session.say()).
Example sequence (conceptual)
1. Your service creates roomName = call-<uuid> and calls createSipParticipant(trunkId, toNumber, roomName, ...).
2. Python SIP call dials and, when answered, LiveKit bridges the SIP leg into roomName.
3. Node.js agent connects: await ctx.connect({ room: roomName }) or uses matching join logic.
4. Agent waits: const p = await ctx.waitForParticipant({ identity: sipIdentity }) or listens for participant_connected.
5. On detection, agent runs await session.generateReply({ instructions: 'Greet caller...' }).
Tips
• Ensure the SIP participant identity or metadata is predictable so the agent can match it.
• If you use wait_until_answered=true, the API call may block until answered, but the agent still needs to join and listen.
• Consider notifying the agent (webhook or message) when the createSipParticipant call succeeds/when answered to avoid race conditions.
If you want, I can provide a short Node.js snippet showing how to join the room and wait for the SIP participant. Would you like that?late-glass-71191
12/28/2025, 8:14 PMtall-airline-16756
12/28/2025, 8:14 PMlate-glass-71191
12/29/2025, 9:10 PMtall-airline-16756
12/29/2025, 9:10 PMcreate_sip_participant you provide the destination (sip_call_to) and optionally a from/originating number. That’s when the number matters.
• You can add or associate phone numbers (DIDs) with a trunk later, or set caller ID per-call if the trunk/provider supports it.
Practical notes:
• If you need a fixed "from" number for regulatory or presentation reasons, configure that on the trunk or pass it when dialing.
• Some providers require provisioning a DID before you can place outbound calls showing that number — check your SIP provider/trunk settings.
• Transport/auth/cert details are set at trunk creation; caller IDs and per-call options are set at call time.
If you want, I can show the exact trunk-creation request shape and a follow-up example that places a call using that trunk. Which would you prefer?