Files
memory-gateway/README.md

691 lines
21 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}`。按文件名
命中的记忆搜索结果会另外通过 `attachments[].internal_uri` 返回真实 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/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` | 是否同时获取用户 profileagent 查询由上游决定是否忽略该参数 |
| `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` 上传成功后,将资源真实 URI 与资源 session 写入
`memory_attachments`。数据库初始化会自动回填已有 `user_resources`
2. `/memories/add` 中含 `uri` 的 content item 会直接登记 URI。
3. `/memories/add` 中只有 `base64` 的 content item 会保存到
`MEMORY_GATEWAY_STORAGE_DIR/{user_id}/memory_attachments/{sha256}/`,再登记
生成的 `file://` URI。相同用户、session、文件名和内容的重试会复用路径。
4. 搜索时根据当前用户和结果 `session_id` 查询附件,递归检查 `raw` 中的字符串
值。只有完整文件名出现时才返回对应附件;匹配不区分大小写。
5. `raw` 中键名为 `base64` 的内容不会参与匹配。未匹配时返回
`"attachments": []`
6. 历史 `/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": "原始上游记忆服务返回内容"
}
}
]
}
```
### 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
```