Help me stub out an agent written in node that wil...
# ask-ai
i
Help me stub out an agent written in node that will transcribe a call and call an endpoint with the notes at the end to store the transcription.
✅ 1
t
Thanks for reaching out, Andres. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I’ll help you stub out a Node.js agent that transcribes a LiveKit call and POSTs the final transcript to an endpoint. Below I’ll: • Acknowledge what you asked for. • Provide a compact, runnable-stub that: ◦ Connects to a LiveKit session/agent pipeline, ◦ Listens for transcription events and collects final transcripts, ◦ Sends the assembled notes to your storage endpoint when the call ends (or when you choose to flush). • Point out small customizations you’ll likely need. By “transcribe a call” I’ll use the transcription event flow where
user_input_transcribed
events contain
transcript.transcript
and
transcript.is_final
to capture finished text. Node.js stub (outline): • Install prerequisites: ensure you have the LiveKit agent SDK for Node.js (your project dependency), and
node-fetch
or
axios
for HTTP. • Replace placeholders:
LIVEKIT_AGENT_URL
,
ROOM
,
API_KEY
,
STORAGE_ENDPOINT
. Example code:
Copy code
// Required packages:
// npm install livekit-agent node-fetch
// or replace node-fetch with axios if you prefer

const fetch = require('node-fetch'); // or: import fetch from 'node-fetch'
const { AgentSession } = require('livekit-agent'); // replace with correct import from your SDK

// Configuration - replace these with your values
const LIVEKIT_AGENT_URL = process.env.LIVEKIT_AGENT_URL || 'wss://your-livekit-agent-endpoint';
const API_KEY = process.env.LIVEKIT_API_KEY || 'your-api-key';
const ROOM = process.env.LIVEKIT_ROOM || 'room-name';
const STORAGE_ENDPOINT = process.env.STORAGE_ENDPOINT || 'https://example.com/store-transcript';

// In-memory buffer to collect final transcript segments for the call
let transcriptBuffer = [];

// Simple function to POST notes to your storage endpoint
async function postNotes(notePayload) {
  try {
    const res = await fetch(STORAGE_ENDPOINT, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(notePayload),
    });
    if (!res.ok) {
      const text = await res.text();
      console.error('Failed to save transcript:', res.status, text);
      // Consider retry logic here
    } else {
      console.log('Transcript saved successfully');
    }
  } catch (err) {
    console.error('Error posting transcript:', err);
  }
}

// Create and start the agent session
async function startAgent() {
  const session = new AgentSession({
    // Provide the correct options required by your LiveKit agent SDK,
    // e.g., url, apiKey, room, agent configuration, etc.
    url: LIVEKIT_AGENT_URL,
    apiKey: API_KEY,
    room: ROOM,
    // other options as required by your setup
  });

  // Connect / start session
  await session.connect();

  // Listen for transcription events. Event name may vary by SDK version:
  session.on('user_input_transcribed', (event) => {
    // Event shape: event.transcript.transcript, event.transcript.is_final
    const t = event.transcript;
    if (!t) return;

    // Only collect final transcripts
    if (t.is_final) {
      console.log('Final transcript segment:', t.transcript);
      transcriptBuffer.push({
        text: t.transcript,
        speaker: event.participant?.identity || event.participant?.sid || 'unknown',
        timestamp: t.timestamp || Date.now(),
      });
    } else {
      // You can log or show interim results if you like
      console.log('Interim transcript:', t.transcript);
    }
  });

  // Optional: Listen for session end/room disconnect to flush notes
  session.on('room_disconnected', async (info) => {
    console.log('Room disconnected, flushing transcript notes...');
    await flushTranscriptNotes();
  });

  // Example: expose a manual flush (e.g., after call ends or on-demand)
  async function flushTranscriptNotes() {
    if (transcriptBuffer.length === 0) {
      console.log('No transcript to send');
      return;
    }
    const combined = transcriptBuffer.map(s => s.text).join('\n');
    const payload = {
      room: ROOM,
      generated_at: new Date().toISOString(),
      notes: combined,
      segments: transcriptBuffer,
    };
    await postNotes(payload);
    // Clear buffer after successful send (or keep for retries if failed)
    transcriptBuffer = [];
  }

  // Keep the session alive; the session lifetime depends on your app design
  console.log('Agent started and listening for transcription events');
  return { session, flushTranscriptNotes };
}

// Run the agent
startAgent()
  .then(({ session, flushTranscriptNotes }) => {
    // Example: after 10 minutes flush (or wire to your app lifecycle)
    // setTimeout(() => flushTranscriptNotes(), 10 * 60 * 1000);
  })
  .catch(err => {
    console.error('Failed to start agent:', err);
  });
Notes / customization suggestions • Replace
AgentSession
/ event names with the exact classes and events from your LiveKit Node agent SDK. The transcription event is named
user_input_transcribed
and the transcript object contains
transcript.transcript
and
transcript.is_final
. • Add retry logic for
postNotes()
(exponential backoff) if your storage endpoint can be temporarily unavailable. • If you need speaker diarization, include participant identity from the event if present (I included
event.participant.identity
as an example). • If you want to call the endpoint only once at the end, call
flushTranscriptNotes()
on room disconnect or when the call is complete. • For large transcripts consider streaming segments to your storage endpoint as they become final (reduce memory use). • Add authentication headers to
postNotes()
if your endpoint requires a token. If you want, I can: • Convert this stub to use
axios
or ES modules (import syntax). • Add exponential backoff retry code for the POST. • Show an example that streams each final segment immediately rather than buffering. • Replace
AgentSession
and event names with the exact imports from your LiveKit agent SDK if you tell me which package/version you’re using. Which of those would you like next, and can you share the exact LiveKit agent package name/version you’re using? Sources: Transcriber | LiveKit Documentation | Build Your First AI Voice Agent in Python: Complete Tutorial | LiveKit | Pipeline nodes and hooks | LiveKit Documentation, with interactive elements
i
actually I'm gonna do it in Python instead can we set that up
t
Thanks — I’ll help you set up a Python LiveKit agent that transcribes a call and POSTs the final notes to your endpoint. I’ll respond to your request to: create a Python agent that listens for
user_input_transcribed
events, collects final transcript segments, and sends them (once the call ends or on demand) to a storage endpoint. Below is a compact, runnable stub you can adapt. 1) Prereqs • Python 3.8+ • Install required packages (example uses requests and dotenv; replace imports with your environment’s packages if needed): ◦ pip install requests python-dotenv • Make sure your LiveKit Agent Python package is installed and available (the stub uses typical names from LiveKit agent examples — adjust imports to match your installed package). 2) Example agent stub
Copy code
# transcriber_agent.py
import os
import datetime
import requests
from dotenv import load_dotenv

# Adjust these imports to match your LiveKit agent package
from livekit.agents import AgentServer, JobContext, AgentSession, Agent, cli, inference

load_dotenv()

# Configuration - set these in your .env or export as env vars
TRANSCRIPTION_ENDPOINT = os.getenv("TRANSCRIPTION_ENDPOINT", "https://example.com/store-transcript")
AGENT_NAME = os.getenv("AGENT_NAME", "transcriber-agent")

# In-memory buffer for final transcript segments
transcript_buffer = []

def post_notes(payload):
    """POST JSON payload to your storage endpoint. Add auth headers if needed."""
    try:
        resp = requests.post(TRANSCRIPTION_ENDPOINT, json=payload, timeout=10)
        resp.raise_for_status()
        print("Notes posted successfully")
        return True
    except Exception as e:
        print("Failed to post notes:", e)
        return False

server = AgentServer()

@server.rtc_session(agent_name=AGENT_NAME)
async def entrypoint(ctx: JobContext):
    # Create an STT-only session (no LLM or TTS required)
    session = AgentSession(
        stt=inference.STT(model="deepgram/nova-3-general"),  # change model/provider as needed
    )

    @session.on("user_input_transcribed")
    def handle_transcript(event):
        # event.transcript should contain .transcript and .is_final
        t = getattr(event, "transcript", None)
        if not t:
            return

        if getattr(t, "is_final", False):
            text = getattr(t, "transcript", "") or ""
            ts = getattr(t, "timestamp", None) or datetime.datetime.utcnow().isoformat()
            participant = None
            if getattr(event, "participant", None):
                participant = getattr(event.participant, "identity", None) or getattr(event.participant, "sid", None)

            segment = {
                "text": text,
                "timestamp": ts,
                "participant": participant or "unknown"
            }
            transcript_buffer.append(segment)
            print("Final segment captured:", text)
        else:
            # optional: handle interim results if you want
            interim_text = getattr(t, "transcript", "")
            if interim_text:
                print("Interim:", interim_text)

    # Start the session and connect to the room
    await session.start(
        agent=Agent(instructions="You are a transcription agent that records user speech."),
        room=ctx.room
    )
    await ctx.connect()
    print("Agent session started and connected")

    # Optionally flush on room disconnect - event name may vary; adapt if needed
    @ctx.on("room_disconnected")
    async def on_room_disconnected(info):
        print("Room disconnected — flushing transcript notes")
        await flush_transcript_notes()

async def flush_transcript_notes():
    """Combine segments and post once (or stream segments if preferred)."""
    global transcript_buffer
    if not transcript_buffer:
        print("No transcript to send")
        return

    combined = "\n".join([s["text"] for s in transcript_buffer])
    payload = {
        "generated_at": datetime.datetime.utcnow().isoformat(),
        "notes": combined,
        "segments": transcript_buffer,
    }

    success = post_notes(payload)
    if success:
        transcript_buffer = []  # clear after successful send
    else:
        print("Keep buffer for retry")

if __name__ == "__main__":
    # Run the agent server CLI (adjust to how your package launches agents)
    cli.run_app(server)
3) Customization suggestions and production notes • Replace
inference.STT(model="...")
with the STT provider and model you prefer or have credentials for. • The exact class names and event shapes (
AgentServer
,
JobContext
,
AgentSession
,
session.on("user_input_transcribed")
) are consistent with LiveKit agent examples — if your package uses different names, let me know which package/version and I’ll adapt. • Authentication: add headers or tokens in
post_notes()
if your endpoint requires auth. • Retry/backoff: implement retries with exponential backoff for
post_notes()
if transient failures are possible. • Memory: for long calls, consider streaming each final segment to your endpoint instead of buffering everything in memory. • Speaker diarization: the example tries to capture
event.participant.identity
— modify if your event shape differs. • Flushing policy: I show flushing on room disconnect; you can also flush on explicit signal (e.g., an API call or a participant leaving). Next steps I can take for you • Convert this to use async HTTP (aiohttp) if your agent is async and you prefer non-blocking calls. • Add retry/backoff logic to
post_notes()
. • Stream each final segment immediately instead of buffering everything. • Update the stub to match the exact LiveKit Python package and version you have installed — tell me the package name/version and I’ll adapt the imports and event handlers precisely. Which of these would you like next, and can you confirm the LiveKit Python package/version you’re using or paste the import names you have available? Sources: Transcriber | LiveKit Documentation | Pi Zero Transcriber | LiveKit Documentation | TTS Translator with Gladia STT | LiveKit Documentation | Agent session | LiveKit Documentation, with interactive elements
i
My project says I am almost at 80% of the free inference credits
Build: STT deepgram - nova_3_multi incl. Inference credits ($0.552 per hour)
Can I change to OPENAI and have it use my personal account to avoid running out?
t
Thanks — that makes sense. You can switch your agent’s STT from Deepgram to OpenAI and have transcription usage billed to your personal OpenAI account so it won’t consume LiveKit inference credits. Quick summary of what to do: • Use the OpenAI STT plugin for LiveKit and configure your agent to use
openai.STT(...)
instead of the Deepgram model. • Set your personal OpenAI API key in the environment (e.g.,
OPENAI_API_KEY
) so requests go through your OpenAI account. • Be aware that costs will then follow OpenAI’s pricing for the chosen model instead of LiveKit Inference credits. Example changes (Python) • Replace the Deepgram STT in your
AgentSession
with the OpenAI STT plugin. For example:
Copy code
from livekit.plugins import openai

session = AgentSession(
    stt=openai.STT(model="gpt-4o-transcribe"),  # choose the OpenAI model you prefer
    # other components...
)
• Ensure your OpenAI key is set in your environment or .env: ◦ export OPENAI_API_KEY="sk-..." ◦ or add to your .env file Operational notes and recommendations • Authentication: Put your OpenAI API key in
OPENAI_API_KEY
. The plugin will use that key so billing goes to your OpenAI account. • Model choice: Pick the OpenAI transcription model you prefer (e.g.,
gpt-4o-transcribe
); pricing varies by model and usage. • Testing: After switching, test with a short call to confirm transcripts arrive and your endpoint receives expected data. • Monitoring: Keep an eye on your OpenAI usage/billing dashboard since transcription can consume tokens/credits quickly for long calls. • Fallbacks: If you want to conserve your OpenAI usage, consider: ◦ lowering sample rate or chunking, ◦ sending only final segments, ◦ or switching back to Deepgram for lower-cost tiers if needed. If you’d like, I can: • Update your Python agent stub to use
openai.STT(...)
and show where to read
OPENAI_API_KEY
. • Add a safe toggle so you can run either Deepgram or OpenAI via an environment flag (e.g.,
STT_PROVIDER=deepgram|openai
). • Show an async version using
aiohttp
for non-blocking POSTs. Which would you prefer, and do you want me to produce the updated Python stub now? Sources: OpenAI STT plugin guide | LiveKit Documentation | OpenAI and LiveKit | LiveKit Documentation, with interactive elements
i
what package do I need to install to have that openai plugin?
t
Yes — to use the OpenAI plugin with LiveKit Agents in Python, install the extras-enabled package: • With pip:
Copy code
pip install "livekit-agents[openai]~=1.4"
• Or with uv (if you use uv tooling):
Copy code
uv add "livekit-agents[openai]~=1.4"
After installing: • Set your OpenAI API key in the environment (e.g.,
OPENAI_API_KEY="sk-..."
or in your .env). • Import the plugin in your agent code:
Copy code
from livekit.plugins import openai
  session = AgentSession(stt=openai.STT(model="gpt-4o-transcribe"))
If you want, I can update your Python agent stub to use
openai.STT(...)
and show where to read
OPENAI_API_KEY
, or add a provider toggle so you can switch between Deepgram and OpenAI with an env var. Which would you prefer? Sources: OpenAI TTS plugin guide | LiveKit Documentation | OpenAI LLM models | LiveKit Documentation | Models overview | LiveKit Documentation, with interactive elements
i
It will work like this right?
Copy code
async def entrypoint(ctx: JobContext):
    ctx.log_context_fields = {"room": ctx.room.name}

    stt_engine = openai.STT(model="gpt-4o-transcribe")
    segments: list[dict] = []
    started_at = datetime.now(timezone.utc)
    tasks: dict[str, asyncio.Task] = {}
t
Yes — that snippet is a good start. It shows the right idea: set context logging, create an OpenAI STT engine, prepare a segments buffer, capture the start time, and track async tasks. A few quick confirmations and suggestions to make it work end-to-end: • Imports and env ◦ Ensure you import required names: e.g.,
datetime
,
timezone
,
asyncio
, and the LiveKit types (
JobContext
,
AgentSession
,
Agent
,
cli
,
inference
/
openai
plugin) at top of the file. ◦ Make sure
OPENAI_API_KEY
is set in env or .env before running. • STT creation ◦
stt_engine = openai.STT(model="gpt-4o-transcribe")
is correct for selecting OpenAI transcribe. That will route billing to your OpenAI key. • Session wiring ◦ Use the
stt_engine
when creating the session, e.g.:
Copy code
session = AgentSession(stt=stt_engine)
• Start the session and connect to the room:
Copy code
await session.start(agent=Agent(instructions="..."), room=ctx.room)
    await ctx.connect()
• Handling transcription events ◦ Listen for
user_input_transcribed
and collect only final segments:
Copy code
@session.on("user_input_transcribed")
    def on_transcript(event):
        t = getattr(event, "transcript", None)
        if not t: return
        if getattr(t, "is_final", False):
            segments.append({
                "text": t.transcript,
                "timestamp": getattr(t, "timestamp", None) or datetime.now(timezone.utc).isoformat(),
                "participant": getattr(event.participant, "identity", "unknown")
            })
• If you want to send each final segment immediately, start an async task to POST it and keep track in
tasks
for retries/cleanup. • Flushing / lifecycle ◦ On room end (e.g.,
room_disconnected
) or when you want to finalize notes, combine
segments
into one
notes
string and POST to your storage endpoint. ◦ Clear
segments
after a successful POST or implement retry/backoff on failure. • Async HTTP ◦ If your agent is async, prefer non-blocking HTTP (e.g.,
aiohttp
) to avoid blocking the event loop when posting notes. Your
tasks
dict is a good place to track those POST tasks. If you like, I can: • Produce a full, runnable example that plugs your snippet into a complete entrypoint with imports, session creation, event handlers, and an async POST using
aiohttp
. • Add provider-toggle code so you can switch between Deepgram and OpenAI via an env var. Which would you prefer? , with interactive elements