Files
memory-gateway/README.md

528 lines
13 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 2
Memory Gateway 2 是一个轻量级 FastAPI 服务,用于在 EverOS 现有
`/api/v1/memory/add``/api/v1/memory/flush``/api/v1/memory/search`
能力之上构建用户资源记忆层。
它只维护 Gateway 自己的 SQLite 元数据表、软删除记录和手动覆盖记录,
不会直接修改 EverOS 的 Markdown、SQLite 或 LanceDB 内部文件。
## 功能范围
- 上传用户资源文件、图片、音频、PDF、HTML、普通文档、纯文本。
- 保存资源元数据到 SQLite。
- 为每个资源生成独立 EverOS `session_id`
- 调用 EverOS `add``flush` 完成资源记忆摄入。
- 提供资源列表、详情、软删除。
- 编排记忆搜索,支持当前聊天、资源记忆、全部用户记忆。
- 支持记忆 tombstone 软删除。
- 支持记忆手动 override。
- 搜索结果返回前自动过滤 tombstone 并应用 override。
## 目录结构
```text
/home/tom/memory-gateway2
├── core/ # Gateway 核心代码
│ ├── api.py # FastAPI 路由
│ ├── config.py # 环境变量配置
│ ├── db.py # SQLite schema 初始化
│ ├── everos_client.py # EverOS HTTP client
│ ├── repository.py # SQLite 读写
│ └── service.py # 业务编排
├── main.py # Python 启动入口
├── tests/ # 测试
├── .env.example # 环境变量示例
└── pyproject.toml
```
## 环境配置
复制示例配置:
```bash
cd /home/tom/memory-gateway2
cp .env.example .env
```
配置项说明:
| 变量 | 默认值 | 说明 |
|---|---|---|
| `EVEROS_BASE_URL` | `http://127.0.0.1:8000` | EverOS API 服务地址 |
| `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_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
自己的存储位置,不要配置成 EverOS 的内部存储目录。
## 安装依赖
```bash
cd /home/tom/memory-gateway2
uv pip install -e .
```
## 启动 API
使用 Python 启动:
```bash
cd /home/tom/memory-gateway2
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 使用说明
`POST /users` 外,所有业务 API 都需要携带 `user_id``user_key`。认证失败返回 `401`
### 1. 创建用户
```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`
### 2. 上传资源
```http
POST /resources
Content-Type: multipart/form-data
```
表单参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| `user_id` | string | 是 | 无 | 用户 ID |
| `user_key` | string | 是 | 无 | 用户 key |
| `app_id` | string | 否 | `default` | EverOS app scope |
| `project_id` | string | 否 | `default` | EverOS 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 类型映射 EverOS content type。
7. 调用 EverOS `/api/v1/memory/add`
8. 调用 EverOS `/api/v1/memory/flush`
9. 成功后状态改为 `extracted`,失败后状态改为 `failed`
content type 映射:
| 文件类型 | EverOS 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://` 路径。
### 3. 查询资源列表
```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": []
}
```
### 4. 查询资源详情
```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。
### 5. 删除资源
```http
DELETE /resources/{resource_id}?user_id={user_id}&user_key={user_key}
```
第一版只做软删除:
- 设置 `deleted_at = now()`
- 设置 `status = deleted`
- 后续 `resources` scope 搜索会排除该资源的 `session_id`
- 不物理删除 EverOS 内部记忆或索引。
请求示例:
```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"
}
```
### 6. 搜索记忆
```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` | 每次 EverOS 搜索返回数量,范围 `1..100` |
| `app_id` | string | 否 | `default` | EverOS app scope |
| `project_id` | string | 否 | `default` | EverOS 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`:调用 EverOS search过滤 `filters.session_id = chat:{conversation_id}`
2. `resources`:先查当前用户的 `user_resources`,只取 `status = extracted` 且未删除资源;再按批次调用 EverOS search过滤这些资源的 `session_id`
3. `all_user_memory`:调用 EverOS 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": "原始 EverOS 返回内容"
}
}
]
}
```
### 7. 修改记忆
```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`,不会修改 EverOS 原始文件。后续搜索结果命中该 `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"
}
```
### 8. 删除记忆
```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`,不会修改 EverOS 原始文件。后续搜索结果如果命中 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"
}
```
## EverOS client 封装
Gateway 内部通过 `core/everos_client.py` 调用 EverOS
- `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`
## 运行测试
```bash
cd /home/tom/memory-gateway2
python -B -m pytest -q -p no:cacheprovider
```