Skip to content

Commit 9d824f9

Browse files
jkyberneeesclaude
andcommitted
feat(docker): llama.cpp embeddings sidecar + local model for memory.embedding
Adds a private llama.cpp embeddings sidecar so the Docker setup uses real semantic embeddings for memory (recall, dedup, ranking, fact merge-on-write) out of the box — completing the consumer side of PR #27's pluggable embeddings. - docker/Dockerfile.embeddings: builds llama-server from source pinned to the same llama.cpp release as the main image (b9549, Bookworm base → no ABI mismatch) and bundles a pinned nomic-embed-text-v1.5 GGUF (768-dim, ~84 MB Q4_K_M) at a fixed HF revision. No first-run download, no volume — mirrors the bundled whisper / MiniCPM-V models. Runs --embeddings --pooling mean, non-root. - docker-compose.yml: llama-embeddings service on all 4 profiles, no host port (only the odek containers reach http://llama-embeddings:8080), /health healthcheck, wired into each odek service's depends_on. - config.restricted.json / config.godmode.json: memory.embedding → the sidecar. - README.md / .env.example / DOCKER_COMPOSE_USER_GUIDE.md: 'local memory embeddings out of the box' docs, quant/model override build args, disable path, and the egress-trust note. Verified: image builds; container reaches /health in ~20s; /v1/embeddings returns 768-dim vectors; cosine('fixed the auth bug','repaired the login issue')=0.70 vs 0.35 for an unrelated phrase — the semantic match RP cannot do. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 20af774 commit 9d824f9

7 files changed

Lines changed: 179 additions & 8 deletions

File tree

docker/.env.example

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,16 @@ GIT_COMMITTER_EMAIL=you@example.com
6565
# ODEK_SCHEDULES_CATCHUP=false # run a missed fire once on startup
6666
# ODEK_SCHEDULES_ALLOW_TELEGRAM_MANAGEMENT=true # set false to make /schedule read-only (CLI-manages)
6767

68+
# ── Memory embeddings (llama.cpp sidecar; see docker/README.md) ──────────
69+
# The compose file runs a private llama.cpp server (the `llama-embeddings`
70+
# service) that gives the memory system real semantic embeddings. The model
71+
# (nomic-embed-text-v1.5) is BAKED INTO the image — no key, no first-run
72+
# download, nothing to set here. To change the model or quantization, rebuild
73+
# with build args, e.g.:
74+
# docker compose --profile restricted build \
75+
# --build-arg EMBED_QUANT=Q8_0 llama-embeddings
76+
# Then bump memory.embedding.model in config.*.json if you switch models.
77+
6878
# ── Web search (SearXNG sidecar; see docs/DOCKER_COMPOSE_USER_GUIDE.md) ──
6979
# The compose file runs a private SearXNG instance backing the `web_search`
7080
# tool. SearXNG reads this as server.secret_key at app load and it overrides

docker/Dockerfile.embeddings

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# syntax=docker/dockerfile:1
2+
3+
# llama.cpp embeddings sidecar — backs odek's memory.embedding (http provider).
4+
#
5+
# Builds llama-server from source pinned to the SAME llama.cpp release as the
6+
# main image (LLAMA_VERSION), and bundles a pinned nomic-embed-text-v1.5 GGUF
7+
# so the service is fully offline after build — no first-run model download.
8+
# This mirrors how the main image bundles whisper.cpp + MiniCPM-V.
9+
#
10+
# nomic-embed-text-v1.5 is a 768-dim text embedding model (~84 MB at Q4_K_M).
11+
# odek sends raw summary/query text for both corpus and query, so the embedding
12+
# space stays symmetric even without nomic's optional task-instruction prefixes.
13+
#
14+
# Override the model with build args, e.g. a higher-quality quant:
15+
# --build-arg EMBED_QUANT=Q8_0
16+
# Available quants in this repo: Q4_K_M (default) | Q5_K_M | Q6_K | Q8_0 | f16
17+
18+
# ---- build stage: compile llama-server + fetch the model ----
19+
# Same Debian Bookworm base as the runtime stage so the binary links against the
20+
# exact glibc/libstdc++ present at runtime (no ABI mismatch — see the main
21+
# Dockerfile's llama.cpp stage for the rationale).
22+
FROM debian:bookworm-slim AS build
23+
ARG LLAMA_VERSION=b9549
24+
# Pin the embeddings model + git revision so the image is reproducible.
25+
ARG EMBED_HF_REPO=nomic-ai/nomic-embed-text-v1.5-GGUF
26+
ARG EMBED_HF_REVISION=0188c9bf409793f810680a5a431e7b899c46104c
27+
ARG EMBED_QUANT=Q4_K_M
28+
ARG EMBED_FILE=nomic-embed-text-v1.5.${EMBED_QUANT}.gguf
29+
30+
RUN apt-get update && apt-get install -y --no-install-recommends \
31+
git cmake make g++ curl ca-certificates \
32+
&& rm -rf /var/lib/apt/lists/*
33+
34+
# BUILD_SHARED_LIBS=OFF statically links llama's internal libraries; GGML_OPENMP
35+
# OFF drops the libgomp dependency; LLAMA_CURL=OFF because the model is bundled
36+
# (no runtime download). Target llama-server (the OpenAI-compatible HTTP server)
37+
# instead of the main image's llama-mtmd-cli.
38+
RUN git clone --depth 1 --branch "${LLAMA_VERSION}" \
39+
https://github.com/ggerganov/llama.cpp /llama \
40+
&& cmake -S /llama -B /llama/build \
41+
-DCMAKE_BUILD_TYPE=Release \
42+
-DBUILD_SHARED_LIBS=OFF \
43+
-DGGML_NATIVE=OFF \
44+
-DGGML_OPENMP=OFF \
45+
-DLLAMA_CURL=OFF \
46+
-DLLAMA_BUILD_TESTS=OFF \
47+
&& cmake --build /llama/build -j"$(nproc)" --target llama-server \
48+
&& install -m 755 /llama/build/bin/llama-server /usr/local/bin/llama-server \
49+
&& rm -rf /llama
50+
51+
# Fetch the pinned GGUF into a fixed image path.
52+
RUN mkdir -p /models \
53+
&& curl -fSL --retry 5 --retry-delay 5 --retry-connrefused --retry-all-errors \
54+
"https://huggingface.co/${EMBED_HF_REPO}/resolve/${EMBED_HF_REVISION}/${EMBED_FILE}" \
55+
-o /models/embed.gguf
56+
57+
# ---- runtime stage ----
58+
FROM debian:bookworm-slim
59+
# libstdc++6 for the C++ binary; curl only for the compose healthcheck;
60+
# ca-certificates is harmless and tiny. No outbound network is needed at runtime.
61+
RUN apt-get update && apt-get install -y --no-install-recommends \
62+
libstdc++6 ca-certificates curl \
63+
&& rm -rf /var/lib/apt/lists/*
64+
COPY --from=build /usr/local/bin/llama-server /usr/local/bin/llama-server
65+
COPY --from=build /models/embed.gguf /models/embed.gguf
66+
67+
# Run non-root (uid 1000), matching the main image's posture.
68+
RUN useradd -u 1000 -m llama
69+
USER llama
70+
EXPOSE 8080
71+
72+
# --embeddings enables the /v1/embeddings endpoint; --pooling mean matches
73+
# nomic-embed-text; --ctx-size 2048 is the model's training context (episode
74+
# summaries are capped at ~1 KB, well within it). Bind all interfaces — only
75+
# the compose network can reach it (no host port is published).
76+
ENTRYPOINT ["llama-server", \
77+
"--host", "0.0.0.0", "--port", "8080", \
78+
"--model", "/models/embed.gguf", \
79+
"--embeddings", "--pooling", "mean", "--ctx-size", "2048"]

docker/README.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ For the full walkthrough, threat model, and tuning, see
2121
```
2222
docker/
2323
├── Dockerfile # multi-stage build of the odek binary
24-
├── docker-compose.yml # 4 services across 4 profiles
24+
├── Dockerfile.embeddings # llama.cpp embeddings sidecar (bundled GGUF)
25+
├── docker-compose.yml # odek (4 profiles) + searxng + llama-embeddings
2526
├── config.restricted.json # Restricted permission policy
2627
├── config.godmode.json # Godmode (YOLO) permission policy
2728
├── .env.example # copy to .env, add your API key
@@ -187,6 +188,41 @@ sidecar** backing the `web_search` tool — no cloud search API, no keys.
187188
- To run **without** web search: comment out the `searxng` service (and the
188189
`depends_on: [searxng]` lines), and remove the `web_search` block from the configs.
189190

191+
## Local memory embeddings (out of the box)
192+
193+
The compose setup runs a **private [llama.cpp](https://github.com/ggerganov/llama.cpp)
194+
embeddings sidecar** backing odek's memory system — no cloud embeddings API, no keys.
195+
196+
Without it, memory similarity (episode recall, dedup, ranking, fact merge-on-write)
197+
runs on local bag-of-words vectors: fast, but purely lexical — *"fixed the auth bug"*
198+
and *"repaired login issue"* don't match. The sidecar swaps that for a real embedding
199+
model, so recall matches by **meaning**. See
200+
[`../docs/MEMORY.md`](../docs/MEMORY.md)*Pluggable Embeddings* and
201+
[`../docs/CONFIG.md`](../docs/CONFIG.md)`embedding`.
202+
203+
- The `llama-embeddings` service co-starts with every profile and is reachable only by
204+
the odek containers at `http://llama-embeddings:8080` (**no host port** — odek's memory
205+
is the only consumer). Both bundled configs set `memory.embedding` to it.
206+
- The image **bundles `llama-server` (built from source, pinned to the same llama.cpp
207+
release as the main image) and `nomic-embed-text-v1.5`** (768-dim, ~84 MB at Q4_K_M)
208+
— so there's **no first-run model download** and no volume, mirroring the bundled
209+
whisper / MiniCPM-V models. The server runs `--embeddings --pooling mean` and exposes
210+
the OpenAI-compatible `/v1/embeddings` endpoint.
211+
- **Graceful by design:** if the sidecar is still loading or unreachable, recall just
212+
degrades to "no context" and retries with a 30s backoff — the agent loop is never
213+
blocked, and a wrong dedup never deletes an episode. Default behavior without the
214+
service is local RandomProjections.
215+
- Want a higher-quality quantization? Rebuild with
216+
`--build-arg EMBED_QUANT=Q8_0` (available: `Q4_K_M` default, `Q5_K_M`, `Q6_K`, `Q8_0`,
217+
`f16`). To use a different model, override `EMBED_HF_REPO` / `EMBED_HF_REVISION` /
218+
`EMBED_FILE` and update `memory.embedding.model` in the configs.
219+
- To run **without** local embeddings: comment out the `llama-embeddings` service (and
220+
the matching `depends_on` entries), and remove the `embedding` block from the configs
221+
— memory falls back to RandomProjections automatically.
222+
- **Point `base_url` only at a server you trust:** every episode summary and fact entry
223+
is sent there for embedding. Here it's the in-network sidecar, so nothing leaves the
224+
compose network; if you repoint it at a cloud API, that text egresses.
225+
190226
## Verify the profiles differ
191227

192228
- **Restricted**: ask it to `rm -rf` everything in `/workspace` → denied, never runs.

docker/config.godmode.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,12 @@
3232
"llm_extract": true,
3333
"llm_consolidate": true,
3434
"merge_threshold": 0.7,
35-
"add_threshold": 0.3
35+
"add_threshold": 0.3,
36+
"embedding": {
37+
"provider": "http",
38+
"base_url": "http://llama-embeddings:8080/v1",
39+
"model": "nomic-embed-text-v1.5"
40+
}
3641
},
3742
"dangerous": {
3843
"action": "allow",

docker/config.restricted.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,12 @@
3232
"llm_extract": true,
3333
"llm_consolidate": true,
3434
"merge_threshold": 0.7,
35-
"add_threshold": 0.3
35+
"add_threshold": 0.3,
36+
"embedding": {
37+
"provider": "http",
38+
"base_url": "http://llama-embeddings:8080/v1",
39+
"model": "nomic-embed-text-v1.5"
40+
}
3641
},
3742
"dangerous": {
3843
"non_interactive": "deny",

docker/docker-compose.yml

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ services:
3030
- ./workspace:/workspace
3131
- ./config.restricted.json:/home/odek/.odek/config.json:ro
3232
restart: "no"
33-
depends_on: [searxng]
33+
depends_on: [searxng, llama-embeddings]
3434

3535
# ── Godmode (all permissions) — non-interactive, disposable container ──
3636
odek-godmode:
@@ -50,7 +50,7 @@ services:
5050
- ./workspace:/workspace
5151
- ./config.godmode.json:/home/odek/.odek/config.json:ro
5252
restart: "no"
53-
depends_on: [searxng]
53+
depends_on: [searxng, llama-embeddings]
5454

5555
# ── Telegram bot — Restricted (approvals via inline keyboards) ──
5656
# State (sessions, skills, telegram.pid) lives in the local ./.odek folder —
@@ -74,7 +74,7 @@ services:
7474
- ./.odek:/home/odek/.odek
7575
- ./config.restricted.json:/home/odek/.odek/config.json:ro
7676
restart: unless-stopped
77-
depends_on: [searxng]
77+
depends_on: [searxng, llama-embeddings]
7878

7979
# ── Telegram bot — Godmode (no prompts; unrestricted) ──
8080
# Same ./.odek state folder; config.godmode.json is layered on top of
@@ -93,7 +93,35 @@ services:
9393
- ./.odek:/home/odek/.odek
9494
- ./config.godmode.json:/home/odek/.odek/config.json:ro
9595
restart: unless-stopped
96-
depends_on: [searxng]
96+
depends_on: [searxng, llama-embeddings]
97+
98+
# ── llama.cpp embeddings — local backend for memory.embedding ──
99+
# Co-starts with every odek profile so the memory system gets real semantic
100+
# embeddings (recall, dedup, ranking, fact merge) out of the box — no cloud
101+
# embeddings API, no keys. Reachable only by the odek containers over the
102+
# compose network at http://llama-embeddings:8080 — NO host port is published
103+
# (odek's memory system is the only consumer). The GGUF is baked into the
104+
# image (see docker/Dockerfile.embeddings), so there is no first-run download
105+
# and no volume. Both bundled configs point memory.embedding.base_url here.
106+
# To run without local embeddings, comment this service and the matching
107+
# depends_on entries, and drop the "embedding" block from the configs (memory
108+
# then falls back to local RandomProjections — lexical, zero-cost).
109+
llama-embeddings:
110+
profiles: ["restricted", "godmode", "telegram-restricted", "telegram-godmode"]
111+
build:
112+
context: ..
113+
dockerfile: docker/Dockerfile.embeddings
114+
image: odek-embeddings:local
115+
# Health is observability only — odek tolerates the server being briefly
116+
# unavailable (recall degrades to "no context" and rebuilds back off 30s),
117+
# so startup is not gated on it. The model is bundled, so load is quick.
118+
healthcheck:
119+
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8080/health"]
120+
interval: 10s
121+
timeout: 5s
122+
retries: 30
123+
start_period: 60s
124+
restart: unless-stopped
97125

98126
# ── SearXNG — self-hosted metasearch backing the `web_search` tool ──
99127
# Co-starts with every odek profile so web search works out of the box.

docs/DOCKER_COMPOSE_USER_GUIDE.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,17 @@ The compose file also runs a private **SearXNG** metasearch sidecar that backs t
138138
It co-starts with every profile, is reachable only by the odek containers at
139139
`http://searxng:8080` (no host port), and needs only `SEARXNG_SECRET` set above —
140140
no Redis/Valkey. To disable web search, comment the `searxng` service and the
141-
`depends_on: [searxng]` lines in the compose file and drop the `web_search` block
141+
`depends_on` entries in the compose file and drop the `web_search` block
142142
from the config files.
143143

144+
A second sidecar, **`llama-embeddings`** (a llama.cpp server with a bundled
145+
`nomic-embed-text-v1.5` GGUF), gives the memory system real semantic embeddings —
146+
see [docker/README.md](../docker/README.md#local-memory-embeddings-out-of-the-box).
147+
It also co-starts on every profile, is reachable only at `http://llama-embeddings:8080`
148+
(no host port, no key, no first-run download), and both bundled configs point
149+
`memory.embedding` at it. Disable the same way: comment the service + its `depends_on`
150+
entries and drop the `embedding` block (memory falls back to local RandomProjections).
151+
144152
---
145153

146154
## 5. Permission policy files

0 commit comments

Comments
 (0)