A sample that adds attention labs SAA device-directed gating to a Pipecat voice agent running on Daily. SAA decides, per utterance, whether speech in the room was meant for the agent or not.
The web/ demo is a single uvicorn process that creates an ephemeral Daily room, summons the SAA agent, spawns an OpenAI Realtime voice agent into the same room (when OPENAI_API_KEY is set), and serves a vanilla HTML/JS frontend that renders SAA's prediction stream as a live overlay.
When the layout turns green, that means after the user's utterance the voice agent will respond.
- Your bot calls
start_attention_session(...)to start a SAA session for your Daily room. - SAA joins your Daily room, subscribes to the user's audio and video, and runs the classifier.
- It publishes events (
prediction,vad,turn_ready,interrupt,interjection) on the"saa"Daily app-message topic. - Your bot consumes them via
AttentionEngine, which hooks@transport.event_handler("on_app_message")on yourDailyTransport. - Upstream actions (
responding_start,set_threshold, ...) queue aDailyOutputTransportMessageUrgentFrameonto your boundPipelineTask.
The client (packages/saa-pipecat-client) is pure Python.
| Platform | Status | Notes |
|---|---|---|
| macOS (arm64 + x86_64) | supported | daily-python ships wheels for both architectures. |
| Linux (x86_64 + aarch64) | supported | daily-python wheels are built against manylinux_2_28. |
| Windows (native) | not supported | daily-python publishes no Windows wheels and no source distribution. Use WSL2 and follow the Linux instructions inside your WSL shell. |
pipecat-ai 1.x dropped Python 3.10 support, so 3.11+ is required. pip install on 3.10 fails with Package 'saa-pipecat-client' requires a different Python: 3.10.x not in '>=3.11'.
| Platform | Install Python 3.11 |
|---|---|
| macOS (Homebrew) | brew install python@3.11 |
| Debian / Ubuntu | sudo apt-get update && sudo apt-get install -y python3.11 python3.11-venv |
| Fedora / RHEL | sudo dnf install -y python3.11 |
| Arch | sudo pacman -S python311 (AUR; or use pyenv install 3.11) |
| WSL2 (Ubuntu) | Same as Debian / Ubuntu, inside the WSL shell, not PowerShell. |
| Any platform | pyenv install 3.11 is the framework-agnostic option. |
SAA_API_KEY= # attention labs API key (shared with the LiveKit samples)
DAILY_API_KEY= # Daily.co REST key from dashboard.daily.co
OPENAI_API_KEY= # optional: enables the talkback voice agent
git clone https://github.com/attenlabs/saa-sdk.git
cd saa-sdk/examples/pipecat/web
# Python 3.11+ is required (pipecat-ai 1.x dropped 3.10 support).
# macOS: brew install python@3.11
# Debian/Ubuntu/WSL2: sudo apt-get install python3.11 python3.11-venv
python3.11 -m venv .venv && source .venv/bin/activate
# install the in-tree client FIRST so the requirements.txt version spec
# resolves locally; saa-pipecat-client is not on PyPI yet
pip install -e ../../../packages/saa-pipecat-client
pip install -r requirements.txt
cp .env.example .env # fill in the keys: at minimum SAA_API_KEY + DAILY_API_KEY
python -m uvicorn token_server:app --port 8000
# open http://localhost:8000 and click StartThe voice-agent dependency in requirements.txt is pipecat-ai[daily,openai].
The demo runs in one of two modes depending on what's in .env:
| Mode | Requires | What you get |
|---|---|---|
| Talkback | SAA_API_KEY, DAILY_API_KEY, OPENAI_API_KEY |
OpenAI Realtime joins your room and responds via speech-to-speech, but only when SAA says you're addressing the device. |
| Overlay only | SAA_API_KEY, DAILY_API_KEY only |
Browser renders SAA predictions live (use it to tune vad_threshold or watch class-1 / class-2 transitions), but nothing talks back. |
The token_server logs which mode it's in on startup, and the UI's header shows it too once you click Start.
session = await start_attention_session(
api_key=SAA_API_KEY, room_url=ROOM_URL,
agent_token=attention_agent_token(daily_api_key=DAILY_API_KEY, room_name=room_name),
participant_identity=human_identity,
)
engine = AttentionEngine(transport, agent_identity=session.agent_identity)
engine.bind_task(task)
@engine.on_prediction
def _(p): addressee_gate.suppressed = (p.aligned_class == 1 and p.confidence > 0.7) # the gate
@engine.on_interrupt
async def _(ev): await task.queue_frames([InterruptionTaskFrame()]) # barge-in
@engine.on_interjection
async def _(ev): await task.queue_frames([LLMMessagesAppendFrame(messages=[...], run_llm=True)])Plus a BotSpeakingObserver FrameProcessor that watches TTSStartedFrame / TTSStoppedFrame and calls engine.responding_start() / responding_stop() so SAA knows when your agent is the one speaking. This is required for interrupt and interjection to fire correctly.
SAA events arrive as JSON on Daily's app-message channel under the "saa" topic. The integration surface in app.js is two functions, identical to the LiveKit demo:
renderPrediction(msg)readsmsg.aligned_class(0/1/2),msg.confidence,msg.num_faces.renderVAD(msg)readsmsg.is_speech.
The bot publishes envelopes like {topic:"saa", type:"prediction", ...}. The Daily app-message event wraps that as { data, fromId }, so app.js destructures data and filters on data.topic === "saa".
Daily has no byte-stream primitive, so the per-turn binary blob (PCM16 + JPEGs) is base64-encoded and split across multiple app messages. app.js keeps a small pending map keyed on stream_id:
- A
turn_ready(orinterjection) envelope arrives withtotal_chunks+byte_len; start an empty buffer. - Each subsequent
turn_chunkcarries a base64 slice +index; store it. - Once all chunks are gathered, concat them, call
parseTurnPayload(buf), and log the result.
The reassembly map is capped at 10 in-flight streams; the oldest is dropped on overflow.
voice_agent.py runs the Realtime LLM service (OpenAIRealtimeLLMService) directly in the Pipecat pipeline. When /session fires, token_server.py mints a Daily meeting token for the bot, hands it plus the SAA session's agent_identity to run_voice_agent(...), and spawns the result as an asyncio task. The agent joins the room a beat after the human does, wires its own AttentionEngine against the same SAA session the browser is listening to, and runs:
transport.input() -> AddresseeGate -> OpenAIRealtimeLLMService -> BotSpeakingObserver -> transport.output()
- AddresseeGate drops
InputAudioRawFrames when SAA saysaligned_class == 1and confidence is high; that audio never reaches OpenAI. - BotSpeakingObserver watches
TTSStartedFrame/TTSStoppedFrameand togglesengine.responding_start()/responding_stop()so SAA's interrupt detector arms only during playback. - Interrupt queues
InterruptionTaskFrameto cancel the in-flight Realtime turn on a confident barge-in. - Interjection queues
LLMMessagesAppendFrame(messages=[...], run_llm=True)so Realtime injects the system nudge and runs the model without waiting for further user audio.
Lifecycle: the agent shuts down on on_participant_left (when you click Stop in the browser). The SAA session is owned by token_server.py and stays alive until it is reaped on idle (~5 min).
- The Daily room must be reachable from the SAA cloud (Daily Cloud rooms are public by default).
- Both audio and video tracks should be available.
- One target participant per session. Multi-user rooms need one
start_attention_sessioncall each. DailyParams(audio_in_user_tracks=True)is required when your bot shares the room with the human, otherwise the bot's own TTS feeds back asInputAudioRawFrames.- Identity matching uses the nested
participant["info"]["userName"], not the top-leveluserName.
Try three send thresholds and keep the one that performs best: 0.6, 0.77, 0.88.
Raise it for fewer false triggers, lower it to catch borderline speech. Set it on the engine with set_threshold(v).
The same agent code runs on Pipecat Cloud, Modal, k8s, or your own VM, anywhere pipecat-ai[daily] runs. Put the Daily REST key (from dashboard.daily.co -> Developers, or the Pipecat Cloud Settings -> Daily (WebRTC) tab) in DAILY_API_KEY.
token_server.py is dev-only: open CORS, creates a billed Daily room on every /session hit, starts a billed SAA session, and in talkback mode burns OpenAI Realtime audio-seconds per turn. For production you need auth on /session, rate limiting, a real room/identity policy, and your customers should mint the bot token using their Daily API key, not yours. The SAA API key, the OpenAI key, and the Daily API key must always stay server-side; the browser only ever receives the Daily room URL, its own user meeting token, and the SAA agent identity.