Goal: mission 00's check running and naming the first TODO for you, in about five minutes (the Docker way adds the image build, a few minutes the first time). On your own machine that means installing uv, plus Node.js and make for the checks; Codespaces and Docker bring all three.
You'll learn
- What gets installed, and why Node.js is needed even for Python (it runs the checks)
- Where your key lives (
.envin the project folder) and which commands need it - What a failing and a passing check look like, so you recognize them later
Want TypeScript or Java instead? See TypeScript or Java.
You'll work in two windows the whole time: a terminal and a code editor (VS Code or whatever you already use). Open both now. On macOS the terminal is Terminal; on Linux, any terminal. On Windows, read On Windows first: the project runs there in GitHub Codespaces (a machine GitHub runs for you, in your browser) or WSL (Windows Subsystem for Linux). In Codespaces, both windows sit in one browser tab.
Prefer not to install anything? Two ways round the three tools below: GitHub Codespaces (Can't install anything?, further down) and Docker on your own machine (Run it in Docker). Otherwise:
-
Node.js 22 (22.12 or newer; 20.19 or newer works too). It runs the checks. Check by pasting this into the terminal:
node --version
If that prints
v22.12.0or higher (any newer major,v24orv26say, is fine), or av20fromv20.19.0up, you're set. If it errors or prints something older, install it, then reopen your terminal:- macOS, or any system: the installer from nodejs.org, or nvm, a Node.js version manager (github.com/nvm-sh/nvm):
nvm install 22. - Ubuntu 24.04 or Debian 12: the
nodejspackage there is version 18, too old.curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -thensudo apt-get install -y nodejsgets 22. - Fedora:
sudo dnf install nodejs npm(Fedora 42's is new enough).
- macOS, or any system: the installer from nodejs.org, or nvm, a Node.js version manager (github.com/nvm-sh/nvm):
-
make. Check with
make --version; the first line names it (GNU Make 4.3, say). If it errors: on macOS,xcode-select --installadds it; on Ubuntu or Debian,sudo apt update && sudo apt install make curl git; on Fedora,sudo dnf install make curl git. -
uv, the Python package manager (install page). It installs Python 3.12 for you if needed (3.10 or newer works), so it doesn't matter which Python your machine has. One command (
curl -LsSf https://astral.sh/uv/install.sh | shon macOS and Linux; on Fedora,sudo dnf install uvworks too), then reopen your terminal souvis on your PATH (the list of folders your terminal searches for commands), and check:uv --version
Can't install anything? Work laptop, Chromebook, IT policies: run the whole workshop in GitHub Codespaces, free.
That opens an editor and a terminal in the browser, with Node.js, uv and make installed and make setup already run. From there:
- Skip
git cloneandmake setup: you are inside the project and it is set up. - Run
make use py. - Go to Check that it works: the offline check needs no key.
- Before
make run, add your key (or the two token lines) to.env(see Your API key).
A codespace has no microphone or speakers: type your turns and download the agent's voice to listen (see Hear your agent). VS Code's Dev Containers uses the same setup (.devcontainer/).
Docker instead? Skip to Run it in Docker; it has its own clone line.
Paste these into your terminal, one at a time:
git clone https://github.com/deepgram-devs/voice-agent-workshop.git
cd voice-agent-workshop
make setup SDK=pySDK=py installs the check harness and Python, and creates .env. The check harness is the Node.js program under tools/replay-server/ that replays recorded conversations against your code for make check. Its own install prints first (npm ci, then added 1 package), then uv's lines (Resolved ... packages, Installed ... packages, one line per package); the first time, it ends with:
Created .env: at the workshop, paste the two WORKSHOP_ lines from the /code page into it; at home, put your DEEPGRAM_API_KEY in it
Pick your language first: make use py (Python), make use ts (TypeScript) or make use java (Java)
Run it again later (a Troubleshooting row says to, or you come back to Docker after down -v) and uv prints lines like Resolved 29 packages in 1ms and Checked 21 packages in 3ms in place of its install lines; with your language picked, it ends with Your language: Python (saved in .workshop-lang). To switch: make use ts or make use java instead.
No git on your machine? Download the ZIP instead: green Code button, Download ZIP on the repo page. Unzip it, cd into the folder it makes (voice-agent-workshop-main), and run make setup SDK=py there.
Then pick your language, once:
make use pyIt prints Saved: Python. make check, run and live use it now. Add SDK=ts to run one command in TypeScript. From then on make check, make run and make live use Python and name the file they work on; the choice is saved in .workshop-lang (Git ignores it). Pick a language in the README covers switching.
The offline check below needs no key. Before make run or make live, open .env in the project folder (make setup created it), find the line DEEPGRAM_API_KEY= and put your key after the =, with no quotes or spaces; or paste the workshop's two token lines. Your API key in the README says where a key comes from and how a Codespaces secret works, the same for every language.
make check 00 # offline: no key, no network, a few secondsThis one fails on purpose: you haven't done mission 00 yet. The command prints Checking your Python file: python/missions/00-first-words/start.py, then pytest, Python's test runner, prints F, then [100%] at the end of that line, and a FAILURES heading. In the middle of the output is this block. It has a PASS line for each part the start file already has, a FAIL line for each open TODO, then the file and line to open:
PASS Your program sends a Settings message after Welcome
PASS Settings is the first message you send
PASS Settings has audio.input (encoding and sample_rate)
PASS Settings has audio.output
PASS Settings has agent.listen.provider.model
PASS Settings has agent.think.provider
FAIL TODO 1: the prompt is your own, not the starter one
PASS TODO 1: the prompt is at least a sentence (20+ characters)
FAIL TODO 2: you picked a voice other than flux-hannah-en
FAIL TODO 3: agent.greeting is set
FAIL TODO 3: the agent says your greeting back to you
Your file: python/missions/00-first-words/start.py:13 (TODO 1)
If you see the Your file: line, everything is installed. After the block: Captured stdout call with the [replay] lines, then 1 failed, 1 deselected, and the make: *** [Makefile:...: check] Error 1 under it is the failing check, which is the point. Open the file it names and start mission 00; when its checks pass, come back to the README's missions table for the rest (How a mission works there is the reference for the three commands).
Once every TODO passes, the summary reads 1 passed, 1 deselected, then two lines. First Mission 00 passes its offline check: python/missions/00-first-words/start.py, then the next step: Next: make run 00 to talk to your agent (type a line, press Enter; Ctrl-D or Ctrl-C hangs up). It needs DEEPGRAM_API_KEY or your workshop token in .env. Then make live 00 runs the live check. Missions with two recorded calls (02 to 05) end 2 passed, 1 deselected instead.
In Docker, each make in the Next: line reads docker compose run --rm workshop make. With WORKSHOP_URL in .env, two more lines name the mission's page on the workshop site and the checkpoint to tick there, I did this on Your code passed make check NN (At the workshop or remote). Anything else, a setup error say, is under Troubleshooting.
make check 00 MISSION_TARGET=solution runs the reference answer, solution.py beside your start file, instead of your file. It prints Checking the Python reference answer: python/missions/00-first-words/solution.py, a . and 1 passed, 1 deselected, and nothing else: pytest hides the PASS lines on a pass, and the Mission 00 passes and Next: lines are for your start file.
Check yourself. You change PROMPT in your start file, save, and run make check 00 again. Does the check need your key?
Answer
No. The offline check replays a recorded conversation against your code on your machine; nothing reaches Deepgram. make run 00 and make live 00 are the two that do, and they need your key or a workshop token in .env.
Prefer not to install Node.js or uv? Two ways, both with Docker on your own machine. Both need Docker Desktop (macOS, Windows, Linux) or Docker Engine with the Compose plugin (Linux); docker compose version tells you it is there.
- VS Code: install the Dev Containers extension, open this folder, and choose Reopen in Container. On Windows, choose Dev Containers: Clone Repository in Container Volume instead: the project then lives inside Docker, where it runs faster. It builds the same container Codespaces uses (
.devcontainer/) and runsmake setupfor you. Put your key in.envand pick your language withmake use py. - Docker only:
compose.yamlandDockerfilein this repo give you a container with Node.js 22, Python 3.12 with uv and make, on Debian 12 (bookworm), as root with nosudo. Paste these into a terminal, one at a time, in this order. The first two lines clone the repo and enter it; the rest run from that folder:
git clone https://github.com/deepgram-devs/voice-agent-workshop.git
cd voice-agent-workshop
docker compose run --rm workshop make setup SDK=py # once: builds the image, installs the harness and Python, creates .env
docker compose run --rm workshop make use py # once: pick your language
docker compose run --rm workshop make check 00 # offline check: no keyBefore the next two commands, open .env in this folder and put your key after DEEPGRAM_API_KEY= (Your API key), or paste the two token lines from the workshop's /code page. A key exported in your shell does not reach the container: put it in .env.
docker compose run --rm workshop make run 00 # talk to your agent: type your turns. Needs the key or token
docker compose run --rm workshop make live 00 # live check against the real API. Needs the key or token
docker compose run --rm workshop # a shell inside the container, for anything else; exit leaves it
docker compose down -v # when you're done: removes the installs (the volumes). To come back, run the make setup line the next make check printsNo git? The ZIP works here too: download it as under Do this, cd voice-agent-workshop-main, and run the docker compose lines from there.
The first Python make setup prints uv's lines before its result, lines like Using CPython 3.12..., Creating virtual environment at: /opt/venv, Resolved ... packages, Building voice-agent-workshop @ file:///workspace/python, Downloading pygments (1.2MiB) and Downloaded ... for some packages, Built ..., Prepared ... packages, Installed ... packages and one line per package. A later Python make check prints them again only when uv has to refresh the project (after a git pull, say); most runs print none of them. In the container the Python environment is /opt/venv (a volume: Docker's named storage, outside the repo), not python/.venv.
Normal, every time:
- The first command builds the image before anything runs. After the volume and network lines (next bullet), Docker's build steps print (
#1,#2, ... lines, thenImage voice-agent-workshop-workshop Built): a few minutes and about 1 GB of downloads, so it needs the network. The image on disk is about 1.9 GB. Later runs start in a second or two. - Every
docker compose runprints twoContainer voice-agent-workshop-workshop-run-... Creating/Createdlines first. The very first run, and the first run afterdown -v, also printVolume ... Creating/Createdlines for the four volumes and the network, each twice. - The prefix is the project name
compose.yamlsets, so it is the same whatever your folder is called. A-pflag on the command changes it, and then every name above reads with yours in place ofvoice-agent-workshop(the image, the containers, thedocker rmibelow). No printed command carries a-p(theNext:lines, theRun docker compose run --rm workshop make setup onceline, the "run ... again" lines), so add yours when you paste one. - The repo is mounted into the container, so you edit files on your machine as usual.
.env,.workshop-langandout/last-call.wavare the files it writes in this folder, plus two empty folders where the volumes mount (typescript/node_modules/andtools/replay-server/node_modules/; Git ignores them).down -vleaves them all alone. make checkruns offline with no key.make runandmake livewithout a key or token stop with lines saying so (make liveinside pytest's framing). Those lines, Docker Compose's own.envcheck andResource is still in useafterdown -vare in the README's Troubleshooting.down -vremoves the volumes, not the image. The nextdocker compose runrecreates them empty, somake checkstops withThe check harness isn't installed. Run docker compose run --rm workshop make setup once, then this command again.andmake: *** [Makefile:...: need-sdk] Error 2.- Run that line as printed. It is the plain
make setup: it installs the harness and every track whose tool is in the image, so other tracks' install lines scroll by first, including pnpm's boxedUpdate available!notice endingcurl -fsSL https://get.pnpm.io/install.sh | sh -. Ignore that: nothing here asks you to update anything, and the pnpm in the image is fine.make setup SDK=pyinstalls the harness and Python only; both work here. - To get the space back as well:
docker rmi voice-agent-workshop-workshop.
To pipe lines in, add -T: printf 'hello\n' | docker compose run --rm -T workshop make run 00 (in PowerShell: "hello" | docker compose run --rm -T workshop make run 00). A container has no microphone or speakers, so MIC=1 doesn't work there: type your turns, then play out/last-call.wav on your machine. With Docker Engine on Linux (the usual rootful install), files and folders the container creates belong to root. From the project folder, sudo chown -R "$(id -u):$(id -g)" . gives them all back to you.
Every make run saves the agent's side of the call to out/last-call.wav when you hang up. So you can hear the voice, speed and greeting you picked. On a laptop, open it in any audio player (on macOS, open out/last-call.wav; on Linux, xdg-open out/last-call.wav; in WSL, explorer.exe out). A call that brought back no audio prints a [no audio] line instead and leaves the file as it was.
In Codespaces, a remote machine or a container, there is no audio device: type your turns as usual, then download the file. In Codespaces or VS Code, right-click out/last-call.wav in the Explorer and choose Download. Over SSH, copy it with scp. Each run writes a new last-call.wav, so download the one you want before the next run.
On your own machine, Python can also use your microphone and speakers: make run 00 MIC=1 (it installs the audio extra the first time; on Linux it needs PortAudio, the library that opens your microphone: sudo apt install libportaudio2 or sudo dnf install portaudio). Wear headphones: without them your agent hears itself through your speakers, and your neighbor's agent too, and everyone's conversation gets weird. No headphones? Type your turns and keep the volume low; every mission works typed.
Every one of these has happened to someone smart. The README's Troubleshooting has the rows every language hits (the credentials lines, the .env lines, the first failing check); these are Python's own. Find the line you see, read what it means, apply the fix, carry on.
| You see | What's happening | Fix |
|---|---|---|
uv: command not found |
uv isn't installed, or this terminal opened before you installed it. | Install it (Before you start, above), open a new terminal, and run make setup SDK=py again. |
Python needs uv |
Python is your language and uv isn't installed. | Install it (Before you start, above), open a new terminal, and run make setup. |
Skipping Python: it needs uv (https://docs.astral.sh/uv/). during make setup |
Plain make setup found no uv. With Python your language, setup then ends on Python needs uv (the row above). |
Install uv (Before you start, above), open a new terminal, and run make setup again. |
.env line N couldn't be read |
A quote on line N never closes or has words after it, or the line isn't KEY=value. On the key's line, the is empty line follows it: fix line N, not the key. On make run it prints first; make check and make live show it inside pytest's Captured stderr call on a failing check. |
Write the line as KEY=value, no quotes. In Docker, Compose usually stops on these lines first: the two failed to read rows in the README's Troubleshooting. One word after a closing quote gets through and reaches this row. |
Your Python file doesn't run: python/missions/...:LINE SyntaxError |
A syntax error at that line (a bracket or quote never closed, a missing colon). The same line with NameError: name 'true' is not defined means true instead of True; that one comes inside pytest's ERROR collecting lines. |
Fix it and run make check NN again (in Docker the line's command carries the docker compose run --rm workshop make prefix). |
A FAIL TODO n: line with RuntimeWarning: coroutine 'Agent....' was never awaited at the bottom of the output, under pytest's warnings summary |
An agent. call ran without await, so nothing was sent. Every agent. method is async. |
Put await in front of it: await agent.update_prompt(...), await agent.close(). The mission's TODO comment shows the call with await. |
[note] Settings sent as raw JSON: this SDK release has no field for part of it (the check reads it the same), the first line of pytest's Captured stdout call on a failing check, or, on a make run that connects, the line between Running your Python file: and [ready] |
The pinned deepgram-sdk 7.12.0 could not model your Settings, so the helpers sent the same JSON raw. Two causes: THINKING_MODEL set in .env (mission 03's thinking stretch: the SDK has no model for that provider), or a decimal EXPRESSIVITY in mission 06 (1.5, or 1.0). |
Nothing to change for the stretch. In mission 06, write a whole number (1, not 1.0): the SDK's field takes nothing else. The check reads the JSON either way, so 1.5 also fails TODO 3 and 1.0 passes it; SPEED never prints this line. |
Could not reach the agent: connection refused. Check DEEPGRAM_AGENT_URL. on make run (... Check WORKSHOP_URL. Is the workshop site up? with a token), then make: *** [Makefile:...: run] Error 2; in make live, FAIL Could not reach production: ... inside pytest's failure output |
Nothing answered, or the address did not resolve: venue wifi, a proxy, a VPN, or a wrong DEEPGRAM_AGENT_URL. Other details here: the system's words (Name or service not known on Linux and in Docker, nodename nor servname provided, or not known on macOS) or TimeoutError. |
Leave DEEPGRAM_AGENT_URL empty for production; if it is, try a phone hotspot. make check NN needs no network. The README's Troubleshooting has the three rows (custom endpoint or workshop proxy in place of production for another address or a token). |
MIC=1 uses the PortAudio library |
The library isn't installed. | Run the install command the message gives (sudo apt install libportaudio2 or sudo dnf install portaudio), or drop MIC=1 and type your turns. |
The missions are in the README: Missions, then Keep going. Three Python things to know on the way:
-
The smallest program, with no helpers:
uv run python examples/first_settings.py(frompython/; in Docker,docker compose run --rm workshop sh -c 'cd python && uv run python examples/first_settings.py'). It sends Settings, waits forSettingsApplied, lets you type turns and saves the agent's voice toout/last-call.wav. With no key it stops onDEEPGRAM_API_KEY is empty in .env (this folder). Set it: this example runs on your own key only (make run takes a workshop token too). make check NN runs offline with no key.(NN: the example has no check). Nothing answering atDEEPGRAM_AGENT_URLprintsCould not reach the agent: connection refused. Check DEEPGRAM_AGENT_URL., asmake rundoes; a scheme-less address still ends in a traceback. -
Staying connected.
runends when the connection drops. For a program you'll leave running, userun_resilient(agent/resilient.py) in its place: it reconnects with a growing wait, sends your Settings again on every new connection, and prints each close code (Staying connected says when it stops).from agent.resilient import run_resilient run_resilient(build_settings, on_event)
-
Coming from SDK v3? The pin is
deepgram-sdk7.12.0. You make anAsyncDeepgramClient, open the agent withasync with client.agent.v1.connect() as socket, then read events withasync for. v3's agent websocket client and its.on(...)event names are gone, so v3 code won't run on this pin. Install into a fresh environment (make setupmakes one inpython/.venv; in Docker,/opt/venv), not one that already has v3. Coming from SDK v3 has the renamed fields.