Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
72f93f0
feat(examples): publish examples manifest to agents-jukebox
theomonnom May 16, 2026
7e90357
refactor(examples): consolidate metadata into a single playground.yaml
theomonnom May 16, 2026
20ffede
feat(examples): assign deterministic agent_name per deployed example
theomonnom May 16, 2026
9a84f12
refactor(examples): drop livekit.toml merge from manifest build
theomonnom May 16, 2026
c05d0fd
refactor(examples): centralize agent ids in playground.yaml
theomonnom May 16, 2026
4a8d12f
refactor(examples): switch playground.yaml to map shape
theomonnom May 16, 2026
b2ef6a6
refactor(examples): drop redundant agent_name field
theomonnom May 16, 2026
2bda307
fix(deploy-examples): drop [skip ci] from manifest commit
theomonnom May 17, 2026
bf56d06
fix(deploy-examples): publish manifest to next/public not next/data
theomonnom May 17, 2026
987d04b
examples: shorten descriptions to one-line taglines
theomonnom May 17, 2026
7a127b9
examples-manifest: include per-example file tree + contents
theomonnom May 17, 2026
8912bab
examples-manifest: drop separate readme blob
theomonnom May 17, 2026
80f87fb
examples-manifest: inline GitHub permalink references in READMEs
theomonnom May 17, 2026
893d525
examples-manifest: drop the attribution line under inlined snippets
theomonnom May 17, 2026
943ed22
examples-manifest: textwrap.dedent inlined snippets
theomonnom May 17, 2026
34d04d0
examples-manifest: inline references in prose + repo URL config
theomonnom May 17, 2026
356cb78
examples-manifest: per-example github URL instead of shared template
theomonnom May 17, 2026
7b25c2c
examples-manifest: pack file/line range into the inlined fence info
theomonnom May 17, 2026
f77b579
examples: brand-palette accents, 8x8 icons, trimmed tags
theomonnom May 17, 2026
5f72a5f
examples: rework 8x8 pixel-art icons
theomonnom May 17, 2026
7612e8d
examples: refine 8x8 icons (heart, speech bubble, bell, burger)
theomonnom May 17, 2026
fde1bf0
examples: longer per-agent descriptions (full sentence)
theomonnom May 17, 2026
d6469a6
examples-manifest: ship metadata only, no inlined source
theomonnom May 17, 2026
14ed4b2
examples/inference: live model-swap voice demo
theomonnom May 18, 2026
db0d2a9
examples: sync playground.yaml to 12x12 dual-color icons + add infere…
theomonnom May 18, 2026
d73e128
examples: explicit `rpc` field + agent-pushed cart view (fire-and-for…
theomonnom May 18, 2026
9b15c38
examples: priced cart for drive-thru + appointment view for frontdesk
theomonnom May 18, 2026
9a7d5ec
examples: fix ruff (Awaitable/Callable from collections.abc, isort)
theomonnom May 18, 2026
cf1e23a
frontdesk: isolate playground UI into ui_view.py
theomonnom May 18, 2026
eb07bf2
frontdesk/ui_view: restore FA glyph codepoints (calendar / circle-check)
theomonnom May 18, 2026
f64d630
examples: write FA glyphs as \u escapes so they're readable in source
theomonnom May 18, 2026
f648444
examples/views: rely on yaml `views[].title` for the card heading
May 18, 2026
c188b2c
drive-thru: coalesce + serialize cart pushes to the playground view
May 18, 2026
7422544
frontdesk: card shows the search range, not every slot
May 18, 2026
1a0194f
inference example: use update_options for LLM swap + announce changes
May 18, 2026
fd70687
inference example: let the LLM voice the model-swap acknowledgement
May 18, 2026
b12800c
inference example: drop redundant comments
May 18, 2026
69e849a
inference example: register RPC methods after session.start
May 18, 2026
abc293a
Merge remote-tracking branch 'origin/main' into examples-manifest-pub…
May 18, 2026
d7f4a85
inference example: wire up to the deploy-examples CI
May 18, 2026
cc4103d
examples: ruff format
May 18, 2026
e019bf5
examples: share one byte-identical Dockerfile across every example
May 18, 2026
4dd4731
examples: rename entry scripts to agent.py so the Dockerfile is shared
May 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 87 additions & 1 deletion .github/workflows/deploy-examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
strategy:
fail-fast: false
matrix:
example: [healthcare, survey, frontdesk, drive-thru]
example: [healthcare, survey, frontdesk, drive-thru, inference]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
Expand All @@ -47,12 +47,36 @@ jobs:
--api-secret "$LIVEKIT_API_SECRET" \
--default

- name: Regenerate livekit.toml from playground.yaml
run: |
python3 -m pip install --quiet pyyaml
python3 <<'PY'
import yaml
from pathlib import Path
data = yaml.safe_load(Path("examples/playground.yaml").read_text())
subdomain = data["project"]["subdomain"]
slug = "${{ matrix.example }}"
entry = data["examples"][slug]
toml = (
"[project]\n"
f' subdomain = "{subdomain}"\n'
"\n"
"[agent]\n"
f' id = "{entry["agent_id"]}"\n'
)
Path(f"examples/{slug}/livekit.toml").write_text(toml)
print(f"wrote examples/{slug}/livekit.toml")
PY

- name: Build secrets file
working-directory: examples/${{ matrix.example }}
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
: > .env.deploy
# Register the agent under a deterministic name so the playground
# can dispatch to it explicitly. Matches the slug in playground.yaml.
echo "LIVEKIT_AGENT_NAME=${{ matrix.example }}" >> .env.deploy
case "${{ matrix.example }}" in
healthcare)
keys="OPENAI_API_KEY"
Expand All @@ -76,3 +100,65 @@ jobs:
args+=(--secrets-file .env.deploy)
fi
lk agent deploy "${args[@]}" .

publish-manifest:
name: Publish examples manifest to agents-jukebox
runs-on: ubuntu-latest
needs: deploy
if: ${{ always() && !cancelled() }}
steps:
- name: Checkout agents
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ inputs.ref || 'main' }}
path: agents

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- run: python -m pip install --quiet pyyaml

- name: Build manifest
run: |
python <<'PY' > examples-manifest.json
# The playground links out to GitHub for source / README; we only
# need the yaml metadata in the manifest now.
import json, yaml
from pathlib import Path
root = Path("agents/examples")
m = yaml.safe_load((root / "playground.yaml").read_text())
print(json.dumps(m, indent=2, ensure_ascii=False))
PY

- name: Checkout agents-jukebox
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: livekit/agents-jukebox
ssh-key: ${{ secrets.JUKEBOX_DEPLOY_KEY }}
path: jukebox

- name: Commit and push if changed
working-directory: jukebox
run: |
mkdir -p next/public
cp ../examples-manifest.json next/public/examples-manifest.json
git config user.name "livekit-examples-bot"
git config user.email "examples-bot@livekit.io"
git add next/public/examples-manifest.json
if git diff --cached --quiet; then
echo "Manifest unchanged, nothing to commit."
exit 0
fi
git commit -m "chore: refresh examples manifest"
for i in 1 2; do
if git push origin HEAD; then
echo "Pushed manifest update."
exit 0
fi
echo "Push failed (attempt $i), pulling and retrying..."
git pull --rebase origin HEAD
done
Comment on lines +155 to +162
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot May 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Push retry loop wastes last rebase and logs misleading "retrying" message

The for i in 1 2 loop performs a git pull --rebase after every failed push, including the final iteration. On the last iteration (i=2), if the push fails, the script rebases and prints "Push failed (attempt 2), pulling and retrying..." but the loop then exits and no subsequent push attempt is made. This means: (1) the rebase after the second failed push is wasted work, (2) the log message promises a retry that never happens, and (3) you get only 2 push attempts when the structure suggests 2 retries after the initial attempt (i.e., 3 total). The fix is either to change the loop to for i in 1 2 3 (giving 3 push attempts with 2 rebases between them) or to move the rebase/message into a conditional that skips on the last iteration.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

echo "Failed to push manifest after retries." >&2
exit 1
39 changes: 6 additions & 33 deletions examples/drive-thru/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
# This is an example Dockerfile that builds a minimal container for running LK Agents
# For more information on the build process, see https://docs.livekit.io/agents/ops/deployment/builds/
# syntax=docker/dockerfile:1

# Use the official Python base image with Python 3.13
# We use the slim variant to keep the image size smaller while still having essential tools
#
# Shared Dockerfile for every example under examples/. Byte-identical
# across the tree — each example's entry script is named `agent.py`,
# so there's no per-example variation left.
ARG PYTHON_VERSION=3.13
FROM python:${PYTHON_VERSION}-slim AS base

# Keeps Python from buffering stdout and stderr to avoid situations where
# the application crashes without emitting any logs due to buffering.
ENV PYTHONUNBUFFERED=1

# Disable pip version check to speed up builds
ENV PIP_DISABLE_PIP_VERSION_CHECK=1

# Create a non-privileged user that the app will run under.
# See https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#user
ARG UID=10001
RUN adduser \
--disabled-password \
Expand All @@ -25,42 +18,22 @@ RUN adduser \
--uid "${UID}" \
appuser

# Install build dependencies required for Python packages with native extensions
# gcc: C compiler needed for building Python packages with C extensions
# g++: C++ compiler needed for building Python packages with C++ extensions
# python3-dev: Python development headers needed for compilation
# We clean up the apt cache after installation to keep the image size down
RUN apt-get update && apt-get install -y \
gcc \
g++ \
python3-dev \
&& rm -rf /var/lib/apt/lists/*

# Create a new directory for our application code
# And set it as the working directory
WORKDIR /app

# Switch to the non-privileged user for all subsequent operations
# This improves security by not running as root
USER appuser

# Copy just the dependency files first, for more efficient layer caching
COPY requirements.txt ./

# Install Python dependencies using pip
# --no-cache-dir ensures we don't use the system cache
RUN pip install --user --no-cache-dir -r requirements.txt

# Pre-download any ML models or files the agent needs
# This ensures the container is ready to run immediately without downloading
# dependencies at runtime, which improves startup time and reliability
# Pre-download model weights plugins ship (silero VAD, turn-detector, …)
# so the container is ready to take traffic without a cold-download stall.
RUN python -m livekit.agents download-files

# Copy all remaining pplication files into the container
# This includes source code, configuration files, and dependency specifications
# (Excludes files specified in .dockerignore)
COPY . .

# Run the application
# The "start" command tells the worker to connect to LiveKit and begin waiting for jobs.
CMD ["python", "agent.py", "start"]
105 changes: 105 additions & 0 deletions examples/drive-thru/agent.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
import json
import logging
import os
Expand Down Expand Up @@ -379,6 +380,62 @@ async def list_order_items(self, ctx: RunContext[Userdata]) -> str:
return "\n".join(item.model_dump_json() for item in items)


def _find(items: list[MenuItem], id: str, size=None) -> MenuItem | None:
found = find_items_by_id(items, id, size)
return found[0] if found else None


def format_cart(userdata: Userdata) -> str:
"""Render the current order as markdown for the playground card.

Returns an empty string when the cart is empty, which signals the
UI to hide the card. The card itself already shows "Current order"
in its title bar, so the body skips a heading and goes straight to
the line items.
"""
if not userdata.order.items:
return ""
lines: list[str] = []
total = 0.0
for item in userdata.order.items.values():
if isinstance(item, OrderedCombo):
meal = _find(userdata.combo_items, item.meal_id)
drink = _find(userdata.drink_items, item.drink_id, item.drink_size)
extras = [f"fries {item.fries_size}"]
if drink:
extras.append(f"{drink.name} ({item.drink_size})")
if item.sauce_id:
sauce = _find(userdata.sauce_items, item.sauce_id)
if sauce:
extras.append(sauce.name)
name = meal.name if meal else item.meal_id
price = meal.price if meal else 0.0
elif isinstance(item, OrderedHappy):
meal = _find(userdata.happy_items, item.meal_id)
drink = _find(userdata.drink_items, item.drink_id, item.drink_size)
extras = []
if drink:
extras.append(f"{drink.name} ({item.drink_size})")
if item.sauce_id:
sauce = _find(userdata.sauce_items, item.sauce_id)
if sauce:
extras.append(sauce.name)
name = meal.name if meal else item.meal_id
price = meal.price if meal else 0.0
else:
assert isinstance(item, OrderedRegular)
reg = _find(userdata.regular_items, item.item_id, item.size)
name = reg.name if reg else item.item_id
price = reg.price if reg else 0.0
extras = [f"size {item.size}"] if item.size else []
total += price
extras_str = f" · {', '.join(extras)}" if extras else ""
lines.append(f"- **{name}**{extras_str} · [[${price:.2f}]]")
lines.append("")
lines.append(f"**Total · [[${total:.2f}]]**")
return "\n".join(lines)


async def new_userdata() -> Userdata:
fake_db = FakeDB()
drink_items = await fake_db.list_drinks()
Expand Down Expand Up @@ -442,6 +499,54 @@ async def drive_thru_agent(ctx: JobContext) -> None:
),
)

# Push the cart as markdown to the playground's cart view
# whenever it changes. Coalesced + serialized: rapid changes
# (e.g. batch-remove that pops items one at a time) collapse
# into a single trailing push of the *latest* cart state, so
# an empty-cart payload can't get reordered behind a stale
# mid-state push. Fire-and-forget at the call site — the
# function tool that mutated the order shouldn't block on the
# RPC round-trip.
push_pending = False
push_running = False

async def _push_to(identity: str, payload: str) -> None:
try:
await ctx.room.local_participant.perform_rpc(
destination_identity=identity,
method="set_cart_content",
payload=payload,
)
except Exception:
logger.exception("cart push to %s failed", identity)

async def _push_runner() -> None:
nonlocal push_pending, push_running
push_running = True
try:
while push_pending:
push_pending = False
payload = format_cart(userdata)
logger.info("push_cart: %d chars", len(payload))
peers = list(ctx.room.remote_participants.values())
if not peers:
continue
await asyncio.gather(
*(_push_to(p.identity, payload) for p in peers),
return_exceptions=True,
)
finally:
push_running = False

async def push_cart() -> None:
nonlocal push_pending
push_pending = True
if push_running:
return
asyncio.create_task(_push_runner())

userdata.order.on_change = push_cart

await session.start(agent=DriveThruAgent(userdata=userdata), room=ctx.room)
await background_audio.start(room=ctx.room, agent_session=session)

Expand Down
5 changes: 0 additions & 5 deletions examples/drive-thru/livekit.toml

This file was deleted.

23 changes: 21 additions & 2 deletions examples/drive-thru/order.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
from __future__ import annotations

import logging
import secrets
import string
from dataclasses import dataclass
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
from typing import Annotated, Literal

from pydantic import BaseModel, Field

logger = logging.getLogger("drive-thru.order")


def order_uid() -> str:
alphabet = string.ascii_uppercase + string.digits # b36
Expand Down Expand Up @@ -45,12 +49,27 @@ class OrderedRegular(BaseModel):
@dataclass
class OrderState:
items: dict[str, OrderedItem]
# Optional async hook fired after every add/remove. The agent
# wires this up to push the current cart to the playground UI;
# exceptions inside the hook never block the order mutation.
on_change: Callable[[], Awaitable[None]] | None = field(default=None)

async def _fire(self) -> None:
if self.on_change is None:
return
try:
await self.on_change()
except Exception:
logger.exception("OrderState.on_change failed")

async def add(self, item: OrderedItem) -> None:
self.items[item.order_id] = item
await self._fire()

async def remove(self, order_id: str) -> OrderedItem:
return self.items.pop(order_id)
removed = self.items.pop(order_id)
await self._fire()
return removed

def get(self, order_id: str) -> OrderedItem | None:
return self.items[order_id]
Loading
Loading