Cosmo Realtime SDK
Examples

Meeting bot

A bot that joins a meeting as a participant, streams live transcripts, and writes a follow-up summary. Requires running outside the browser — Python or a Node server process.

The Meeting Bot joins a Zoom or Google Meet call as an audio participant, collects a live transcript from the session's event stream, and produces a structured post-meeting summary when the call ends. Because it runs headless without a browser UI, it is best implemented as a Python background service or a Node server process.

Architecture

Meeting call (Zoom / Meet)
    ↓ audio feed via meeting SDK or virtual audio device
Python / Node service
    ↓ cosmo-ai-sdk session
Cosmo agent
    ↓ transcript / model-text events
Transcript buffer + summary (your app)
    ↓ at meeting end
Follow-up note (your storage)

The bot does not use the browser SDK. It uses either cosmo-ai-sdk (Python) or a Node.js environment with cosmo-ai (non-browser). Audio is piped in from the call recording layer — the details of how audio is captured from the meeting depend on which meeting platform you use and is outside the scope of this guide.

Two artifacts come out of the session: the transcript, which your app buffers from final transcript events, and the summary, which you request from the agent over the text channel when the meeting ends, and capture from model-text events. Both are yours to persist wherever follow-up notes live.

Joining a meeting as a bot typically requires a meeting platform API key (Zoom SDK, Google Meet REST API, etc.) or a virtual audio device. Cosmo does not provide a meeting ingestion layer — cosmo-ai-sdk handles the Cosmo side of the session once audio is routed to it.

Python skeleton

"""
meeting_bot.py — minimal skeleton for a Cosmo meeting transcription bot.

Prerequisites:
  pip install cosmo-ai-sdk

Audio piping: your meeting SDK captures the call audio and feeds it into an
rtc.AudioSource. The exact method depends on the meeting platform.
"""

from __future__ import annotations

import asyncio
import os

import structlog
from livekit import rtc

from cosmo_ai import (
    CosmoRealtime,
    RealtimeModelText,
    RealtimeReady,
    RealtimeSessionEnded,
    RealtimeTranscriptDelta,
)

logger = structlog.get_logger(__name__)

NOTETAKER_INSTRUCTIONS = """You are a silent meeting notetaker. Do not speak
unless addressed directly. When asked for a summary, write a structured note
with key decisions and action items."""

TRANSCRIPT_BUFFER: list[dict[str, str]] = []
SUMMARY_PARTS: list[str] = []


async def run_meeting_bot(api_key: str) -> None:
    async with CosmoRealtime(api_key=api_key) as client:
        agent = client.agent(instructions=NOTETAKER_INSTRUCTIONS)
        async with agent.start(store_recording=True) as session:
            logger.info("bot.connected", room=session.response.room_name)

            # Publish the meeting audio as the bot's voice input. livekit-rtc
            # has no OS capture — your meeting integration keeps this source
            # fed via source.capture_frame(...).
            source = rtc.AudioSource(sample_rate=48000, num_channels=1)
            await session.publish_audio_source(source)

            async def consume_events() -> None:
                async for event in session:
                    if isinstance(event, RealtimeReady):
                        logger.info("bot.ready", session_id=event.session_id)
                    elif isinstance(event, RealtimeTranscriptDelta):
                        if event.is_final:
                            entry = {"role": event.role.value, "text": event.text}
                            TRANSCRIPT_BUFFER.append(entry)
                            logger.info("transcript", **entry)
                    elif isinstance(event, RealtimeModelText):
                        if event.is_final:
                            SUMMARY_PARTS.append(event.text)
                    elif isinstance(event, RealtimeSessionEnded):
                        logger.info("bot.session_ended", reason=event.reason)

            consumer = asyncio.create_task(consume_events())

            # Wait for the meeting to end. In production, this would be
            # signaled by your meeting platform webhook or a shutdown event.
            meeting_ended = asyncio.Event()
            await meeting_ended.wait()

            # Ask the agent for the summary over the text channel.
            await session.send_text(
                "The meeting has ended. Write a structured summary with "
                "key decisions and action items.",
            )

            # Give the agent time to finish writing.
            await asyncio.sleep(10)
            await session.end()
            await consumer

    persist_follow_up_note(TRANSCRIPT_BUFFER, "".join(SUMMARY_PARTS))


if __name__ == "__main__":
    asyncio.run(run_meeting_bot(api_key=os.environ["COSMO_API_KEY"]))

TypeScript (Node server)

import { RealtimeClient } from 'cosmo-ai';

const NOTETAKER_INSTRUCTIONS = `You are a silent meeting notetaker. …`;

const transcript: Array<{ role: string; text: string }> = [];
const summaryParts: string[] = [];

const client = new RealtimeClient({
  baseUrl: 'https://app.askcosmo.ai',
  apiKey: process.env.COSMO_API_KEY,
});

const agent = client.agent({ instructions: NOTETAKER_INSTRUCTIONS });
const session = await agent.start({ storeRecording: true });

session.on('ready', (event) => {
  console.log('session ready', event.sessionId);
});

session.on('transcript', (event) => {
  if (event.isFinal) {
    transcript.push({ role: event.role, text: event.text });
    console.log(`[${event.role}]`, event.text);
  }
});

session.on('model_text', (event) => {
  if (event.isFinal) summaryParts.push(event.text);
});

// …meeting runs; audio is piped in via your platform integration…

// When the meeting ends:
await session.sendText(
  'The meeting has ended. Write a structured summary with key decisions and action items.',
);
// Allow time for the summary to finish streaming.
await new Promise((resolve) => setTimeout(resolve, 10_000));
await session.end();

persistFollowUpNote(transcript, summaryParts.join(''));

Resuming after a crash

If the bot process restarts mid-meeting, resume the prior session so the agent keeps the conversation context it already heard:

async with agent.start(resume_session_id=prior_session_id) as session:
    ...

In TypeScript:

const session = await agent.start({ resumeSessionId: priorSessionId });

store_recording=True also keeps the server-side recording artifacts (audio, transcript, tool events) for the run — see Recording and privacy.

Tools

The notetaker needs no tools by default — transcription arrives as events, and the summary comes over the text channel. Opt into web search if attendees expect the bot to look things up when addressed:

from cosmo_ai import WebSearchTool

agent = client.agent(
    instructions=NOTETAKER_INSTRUCTIONS,
    tools=[WebSearchTool()],
)

Audio piping (platform-specific)

How you route audio from the meeting call to the LiveKit room depends on your platform:

  • Zoom: Zoom's Meeting SDK provides a raw PCM audio stream that can be piped to the rtc.AudioSource you pass to publish_audio_source.
  • Google Meet: Use the Google Meet REST API or a virtual audio device via PulseAudio / BlackHole.
  • Custom VOIP: Any source that produces a PCM stream at 48 kHz mono can feed the rtc.AudioSource.

The specifics are outside the scope of this guide — the Cosmo SDK is agnostic to how audio arrives at the LiveKit room.

Next steps

On this page