diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-16 13:27:58 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-16 13:27:58 +0200 |
| commit | 0185271050ff66d7ce14aa83a990035fc62e73e4 (patch) | |
| tree | 7095368181d272c1cf53fdb0870203ed4cb72445 | |
| parent | 185a4ed7a605a8b22ab51beb417c155e85c811ef (diff) | |
| download | desktop-assistant-0185271050ff66d7ce14aa83a990035fc62e73e4.tar.gz desktop-assistant-0185271050ff66d7ce14aa83a990035fc62e73e4.zip | |
Docs: desktop-assistant is a text daemon now
README and AGENTS.md describe the socket protocol and the history instead
of the voice pipeline; the voice-era findings stay, marked as such, since
a return to voice starts from them and from the voice-v1 tag. The overlay
spec is marked superseded, the daemon spec records what changed while
planning, and the example tools lose their spoken announcements.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
| -rw-r--r-- | AGENTS.md | 100 | ||||
| -rw-r--r-- | README.md | 102 | ||||
| -rw-r--r-- | docs/superpowers/specs/2026-09-26-overlay-design.md | 4 | ||||
| -rw-r--r-- | docs/superpowers/specs/2026-10-16-text-daemon-design.md | 16 | ||||
| -rw-r--r-- | examples/config.toml | 30 | ||||
| -rw-r--r-- | examples/tools/finance.toml | 2 | ||||
| -rw-r--r-- | examples/tools/mail.toml | 2 | ||||
| -rw-r--r-- | examples/tools/search.toml | 2 | ||||
| -rw-r--r-- | examples/tools/weather.toml | 2 |
9 files changed, 128 insertions, 132 deletions
@@ -1,42 +1,49 @@ # desktop-assistant -A local desktop voice assistant: speak, get a spoken answer, nothing leaves -the machine. Everything runs on already-packaged local tools; this repo is -the glue between them. +A local text assistant daemon: questions in and streamed replies out on a +Unix socket, answered by the model already loaded in llama-server, with +read-only tools and a SQLite history. Nothing leaves the machine except +what an enabled tool fetches. -Status: **first working loop** in `assistant.py` (2026-09-25). Mic button -on/off is push-to-talk, whisper large-v3-turbo on the Arc transcribes, -the resident llama-server model answers, Kokoro speaks it. Button-off to -first spoken word ~2 s. See "Findings" and "Next steps". +Status (2026-10-16): **text daemon**. The front end is the quickshell +canvas (`desktop/assistant/` in the quickshell repo, SUPER+Home). Spec: +`docs/superpowers/specs/2026-10-16-text-daemon-design.md`. -Tools (2026-09-26, phase 1 of 2): the model can call read-only tools -(time, calendar, weather, search) defined as shell-command TOML files in +The voice version is the tag `voice-v1`: push-to-talk on the mic's mute +button, whisper large-v3-turbo, Kokoro. The sections from "Hardware" +through "Kokoro-82M" describe that version and its measurements. They are +kept because a return to voice starts from them. + +Tools (2026-09-26): read-only shell-command TOML files in `~/.config/desktop-assistant/tools/`; `config.toml` there overrides the -built-in settings. Examples in `examples/`. STT, LLM and TTS stay local; -only the weather and search tools reach the network, and only when a -question needs them. Spec: `docs/superpowers/specs/2026-09-26-tools-and-config-design.md`. +built-in settings. Examples in `examples/`. Spec: +`docs/superpowers/specs/2026-09-26-tools-and-config-design.md`. -Run: `python3 assistant.py` (`--test` for the self-check), or `./assistant.sh restart` -to replace a running one (and its overlay) detached from the terminal. +Run: `python3 assistant.py` (`--test` for the self-check), or +`./assistant.sh restart` to replace a running one detached from the +terminal. --- ## Pipeline ``` -mic button ──> arecord ──> whisper-server (STT) ──> llama-server (LLM) ──> Kokoro (TTS) ──> aplay +socket (ask) ──> llama-server (LLM, tools) ──> socket (think, token, tool, done) + └─> history.db (SQLite, FTS5) ``` -| Stage | Package (installed) | Binary | Notes | -|-------|---------------------|--------|-------| -| STT | `whisper.cpp` 1.9.4 | `whisper-server` (also `whisper-stream`, `whisper-cli`) | SDL2 mic capture, ffmpeg decoding, Vulkan + OpenVINO backends | -| LLM | `llama.cpp-vulkan` 0.5.0 | `llama-server` | already running as a system service, see below | -| TTS | `python3-kokoro-onnx` 0.6.1 | (Python module) | uses system `python3-onnxruntime` and `espeak-ng`, no `espeakng-loader` | +- **Concurrency:** one reader thread per client, one reply thread per + question. One lock guards the conversation, the turn counter and the + database. `say()` checks the turn under it before each emit, and stop + bumps the turn under it, so a stopped reply cannot emit after its `done`. +- **One daemon:** `serve()` probe-connects first and refuses to start over a socket that answers; `main()` removes the socket only if it served it. +- **Commands** are type-checked before they run (ids must be `int`: SQLite would match `"1"` to row 1 while the Python comparison with the current id would not) and run under a try, so a failing command answers `error` instead of killing the client's reader. +- **Client sockets** have a 2 s timeout, not non-blocking mode: a large + `hello` would not fit a non-blocking send and would drop the client. +- **Search** quotes the query as one FTS5 phrase with a prefix star. The + self-check proves typed FTS syntax (`"`, `*`, `NOT`, `OR`, `)`) is + matched as text and raises nothing. -The three packages are in the `my-slackbuilds` repo -(`../slackbuilds-superrepo/my-slackbuilds/`). Package-level changes (build -options, backends, bumps) happen there, under that repo's `AGENTS.md`, not -here. `openvino` 2026.4.0 is also packaged there, solely to feed whisper.cpp. ## Hardware @@ -106,6 +113,26 @@ not import from it. ## Findings +### Text daemon, live check (2026-10-16) + +Model Gemma4-12B-qat-mtp, ctx 32768, preset reasoning-budget 4096. + +- Thinking on: `think` events arrive (1017 deltas for the symlink question; + the first after 2.8 s). Reasoning took about 21 s, so the first token came + at 23.8 s for a two-line question. This is why `thinking` became a config + key (default true). +- "What time is it?": no tool call; the date stamp in the prompt answered + it. First token 4.9 s. +- "How many unread mails do I have?": tool round in order `tool` mail + done=false, `state` tool, `tool` mail done=true (0.12 s later), `state` + thinking, tokens; first token 6.7 s. +- ctx used went 1166, 1247, 1275, 1603 across the three questions and the + tool round. +- `list` returned the one conversation, `search` "symlink" returned it with + a `**symlink**` snippet. +- History dir 700; `history.db`, `-wal` and `-shm` 600. +- SIGTERM logged `exit: SystemExit 0` and removed the socket. + ### Models (2026-09-25) Everything under `/data/voice-models/` (outside the repo, kept apart from @@ -645,22 +672,17 @@ kokoro-onnx GitHub release `model-files-v1.0`. ## Next steps -Steps 1-5 of the original plan are done (models, backend comparison, live -mic, TTS, glue); push-to-talk came for free from the mic button. Open, in -no particular order: - -- **Latency:** STT ~1.2 s is the biggest share; the double encoder pass - (detect + transcribe) is the target if it matters. +- **Canvas:** the quickshell side, `desktop/assistant/`, its own plan in + that repo. +- **Local memory:** `docs/superpowers/plans/2026-09-30-local-memory.md` + still applies; its tasks 1-4 target this daemon, the voice-specific + bits of task 4 aside. +- **llamachat ports**, if the canvas replaces llamachat for good: + attachments, skills, a model picker, model-written titles. - **Phase 2 tools:** tools that write or modify (events, messages, - desktop), own spec, after phase 1 has been used for a while. -- OpenVINO in the whisper.cpp package: runs on CPU only here (no Intel - compute-runtime), decision goes to `my-slackbuilds`. -- **Wake word:** `hey_jarvis` works in quiet rooms, fails under loud - music (3/8); no echo canceller in pipewire. Pick an option from - "Wake word trial" before building anything. - -Open questions: whether an audio-capable LLM makes whisper unnecessary. -License: GPLv2 only (chosen 2026-09-25). + desktop), with a review step, own spec. +- **Voice:** start from `voice-v1`. The open items from that era (STT + latency, wake word, OpenVINO) are in the sections above. ## Package-side leftovers (tracked in my-slackbuilds, not here) @@ -1,55 +1,73 @@ # desktop-assistant -A local push-to-talk voice assistant. Unmute the microphone, speak, mute it: -the assistant transcribes what you said, asks a local LLM, and speaks the -answer. Italian and English. Speech, transcripts and the model stay on the -machine; only tools you enable (weather, web search) reach the network. +A local text assistant daemon. Questions arrive on a Unix socket, the +model already loaded in a local llama-server answers, calling read-only +tools when a question needs them, and the reply streams back token by +token. Every conversation is kept in a local SQLite history with +full-text search. The model and the history stay on the machine; only +tools you enable (weather, web search) reach the network. -``` -mic button ──> arecord ──> whisper-server (STT) ──> llama-server (LLM) ──> Kokoro (TTS) ──> aplay -``` +The front end is the assistant canvas of the quickshell desktop +(`desktop/assistant/` in the quickshell repo, on SUPER+Home). Anything +that speaks JSON lines on a Unix socket can drive it. -## How it works - -- **Push-to-talk** is the microphone's own mute button: the script listens to - the ALSA `Mic Capture Switch` (`amixer events`). Switch on starts a - recording, switch off ends it. -- **Speech to text**: whisper.cpp's `whisper-server` with the - `large-v3-turbo` model on the GPU (Vulkan). The script starts it if it is - not already running and stops it on exit. -- **LLM**: an OpenAI-compatible `llama-server` on `localhost:8181`. The - script uses whichever model the server already has loaded, so it never - swaps models under other clients. -- **Text to speech**: Kokoro-82M through `kokoro-onnx`, one voice per - language, chosen per sentence from the language of the reply. Speech - starts after the first sentence, while the rest is still being generated. -- **Calls**: while the desktop's presentation mode is on - (`statusctl presentation get` returns `1`), button presses are ignored, so - a call can use the microphone. +The voice version (push-to-talk, whisper, Kokoro) is the tag `voice-v1`. ## Requirements -- whisper.cpp (`whisper-server`) and a whisper ggml model -- a running `llama-server` with an OpenAI-compatible API -- Python 3 with `onnxruntime`, `numpy` and `kokoro-onnx` -- `espeak-ng`, ALSA utils (`amixer`, `arecord`, `aplay`), `curl` -- `jq` for the weather and search example tools, `python3-dateutil` for the calendar one -- a USB microphone with a hardware mute button exposed as an ALSA switch - -Paths, the card name, ports and voices can be changed in `config.toml`, see -"Configuration and tools" below. +- a running `llama-server` with an OpenAI-compatible API, router mode +- Python 3 (stdlib only; SQLite with FTS5) +- `curl` and `jq` for the weather and search example tools, + `python3-dateutil` for the calendar one ## Usage ```sh python3 assistant.py # run python3 assistant.py --test # self-check -./assistant.sh start # detached, with the overlay +./assistant.sh start # detached ./assistant.sh stop -./assistant.sh restart # stop a running one, start again +./assistant.sh restart ./assistant.sh status ``` +The daemon uses whichever model the router has loaded, so it never swaps +a model out under another client. + +Only one daemon serves the socket: a second one refuses to start while the +first answers. + +## Protocol + +JSON lines both ways on `$XDG_RUNTIME_DIR/desktop-assistant.sock` (mode +0600). Every connected client sees every event. + +In: `ask {text}`, `stop`, `new`, `list`, `open {id}`, `search {q}`, +`delete {id}`. + +Out: `hello` on connect (state, model, context use, the current +conversation and any reply in flight), then `user`, `think`, `token`, +`tool`, `done`, `state`, `ctx`, `error`, and the answers `conversations`, +`conversation` and `results`. The full table is in +`docs/superpowers/specs/2026-10-16-text-daemon-design.md`. + +Ids are integers; a malformed command or argument answers `error` and the +connection stays open. + +Try it by hand: + +```sh +socat - UNIX-CONNECT:$XDG_RUNTIME_DIR/desktop-assistant.sock +{"type":"ask","text":"What time is it?"} +``` + +## History + +`~/.local/share/desktop-assistant/history.db` (`$XDG_DATA_HOME` +honoured), directory 0700, file 0600. The model is sent the last 10 turns +of the open conversation. Tool results are stored as a placeholder, not +their output. Reasoning is shown live and not stored. + ## Configuration and tools Everything is optional. Settings and tools live in @@ -71,8 +89,6 @@ cp -r examples/tools ~/.config/desktop-assistant/ description = "What the tool does, for the model" command = ["script.sh", "{param}"] # looked up in tools/ first, then PATH timeout = 10 -announce.it = ["Cerco un attimo."] # optional, said while it runs -announce.en = ["Let me look that up."] [params.param] type = "string" @@ -99,18 +115,6 @@ The example tools only read: full markdown recap opened in `MD_EDITOR` (default `xdg-open`); `CHANGELOG` and `PKGDIR` in `[env]` override the mirror path and package database -## Overlay - -The assistant broadcasts its state, context use, the running tool and the -audio levels as JSON lines on `$XDG_RUNTIME_DIR/desktop-assistant.sock` -(mode 0600). The overlay is the `assistant/` component of the quickshell -repo: a circle with the name, `used / max` context and the tool, pulsing -dots while it transcribes or thinks, and a ring of ripples following your -voice and the reply. It fades out after the reply. Name, position, delay -and colours are the `[overlay]` table in `config.toml`. With no overlay -running the assistant works the same. A press ignored in presentation or -gaming mode sends a normal-urgency notification instead. - ## License Copyright (C) 2026 Danilo M. <danix@danix.xyz> diff --git a/docs/superpowers/specs/2026-09-26-overlay-design.md b/docs/superpowers/specs/2026-09-26-overlay-design.md index e45819f..3c4aef3 100644 --- a/docs/superpowers/specs/2026-09-26-overlay-design.md +++ b/docs/superpowers/specs/2026-09-26-overlay-design.md @@ -1,6 +1,8 @@ # Assistant overlay: design -Date: 2026-09-26. Status: approved in brainstorming, not implemented. +Date: 2026-09-26. Status: **superseded** by +`2026-10-16-text-daemon-design.md`; the overlay and the voice pipeline it +drew were removed (tag `voice-v1`). ## Goal diff --git a/docs/superpowers/specs/2026-10-16-text-daemon-design.md b/docs/superpowers/specs/2026-10-16-text-daemon-design.md index 30f202b..40a73c8 100644 --- a/docs/superpowers/specs/2026-10-16-text-daemon-design.md +++ b/docs/superpowers/specs/2026-10-16-text-daemon-design.md @@ -54,7 +54,7 @@ merging, `--test`, `assistant.sh`. - **Thinking.** `chat_template_kwargs.enable_thinking` becomes true. `delta.reasoning_content` is streamed as its own `think` event, so the canvas can collapse it. Reasoning is not stored and not sent back to the - model. + model. It is the config key `thinking` (default true): measured live, reasoning added about 21 s before the first token of a two-line answer. - **System prompt.** The default is rewritten for text: markdown is allowed, there is no instruction to answer in short spoken sentences. - **Input.** The mic loop is replaced by a reader thread per client. Each @@ -71,6 +71,7 @@ merging, `--test`, `assistant.sh`. JSON lines both ways on the existing socket. Every connected client sees every outbound event, so a canvas reopened mid-reply catches up. +A client stalled for 2 s on a send is dropped. Inbound: @@ -84,6 +85,8 @@ Inbound: | `{"type":"search","q":"..."}` | answers `results` | | `{"type":"delete","id":N}` | deletes N, answers `conversations`; deleting the current one acts as `new` | +Ids in `open` and `delete` must be integers; a bad id, a non-string `text` or `q`, or a command that raises answers `error` (`bad id`, `bad text`, `bad query`, `<type> failed`) and leaves the conversation untouched. + An `ask` while a reply is streaming stops the old reply first. An unknown or malformed line is logged and answered with `error`; it never closes the connection. @@ -92,21 +95,20 @@ Outbound: | Event | Fields | | --- | --- | -| `hello` | `state`, `model`, `ctx_used`, `ctx_max`, `id`, `turns` (the current conversation) | +| `hello` | `state`, `model`, `ctx_used`, `ctx_max`, `id`, `turns` (the current conversation), `pending` (`{user, reply, think, tools}` of a reply in flight, or null) | | `state` | `state`: `idle`, `thinking`, `tool` (with `tool`) | | `user` | `text`: the question, echoed so every client draws it | | `think` | `delta` | | `token` | `delta` | | `tool` | `name`, `done` (false when it starts, true when it returns) | -| `done` | `id`: the conversation the turn was saved in | +| `done` | `id`: the conversation the turn was saved in; `stopped`: true for a reply that was stopped | | `error` | `message` | | `ctx` | `used`, `max` | | `conversations` | `items`: `[{id, title, updated}]`, newest first | | `conversation` | `id`, `turns`: `[{user, reply, tools}]` | | `results` | `items`: `[{id, title, snippet}]`, best first | -`hello` is sent on connect. A reply that was stopped emits `done` with -whatever text it had, and that partial turn is not saved. +`hello` is sent on connect. A stopped reply gets a `done` with `stopped: true` from whatever stopped it, and that partial turn is not saved. ## Storage @@ -142,12 +144,12 @@ delete trigger cleans the index. - `open` loads the last 10 turns' `messages` as the model history and all turns' `user`, `reply`, `tools` for the canvas. Tool results come back as the stored placeholder. -- `search` wraps the user's text as an FTS5 phrase (double quotes doubled - inside), so FTS syntax in the query cannot break or widen it. Results are +- `search` wraps the user's text as an FTS5 phrase (double quotes doubled inside) with a prefix star, so FTS syntax in the query cannot break or widen it and a half-typed word still matches. Snippets mark matches with `**`. Results are ranked by `bm25` and grouped to one row per conversation, with `snippet()` from its best turn. - One connection guarded by a lock: the reply threads and the client reader threads all go through it. Only the daemon touches the file. +- An index on `turns(conv, seq)`; `db_open` chmods the directory and file even when they already exist. ## assistant.sh diff --git a/examples/config.toml b/examples/config.toml index 19e1e8b..94bc1e6 100644 --- a/examples/config.toml +++ b/examples/config.toml @@ -3,37 +3,11 @@ # shown are the built-in defaults. An unknown key stops the assistant # with its name, so a typo cannot fall back to a default in silence. -# card = "Microphone" # ALSA card id of the mic # llm = "http://localhost:8181/v1" # llama-server, OpenAI API -# system = "You are a voice assistant. ..." +# system = "You are a desktop assistant. ..." # tool_rounds = 3 # tool round trips per question # temperature = 0.6 # per request; unset = the model preset's - -# [whisper] -# model = "/data/voice-models/whisper/ggml-large-v3-turbo-q5_0.bin" -# device = 0 # -dev index, GPU devices only -# port = 8182 - -# [kokoro] -# model = "/data/voice-models/kokoro/kokoro-v1.0.onnx" -# voices = "/data/voice-models/kokoro/voices-v1.0.bin" - -# Style name = weight; a blend replaces the default blend whole. -# [voice.it] -# lang = "it" -# blend = { if_sara = 0.8, af_bella = 0.2 } -# [voice.en] -# lang = "en-us" -# blend = { af_heart = 1.0 } - -# The quickshell overlay (quickshell repo, assistant/). udt palette names -# for the colours. -# [overlay] -# name = "Assistant" -# position = "center" # center, top, bottom, top-left, top-right, bottom-left, bottom-right -# hide_delay = 3.0 # seconds idle before it fades out -# user_color = "accent" -# assistant_color = "green" +# thinking = true # false skips the model's reasoning (faster first word) # Added to every tool's environment. Personal values live here, not in # the tool files. diff --git a/examples/tools/finance.toml b/examples/tools/finance.toml index 1cfdc4a..8f3677f 100644 --- a/examples/tools/finance.toml +++ b/examples/tools/finance.toml @@ -1,8 +1,6 @@ description = "The user's money, from Firefly III: account balances, this month's budgets, or recent spending. Read-only." command = ["finance.sh", "{what}", "{since}", "{account}"] timeout = 20 -announce.it = ["Controllo i conti."] -announce.en = ["Checking your accounts."] [params.what] type = "string" diff --git a/examples/tools/mail.toml b/examples/tools/mail.toml index 2f1a9d8..b94ef2b 100644 --- a/examples/tools/mail.toml +++ b/examples/tools/mail.toml @@ -1,8 +1,6 @@ description = "The user's email: how many unread messages and who they are from. Read-only." command = ["mail.sh", "{query}"] timeout = 15 -announce.it = ["Controllo la posta."] -announce.en = ["Checking your mail."] [params.query] type = "string" diff --git a/examples/tools/search.toml b/examples/tools/search.toml index 2c6919e..7443658 100644 --- a/examples/tools/search.toml +++ b/examples/tools/search.toml @@ -1,8 +1,6 @@ description = "Search the web. Use it for facts, news, distances, prices, anything you are not sure about, instead of guessing." command = ["search.sh", "{query}"] timeout = 10 -announce.it = ["Cerco un attimo.", "Vediamo cosa trovo.", "Controllo."] -announce.en = ["Let me look that up.", "Let me see what I can find.", "Checking."] [params.query] type = "string" diff --git a/examples/tools/weather.toml b/examples/tools/weather.toml index ddf3f8e..d9a2e8c 100644 --- a/examples/tools/weather.toml +++ b/examples/tools/weather.toml @@ -1,8 +1,6 @@ description = "Weather now, or the forecast for today or tomorrow. Leave place empty for the user's home." command = ["weather.sh", "{place}", "{when}"] timeout = 12 -announce.it = ["Controllo il meteo.", "Vediamo che tempo fa."] -announce.en = ["Checking the weather.", "Let me check the weather."] [params.place] type = "string" |
