Files
memory-gateway/README.md

688 lines
20 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 服务,位于调用方和上游 memory service
之间。它负责用户鉴权、文件接收、资源元数据管理、
附件映射、软删除、手动覆盖,以及按范围编排记忆搜索。
Gateway 不直接修改上游记忆服务的 Markdown、SQLite 或向量索引文件。它通过
上游现有 API 完成记忆写入、flush 和搜索:
- `POST /api/v1/memory/add`
- `POST /api/v1/memory/flush`
- `POST /api/v1/memory/search`
## 适用场景
- 客户端要把聊天消息写入记忆。
- 客户端要在同一个聊天 session 中上传图片、音频或文件,但不能自行转 base64。
- 用户要上传独立资源如图片、音频、PDF、HTML、文档、文本文件。
- 文件已经在外部存储中,需要登记长期 URI 和本次摄入 URI。
- 搜索时需要同时覆盖当前聊天、资源记忆、全部用户记忆。
- 需要在 Gateway 层实现用户隔离、资源软删除、记忆 tombstone 和人工覆盖。
## 关键原则
`file://` 不是上传协议。它只表示“某台机器上的本地路径”。如果客户端和 Gateway
不在同一台机器、同一个容器、或同一个挂载路径下,直接把客户端本机 `file://`
传给 Gateway 或上游服务会失败。
正确选择:
| 需求 | 推荐接口 |
|---|---|
| 聊天消息里带本地文件,且客户端不能转 base64 | `POST /memories/add/multipart` |
| 上传一个独立、长期可检索资源 | `POST /resources` |
| 文件已经在 MinIO/S3/Beaver user_files 等外部存储 | `POST /resources/external` |
| 调用方已经有 base64 或上游可访问 URI | `POST /memories/add` |
## 项目结构
```text
memory-gateway/
├── core/
│ ├── api.py # FastAPI 路由、请求校验、HTTP 错误映射
│ ├── backend_client.py # 上游 memory service HTTP client
│ ├── config.py # 环境变量配置
│ ├── db.py # SQLite schema 初始化
│ ├── repository.py # SQLite 读写
│ └── service.py # 业务编排、上传、附件、搜索、软删除
├── main.py # uvicorn 启动入口
├── skill/ # Memory Gateway agent skill 和 CLI
├── tests/ # 单元、集成、实测命令记录
├── pyproject.toml
└── uv.lock
```
## 安装和启动
安装依赖:
```bash
cd /home/tom/memory-gateway
uv pip install -e .
```
启动 API
```bash
python main.py
```
默认服务地址:
```text
http://127.0.0.1:8010
```
健康检查:
```bash
curl http://127.0.0.1:8010/health
```
健康响应中:
- `status: ok` 表示 Gateway 和上游服务都可访问。
- `status: degraded` 表示 Gateway 可访问,但上游服务不可访问。
## 配置
配置来自环境变量。常用 `.env` 示例:
```bash
MEMORY_GATEWAY_BACKEND_BASE_URL=http://127.0.0.1:1995
MEMORY_GATEWAY_DB_PATH=./data/memory_gateway.sqlite3
MEMORY_GATEWAY_STORAGE_DIR=./data/storage
MEMORY_GATEWAY_RESOURCE_SEARCH_BATCH_SIZE=50
MEMORY_GATEWAY_MAX_UPLOAD_BYTES=26214400
MEMORY_GATEWAY_ALLOWED_MIME_TYPES=image/*,audio/*,application/pdf,text/html,application/xhtml+xml,text/plain,text/markdown,text/csv,application/json,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-powerpoint,application/vnd.openxmlformats-officedocument.presentationml.presentation
MEMORY_GATEWAY_BACKEND_INGEST_ATTEMPTS=3
MEMORY_GATEWAY_BACKEND_RETRY_DELAY_SECONDS=0.25
MEMORY_GATEWAY_BACKEND_TIMEOUT_SECONDS=120
MEMORY_GATEWAY_HOST=127.0.0.1
MEMORY_GATEWAY_PORT=8010
MEMORY_GATEWAY_RELOAD=false
```
配置说明:
| 变量 | 默认值 | 说明 |
|---|---|---|
| `MEMORY_GATEWAY_BACKEND_BASE_URL` | `http://127.0.0.1:1995` | 上游 memory service 地址 |
| `MEMORY_GATEWAY_DB_PATH` | `./data/memory_gateway.sqlite3` | Gateway SQLite 数据库 |
| `MEMORY_GATEWAY_STORAGE_DIR` | `./data/storage` | Gateway 保存上传文件的位置 |
| `MEMORY_GATEWAY_RESOURCE_SEARCH_BATCH_SIZE` | `50` | resources 搜索每批 session 数量 |
| `MEMORY_GATEWAY_MAX_UPLOAD_BYTES` | `26214400` | 单文件上传大小限制,默认 25 MiB |
| `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` | 上游请求超时 |
| `MEMORY_GATEWAY_HOST` | `127.0.0.1` | Gateway 监听地址 |
| `MEMORY_GATEWAY_PORT` | `8010` | Gateway 监听端口 |
| `MEMORY_GATEWAY_RELOAD` | `false` | 是否启用 uvicorn reload |
不要把 Gateway 的数据库或 storage 配到上游服务内部目录。Gateway 只管理自己的状态。
## 数据模型
Gateway 启动时自动创建 SQLite 表:
| 表 | 用途 |
|---|---|
| `users` | 用户 ID 和 Gateway 生成的 `user_key` |
| `user_resources` | 独立资源元数据、状态、内部 URI、软删除时间 |
| `memory_attachments` | 聊天/资源 session 到真实附件 URI 的映射 |
| `memory_tombstones` | 用户删除的 memory id 或 session_id |
| `memory_overrides` | 用户人工修正后的 memory 文本 |
附件来源 `source`
| source | 来源 |
|---|---|
| `resource_upload` | `/resources` 上传 |
| `external_resource` | `/resources/external` 登记 |
| `memory_add_uri` | `/memories/add` 中的 URI item |
| `memory_add_base64` | `/memories/add` 中的 base64 item |
| `memory_add_upload` | `/memories/add/multipart` 中的上传文件 |
## Session 约定
| 场景 | session_id |
|---|---|
| 普通聊天 | `chat:{conversation_id}` |
| 独立上传资源 | `resource:{user_id}:{resource_id}` |
| 手动修正 | `memory_edit:{user_id}` |
`/resources` 会自动生成资源 session。`/memories/add`
`/memories/add/multipart` 使用调用方传入的 session。
## 鉴权
`POST /users``GET /health` 外,所有业务接口都需要 `user_id`
`user_key`。认证失败返回 `401`
创建用户:
```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-22T06:54:35.823262+00:00"
}
```
同一个 `user_id` 重复创建会返回已有用户记录。
## API 概览
| 方法 | 路径 | 说明 |
|---|---|---|
| `GET` | `/health` | Gateway 和上游健康检查 |
| `POST` | `/users` | 创建或读取用户 key |
| `POST` | `/resources` | multipart 上传独立资源 |
| `POST` | `/resources/external` | 登记外部资源 |
| `GET` | `/resources` | 列出用户资源 |
| `GET` | `/resources/{resource_id}` | 读取资源详情 |
| `DELETE` | `/resources/{resource_id}` | 软删除资源 |
| `POST` | `/memories/add` | JSON 追加记忆消息 |
| `POST` | `/memories/add/multipart` | multipart 追加消息并上传附件 |
| `POST` | `/memories/flush` | flush 指定 session |
| `POST` | `/memories/search` | 编排搜索 |
| `PATCH` | `/memories/{memory_id}` | 人工覆盖 memory 文本 |
| `DELETE` | `/memories/{memory_id}` | tombstone 删除 memory |
## 上传聊天附件:`/memories/add/multipart`
用于“同一个聊天 session 中发送消息并上传附件”。客户端不需要转 base64也不需要
`file://`
请求类型:
```http
POST /memories/add/multipart
Content-Type: multipart/form-data
```
表单字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `user_id` | string | 是 | 用户 ID |
| `user_key` | string | 是 | 用户 key |
| `session_id` | string | 是 | 通常是 `chat:{conversation_id}` |
| `app_id` | string | 否 | 默认 `default` |
| `project_id` | string | 否 | 默认 `default` |
| `messages` | string 或 JSON file | 是 | JSON array结构同 `/memories/add` |
| 动态文件字段 | file | 条件必填 | 如果 content item 包含 `upload_id`,必须上传同名文件字段 |
`upload_id` 由调用方自定义。Gateway 不会随机生成,也不要求固定格式
(例如 `user_id_filetype_number`)。它只需要在同一次请求里非空、唯一,并且和
multipart 文件字段名一致。推荐使用简单可读的值,如 `image_1``audio_1`
`doc_1`
`messages` 示例:
```json
[
{
"sender_id": "u_123",
"role": "user",
"timestamp": 1782111275810,
"content": [
{"type": "text", "text": "请记住这张图片和音频"},
{
"type": "image",
"upload_id": "image_1",
"name": "simple-multimodal-image.png",
"ext": "png"
},
{
"type": "audio",
"upload_id": "audio_1",
"name": "simple-tone.wav",
"ext": "wav"
}
]
}
]
```
curl 示例:
```bash
curl -X POST http://127.0.0.1:8010/memories/add/multipart \
-F user_id=u_123 \
-F user_key=uk_xxx \
-F session_id=chat:c_456 \
-F app_id=default \
-F project_id=default \
-F 'messages=@messages.json;type=application/json' \
-F 'image_1=@tests/simple-multimodal-image.png;type=image/png' \
-F 'audio_1=@tests/simple-tone.wav;type=audio/wav'
```
处理流程:
1. Gateway 校验用户。
2. 解析 `messages`
3. 找到 content item 中的 `upload_id`
4. 从 multipart 中读取同名文件字段。
5. 校验 MIME 和大小。
6. 保存文件到 `MEMORY_GATEWAY_STORAGE_DIR/{user_id}/memory_attachments/{sha256}/`
7. 将 item 转成上游可消费的 `text``base64`,不透传客户端本机路径。
8. 调用上游 memory add。
9. 上游 add 成功后写入 `memory_attachments``source = memory_add_upload`
该接口只追加消息,不自动 flush。需要抽取和索引时继续调用 `/memories/flush`
## 追加 JSON 记忆:`/memories/add`
用于调用方已经有纯文本、base64或明确有上游可读取 URI 的场景。
请求:
```http
POST /memories/add
Content-Type: application/json
```
示例:
```bash
curl -X POST http://127.0.0.1:8010/memories/add \
-H 'Content-Type: application/json' \
-d '{
"user_id": "u_123",
"user_key": "uk_xxx",
"session_id": "chat:c_456",
"app_id": "default",
"project_id": "default",
"messages": [
{
"sender_id": "u_123",
"role": "user",
"timestamp": 1782111275810,
"content": [
{"type": "text", "text": "用户喜欢简洁的中文回答"},
{
"type": "audio",
"base64": "BASE64_DATA",
"ext": "wav",
"name": "tone.wav"
}
]
}
]
}'
```
`/memories/add` 中:
- `uri` item 会原样转发给上游,并登记附件映射。
- `base64` item 会保存到 Gateway storage并登记生成的内部 `file://`
- 如果 URI 是客户端本机路径,且上游读不到该路径,请改用 `/memories/add/multipart`
## Flush`/memories/flush`
```bash
curl -X POST http://127.0.0.1:8010/memories/flush \
-H 'Content-Type: application/json' \
-d '{
"user_id": "u_123",
"user_key": "uk_xxx",
"session_id": "chat:c_456",
"app_id": "default",
"project_id": "default"
}'
```
响应示例:
```json
{
"session_id": "chat:c_456",
"backend": {
"request_id": "request_id",
"data": {
"status": "extracted"
}
}
}
```
## 上传独立资源:`/resources`
用于把文件作为长期资源纳入 `resources` 搜索范围。
请求类型:
```http
POST /resources
Content-Type: multipart/form-data
```
表单字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `user_id` | string | 是 | 用户 ID |
| `user_key` | string | 是 | 用户 key |
| `app_id` | string | 否 | 默认 `default` |
| `project_id` | string | 否 | 默认 `default` |
| `title` | string | 否 | 资源标题 |
| `description` | string | 否 | 资源描述 |
| `file` | file | 是 | 上传文件 |
示例:
```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="Gateway image resource" \
-F description="Example image resource" \
-F 'file=@tests/simple-multimodal-image.png;type=image/png'
```
响应:
```json
{
"resource_id": "r_xxx",
"session_id": "resource:u_123:r_xxx",
"uri": "resource://u_123/r_xxx",
"status": "extracted"
}
```
`/resources` 会:
1. 保存文件到 Gateway storage。
2. 写入 `user_resources`
3. 写入 `memory_attachments`
4. 构造上游 content item文本用 `text`,二进制用 `base64`
5. 自动调用上游 add 和 flush。
6. 成功后状态为 `extracted`
同一用户、同一 app/project 下相同 sha256 的活跃资源会复用已有资源。
## 登记外部资源:`/resources/external`
用于文件已经在外部存储中Gateway 只保存元数据和 URI 映射。
示例:
```bash
curl -X POST http://127.0.0.1:8010/resources/external \
-H 'Content-Type: application/json' \
-d '{
"user_id": "u_123",
"user_key": "uk_xxx",
"app_id": "default",
"project_id": "default",
"filename": "chart.png",
"mime_type": "image/png",
"size_bytes": 12345,
"sha256": "abc123",
"source_uri": "minio://bucket/users/u_123/chart.png",
"ingest_uri": "https://minio.example/presigned/chart.png",
"title": "chart.png",
"description": "External image"
}'
```
字段含义:
- `source_uri`:长期保存的真实映射 URI。
- `ingest_uri`:上游本次摄入可读取的 URI通常是短期 presigned URL。
Gateway 不会下载 `source_uri``ingest_uri`
## 资源读取和删除
列出资源:
```bash
curl "http://127.0.0.1:8010/resources?user_id=u_123&user_key=uk_xxx"
```
读取资源详情:
```bash
curl "http://127.0.0.1:8010/resources/r_xxx?user_id=u_123&user_key=uk_xxx"
```
软删除资源:
```bash
curl -X DELETE "http://127.0.0.1:8010/resources/r_xxx?user_id=u_123&user_key=uk_xxx"
```
删除资源会:
- 设置资源 `status = deleted`
- 设置资源 `deleted_at`
- 软删除相关附件映射。
- 清理 Gateway storage 中属于该资源的本地文件。
- 不直接删除上游记忆服务内部文件或索引。
## 搜索:`/memories/search`
请求:
```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"],
"method": "hybrid",
"top_k": 8,
"include_profile": true,
"enable_llm_rerank": true,
"app_id": "default",
"project_id": "default"
}'
```
搜索 scope
| scope | 行为 |
|---|---|
| `current_chat` | 搜索 `chat:{conversation_id}`,需要传 `conversation_id` |
| `resources` | 搜索当前用户已提取且未删除的资源 session |
| `all_user_memory` | 搜索用户全部记忆,不加 session 过滤 |
返回结果会标准化为:
```json
{
"results": [
{
"id": "memory_id",
"session_id": "chat:c_456",
"text": "memory text",
"score": 0.61,
"source_scope": "current_chat",
"resource_id": null,
"resource_uri": null,
"attachments": [
{
"type": "image",
"name": "image.png",
"internal_uri": "file:///..."
}
],
"raw": {}
}
]
}
```
附件匹配规则:
- Gateway 只返回当前用户、当前 session 的附件映射。
- Gateway 根据搜索结果 raw/text 中出现的文件名匹配附件。
- `base64` 字段内容不会参与匹配。
- 未匹配到附件时不返回 `attachments`
## 手动覆盖和删除记忆
覆盖 memory 文本:
```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": "chat:c_456",
"override_text": "修正后的记忆文本"
}'
```
删除 memory
```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": "chat:c_456",
"reason": "用户要求删除"
}'
```
Gateway 会校验 session 归属,防止用户覆盖或删除其他用户资源 session 中的记忆。
## `/memories/add/multipart` 与 `/resources` 的区别
| 对比项 | `/memories/add/multipart` | `/resources` |
|---|---|---|
| 目标 | 聊天 session 中的消息附件 | 独立长期资源 |
| session | 调用方传入,如 `chat:c_456` | Gateway 生成 `resource:{user_id}:{resource_id}` |
| 文件字段 | 动态字段名匹配 `upload_id` | 固定字段 `file` |
| 元数据表 | 不写 `user_resources` | 写 `user_resources` |
| 附件 source | `memory_add_upload` | `resource_upload` |
| 上游调用 | 只 add调用方按需 flush | Gateway 自动 add + flush |
| 常用搜索 | `current_chat``all_user_memory` | `resources` |
## 错误码
| 状态码 | 常见原因 |
|---|---|
| `400` | `messages` 不是合法 JSON array |
| `401` | `user_id``user_key` 无效 |
| `403` | 操作的 session 不属于当前用户 |
| `404` | 删除资源时资源不存在 |
| `413` | 上传文件超过 `MEMORY_GATEWAY_MAX_UPLOAD_BYTES` |
| `415` | MIME 类型不在白名单 |
| `422` | 请求字段缺失、`upload_id` 缺文件、重复文件字段、无效 base64 |
## API 日志
Gateway 使用 `memory_gateway.api` logger 输出 JSON 日志:
- 请求时间、耗时、方法、路径、URL、客户端。
- Query 参数和小型请求体。
- `user_key`、token、password、secret、API key 等敏感字段会遮蔽。
- multipart 请求只记录 content type 和大小,不记录文件内容。
- 响应体会按同样规则遮蔽敏感字段。
## Agent CLI
项目包含 Memory Gateway agent skill CLI
```bash
python skill/memory-gateway-agent/scripts/memory_gateway.py --help
```
常用环境变量:
```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
```
示例:
```bash
CLI="python skill/memory-gateway-agent/scripts/memory_gateway.py"
$CLI health
$CLI create-user u_123
$CLI upload-resource ./document.pdf --title "Document"
$CLI search "payment terms" --scope resources
$CLI add-memory --session-id chat:c_456 --messages ./messages.json
$CLI flush-memory --session-id chat:c_456
```
CLI 的 `upload-resource` 使用 multipart不会暴露本地 `file://` 路径。
## 测试
运行自动化测试:
```bash
uv run pytest -q
```
当前验证结果:
```text
56 passed, 2 skipped
```
运行聚焦测试:
```bash
uv run pytest tests/test_gateway.py -k "add_memory or upload_resource or attachment" -q
```
当前验证结果:
```text
16 passed, 33 deselected
```
真实部署命令记录:
```text
tests/test_command.md
```
该文件记录了 2026-06-22 对已部署 Gateway 和上游 memory service 执行的真实 curl
命令,包括:
- `GET /health`
- `POST /memories/add/multipart`
- `POST /memories/flush`
- `POST /memories/search`
- `POST /resources`
- `GET /resources`
- resources scope 搜索
## 开发注意事项
- 保持 Gateway 与上游服务职责分离;不要直接写上游内部文件。
- 新上传入口应优先使用 multipart 或外部存储 URI避免要求客户端构造 base64。
- 不要在响应中泄露内部 `file://`,除搜索附件映射 `attachments[].internal_uri` 外。
- 资源详情和列表返回公开 `resource://{user_id}/{resource_id}`
- 删除资源只做 Gateway 层软删除和本地文件清理,不直接删除上游记忆。
- 搜索必须保持用户隔离,尤其是附件映射和资源 session。