886 lines
28 KiB
Markdown
886 lines
28 KiB
Markdown
# Memory Gateway
|
||
|
||
Memory Gateway 是一个轻量级 FastAPI 服务,用于在上游记忆服务现有
|
||
`/api/v1/memory/add`、`/api/v1/memory/flush`、`/api/v1/memory/search`
|
||
能力之上构建用户资源记忆层。
|
||
|
||
它只维护 Gateway 自己的 SQLite 元数据表、软删除记录和手动覆盖记录,
|
||
不会直接修改上游记忆服务的 Markdown、SQLite 或 LanceDB 内部文件。
|
||
|
||
## 功能范围
|
||
|
||
- 上传用户资源:文件、图片、音频、PDF、HTML、普通文档、纯文本。
|
||
- 登记外部资源:文件真实保存在 Beaver user_files/MinIO 等外部存储,
|
||
Gateway 只保存元数据和 URI 映射,不保存原始文件副本。
|
||
- 保存资源元数据到 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}`。按文件名
|
||
命中的记忆搜索结果会另外通过 `attachments[].internal_uri` 返回真实 URI。
|
||
|
||
### 3b. 登记外部资源
|
||
|
||
```http
|
||
POST /resources/external
|
||
Content-Type: application/json
|
||
```
|
||
|
||
当文件已经保存在 Beaver user_files/MinIO 等外部存储时,调用该接口登记资源。
|
||
Gateway 不会保存原始文件副本,也不会下载 `source_uri` 或 `ingest_uri`;它只写入
|
||
SQLite 元数据、建立附件映射,并把 `ingest_uri` 作为 content item URI 传给上游
|
||
记忆服务完成解析和索引。
|
||
|
||
请求参数:
|
||
|
||
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
||
|---|---|---|---|---|
|
||
| `user_id` | string | 是 | 无 | 用户 ID |
|
||
| `user_key` | string | 是 | 无 | 用户 key |
|
||
| `app_id` | string | 否 | `default` | 上游记忆服务 app scope |
|
||
| `project_id` | string | 否 | `default` | 上游记忆服务 project scope |
|
||
| `filename` | string | 是 | 无 | 文件名,用于检索附件匹配和上游解析 |
|
||
| `mime_type` | string | 否 | `null` | MIME 类型,如 `image/png`、`application/pdf` |
|
||
| `content_type` | string | 否 | 自动推断 | 上游 content type:`image`、`audio`、`pdf`、`html`、`text` 或 `doc`。如果传入 MIME 类型,Gateway 会按 MIME/扩展名重新推断 |
|
||
| `size_bytes` | integer | 否 | `null` | 文件大小 |
|
||
| `sha256` | string | 否 | `null` | 文件内容 sha256;同用户同 app/project 下相同 sha256 的活跃资源会复用 |
|
||
| `source_uri` | string | 是 | 无 | 长期真实映射 URI,如 `minio://bucket/users/u_123/outputs/chart.png` 或本地开发的 `file://...` |
|
||
| `ingest_uri` | string | 否 | `source_uri` | 上游记忆服务当次摄入可读取的 URI。MinIO 场景推荐传短期 presigned GET URL |
|
||
| `title` | string | 否 | `null` | 资源标题 |
|
||
| `description` | string | 否 | `null` | 资源描述 |
|
||
|
||
处理流程:
|
||
|
||
1. 生成 `resource_id`。
|
||
2. 生成 `session_id = resource:{user_id}:{resource_id}`。
|
||
3. 写入 `user_resources`,`uri = source_uri`,状态为 `ingesting`。
|
||
4. 写入 `memory_attachments`,`internal_uri = source_uri`,`source = external_resource`。
|
||
5. 构造上游 content item:`{"type": "...", "name": filename, "uri": ingest_uri, "extras": {"resource_id": "...", "source": "external_resource"}}`。
|
||
6. 调用上游记忆服务 `/api/v1/memory/add`。
|
||
7. 调用上游记忆服务 `/api/v1/memory/flush`。
|
||
8. 成功后状态改为 `extracted`,失败后状态改为 `failed` 并记录错误。
|
||
|
||
请求示例:
|
||
|
||
```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://beaver-user-files/users/u_123/outputs/chart.png",
|
||
"ingest_uri": "https://minio.example/presigned/chart.png",
|
||
"title": "chart.png",
|
||
"description": "Beaver user file outputs/chart.png"
|
||
}'
|
||
```
|
||
|
||
响应示例:
|
||
|
||
```json
|
||
{
|
||
"resource_id": "r_xxx",
|
||
"session_id": "resource:u_123:r_xxx",
|
||
"uri": "resource://u_123/r_xxx",
|
||
"status": "extracted"
|
||
}
|
||
```
|
||
|
||
`source_uri` 是 Gateway 长期保存和搜索结果附件映射使用的真实 URI;
|
||
`ingest_uri` 只用于本次上游解析。不要把短期 presigned URL 作为 `source_uri`
|
||
长期入库。
|
||
|
||
### 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/add
|
||
Content-Type: application/json
|
||
```
|
||
|
||
该接口把一批消息追加到指定 `session_id`,并调用上游记忆服务
|
||
`/api/v1/memory/add`。如果消息 content item 中包含 `uri` 或 `base64` 附件,
|
||
Gateway 会额外登记附件映射,供后续 `/memories/search` 返回
|
||
`attachments[].internal_uri`。
|
||
|
||
请求参数:
|
||
|
||
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
||
|---|---|---|---|---|
|
||
| `user_id` | string | 是 | 无 | 用户 ID |
|
||
| `user_key` | string | 是 | 无 | 用户 key |
|
||
| `session_id` | string | 是 | 无 | 记忆 session,如 `chat:{conversation_id}`、`resource:{user_id}:{resource_id}` 或 `memory_edit:{user_id}` |
|
||
| `messages` | object[] | 是 | 无 | 追加到该 session 的消息数组,至少 1 条 |
|
||
| `app_id` | string | 否 | `default` | 上游记忆服务 app scope |
|
||
| `project_id` | string | 否 | `default` | 上游记忆服务 project scope |
|
||
|
||
`messages[]` 字段:
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|
|
||
| `sender_id` | string | 是 | 发送方 ID,通常为当前用户 ID |
|
||
| `role` | string | 是 | `user`、`assistant` 或 `tool` |
|
||
| `timestamp` | integer | 是 | Unix 毫秒时间戳,必须大于 0 |
|
||
| `content` | string 或 object[] | 是 | 纯文本,或上游记忆服务 content item 数组 |
|
||
|
||
常见 content item:
|
||
|
||
```json
|
||
{"type": "text", "text": "用户喜欢简洁的中文回答"}
|
||
```
|
||
|
||
```json
|
||
{
|
||
"type": "image",
|
||
"uri": "file:///home/tom/memory-gateway/tests/simple-multimodal-image.png",
|
||
"ext": "png",
|
||
"name": "simple-multimodal-image.png"
|
||
}
|
||
```
|
||
|
||
```json
|
||
{
|
||
"type": "audio",
|
||
"base64": "BASE64_DATA",
|
||
"ext": "wav",
|
||
"name": "tone.wav"
|
||
}
|
||
```
|
||
|
||
处理流程:
|
||
|
||
1. 校验 `user_id` 和 `user_key`。
|
||
2. 检查 `messages` 基本结构。
|
||
3. 遍历 object[] content item:含 `uri` 时登记该 URI;只有 `base64` 时先校验并保存到 `MEMORY_GATEWAY_STORAGE_DIR/{user_id}/memory_attachments/{sha256}/`,再登记生成的 `file://` URI。
|
||
4. 将原始 `session_id`、`app_id`、`project_id`、`messages` 原样转发给上游记忆服务 `/api/v1/memory/add`。
|
||
5. 上游 add 成功后写入附件映射;失败时不会保留本次新生成的 base64 附件文件。
|
||
|
||
请求示例:
|
||
|
||
```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": 1781172177000,
|
||
"content": [
|
||
{
|
||
"type": "text",
|
||
"text": "记住:我偏好 concise 的中文说明"
|
||
},
|
||
{
|
||
"type": "image",
|
||
"uri": "file:///home/tom/memory-gateway/tests/simple-multimodal-image.png",
|
||
"ext": "png",
|
||
"name": "simple-multimodal-image.png"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}'
|
||
```
|
||
|
||
响应示例:
|
||
|
||
```json
|
||
{
|
||
"session_id": "chat:c_456",
|
||
"backend": {
|
||
"request_id": "add",
|
||
"data": {
|
||
"status": "accumulated"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
`/memories/add` 只负责追加消息。需要让上游记忆服务对该 session 完成抽取和索引时,
|
||
继续调用 `/memories/flush`。
|
||
|
||
### 8. 搜索记忆
|
||
|
||
```http
|
||
POST /memories/search
|
||
Content-Type: application/json
|
||
```
|
||
|
||
请求参数:
|
||
|
||
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
||
|---|---|---|---|---|
|
||
| `user_id` | string | 是 | 无 | 用户 ID |
|
||
| `user_key` | string | 是 | 无 | 用户 key |
|
||
| `agent_id` | string | 否 | `null` | 设置后查询该 agent 的记忆;`user_id` 仍用于 Gateway 鉴权和本地数据隔离 |
|
||
| `query` | string | 是 | 无 | 搜索问题 |
|
||
| `conversation_id` | string | 否 | `null` | `scope` 包含 `current_chat` 时使用 |
|
||
| `scope` | string[] | 否 | `["current_chat", "resources"]` | 搜索范围 |
|
||
| `method` | string | 否 | `hybrid` | 搜索方法:`keyword`、`vector`、`hybrid` 或 `agentic` |
|
||
| `top_k` | integer | 否 | `8` | 每次上游搜索返回数量,支持 `-1` 或 `1..100`;`-1` 表示使用上游默认数量 |
|
||
| `radius` | number | 否 | `null` | 相似度半径,范围 `0..1`;未提供时不发送给上游 |
|
||
| `include_profile` | boolean | 否 | `true` | 是否同时获取用户 profile;agent 查询由上游决定是否忽略该参数 |
|
||
| `enable_llm_rerank` | boolean | 否 | `true` | 是否启用上游 LLM rerank;具体生效范围由搜索方法和记忆类型决定 |
|
||
| `filters` | object | 否 | `null` | 上游过滤 DSL,支持字段条件以及嵌套 `AND`、`OR` |
|
||
| `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"],
|
||
"method": "hybrid",
|
||
"top_k": 8,
|
||
"include_profile": true,
|
||
"enable_llm_rerank": true,
|
||
"filters": {
|
||
"type": "Conversation"
|
||
},
|
||
"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. 同时存在请求 `filters` 和 scope 生成的 session 条件时,使用 `{"AND": [filters, scope_filters]}` 合并,避免调用方过滤条件覆盖资源或聊天隔离条件。
|
||
5. 设置 `agent_id` 时,上游请求只发送 `agent_id`;否则发送已鉴权的 `user_id`。
|
||
6. 合并结果。
|
||
7. 过滤 `memory_tombstones` 命中的 `memory_id` 或 `session_id`。
|
||
8. 应用 active `memory_overrides`,把 `text` 替换为 `override_text`。
|
||
|
||
响应中的 `memory_type` 对应上游结果类型:
|
||
|
||
| `memory_type` | 说明 |
|
||
|---|---|
|
||
| `episode` | 用户 episode |
|
||
| `profile` | 用户 profile |
|
||
| `agent_case` | agent case |
|
||
| `agent_skill` | agent skill |
|
||
| `unprocessed_message` | 尚未完成边界提取的原始消息 |
|
||
|
||
附件路径映射规则:
|
||
|
||
1. `/resources` 上传成功后,将 Gateway 本地保存的真实 URI 与资源 session 写入
|
||
`memory_attachments`。数据库初始化会自动回填已有 `user_resources`。
|
||
2. `/resources/external` 登记成功后,将外部存储的 `source_uri` 与资源 session
|
||
写入 `memory_attachments`,但 Gateway 不保存原始文件副本。
|
||
3. `/memories/add` 中含 `uri` 的 content item 会直接登记 URI。
|
||
4. `/memories/add` 中只有 `base64` 的 content item 会保存到
|
||
`MEMORY_GATEWAY_STORAGE_DIR/{user_id}/memory_attachments/{sha256}/`,再登记
|
||
生成的 `file://` URI。相同用户、session、文件名和内容的重试会复用路径。
|
||
5. 搜索时根据当前用户和结果 `session_id` 查询附件,递归检查 `raw` 中的字符串
|
||
值。只有完整文件名出现时才返回对应附件;匹配不区分大小写。
|
||
6. `raw` 中键名为 `base64` 的内容不会参与匹配。未匹配时返回
|
||
`"attachments": []`。
|
||
7. 历史 `/memories/add` 请求未保存在 Gateway 数据库中,无法自动补录映射;新
|
||
版本上线后的请求会建立映射。
|
||
|
||
`attachments[].internal_uri` 会按配置和调用方输入直接暴露服务器真实 URI,调用
|
||
该接口的客户端必须被视为可信客户端。
|
||
|
||
响应示例:
|
||
|
||
```json
|
||
{
|
||
"results": [
|
||
{
|
||
"id": "mem_abc",
|
||
"memory_type": "episode",
|
||
"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",
|
||
"attachments": [
|
||
{
|
||
"type": "pdf",
|
||
"name": "contract.pdf",
|
||
"internal_uri": "file:///srv/memory-gateway/u_123/r_xxx/contract.pdf"
|
||
}
|
||
],
|
||
"raw": {
|
||
"id": "mem_abc",
|
||
"session_id": "resource:u_123:r_xxx",
|
||
"episode": "原始上游记忆服务返回内容"
|
||
}
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### 9. 修改记忆
|
||
|
||
```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"
|
||
}
|
||
```
|
||
|
||
### 10. 删除记忆
|
||
|
||
```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
|
||
```
|