diff --git a/AGENTS.md b/AGENTS.md index 6f64946fc..9a5a55467 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -117,7 +117,7 @@ Hook scripts in `src/hooks/` are standalone Node.js scripts (no iii-sdk import). ## Current Stats (v0.9.28) - 54 MCP tools (8 visible by default, `AGENTMEMORY_TOOLS=all` for all) -- 129 REST endpoints +- 130 REST endpoints - 6 MCP resources, 3 MCP prompts - 12 hooks, 15 skills - 260+ iii functions diff --git a/README.md b/README.md index eb6bd1fa6..7e15144f1 100644 --- a/README.md +++ b/README.md @@ -1499,7 +1499,7 @@ Create `~/.agentmemory/.env`:

API

-129 endpoints on port `3111`. The REST API binds to `127.0.0.1` by default. Protected endpoints require `Authorization: Bearer ` when `AGENTMEMORY_SECRET` is set, and mesh sync endpoints require `AGENTMEMORY_SECRET` on both peers. +130 endpoints on port `3111`. The REST API binds to `127.0.0.1` by default. Protected endpoints require `Authorization: Bearer ` when `AGENTMEMORY_SECRET` is set, and mesh sync endpoints require `AGENTMEMORY_SECRET` on both peers.
Key endpoints diff --git a/deploy/docker-compose/.env.example b/deploy/docker-compose/.env.example new file mode 100644 index 000000000..72219fbb4 --- /dev/null +++ b/deploy/docker-compose/.env.example @@ -0,0 +1,39 @@ +# ========================================== +# AgentMemory Configuration +# ========================================== + +# ------------------------------------------ +# 1. LLM Provider (Deepseek Flash V4 / openrouter) +# ------------------------------------------ +# To use Deepseek, you can use OpenRouter or direct DeepSeek API (OpenAI compatible) +# Uncomment one of the following approaches: + +# Approach A: OpenRouter (Recommended for Deepseek) +OPENROUTER_API_KEY=your_openrouter_api_key_here +OPENROUTER_MODEL=deepseek/deepseek-chat + +# Approach B: Direct DeepSeek API +# OPENAI_API_KEY=your_deepseek_api_key_here +# OPENAI_BASE_URL=https://api.deepseek.com +# OPENAI_MODEL=deepseek-chat + +# ------------------------------------------ +# 2. Embeddings Configuration (CPU-only Local) +# ------------------------------------------ +# Set embedding provider to local +EMBEDDING_PROVIDER=local + +# Default model is Xenova/all-MiniLM-L6-v2. If you change it, +# make sure you update the dimensions to match your model! +# NOTE: The custom model support requires building from the latest modified source. +LOCAL_EMBEDDING_MODEL=Xenova/all-MiniLM-L6-v2 +LOCAL_EMBEDDING_DIMENSIONS=384 + +# ------------------------------------------ +# 3. AgentMemory Features +# ------------------------------------------ +# Run LLM compression on every observation batch (Warning: uses LLM tokens) +AGENTMEMORY_AUTO_COMPRESS=false + +# Inject recalled memories back into agent prompts +AGENTMEMORY_INJECT_CONTEXT=false diff --git a/deploy/docker-compose/Dockerfile b/deploy/docker-compose/Dockerfile new file mode 100644 index 000000000..3e6b77c36 --- /dev/null +++ b/deploy/docker-compose/Dockerfile @@ -0,0 +1,39 @@ +# Build stage +FROM node:22-slim AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +RUN npm run build +RUN npm prune --production + +# Final stage +FROM iiidev/iii:0.11.2 AS iii-image + +FROM node:22-slim +WORKDIR /opt/agentmemory +RUN apt-get update \ + && apt-get install -y --no-install-recommends openssl ca-certificates tini gosu curl \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=iii-image /app/iii /usr/local/bin/iii +COPY --from=builder /app/package*.json ./ +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/dist ./dist + +# Symlink so `agentmemory` works globally +RUN ln -s /opt/agentmemory/dist/cli.mjs /usr/local/bin/agentmemory \ + && chmod +x /opt/agentmemory/dist/cli.mjs + +ENV AGENTMEMORY_III_VERSION=0.11.2 \ + TINI_SUBREAPER=1 + +COPY deploy/docker-compose/entrypoint.sh /usr/local/bin/agentmemory-entrypoint.sh +RUN chmod 0755 /usr/local/bin/agentmemory-entrypoint.sh + +EXPOSE 3111 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ + CMD curl -fsS http://127.0.0.1:3111/agentmemory/livez || exit 1 + +ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/agentmemory-entrypoint.sh"] diff --git a/deploy/docker-compose/docker-compose.yml b/deploy/docker-compose/docker-compose.yml new file mode 100644 index 000000000..dd6a0c6bd --- /dev/null +++ b/deploy/docker-compose/docker-compose.yml @@ -0,0 +1,30 @@ +services: + agentmemory: + build: + context: ../.. + dockerfile: deploy/docker-compose/Dockerfile + restart: unless-stopped + env_file: + - .env + expose: + - "3111" + ports: + - "127.0.0.1:3111:3111" + - "127.0.0.1:3112:3112" + - "127.0.0.1:3113:3113" + volumes: + - agentmemory-data:/data + healthcheck: + test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:3111/agentmemory/livez || exit 1"] + interval: 30s + timeout: 5s + start_period: 30s + retries: 3 + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + +volumes: + agentmemory-data: diff --git a/deploy/docker-compose/entrypoint.sh b/deploy/docker-compose/entrypoint.sh new file mode 100644 index 000000000..3a3cef7bf --- /dev/null +++ b/deploy/docker-compose/entrypoint.sh @@ -0,0 +1,97 @@ +#!/bin/sh +# agentmemory first-boot entrypoint. +# +# Runs as root so it can: +# 1. Overwrite the iii-config.yaml with a deploy-tuned version that +# binds 0.0.0.0 and uses absolute /data paths. +# 2. chown the platform-mounted /data volume to the runtime user +# (managed platforms mount volumes root-owned 755 by default). +# 3. Generate the HMAC secret on first boot and persist it to +# /data/.hmac (chmod 600) so the secret survives restarts. +# +# Then it execs the agentmemory CLI under gosu as the unprivileged +# `node` user. + +set -eu + +DATA_DIR="${AGENTMEMORY_DATA_DIR:-/data}" +HMAC_FILE="${AGENTMEMORY_HMAC_FILE:-/data/.hmac}" +RUN_AS="node:node" +III_CONFIG="/opt/agentmemory/dist/iii-config.yaml" + +mkdir -p "$DATA_DIR" +chown -R "$RUN_AS" "$DATA_DIR" + +cat > "$III_CONFIG" <<'EOF' +workers: + - name: iii-http + config: + port: 3111 + host: 0.0.0.0 + default_timeout: 180000 + cors: + allowed_origins: + - "http://localhost:3111" + - "http://localhost:3113" + - "http://127.0.0.1:3111" + - "http://127.0.0.1:3113" + allowed_methods: [GET, POST, PUT, DELETE, OPTIONS] + - name: iii-state + config: + adapter: + name: kv + config: + store_method: file_based + file_path: /data/state_store.db + - name: iii-queue + config: + adapter: + name: builtin + - name: iii-pubsub + config: + adapter: + name: local + - name: iii-cron + config: + adapter: + name: kv + - name: iii-stream + config: + port: 3112 + host: 0.0.0.0 + adapter: + name: kv + config: + store_method: file_based + file_path: /data/stream_store + - name: iii-observability + config: + enabled: true + service_name: agentmemory + exporter: memory + sampling_ratio: 1.0 + metrics_enabled: true + logs_enabled: true + logs_console_output: true +EOF +chown "$RUN_AS" "$III_CONFIG" + +if [ ! -s "$HMAC_FILE" ]; then + SECRET="$(openssl rand -hex 32)" + umask 077 + printf '%s\n' "$SECRET" > "$HMAC_FILE" + chmod 600 "$HMAC_FILE" + chown "$RUN_AS" "$HMAC_FILE" + echo "================================================================" + echo "agentmemory: generated HMAC secret on first boot" + echo "AGENTMEMORY_SECRET=$SECRET" + echo "Copy this value now. It will not be printed again." + echo "Stored at: $HMAC_FILE (chmod 600)" + echo "To rotate: delete $HMAC_FILE on the persistent volume and restart." + echo "================================================================" +fi + +AGENTMEMORY_SECRET="$(cat "$HMAC_FILE")" +export AGENTMEMORY_SECRET + +exec gosu "$RUN_AS" agentmemory "$@" diff --git a/src/index.ts b/src/index.ts index 5f66d76c9..198a6dc3d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -540,7 +540,7 @@ async function main() { `Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`, ); bootLog( - `REST API: 129 endpoints at http://localhost:${config.restPort}/agentmemory/*`, + `REST API: 130 endpoints at http://localhost:${config.restPort}/agentmemory/*`, ); bootLog( `MCP surface (opt-in via \`npx @agentmemory/mcp\`): ${getAllTools().length} tools · 6 resources · 3 prompts`, diff --git a/src/providers/embedding/local.ts b/src/providers/embedding/local.ts index 7646815df..db5d4057f 100644 --- a/src/providers/embedding/local.ts +++ b/src/providers/embedding/local.ts @@ -1,4 +1,5 @@ import type { EmbeddingProvider } from "../../types.js"; +import { getEnvVar } from "../../config.js"; type FeatureExtractor = ( texts: string[], @@ -7,8 +8,21 @@ type FeatureExtractor = ( export class LocalEmbeddingProvider implements EmbeddingProvider { readonly name = "local"; - readonly dimensions = 384; + readonly dimensions: number; private extractor: FeatureExtractor | null = null; + private model: string; + + constructor() { + this.model = getEnvVar("LOCAL_EMBEDDING_MODEL") || "Xenova/all-MiniLM-L6-v2"; + const dim = getEnvVar("LOCAL_EMBEDDING_DIMENSIONS"); + const dimensions = dim ? Number(dim) : 384; + if (!Number.isSafeInteger(dimensions) || dimensions <= 0) { + throw new Error( + "LOCAL_EMBEDDING_DIMENSIONS must be a positive safe integer", + ); + } + this.dimensions = dimensions; + } async embed(text: string): Promise { const [result] = await this.embedBatch([text]); @@ -39,7 +53,7 @@ export class LocalEmbeddingProvider implements EmbeddingProvider { } this.extractor = (await transformers.pipeline( "feature-extraction", - "Xenova/all-MiniLM-L6-v2", + this.model, { dtype: "q8" }, )) as FeatureExtractor; return this.extractor;