feat: Phase 2 — embeddings + hybrid memory.search
Adds a small embedder sidecar (Xenova/bge-small-en-v1.5, ONNX, CPU-only) that the web app calls inline on memory.write and memory.update, and on demand from the new memory.search tool. memory.search performs three candidate fetches in parallel — pgvector cosine similarity, Postgres full-text via plainto_tsquery + ts_rank_cd, and tag-set overlap — then fuses them with Reciprocal Rank Fusion (k=60). Each result carries its per-source rank so the model can see *why* a memory surfaced. The migrator boot step gained an idempotent embedding backfill: any row with embedding IS NULL is batched (32 at a time) through the embedder after SQL migrations apply. Safe to run on every boot. New tool memory.update fixes the missing edit path; centralises the re-embed-on-content-change rule alongside write. Stack additions: - apps/embedder/ — Fastify server, persistent /data/models volume so the ~30 MB model only downloads once - apps/web/lib/embedder.ts — typed HTTP client with batched embed + health probe - packages/schemas — MemoryUpdateInput, MemorySearchInput - docker-compose — embedder service, healthcheck, app + migrator both depend_on it healthy; EMBEDDER_URL promoted to a required env var Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+33
-3
@@ -41,8 +41,32 @@ services:
|
||||
networks:
|
||||
- internal
|
||||
|
||||
# One-shot migration runner. Exits 0 when migrations are up-to-date;
|
||||
# `app` waits on its successful completion before starting.
|
||||
# Embedding sidecar — loads bge-small-en-v1.5 once and serves /embed.
|
||||
# First boot downloads the model (~30 MB) into a named volume so future
|
||||
# boots are warm.
|
||||
embedder:
|
||||
image: ${EMBEDDER_IMAGE_REF:-shared-memory-embedder:local}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: apps/embedder/Dockerfile
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
EMBEDDING_MODEL: ${EMBEDDING_MODEL:-Xenova/bge-small-en-v1.5}
|
||||
EMBEDDING_DIM: ${EMBEDDING_DIM:-384}
|
||||
LOG_LEVEL: ${LOG_LEVEL:-info}
|
||||
volumes:
|
||||
- embedder_models:/data/models
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q -O - http://127.0.0.1:8080/health | grep -q '\"ready\":true' || exit 1"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 180s
|
||||
networks:
|
||||
- internal
|
||||
|
||||
# One-shot migration runner + embedding backfill. Exits 0 when both are
|
||||
# up-to-date; `app` waits on its successful completion before starting.
|
||||
migrator:
|
||||
image: ${IMAGE_REF:-shared-memory-web:local}
|
||||
build:
|
||||
@@ -52,8 +76,11 @@ services:
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
embedder:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
||||
EMBEDDER_URL: ${EMBEDDER_URL:-http://embedder:8080}
|
||||
command: ["node", "apps/web/migrate.mjs"]
|
||||
networks:
|
||||
- internal
|
||||
@@ -67,6 +94,8 @@ services:
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
embedder:
|
||||
condition: service_healthy
|
||||
migrator:
|
||||
condition: service_completed_successfully
|
||||
environment:
|
||||
@@ -87,7 +116,7 @@ services:
|
||||
|
||||
DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
||||
|
||||
EMBEDDER_URL: ${EMBEDDER_URL:-}
|
||||
EMBEDDER_URL: ${EMBEDDER_URL:-http://embedder:8080}
|
||||
EMBEDDING_MODEL: ${EMBEDDING_MODEL:-Xenova/bge-small-en-v1.5}
|
||||
EMBEDDING_DIM: ${EMBEDDING_DIM:-384}
|
||||
|
||||
@@ -137,6 +166,7 @@ volumes:
|
||||
db_data:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
embedder_models:
|
||||
|
||||
networks:
|
||||
internal:
|
||||
|
||||
Reference in New Issue
Block a user