Make every example a uv workspace member - #6649
Conversation
The pinning logic ran as a heredoc inside deploy-examples.yml, so it could not be run by hand before `lk agent deploy` and had no linting or type checking. Behavior is unchanged.
Each example declared its dependencies in a requirements.txt that nothing in the root sync path read, so `uv sync` left them uninstalled and an example crashed at import on its first missing dependency (apsw, in hotel_receptionist). The lk CLI classified the examples as pip projects from that file, launched a bare interpreter resolved from the enclosing workspace, and had no gate for missing dependencies on that path. Declaring the dependencies in a pyproject.toml and joining the workspace puts them in the root lock, so `uv sync --all-extras --dev` installs them, and [tool.uv.sources] keeps in-repo dependencies resolving to the local editable packages. lk now detects python.uv and launches `uv run`. A deploy build context is the example directory alone, where workspace sources cannot resolve, so requirements.txt is rendered from the declared dependencies at deploy time instead of being checked in.
An example carried its own [tool.uv.sources] with workspace = true, and its requirements.txt was rendered only at deploy time, so a copy of the directory taken out of the repo could neither resolve (uv refuses a workspace reference with no workspace) nor build (the Dockerfile copied a file that isn't checked in). The workspace root already declares those same sources, and members inherit them β the lock is byte-identical without the per-example block β so dropping it costs nothing in-repo and leaves a plain project that resolves from PyPI anywhere. The Dockerfile installs from pyproject.toml with uv instead of a rendered requirements.txt, so `lk agent deploy .` in a copied directory needs nothing from this repo. A deploy from the repo still builds against the ref being deployed: pin_example_to_ref.py appends the git-pinned sources the workspace would otherwise supply, replacing the requirements.txt rendering.
| COPY pyproject.toml ./ | ||
| RUN uv sync --no-cache | ||
| ENV PATH="/app/.venv/bin:${PATH}" |
There was a problem hiding this comment.
π‘ Example container images may be built with a downloaded Python instead of the one the image is pinned to
The dependency install step for every example image is run without telling the tool which Python to use (uv sync --no-cache at examples/avatar/Dockerfile:43), so it can fetch and use its own newest Python instead of the version the image is pinned to.
Impact: Example deployments can silently run on a different Python than intended, and the image build can fail when a dependency has no prebuilt package for that newer Python.
Why the interpreter is not the image's python3.13
The base image is python:${PYTHON_VERSION}-slim with ARG PYTHON_VERSION=3.13 (examples/avatar/Dockerfile:6-7), but uv's default python-preference is managed with automatic downloads enabled, which is exactly why Astral's own Docker guide sets UV_PYTHON_DOWNLOADS=0 "because we want to use the system interpreter". The example pyproject.toml only declares requires-python = ">=3.10" (examples/avatar/pyproject.toml:4) and there is no .python-version, so uv is free to install a managed interpreter (e.g. 3.14, still allowed by livekit-agents' >=3.10,<3.15) and create /app/.venv from it. Consequences: (1) ARG PYTHON_VERSION no longer controls the runtime interpreter; (2) dependencies are resolved for that interpreter, so packages such as av (a livekit-agents dependency) or apsw may have no wheel and fall back to an sdist build, which for av needs ffmpeg dev libraries not installed in the image. All 8 example Dockerfiles are byte-identical, so this applies to avatar, drive-thru, frontdesk, healthcare, homepage, hotel_receptionist, inference and survey.
The previous pip install --user -r requirements.txt always used the image's interpreter.
| COPY pyproject.toml ./ | |
| RUN uv sync --no-cache | |
| ENV PATH="/app/.venv/bin:${PATH}" | |
| COPY pyproject.toml ./ | |
| # Use the interpreter this image is pinned to instead of letting uv download | |
| # and prefer a managed Python of its own choosing. | |
| ENV UV_PYTHON_DOWNLOADS=0 | |
| RUN uv sync --no-cache --python "$(command -v python3)" | |
| ENV PATH="/app/.venv/bin:${PATH}" |
Was this helpful? React with π or π to provide feedback.
| COPY pyproject.toml ./ | ||
| RUN uv sync --no-cache | ||
| ENV PATH="/app/.venv/bin:${PATH}" |
There was a problem hiding this comment.
π‘ Avatar example image can be broken by a virtual environment left in the copied directory
The avatar example builds its dependencies into a virtual environment inside the image (uv sync at examples/avatar/Dockerfile:43) and then copies the whole directory on top of it, but that directory has no ignore file, so a virtual environment sitting next to the sources overwrites the one built in the image and the agent fails to start.
Impact: Deploying the avatar example from a working copy where someone already installed its dependencies locally produces an image that cannot run.
Missing .dockerignore in examples/avatar
Every other example with a Dockerfile ships a .dockerignore that excludes **/.venv/ (e.g. examples/hotel_receptionist/.dockerignore), so the final COPY . . (examples/avatar/Dockerfile:50) cannot clobber /app/.venv. examples/avatar/ has no .dockerignore at all. With the old pip install --user layout a stray host .venv in the context was inert; now /app/.venv is the interpreter selected by ENV PATH="/app/.venv/bin:${PATH}", so a host .venv (wrong platform, absolute paths baked in) replaces it and CMD ["python", "agent.py", "start"] breaks. examples/README.md:99-111 now explicitly tells users to copy an example directory and run uv sync there before lk agent deploy ., which is exactly how such a .venv appears.
Prompt for agents
examples/avatar/ is the only example with a Dockerfile that has no .dockerignore. The new Dockerfile installs dependencies into /app/.venv and puts it first on PATH, then runs `COPY . .`, so any .venv present in the build context replaces the image's environment and the container's `python` becomes a host-built venv. Add a .dockerignore to examples/avatar mirroring the one used by the other examples (e.g. examples/hotel_receptionist/.dockerignore), which excludes **/.venv/, caches, .env files and tests.
Was this helpful? React with π or π to provide feedback.
Each example declared its dependencies in a
requirements.txtthat nothing in the root sync path read β[tool.uv.workspace].memberscovered onlylivekit-plugins/*andlivekit-agents, and pytest ignoresexamples. Souv syncleft example dependencies uninstalled, and an example crashed at import on its first missing one:apsw>=3.46had been declared inexamples/hotel_receptionist/requirements.txtthe whole time. The lk CLI also classified the examples as pip projects from that file, launched a bare interpreter resolved from the enclosing workspace, and has no missing-dependency gate on that path β the uv path fast-fails withrun uv sync, the pip path does not.What changed
Every example that had a
requirements.txt(9 of them) now declares its dependencies in apyproject.tomland joins the workspace. Dependencies land in the root lock, souv sync --all-extras --devinstalls them, and[tool.uv.sources]keeps in-repo dependencies resolving to the local editable packages rather than PyPI.A deploy build context is the example directory alone, where workspace sources cannot resolve, so
requirements.txtis rendered from the declared dependencies at deploy time instead of being checked in. The pinning logic moved out of thedeploy-examples.ymlheredoc intoscripts/render_example_requirements.pyfirst, unchanged, so it can also be run by hand beforelk agent deploy:Verified
uv sync --all-extras --dev --dry-runnow includesapsw,aiocsvandduckduckgo-search.uv.lockrecords each example assource = { virtual = ... }withlivekit-agentsaseditable = "livekit-agents"β local SDK changes are still what the examples run against.lk agent dockerfilein an example now reportsDetected agent language [python.uv], waspython.pip.uv lock --check, ruff andmypy --strictclean. Ruff still resolves the root config for example files, since the newpyproject.tomls carry no[tool.ruff].pyproject.tomloverrequirements.txt, and it reads only.dockerignore, never.gitignore, so the rendered file still uploads.Notes
uv.lock. One would pinlivekit-agentsto a path source, so an isolated Docker build context still could notuv sync --lockedβ 9 lockfiles to keep in sync for no deploy benefit.pip install -r requirements.txt. The upstreampython.uvDockerfile template doesCOPY pyproject.toml uv.lock+uv sync --locked, which needs a self-contained project; a workspace member is not one. All 8 stay byte-identical, with a comment pointing at the render script.livekit-agents[evals]dropped from avatar, frontdesk, homepage and hotel_receptionist. There is noevalsextra inlivekit-agentsβ pip silently ignored it, uv rejects it.cd examples/foo && uv syncno longer works standalone, and a first sync pulls the whole workspace.examples/README.mdalready documented rootuv sync --all-extras --devplusuv run; the three example READMEs that still saidpip install -r requirements.txtnow match.