Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
859 changes: 859 additions & 0 deletions DEPLOY_MANUAL.md

Large diffs are not rendered by default.

55 changes: 20 additions & 35 deletions mem0/memory/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1025,17 +1025,19 @@ def get_all(

Args:
filters (dict): Filter dict containing entity IDs and optional metadata filters.
Must contain at least one of: user_id, agent_id, run_id.
Optional: when provided, can contain user_id, agent_id, run_id.
Example: filters={"user_id": "u1", "agent_id": "a1"}
When omitted or empty, searches across all users.
top_k (int, optional): The maximum number of memories to return. Defaults to 20.

Returns:
dict: A dictionary containing a list of memories under the "results" key.
Example for v1.1+: `{"results": [{"id": "...", "memory": "...", ...}]}`

Raises:
ValueError: If filters doesn't contain at least one of user_id, agent_id, run_id,
or if top_k is invalid.
ValueError: If top_k is invalid.


"""
# Reject top-level entity params - must use filters instead
_reject_top_level_entity_params(kwargs, "get_all")
Expand All @@ -1058,12 +1060,8 @@ def get_all(
effective_filters["run_id"], "run_id"
)

# Validate filters contains at least one entity ID
if not any(key in effective_filters for key in ("user_id", "agent_id", "run_id")):
raise ValueError(
"filters must contain at least one of: user_id, agent_id, run_id. "
"Example: filters={'user_id': 'u1'}"
)
# Allow empty filters to fetch all memories
# (previously required at least one of user_id/agent_id/run_id)

limit = top_k

Expand Down Expand Up @@ -1140,8 +1138,9 @@ def search(
query (str): Query to search for.
top_k (int, optional): Maximum number of results to return. Defaults to 20.
filters (dict): Filter dict containing entity IDs and optional metadata filters.
Must contain at least one of: user_id, agent_id, run_id.
Optional: when provided, can contain user_id, agent_id, run_id.
Example: filters={"user_id": "u1", "agent_id": "a1"}
When omitted or empty, searches across all users.

Enhanced metadata filtering with operators:
- {"key": "value"} - exact match
Expand All @@ -1167,8 +1166,7 @@ def search(
Example for v1.1+: `{"results": [{"id": "...", "memory": "...", "score": 0.8, ...}]}`

Raises:
ValueError: If filters doesn't contain at least one of user_id, agent_id, run_id,
or if threshold/top_k values are invalid.
ValueError: If threshold/top_k values are invalid.
"""
# Reject top-level entity params - must use filters instead
_reject_top_level_entity_params(kwargs, "search")
Expand All @@ -1190,11 +1188,8 @@ def search(
effective_filters["run_id"] = _validate_and_trim_entity_id(
effective_filters["run_id"], "run_id"
)
if not any(key in effective_filters for key in ("user_id", "agent_id", "run_id")):
raise ValueError(
"filters must contain at least one of: user_id, agent_id, run_id. "
"Example: filters={'user_id': 'u1'}"
)
# Allow empty filters to search across all users
# (previously required at least one of user_id/agent_id/run_id)

limit = top_k

Expand Down Expand Up @@ -2440,17 +2435,17 @@ async def get_all(

Args:
filters (dict): Filter dict containing entity IDs and optional metadata filters.
Must contain at least one of: user_id, agent_id, run_id.
Optional: when provided, can contain user_id, agent_id, run_id.
Example: filters={"user_id": "u1", "agent_id": "a1"}
When omitted or empty, searches across all users.
top_k (int, optional): The maximum number of memories to return. Defaults to 20.

Returns:
dict: A dictionary containing a list of memories under the "results" key.
Example for v1.1+: `{"results": [{"id": "...", "memory": "...", ...}]}`

Raises:
ValueError: If filters doesn't contain at least one of user_id, agent_id, run_id,
or if top_k is invalid.
ValueError: If top_k is invalid.
"""
# Reject top-level entity params - must use filters instead
_reject_top_level_entity_params(kwargs, "get_all")
Expand All @@ -2473,12 +2468,7 @@ async def get_all(
effective_filters["run_id"], "run_id"
)

# Validate filters contains at least one entity ID
if not any(key in effective_filters for key in ("user_id", "agent_id", "run_id")):
raise ValueError(
"filters must contain at least one of: user_id, agent_id, run_id. "
"Example: filters={'user_id': 'u1'}"
)
# Allow empty filters to fetch all memories

limit = top_k

Expand Down Expand Up @@ -2555,8 +2545,9 @@ async def search(
query (str): Query to search for.
top_k (int, optional): Maximum number of results to return. Defaults to 20.
filters (dict): Filter dict containing entity IDs and optional metadata filters.
Must contain at least one of: user_id, agent_id, run_id.
Optional: when provided, can contain user_id, agent_id, run_id.
Example: filters={"user_id": "u1", "agent_id": "a1"}
When omitted or empty, searches across all users.

Enhanced metadata filtering with operators:
- {"key": "value"} - exact match
Expand All @@ -2582,8 +2573,7 @@ async def search(
Example for v1.1+: `{"results": [{"id": "...", "memory": "...", "score": 0.8, ...}]}`

Raises:
ValueError: If filters doesn't contain at least one of user_id, agent_id, run_id,
or if threshold/top_k values are invalid.
ValueError: If threshold/top_k values are invalid.
"""
# Reject top-level entity params - must use filters instead
_reject_top_level_entity_params(kwargs, "search")
Expand All @@ -2606,12 +2596,7 @@ async def search(
effective_filters["run_id"], "run_id"
)

# Validate filters contains at least one entity ID
if not any(key in effective_filters for key in ("user_id", "agent_id", "run_id")):
raise ValueError(
"filters must contain at least one of: user_id, agent_id, run_id. "
"Example: filters={'user_id': 'u1'}"
)
# Allow empty filters to search across all users

limit = top_k

Expand Down
44 changes: 44 additions & 0 deletions server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,45 @@ Wire the command into cron or a systemd timer in production. The `created_at` co
- API: `http://localhost:8888`
- OpenAPI docs: `http://localhost:8888/docs`

> These are localhost defaults. For LAN access, set `DASHBOARD_URL` and `NEXT_PUBLIC_API_URL` to your machine's LAN IP in `.env`.

### LAN / Network Deployment

The stack supports serving LAN clients out of the box. Set these environment variables in your `.env` file:

```env
DASHBOARD_URL=http://YOUR_LAN_IP:3000
NEXT_PUBLIC_API_URL=http://YOUR_LAN_IP:8888
INSTANCE_NAME=Mem0
EXTRA_CORS_ORIGINS=
```

**CORS behavior:**
- When `AUTH_DISABLED=true` (local dev), CORS allows all origins (`*`) — any LAN client can call the API without origin configuration.
- When auth is enabled, CORS allows `DASHBOARD_URL` plus any origins listed in `EXTRA_CORS_ORIGINS` (comma-separated).
- Use `EXTRA_CORS_ORIGINS` to whitelist additional frontend apps or tools running on other LAN machines.

**PostgreSQL access:**
- By default, the Postgres port (8432) is bound to `127.0.0.1` (localhost only) for security.
- To expose Postgres to the LAN, change the port mapping in `docker-compose.yaml` from `127.0.0.1:8432:5432` to `8432:5432`.

**Local source install:**
- The API container installs from the local `mem0/` source directory (`/opt/mem0-src`) instead of PyPI, so code changes are picked up on container rebuild without publishing a package.

### Transient Error Retry

The API automatically retries transient upstream errors (provider timeouts, rate limits, service unavailable) on the following endpoints:

- `POST /memories`
- `POST /search`

Retry settings:
- **Attempts:** 3
- **Backoff:** Linear (1s × attempt number)
- **Transient codes:** `provider_timeout`, `provider_rate_limited`, `provider_unavailable`, `datastore_unavailable`, `vector_store_unavailable`, `provider_bad_request`

Client errors (400/401/403/404/422) are never retried.

## Dashboard

Once logged in, the dashboard exposes:
Expand Down Expand Up @@ -121,6 +160,11 @@ The dashboard sets the following response headers on every path (see `server/das

Together these prevent iframe embedding, sniffing of mislabelled MIME types, and cross-origin referrer leaks. Harden further behind your own reverse proxy if needed.

## API Notes

- `POST /search` with no `filters` field (or empty filters) searches across all users.
- `DELETE /memories/{id}` returns 404 for invalid or not-found memory IDs.

## Reference

Additional product and API documentation lives at [docs.mem0.ai](https://docs.mem0.ai/open-source/overview).
4 changes: 2 additions & 2 deletions server/dashboard/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:20-alpine AS base
FROM node:22-alpine AS base
RUN apk add --no-cache libc6-compat
WORKDIR /app

Expand All @@ -8,7 +8,7 @@ COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile --network-timeout 600000; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i; \
elif [ -f pnpm-lock.yaml ]; then npm install -g pnpm@9 && pnpm i; \
else npm install; \
fi

Expand Down
2 changes: 1 addition & 1 deletion server/dashboard/src/app/api/auth/refresh/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { getServerApiUrl } from "@/lib/server-api-url";
const COOKIE_NAME = "mem0_refresh_token";
const COOKIE_OPTIONS = {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
secure: process.env.COOKIE_SECURE === "true",
sameSite: "lax" as const,
path: "/",
maxAge: 30 * 24 * 60 * 60, // 30 days
Expand Down
20 changes: 14 additions & 6 deletions server/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,28 @@ services:
volumes:
- ./history:/app/history
- .:/app
- ..:/opt/mem0-src
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s
command: >
sh -c "rm -rf /app/packages && pip install -q --force-reinstall --no-deps mem0ai && alembic upgrade head && uvicorn main:app --host 0.0.0.0 --port 8000 --reload"
sh -c "pip install -q --force-reinstall --no-deps /opt/mem0-src && alembic upgrade head && uvicorn main:app --host 0.0.0.0 --port 8000 --reload"
environment:
- PYTHONDONTWRITEBYTECODE=1
- PYTHONUNBUFFERED=1
- PYTHONPATH=
- DASHBOARD_URL=http://localhost:3000
- DASHBOARD_URL=${DASHBOARD_URL:-http://localhost:3000}
- APP_DB_NAME=mem0_app
- JWT_SECRET=${JWT_SECRET}
- AUTH_DISABLED=${AUTH_DISABLED:-false}
- MEM0_TELEMETRY=${MEM0_TELEMETRY:-true}
- EXTRA_CORS_ORIGINS=${EXTRA_CORS_ORIGINS:-}

postgres:
image: ankane/pgvector:v0.5.1
Expand All @@ -47,7 +55,7 @@ services:
- postgres_db:/var/lib/postgresql/data
- ./init-db.sh:/docker-entrypoint-initdb.d/init-db.sh
ports:
- "8432:5432"
- "127.0.0.1:8432:5432"

mem0-dashboard:
build: ./dashboard
Expand All @@ -56,14 +64,14 @@ services:
networks:
- mem0_network
environment:
- NEXT_PUBLIC_API_URL=http://localhost:8888
- NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-http://localhost:8888}
- API_INTERNAL_URL=http://mem0:8000
- NEXT_PUBLIC_INSTANCE_NAME=Mem0
- NEXT_PUBLIC_INSTANCE_NAME=${INSTANCE_NAME:-Mem0}
depends_on:
mem0:
condition: service_started
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/health"]
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/api/health"]
interval: 10s
timeout: 5s
retries: 3
Expand Down
Loading