# Smart Assistant Voice assistant on a Raspberry Pi talking to the OpenAI Realtime API via an ASP.NET backend deployed on Coolify. See `docs/superpowers/specs/2026-06-11-smart-assistant-design.md` for the full design. Implementation plans: - Plan 1 — `docs/superpowers/plans/2026-06-11-smart-assistant-backend-foundation.md` (Identity, devices, pairing, install endpoints). - Plan 2 — `docs/superpowers/plans/2026-06-11-smart-assistant-device-hub-realtime-tools.md` (device WebSocket hub, OpenAI Realtime relay, tools registry). ## Repo layout - `backend/` — ASP.NET 9 backend (Identity, Devices, Pairing, Install). - `backend.tests/` — xUnit integration + unit tests via `WebApplicationFactory`. - `client/` — Python client. **Plan 1 ships placeholders only**; the full wakeword + Realtime client lands in Plan 3. - `Dockerfile` + `docker-compose.yml` — multi-stage build; produces one image with the published ASP.NET app + a bundled `client.tar.gz` in `wwwroot/client.tar.gz`. - `deploy.json` — Coolify config (gitignored; contains the OpenAI key). - `docs/superpowers/` — specs and plans. ## Local development ```sh dotnet test # all backend tests dotnet run --project backend # starts on http://localhost:5252 ``` Hit `http://localhost:5252/health` to verify it's up. ## Smoke test against a live deploy The full pairing flow can be exercised with `curl` — no UI needed yet. ```sh DOMAIN="https://assistant.volcanic.tes.gd" # or your domain COOKIE=$(mktemp) # Register the first user (becomes Admin automatically) and sign in curl -sf -c "$COOKIE" -X POST "$DOMAIN/api/auth/register" \ -H 'content-type: application/json' \ -d '{"email":"you@example.com","password":"Passw0rd!"}' curl -sf -c "$COOKIE" -b "$COOKIE" -X POST "$DOMAIN/api/auth/login" \ -H 'content-type: application/json' \ -d '{"email":"you@example.com","password":"Passw0rd!"}' # Generate a pairing code (auth required) CODE=$(curl -sf -c "$COOKIE" -b "$COOKIE" -X POST "$DOMAIN/api/pair-code" \ -H 'content-type: application/json' \ -d '{"name":"kitchen"}' \ | python3 -c 'import sys,json; print(json.load(sys.stdin)["code"])') echo "code=$CODE" # Pair as a device (public endpoint) and capture the token curl -sf -X POST "$DOMAIN/api/pair" \ -H 'content-type: application/json' \ -d "{\"code\":\"$CODE\",\"hostname\":\"smoke\",\"client_version\":\"0.1\"}" \ | python3 -m json.tool # Verify the install script and client bundle are served curl -sf "$DOMAIN/install.sh" | head -5 curl -sIf "$DOMAIN/client.tar.gz" ``` Expected: `{"ok":true}` on `/health`, JSON with `device_id`/`device_token`/`backend_ws` from `/api/pair`, the bash shebang from `/install.sh`, and a `200 OK` with `Content-Type: application/gzip` for the bundle. > **Plan 1 ships a placeholder Python bundle** — running the bundled > `client.main` will exit with the placeholder message. Plan 3 replaces it > with the real wakeword + Realtime client. ## Deploying Use the Coolify v4 API. The `deploy.json` file (gitignored) carries the project + server UUIDs and the OpenAI key. ```sh APP=$(python3 -c "import json; print(json.load(open('deploy.json'))['app']['uuid'])") curl -sf -X POST "$COOLIFY_URL/api/v1/deploy?uuid=$APP" \ -H "Authorization: Bearer $COOLIFY_KEY" ``` Fire-and-forget; the API returns a `deployment_uuid` immediately. Don't poll (per the global Coolify rule in `CLAUDE.md`). ## Plan 2: device hub + Realtime relay - `wss:///device` — bearer-token WebSocket. Pair a device first to get a token. - Handshake: client sends `{type:"hello", device_id, client_version}`, backend replies `hello_ack` with per-device config (voice, model, system prompt, idle timeout, enabled tools). - Heartbeat: client sends `{type:"ping"}` every 15 s, backend replies `pong` and updates `Devices.LastSeenAt`. - Wake: `{type:"wake"}` opens a Realtime session against OpenAI; backend emits `session_started` (with `conversation_id`), forwards `response.audio.delta` as binary frames to the device, persists `response.audio_transcript.done` as an assistant turn, persists `conversation.item.input_audio_transcription.completed` as a user turn, and emits `assistant_done` after each `response.done`. - Tools shipped (per-device toggle via `DeviceConfig.EnabledToolsJson`): - `get_current_time` (server-side, returns `{"now":""}`), - `end_session` (closes the session after the current `response.done` completes; emits `session_ended(reason="tool")`), - `set_volume` (round-trips a `tool_call`/`tool_result` envelope to the Pi; the Pi side will run `amixer` in Plan 3). - Idle timeout: server-side. With no `input_audio_buffer.speech_started` upstream event for `IdleTimeoutSeconds`, the relay emits `session_ended(reason="idle")`. The Python Pi client (Plan 3) is the production consumer of this surface. A quick handshake-only smoke against the live deploy: ```sh DOMAIN="https://assistant.volcanic.tes.gd" # Reuse the curl recipe above to log in, generate a pair-code, and pair # a "smoke" device — capture $TOKEN from the /api/pair response. python3 - <