Files
memory-gateway/README.md

640 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Memory Gateway
Memory Gateway 是一个轻量级 FastAPI 服务,用于在上游记忆服务现有
`/api/v1/memory/add``/api/v1/memory/flush``/api/v1/memory/search`
能力之上构建用户资源记忆层。
它只维护 Gateway 自己的 SQLite 元数据表、软删除记录和手动覆盖记录,
不会直接修改上游记忆服务的 Markdown、SQLite 或 LanceDB 内部文件。
## 功能范围
- 上传用户资源文件、图片、音频、PDF、HTML、普通文档、纯文本。
- 保存资源元数据到 SQLite。
- 为每个资源生成独立的上游记忆服务 `session_id`
- 调用上游记忆服务的 `add``flush` 完成资源记忆摄入,并对临时失败做轻量重试。
- 提供资源列表、详情、软删除。
- 支持上传大小限制、MIME 白名单、同用户同 app/project 下按 sha256 幂等复用资源。
- 编排记忆搜索,支持当前聊天、资源记忆、全部用户记忆。
- 支持记忆 tombstone 软删除。
- 支持记忆手动 override。
- 搜索结果返回前自动过滤 tombstone 并应用 override。
## 目录结构
```text
/home/tom/memory-gateway
├── core/ # Gateway 核心代码
│ ├── api.py # FastAPI 路由
│ ├── config.py # 环境变量配置
│ ├── db.py # SQLite schema 初始化
│ ├── backend_client.py # 上游记忆服务 HTTP client
│ ├── repository.py # SQLite 读写
│ └── service.py # 业务编排
├── main.py # Python 启动入口
├── tests/ # 测试
├── .env.example # 环境变量示例
└── pyproject.toml
```
## 环境配置
复制示例配置:
```bash
cd /home/tom/memory-gateway
cp .env.example .env
```
配置项说明:
| 变量 | 默认值 | 说明 |
|---|---|---|
| `MEMORY_GATEWAY_BACKEND_BASE_URL` | `http://127.0.0.1:1995` | 上游记忆服务 API 地址;服务端可监听 `0.0.0.0:1995`,本机客户端通常连接 `127.0.0.1:1995` |
| `MEMORY_GATEWAY_DB_PATH` | `./data/memory_gateway.sqlite3` | Gateway 自己的 SQLite 数据库路径 |
| `MEMORY_GATEWAY_STORAGE_DIR` | `./data/storage` | 用户上传原始文件保存路径 |
| `MEMORY_GATEWAY_RESOURCE_SEARCH_BATCH_SIZE` | `50` | resources scope 搜索时每批 session_id 数量 |
| `MEMORY_GATEWAY_MAX_UPLOAD_BYTES` | `26214400` | 单个上传文件最大字节数,默认 25MB |
| `MEMORY_GATEWAY_ALLOWED_MIME_TYPES` | 常见图片、音频、PDF、HTML、文本和 Office 文档 | 逗号分隔的上传 MIME 白名单,支持 `image/*` 这类前缀匹配 |
| `MEMORY_GATEWAY_BACKEND_INGEST_ATTEMPTS` | `3` | 上游记忆服务 `add``flush` 各自最多重试次数 |
| `MEMORY_GATEWAY_BACKEND_RETRY_DELAY_SECONDS` | `0.25` | 上游记忆服务摄入重试间隔秒数 |
| `MEMORY_GATEWAY_BACKEND_TIMEOUT_SECONDS` | `120` | 单次上游记忆服务 HTTP 请求超时秒数 |
| `MEMORY_GATEWAY_HOST` | `127.0.0.1` | Gateway API 监听地址 |
| `MEMORY_GATEWAY_PORT` | `8010` | Gateway API 监听端口 |
| `MEMORY_GATEWAY_RELOAD` | `false` | 是否启用 uvicorn reload开发时可设为 `true` |
注意:`MEMORY_GATEWAY_DB_PATH``MEMORY_GATEWAY_STORAGE_DIR` 是 Gateway
自己的存储位置,不要配置成上游记忆服务的内部存储目录。
## 安装依赖
```bash
cd /home/tom/memory-gateway
uv pip install -e .
```
## 启动 API
使用 Python 启动:
```bash
cd /home/tom/memory-gateway
python main.py
```
默认监听:
```text
http://127.0.0.1:8010
```
## session_id 规范
Gateway 遵循以下 `session_id` 规范:
| 场景 | 格式 |
|---|---|
| 普通聊天 | `chat:{conversation_id}` |
| 用户上传资源 | `resource:{user_id}:{resource_id}` |
| 用户手动修正 | `memory_edit:{user_id}` |
当前实现中,资源上传会自动生成:
```text
resource:{user_id}:{resource_id}
```
## 数据库表
启动时会自动创建以下 SQLite 表:
- `user_resources`:资源元数据、内部 URI、状态、软删除时间。
- `users`:用户 ID 和 Gateway 生成的 `user_key`
- `memory_tombstones`:用户删除的 memory id 或 session_id。
- `memory_overrides`:用户手动修正后的 memory 文本。
## API 日志
Gateway 会通过 `memory_gateway.api` logger 为每个 API 请求输出一条 JSON 日志,字段包括:
- `request_time`:请求进入 Gateway 的 UTC 时间。
- `duration_ms`:接口处理耗时。
- `method``path``url``client`:请求方法、地址和客户端地址。
- `input`query 参数和请求体。`user_key`、token、password、secret、API key 等敏感字段会记录为 `[REDACTED]`multipart 上传只记录 content type 和大小,不记录文件内容。
- `output`HTTP 状态码和响应体;敏感字段同样会遮蔽。
## API 使用说明
`POST /users` 外,所有业务 API 都需要携带 `user_id``user_key`。认证失败返回 `401`
### 1. 健康检查
```http
GET /health
```
该接口不需要 `user_id``user_key`,用于确认 Gateway API 是否可响应,以及上游记忆服务是否可访问。
请求示例:
```bash
curl http://127.0.0.1:8010/health
```
上游记忆服务正常时响应示例:
```json
{
"status": "ok",
"api": {
"status": "ok"
},
"backend": {
"status": "ok",
"base_url": "http://127.0.0.1:1995",
"data": {
"status": "ok"
}
}
}
```
上游记忆服务不可访问时仍返回 HTTP 200`status` 会变成 `degraded`便于区分“Gateway API 活着”和“上游记忆服务故障”:
```json
{
"status": "degraded",
"api": {
"status": "ok"
},
"backend": {
"status": "unavailable",
"base_url": "http://127.0.0.1:1995",
"error": "Connection refused"
}
}
```
### 2. 创建用户
```http
POST /users
Content-Type: application/json
```
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `user_id` | string | 是 | 用户 ID |
请求示例:
```bash
curl -X POST http://127.0.0.1:8010/users \
-H 'Content-Type: application/json' \
-d '{"user_id":"u_123"}'
```
响应示例:
```json
{
"user_id": "u_123",
"user_key": "uk_xxx",
"created_at": "2026-06-10T10:00:00+00:00"
}
```
`user_key` 需要由调用方保存,后续上传、查询、搜索、修改和删除都要传入。如果同一个 `user_id` 已存在,接口会返回已有 `user_key`
### 3. 上传资源
```http
POST /resources
Content-Type: multipart/form-data
```
表单参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| `user_id` | string | 是 | 无 | 用户 ID |
| `user_key` | string | 是 | 无 | 用户 key |
| `app_id` | string | 否 | `default` | 上游记忆服务 app scope |
| `project_id` | string | 否 | `default` | 上游记忆服务 project scope |
| `file` | file | 是 | 无 | 上传资源文件 |
| `title` | string | 否 | `null` | 资源标题 |
| `description` | string | 否 | `null` | 资源描述 |
处理流程:
1. 保存原始文件到 `MEMORY_GATEWAY_STORAGE_DIR`
2. 计算 `sha256``size_bytes`
3. 生成 `resource_id`
4. 生成 `session_id = resource:{user_id}:{resource_id}`
5. 写入 `user_resources`,状态为 `ingesting`
6. 根据 MIME 类型映射上游记忆服务 content type。
7. 构造上游记忆服务 content item文本类上传以内联 `text` 发送,非文本上传以内联 `base64` 发送,不要求上游记忆服务访问 Gateway 本地 `file://` 路径。
8. 调用上游记忆服务的 `/api/v1/memory/add`
9. 调用上游记忆服务的 `/api/v1/memory/flush`
10. 成功后状态改为 `extracted`,失败后状态改为 `failed`
上传策略:
- 文件会按流式方式写入磁盘,超过 `MEMORY_GATEWAY_MAX_UPLOAD_BYTES` 会返回 `413`,不会写入资源记录。
- MIME 类型不在 `MEMORY_GATEWAY_ALLOWED_MIME_TYPES` 白名单内会返回 `415`
- 同一用户在同一 `app_id``project_id` 下重复上传相同 sha256 的活跃资源,会直接返回已有资源,避免重复调用上游记忆服务摄入。
- 上游记忆服务的 `add``flush` 临时失败时会分别按配置重试;单次请求受 `MEMORY_GATEWAY_BACKEND_TIMEOUT_SECONDS` 控制;全部失败后资源状态为 `failed`,并记录 `error_message`
content type 映射:
| 文件类型 | 上游记忆服务 content type |
|---|---|
| `image/*` | `image` |
| `audio/*` | `audio` |
| PDF | `pdf` |
| HTML | `html` |
| 纯文本、Markdown、CSV、日志 | `text` |
| 其他文档 | `doc` |
请求示例:
```bash
curl -X POST http://127.0.0.1:8010/resources \
-F user_id=u_123 \
-F user_key=uk_xxx \
-F app_id=default \
-F project_id=default \
-F title="合同文件" \
-F description="2026 年付款合同" \
-F file=@./contract.pdf
```
响应示例:
```json
{
"resource_id": "r_xxx",
"session_id": "resource:u_123:r_xxx",
"uri": "resource://u_123/r_xxx",
"status": "extracted"
}
```
对外返回的 `uri` 永远是 `resource://{user_id}/{resource_id}`,不会泄露内部 `file://` 路径。
### 4. 查询资源列表
```http
GET /resources?user_id={user_id}&user_key={user_key}
```
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `user_id` | string | 是 | 用户 ID |
| `user_key` | string | 是 | 用户 key |
只返回当前用户 `deleted_at IS NULL` 的资源。不同用户的资源彼此隔离。
请求示例:
```bash
curl "http://127.0.0.1:8010/resources?user_id=u_123&user_key=uk_xxx"
```
响应示例:
```json
{
"resources": [
{
"resource_id": "r_xxx",
"user_id": "u_123",
"filename": "contract.pdf",
"content_type": "pdf",
"mime_type": "application/pdf",
"uri": "resource://u_123/r_xxx",
"session_id": "resource:u_123:r_xxx",
"status": "extracted",
"title": "合同文件",
"description": "2026 年付款合同",
"created_at": "2026-06-10T10:00:00+00:00",
"updated_at": "2026-06-10T10:00:05+00:00"
}
]
}
```
如果用户没有上传过资源,返回:
```json
{
"resources": []
}
```
### 5. 查询资源详情
```http
GET /resources/{resource_id}?user_id={user_id}&user_key={user_key}
```
路径参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `resource_id` | string | 是 | 资源 ID |
Query 参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `user_id` | string | 是 | 用户 ID |
| `user_key` | string | 是 | 用户 key |
请求示例:
```bash
curl "http://127.0.0.1:8010/resources/r_xxx?user_id=u_123&user_key=uk_xxx"
```
响应示例:
```json
{
"resources": [
{
"resource_id": "r_xxx",
"user_id": "u_123",
"filename": "contract.pdf",
"content_type": "pdf",
"mime_type": "application/pdf",
"uri": "resource://u_123/r_xxx",
"session_id": "resource:u_123:r_xxx",
"status": "extracted",
"title": "合同文件",
"description": "2026 年付款合同",
"created_at": "2026-06-10T10:00:00+00:00",
"updated_at": "2026-06-10T10:00:05+00:00"
}
]
}
```
如果当前用户没有上传过 resources、资源不存在、或资源属于其他用户返回
```json
{
"resources": []
}
```
这种设计避免通过资源 ID 探测其他用户的数据。`uri` 同样只返回公开 `resource://{user_id}/{resource_id}`,不会泄露内部 URI。
### 6. 删除资源
```http
DELETE /resources/{resource_id}?user_id={user_id}&user_key={user_key}
```
第一版只做软删除:
- 设置 `deleted_at = now()`
- 设置 `status = deleted`
- 后续 `resources` scope 搜索会排除该资源的 `session_id`
- 清理 Gateway 自己在 `MEMORY_GATEWAY_STORAGE_DIR` 下保存的原始上传文件。
- 不物理删除上游记忆服务内部记忆或索引。
请求示例:
```bash
curl -X DELETE "http://127.0.0.1:8010/resources/r_xxx?user_id=u_123&user_key=uk_xxx"
```
响应示例:
```json
{
"resource_id": "r_xxx",
"session_id": "resource:u_123:r_xxx",
"uri": "resource://u_123/r_xxx",
"status": "deleted"
}
```
### 7. 搜索记忆
```http
POST /memories/search
Content-Type: application/json
```
请求参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| `user_id` | string | 是 | 无 | 用户 ID |
| `user_key` | string | 是 | 无 | 用户 key |
| `query` | string | 是 | 无 | 搜索问题 |
| `conversation_id` | string | 否 | `null` | `scope` 包含 `current_chat` 时使用 |
| `scope` | string[] | 否 | `["current_chat", "resources"]` | 搜索范围 |
| `top_k` | integer | 否 | `8` | 每次上游记忆服务搜索返回数量,范围 `1..100` |
| `app_id` | string | 否 | `default` | 上游记忆服务 app scope |
| `project_id` | string | 否 | `default` | 上游记忆服务 project scope |
`scope` 支持:
| scope | 说明 |
|---|---|
| `current_chat` | 只搜索 `chat:{conversation_id}` |
| `resources` | 搜索当前用户未删除且已提取完成的上传资源 |
| `all_user_memory` | 搜索用户全部记忆,不加 `session_id` 过滤 |
请求示例:
```bash
curl -X POST http://127.0.0.1:8010/memories/search \
-H 'Content-Type: application/json' \
-d '{
"user_id": "u_123",
"user_key": "uk_xxx",
"conversation_id": "c_456",
"query": "合同里的付款条款是什么?",
"scope": ["current_chat", "resources"],
"top_k": 8,
"app_id": "default",
"project_id": "default"
}'
```
搜索编排逻辑:
1. `current_chat`:调用上游记忆服务 search过滤 `filters.session_id = chat:{conversation_id}`
2. `resources`:先查当前用户的 `user_resources`,只取 `status = extracted` 且未删除资源;再按批次调用上游记忆服务 search过滤这些资源的 `session_id`
3. `all_user_memory`:调用上游记忆服务 search不加 `session_id` 过滤。
4. 合并结果。
5. 过滤 `memory_tombstones` 命中的 `memory_id``session_id`
6. 应用 active `memory_overrides`,把 `text` 替换为 `override_text`
响应示例:
```json
{
"results": [
{
"id": "mem_abc",
"session_id": "resource:u_123:r_xxx",
"text": "付款期限为收到发票后 30 天内。",
"score": 0.82,
"source_scope": "resources",
"resource_id": "r_xxx",
"resource_uri": "resource://u_123/r_xxx",
"raw": {
"id": "mem_abc",
"session_id": "resource:u_123:r_xxx",
"episode": "原始上游记忆服务返回内容"
}
}
]
}
```
### 8. 修改记忆
```http
PATCH /memories/{memory_id}
Content-Type: application/json
```
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `user_id` | string | 是 | 用户 ID |
| `user_key` | string | 是 | 用户 key |
| `session_id` | string | 是 | memory 所属 session必须属于当前用户 |
| `override_text` | string | 是 | 修正后的记忆文本 |
该接口只写入或更新 `memory_overrides`,不会修改上游记忆服务原始文件。写入前会校验 `session_id` 属于当前用户:当前版本支持当前用户的 `resource:{user_id}:{resource_id}``memory_edit:{user_id}`。后续搜索结果命中该 `memory_id` 时,返回的 `text` 会替换为 `override_text`,但保留原始 memory id。
请求示例:
```bash
curl -X PATCH http://127.0.0.1:8010/memories/mem_abc \
-H 'Content-Type: application/json' \
-d '{
"user_id": "u_123",
"user_key": "uk_xxx",
"session_id": "resource:u_123:r_xxx",
"override_text": "修正后的记忆内容"
}'
```
响应示例:
```json
{
"memory_id": "mem_abc",
"override_id": "o_xxx",
"status": "active"
}
```
### 9. 删除记忆
```http
DELETE /memories/{memory_id}
Content-Type: application/json
```
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `user_id` | string | 是 | 用户 ID |
| `user_key` | string | 是 | 用户 key |
| `session_id` | string | 是 | memory 所属 session必须属于当前用户 |
| `reason` | string | 否 | 删除原因 |
该接口只写入 `memory_tombstones`,不会修改上游记忆服务原始文件。写入前会校验 `session_id` 属于当前用户:当前版本支持当前用户的 `resource:{user_id}:{resource_id}``memory_edit:{user_id}`。后续搜索结果如果命中 tombstone 的 `memory_id``session_id`,会被过滤。
请求示例:
```bash
curl -X DELETE http://127.0.0.1:8010/memories/mem_abc \
-H 'Content-Type: application/json' \
-d '{
"user_id": "u_123",
"user_key": "uk_xxx",
"session_id": "resource:u_123:r_xxx",
"reason": "用户手动删除"
}'
```
响应示例:
```json
{
"memory_id": "mem_abc",
"tombstone_id": "t_xxx",
"status": "deleted"
}
```
## 上游记忆服务客户端封装
Gateway 内部通过 `core/backend_client.py` 调用上游记忆服务:
- `add_memory(payload)` -> `POST /api/v1/memory/add`
- `flush_memory(session_id, app_id, project_id)` -> `POST /api/v1/memory/flush`
- `search_memory(payload)` -> `POST /api/v1/memory/search`
- `health_check()` -> `GET /health`
## AI Agent Skill
项目提供可供 AI Agent 使用的 Skill
```text
skill/memory-gateway-agent
```
其中 `SKILL.md` 定义 Agent 工作流,`scripts/memory_gateway.py` 提供无额外依赖的命令行客户端,`references/api.md` 提供完整参数说明。使用前设置:
```bash
export MEMORY_GATEWAY_BASE_URL=http://127.0.0.1:8010
export MEMORY_GATEWAY_USER_ID=u_123
export MEMORY_GATEWAY_USER_KEY=uk_xxx
python skill/memory-gateway-agent/scripts/memory_gateway.py health
python skill/memory-gateway-agent/scripts/memory_gateway.py list-resources
```
## 运行测试
```bash
cd /home/tom/memory-gateway
.venv/bin/python -B -m pytest -q -p no:cacheprovider
```
默认测试不会访问真实上游记忆服务。若要对已部署的上游记忆服务做 health 集成验证,先确认上游记忆服务正在监听 `0.0.0.0:1995`,然后从 Gateway 所在机器用客户端可访问地址访问:
```bash
cd /home/tom/memory-gateway
RUN_BACKEND_INTEGRATION=1 \
MEMORY_GATEWAY_BACKEND_BASE_URL=http://10.6.80.123:1995 \
.venv/bin/python -B -m pytest -q tests/test_backend_integration.py -p no:cacheprovider
```
真实 add/flush 上传会写入上游记忆服务且可能受上游解析、LLM、embedding 服务耗时影响。需要验证完整摄入链路时再打开第二层开关:
```bash
cd /home/tom/memory-gateway
RUN_BACKEND_INTEGRATION=1 \
RUN_BACKEND_INGEST_INTEGRATION=1 \
MEMORY_GATEWAY_BACKEND_BASE_URL=http://10.6.80.123:1995 \
.venv/bin/python -B -m pytest -q tests/test_backend_integration.py -p no:cacheprovider
```