# Memory Gateway

Memory Gateway 是一个轻量级记忆网关项目，用统一的 HTTP API 把上层应用接到两个记忆后端：

- OpenViking：负责会话归档、长期记忆抽取、按 `user_id / session_id` 隔离的语义检索。
- EverOS：负责用户画像、episodic memory、profile 查询等补充记忆能力。

当前重点模块是 `memory_system_api`。它不是 OpenViking 的替代品，而是一个更窄的业务网关：上层只面对 user、user key、session 和 message；网关内部为每个业务用户创建独立的 OpenViking admin account。

## 核心模型

### 身份字段

| 字段 | 含义 | 是否自定义 | 备注 |
|---|---|---:|---|
| `user_id` | 真实终端用户 | 是 | 先通过 `/users` 创建，后续写入、查询、commit 时使用 |
| `user_key` | OpenViking 返回的用户 key | 否 | `/users` 返回并写入 SQLite；业务调用时随请求体或 query 传入 |
| `session_id` | 一次会话 ID | 是 | OpenViking session 容器，同时用于带 session context 的搜索 |

建议 ID 只使用字母、数字、`_`、`-`、`.`、`@`，不要使用空格、斜杠或中文。

### 先创建用户再使用

除健康检查外，业务调用分两步：

1. 调用 `POST /memory-system/users` 创建 OpenViking user。
2. 保存返回的 `user_key`，后续写入消息、commit、extract、task、search、profile 都传 `user_id + user_key`，不需要传 `account_id`。

如果没有先创建 user，或者 `user_key` 与数据库里的 `user_id` 不匹配，`messages`、`commit`、`extract`、`tasks`、`search`、`profile` 都会返回 `401`。

创建业务 user 时，网关直接为该用户创建一个独立 OpenViking account。默认 `account_id` 是 `<user_id>_account`，`admin_user_id` 是传入的 `user_id`：

```http
POST /api/v1/admin/accounts
{"account_id": "<user_id>_account", "admin_user_id": "<user_id>"}
```

网关不再调用 `/api/v1/admin/accounts/admin/users`。所有返回的 `admin_user_id / user_key`、创建过的 `session_id`、commit 返回的 `task_id / archive_uri` 都写入 SQLite，不放在进程内缓存里。

### 鉴权

Memory Gateway 有两类 key：

| Key | 请求头 | 用途 | 是否必需 |
|---|---|---|---|
| Memory System API key | `X-API-Key` | 保护 Memory Gateway 服务本身 | 仅当 `server.api_key` 配置非空时需要 |
| OpenViking user key | 请求体/query 的 `user_key` | 校验具体用户并调用 OpenViking 普通用户 API | 除 `/health` 和 `/users` 外都需要 |

OpenViking 的 root key 配在服务端 `config.yaml` 中，由网关内部使用。调用方不需要也不应该在业务请求中传 OpenViking root key。

OpenViking 内部调用遵循：

| OpenViking 场景 | 鉴权 |
|---|---|
| Admin API，例如创建 account | `X-API-Key: <openviking root key>` |
| 普通用户 API，例如 session/message/commit/task/search | `X-API-Key: <openviking user key>` |

### Session 与搜索范围

OpenViking session 由请求里的 `session_id` 创建和提交。`/memory-system/search` 的 OpenViking 分支固定调用 OpenViking `/api/v1/search/search`，`target_uri` 可选，默认是 `viking://user/memories`，并默认传 `level: 2`、`score_threshold: 0.8`。

## 安装

```bash
cd /home/tom/memory-gateway
python -m venv .venv
source .venv/bin/activate
pip install -U pip
pip install -e ".[dev]"
```

## 配置

复制配置模板：

```bash
cp config.example.yaml config.yaml
```

主要配置：

```yaml
server:
  host: "127.0.0.1"
  port: 1934
  api_key: ""

openviking:
  url: "http://127.0.0.1:1933"
  api_key: "your-secret-root-key"
  timeout: 30
  verify_ssl: true

everos:
  url: "http://127.0.0.1:1995"
  api_key: ""
  timeout: 180
  verify_ssl: true
  health_path: "/health"

storage:
  sqlite_path: "/home/tom/memory-gateway/memory_system_api.sqlite3"
```

环境变量也可以覆盖部分配置：

| 环境变量 | 覆盖字段 |
|---|---|
| `MEMORY_SYSTEM_SERVER_API_KEY` | `server.api_key` |
| `MEMORY_SYSTEM_SERVER_HOST` | `server.host` |
| `MEMORY_SYSTEM_SERVER_PORT` | `server.port` |
| `OPENVIKING_URL` / `OPENVIKING_BASE_URL` | `openviking.url` |
| `OPENVIKING_API_KEY` | `openviking.api_key` |
| `EVEROS_URL` / `EVEROS_BASE_URL` | `everos.url` |
| `EVEROS_API_KEY` | `everos.api_key` |
| `MEMORY_SYSTEM_STORAGE_SQLITE_PATH` | `storage.sqlite_path` |

## 启动

先启动 OpenViking 和 EverOS，再启动 Memory System API：

```bash
python -m memory_system_api.server --config config.yaml --host 0.0.0.0 --port 1934
```

如果 `server.api_key` 非空，所有请求还要加：

```bash
-H "X-API-Key: <memory-system-api-key>"
```

## API 总览

Base URL：

```text
http://127.0.0.1:1934/memory-system
```

下面的示例默认先设置：

```bash
API=http://127.0.0.1:1934/memory-system
```

| 方法 | 路径 | 作用 | User key |
|---|---|---|---|
| `GET` | `/health` | 检查 OpenViking 和 EverOS 健康状态 | 不需要 |
| `POST` | `/users` | 在固定 `admin` 工作区创建 OpenViking user，并保存 user key | 不需要 |
| `POST` | `/messages` | 写入一轮或半轮会话消息 | 需要 |
| `POST` | `/sessions/{session_id}/commit` | 提交会话，触发 OpenViking commit 和 EverOS flush | 需要 |
| `POST` | `/sessions/{session_id}/extract` | 立即触发 OpenViking extract | 需要 |
| `GET/POST` | `/sessions/{session_id}/context` | 查询 OpenViking 会话上下文，并用同一 query 搜索 EverOS 记忆 | 需要 |
| `GET/POST` | `/openviking/tasks/{task_id}` | 查询 OpenViking 后台任务状态 | 需要 |
| `GET` | `/memories` | 列出 OpenViking memory URI | 需要 |
| `GET` | `/memories/content` | 读取某条 memory 内容 | 需要 |
| `POST` | `/memories` | 创建、覆盖或追加写入 memory | 需要 |
| `DELETE` | `/memories` | 删除某条 memory URI | 需要 |
| `POST` | `/resources` | 上传本地文件或远程 URL 到 OpenViking resources | 需要 |
| `DELETE` | `/resources` | 删除 OpenViking resource URI | 需要 |
| `POST` | `/search` | 同时搜索 OpenViking 和 EverOS 记忆 | 需要 |
| `GET/POST` | `/users/{user_id}/profile` | 查询 EverOS profile，并补充 OpenViking 用户记忆搜索结果 | 需要 |

## API 详情

### `GET /health`

检查两个后端是否可访问。

```bash
curl -sS "$API/health"
```

返回字段：

| 字段 | 含义 |
|---|---|
| `status` | `success`、`partial_success` 或 `failed` |
| `backends.openviking` | OpenViking 健康检查结果 |
| `backends.everos` | EverOS 健康检查结果 |

### `POST /users`

创建新的 OpenViking admin account，并把返回的 `admin_user_id / user_key` 保存到本地 SQLite。

请求体：

| 参数 | 类型 | 必需 | 说明 |
|---|---|---:|---|
| `user_id` | string | 是 | 要创建的用户 ID |

示例：

```bash
curl -sS -X POST "$API/users" \
  -H "Content-Type: application/json" \
  -d '{"user_id":"userA"}'
```

返回中的 `account.result.user_key` 是后续业务调用要传的 `user_key`：

```json
{
  "status": "success",
  "account": {
    "status": "ok",
    "result": {
      "account_id": "userA_account",
      "admin_user_id": "userA",
      "user_key": "..."
    }
  }
}
```

### `POST /messages`

写入用户消息和/或助手消息。至少要传 `user_message` 或 `assistant_message` 其中一个。网关会先确保 OpenViking user/session 存在，再把消息写入 OpenViking session，同时写入 EverOS。

请求体：

| 参数 | 类型 | 必需 | 说明 |
|---|---|---:|---|
| `user_id` | string | 是 | 已创建的用户 ID |
| `user_key` | string | 是 | `/users` 返回的 user key |
| `session_id` | string | 是 | 会话 ID |
| `user_message` | string/null | 否 | 用户消息 |
| `assistant_message` | string/null | 否 | 助手消息 |
| `timestamp` | int/null | 否 | 预留字段 |
| `metadata` | object | 否 | 预留字段 |

示例：

```bash
USER_KEY="..."

curl -sS -X POST "$API/messages" \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "userA",
    "user_key": "'"$USER_KEY"'",
    "session_id": "sessionA1",
    "user_message": "请记住：我喜欢拿铁。",
    "assistant_message": "好的，我记住了。"
  }'
```

### `POST /sessions/{session_id}/commit`

提交会话。OpenViking 会归档 session 并异步抽取长期记忆，通常会返回 `task_id`；EverOS 会 flush 当前 session。

路径参数：

| 参数 | 类型 | 说明 |
|---|---|---|
| `session_id` | string | 要提交的会话 ID |

请求体：

| 参数 | 类型 | 必需 | 说明 |
|---|---|---:|---|
| `user_id` | string | 是 | 用户 ID |
| `user_key` | string | 是 | `/users` 返回的 user key |

示例：

```bash
curl -sS -X POST "$API/sessions/sessionA1/commit" \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "userA",
    "user_key": "'"$USER_KEY"'"
  }'
```

如果返回里有 OpenViking `task_id`，需要继续查询任务直到完成。

### `POST /sessions/{session_id}/extract`

立即触发 OpenViking extract，只作用于 OpenViking，不触发 EverOS flush。适合调试或明确要求“现在就抽取”的场景。

参数同 commit：

```bash
curl -sS -X POST "$API/sessions/sessionA1/extract" \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "userA",
    "user_key": "'"$USER_KEY"'"
  }'
```

### `GET/POST /sessions/{session_id}/context`

查询一次会话的当前上下文。网关会调用 OpenViking：

```http
GET /api/v1/sessions/{session_id}/context
X-API-Key: <user_key>
```

同时用同一个 `query` 调用 EverOS `/api/v1/memories/search`，返回相关 episodic/profile/raw message 记忆。适合在回答用户问题前，把“当前 session 工作记忆”和“EverOS 相关记忆”一起取回。

路径参数：

| 参数 | 类型 | 说明 |
|---|---|---|
| `session_id` | string | 要查询的会话 ID |

请求体：

| 参数 | 类型 | 必需 | 说明 |
|---|---|---:|---|
| `user_id` | string | 是 | 用户 ID |
| `user_key` | string | 是 | `/users` 返回的 user key |
| `query` | string | 是 | 用于 EverOS search 的查询文本 |
| `limit` | int | 否 | EverOS 记忆返回条数，默认 10，范围 1 到 100 |

POST 示例：

```bash
curl -sS -X POST "$API/sessions/sessionA1/context" \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "userA",
    "user_key": "'"$USER_KEY"'",
    "query": "我喜欢喝什么？",
    "limit": 10
  }'
```

返回字段：

| 字段 | 含义 |
|---|---|
| `context` | OpenViking `result`，包含 `latest_archive_overview`、`pre_archive_abstracts`、`messages`、`stats` |
| `items` | EverOS 搜索命中的精简记忆结果，含 `source_backend: "everos"` |
| `backends` | 两个后端的精简诊断信息，不重复返回完整 OpenViking context 或 EverOS `original_data` |

### `GET/POST /openviking/tasks/{task_id}`

查询 OpenViking 后台任务状态，例如 commit 返回的任务。

请求体：

| 参数 | 类型 | 必需 | 说明 |
|---|---|---:|---|
| `user_id` | string | 是 | 用户 ID |
| `user_key` | string | 是 | `/users` 返回的 user key |
| `session_id` | string/null | 否 | 会话 ID；传入后会按同一 session identity 查询 |

示例：

```bash
curl -sS -X POST "$API/openviking/tasks/${TASK_ID}" \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "userA",
    "user_key": "'"$USER_KEY"'",
    "session_id": "sessionA1"
  }'
```

### `GET /memories`

列出 OpenViking memory URI。网关会调用 OpenViking：

```http
GET /api/v1/fs/ls?uri=viking://user/memories&recursive=true
X-API-Key: <user_key>
```

Query 参数：

| 参数 | 类型 | 必需 | 说明 |
|---|---|---:|---|
| `user_id` | string | 是 | 用户 ID |
| `user_key` | string | 是 | `/users` 返回的 user key |
| `uri` | string | 否 | 要列出的 memory 根 URI，默认 `viking://user/memories` |
| `recursive` | bool | 否 | 是否递归列出，默认 `true` |

示例：

```bash
curl -sS -G "$API/memories" \
  --data-urlencode "user_id=userA" \
  --data-urlencode "user_key=$USER_KEY" \
  --data-urlencode "uri=viking://user/memories" \
  --data-urlencode "recursive=true"
```

### `GET /memories/content`

读取某条 memory 内容。先用 `/memories` 或 `/search` 找到 `viking://user/memories/...` URI，再读取：

```bash
curl -sS -G "$API/memories/content" \
  --data-urlencode "user_id=userA" \
  --data-urlencode "user_key=$USER_KEY" \
  --data-urlencode "uri=viking://user/memories/preferences/python.md"
```

### `POST /memories`

创建、覆盖或追加写入 memory。网关会调用 OpenViking `/api/v1/content/write`，写入后由 OpenViking 刷新语义和向量索引。

请求体：

| 参数 | 类型 | 必需 | 说明 |
|---|---|---:|---|
| `user_id` | string | 是 | 用户 ID |
| `user_key` | string | 是 | `/users` 返回的 user key |
| `uri` | string | 是 | 目标 memory URI，例如 `viking://user/memories/profile.md` |
| `content` | string | 是 | 要写入的 Markdown/text 内容 |
| `mode` | `create`/`replace`/`append` | 否 | 写入模式，默认 `create` |
| `wait` | bool | 否 | 是否等待索引刷新，默认 `true` |

覆盖修改：

```bash
curl -sS -X POST "$API/memories" \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "userA",
    "user_key": "'"$USER_KEY"'",
    "uri": "viking://user/memories/preferences/python.md",
    "content": "# Python 偏好\n\n用户偏好使用 Python 做数据分析，常用 pandas。",
    "mode": "replace",
    "wait": true
  }'
```

追加补充时把 `mode` 改为 `append`；新增 memory 时可用默认的 `create`。

### `DELETE /memories`

删除某条 memory。默认非递归删除；如果 OpenViking 提示目标是目录或复合资源，再把 `recursive` 设为 `true`。

```bash
curl -sS -X DELETE -G "$API/memories" \
  --data-urlencode "user_id=userA" \
  --data-urlencode "user_key=$USER_KEY" \
  --data-urlencode "uri=viking://user/memories/preferences/python.md" \
  --data-urlencode "recursive=false"
```

返回中的 `memory` 是 OpenViking 对应接口的原始响应。

### `POST /resources`

上传文件资源到 OpenViking。网关只调用 OpenViking，不写 EverOS。

如果 `path` 是本地路径，文件必须能被 Memory System API 服务进程读取。网关会先调用 OpenViking `/api/v1/resources/temp_upload` 上传临时文件，取返回的 `temp_file_id`，再调用 `/api/v1/resources` 添加资源。

如果 `path` 是 `http://` 或 `https://` URL，网关会直接调用 OpenViking `/api/v1/resources`，并把 URL 作为 `path` 传给 OpenViking。

请求体：

| 参数 | 类型 | 必需 | 说明 |
|---|---|---:|---|
| `user_id` | string | 是 | 用户 ID |
| `user_key` | string | 是 | `/users` 返回的 user key |
| `path` | string | 是 | 本地文件路径，或 `http://` / `https://` URL |
| `to` | string | 是 | 目标 OpenViking resource URI |
| `reason` | string/null | 否 | 上传原因，透传给 OpenViking |
| `wait` | bool | 否 | 是否等待处理完成，默认 `true` |
| `directly_upload_media` | bool | 否 | 是否直接上传媒体，默认 `true` |

本地文件示例：

```bash
curl -sS -X POST "$API/resources" \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "userA",
    "user_key": "'"$USER_KEY"'",
    "path": "/home/tom/memory-gateway/tests/大语言模型应用.pdf",
    "to": "viking://resources/userA/files/大语言模型应用.pdf",
    "reason": "userA 上传的文件",
    "wait": true,
    "directly_upload_media": true
  }'
```

远程 URL 示例：

```bash
curl -sS -X POST "$API/resources" \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "userA",
    "user_key": "'"$USER_KEY"'",
    "path": "https://example.com/images/photo.png",
    "to": "viking://resources/userA/images/photo.png",
    "reason": "userA 上传的远程图片",
    "wait": true,
    "directly_upload_media": true
  }'
```

返回中的 `resource` 是 OpenViking `/api/v1/resources` 的原始响应，`backends.openviking` 保留调用状态和错误信息。

### `DELETE /resources`

删除 OpenViking resource URI。网关会调用 OpenViking：

```http
DELETE /api/v1/fs?uri=<OPENVIKING_URI>&recursive=<true|false>
X-API-Key: <user_key>
```

Query 参数：

| 参数 | 类型 | 必需 | 说明 |
|---|---|---:|---|
| `user_id` | string | 是 | 用户 ID |
| `user_key` | string | 是 | `/users` 返回的 user key |
| `uri` | string | 是 | 要删除的 OpenViking URI |
| `recursive` | bool | 否 | 是否递归删除，默认 `true` |

示例：

```bash
curl -sS -X DELETE -G "$API/resources" \
  --data-urlencode "user_id=userA" \
  --data-urlencode "user_key=$USER_KEY" \
  --data-urlencode "uri=viking://resources/userA/files/大语言模型应用.pdf" \
  --data-urlencode "recursive=true"
```

返回中的 `resource` 是 OpenViking `/api/v1/fs` 删除接口的原始响应。

### `POST /search`

同时查询 OpenViking 和 EverOS，并合并结果。

请求体：

| 参数 | 类型 | 必需 | 说明 |
|---|---|---:|---|
| `user_id` | string | 是 | 用户 ID |
| `user_key` | string | 是 | `/users` 返回的 user key |
| `session_id` | string/null | 否 | 会话 ID；用于 EverOS 过滤和鉴权身份 |
| `query` | string | 是 | 查询文本 |
| `use_llm` | bool | 否 | 只影响 EverOS 检索方式：`false` 使用 hybrid，`true` 使用 agentic |
| `limit` | int | 否 | 返回条数，默认 10，范围 1 到 100 |
| `level` | int | 否 | OpenViking search level，默认 2 |
| `score_threshold` | float | 否 | OpenViking 最低分数阈值，默认 0.8，范围 0 到 1 |
| `target_uri` | string | 否 | OpenViking 搜索范围，默认 `viking://user/memories` |

示例：

```bash
curl -sS -X POST "$API/search" \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "userA",
    "user_key": "'"$USER_KEY"'",
    "session_id": "sessionA1",
    "query": "我喜欢喝什么？",
    "use_llm": false,
    "limit": 10,
    "level": 2,
    "score_threshold": 0.8,
    "target_uri": "viking://user/memories"
  }'
```

返回字段：

| 字段 | 含义 |
|---|---|
| `status` | 总体状态 |
| `items` | 合并后的记忆结果，含 `source_backend` |
| `backends` | 两个后端的精简诊断信息，例如命中数量、query plan 或查询过滤条件；不再回传完整原始命中和 `original_data` |

### `GET/POST /users/{user_id}/profile`

读取用户画像。该接口需要 `user_key`，用于确认调用方属于该 user。网关会读取 EverOS profile，并用同一个 `query` 调 OpenViking `/api/v1/search/search`，固定传 `target_uri: viking://user/memories` 和 `level`。

请求体：

| 参数 | 类型 | 必需 | 说明 |
|---|---|---:|---|
| `user_key` | string | 是 | `/users` 返回的 user key |
| `query` | string | 否 | OpenViking 搜索文本，默认 `用户画像` |
| `limit` | int | 否 | OpenViking 返回条数，默认 10，范围 1 到 100 |
| `level` | int | 否 | OpenViking search level，默认 2 |

示例：

```bash
curl -sS -X POST "$API/users/userA/profile" \
  -H "Content-Type: application/json" \
  -d '{
    "user_key": "'"$USER_KEY"'",
    "query": "我想喝东西",
    "limit": 10,
    "level": 2
  }'
```

返回中的 `profile` 是 EverOS profile 原始结果，`items` 是 OpenViking 命中的用户记忆，`backends` 只保留两个后端的状态和计数诊断，不重复返回完整 EverOS profile 或 OpenViking 命中列表。

## 完整测试流程

```bash
API=http://127.0.0.1:1934/memory-system

curl -sS -X POST "$API/users" \
  -H "Content-Type: application/json" \
  -d '{"user_id":"userA"}'

USER_KEY="把上一步返回的 account.result.user_key 填到这里"

curl -sS -X POST "$API/messages" \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "userA",
    "user_key": "'"$USER_KEY"'",
    "session_id": "sessionA1",
    "user_message": "请记住：我在 sessionA1 喜欢拿铁，项目代号 alpha-session。",
    "assistant_message": "好的。"
  }'

curl -sS -X POST "$API/sessions/sessionA1/commit" \
  -H "Content-Type: application/json" \
  -d '{"user_id":"userA","user_key":"'"$USER_KEY"'"}'

curl -sS -X POST "$API/search" \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "userA",
    "user_key": "'"$USER_KEY"'",
    "session_id": "sessionA1",
    "query": "我喜欢喝什么？",
    "limit": 10
  }'
```

OpenViking commit 返回 `task_id` 后，轮询到 `completed` 再搜索长期 memory：

```bash
curl -sS -X POST "$API/openviking/tasks/<task_id>" \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "userA",
    "user_key": "'"$USER_KEY"'",
    "session_id": "sessionA1"
  }'
```

## 开发检查

```bash
PYTHONPATH=/home/tom/memory-gateway pytest -q
python -m compileall -q memory_system_api plugins eval tests
```

## Hermes Plugin 状态

仓库里仍保留 Hermes memory provider：`plugins/memory/memory_system`。

注意：Memory System API 已切换为 `user_id + user_key` 鉴权，plugin 代码如需继续使用，需要同步传递这两个字段。
