Files
memory-gateway/README.md

18 KiB
Raw Blame History

Memory Gateway

Memory Gateway 是一个轻量级 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 addflush 完成资源记忆摄入,并对临时失败做轻量重试。
  • 提供资源列表、详情、软删除。
  • 支持上传大小限制、MIME 白名单、同用户同 app/project 下按 sha256 幂等复用资源。
  • 编排记忆搜索,支持当前聊天、资源记忆、全部用户记忆。
  • 支持记忆 tombstone 软删除。
  • 支持记忆手动 override。
  • 搜索结果返回前自动过滤 tombstone 并应用 override。

目录结构

/home/tom/memory-gateway
├── 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

环境配置

复制示例配置:

cd /home/tom/memory-gateway
cp .env.example .env

配置项说明:

变量 默认值 说明
EVEROS_BASE_URL http://127.0.0.1:1995 EverOS API 服务地址EverOS 可监听 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_EVEROS_INGEST_ATTEMPTS 3 EverOS addflush 各自最多重试次数
MEMORY_GATEWAY_EVEROS_RETRY_DELAY_SECONDS 0.25 EverOS 摄入重试间隔秒数
MEMORY_GATEWAY_EVEROS_TIMEOUT_SECONDS 120 单次 EverOS 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_PATHMEMORY_GATEWAY_STORAGE_DIR 是 Gateway 自己的存储位置,不要配置成 EverOS 的内部存储目录。

安装依赖

cd /home/tom/memory-gateway
uv pip install -e .

启动 API

使用 Python 启动:

cd /home/tom/memory-gateway
python main.py

默认监听:

http://127.0.0.1:8010

session_id 规范

Gateway 遵循以下 session_id 规范:

场景 格式
普通聊天 chat:{conversation_id}
用户上传资源 resource:{user_id}:{resource_id}
用户手动修正 memory_edit:{user_id}

当前实现中,资源上传会自动生成:

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:接口处理耗时。
  • methodpathurlclient:请求方法、地址和客户端地址。
  • inputquery 参数和请求体。user_key、token、password、secret、API key 等敏感字段会记录为 [REDACTED]multipart 上传只记录 content type 和大小,不记录文件内容。
  • outputHTTP 状态码和响应体;敏感字段同样会遮蔽。

API 使用说明

POST /users 外,所有业务 API 都需要携带 user_iduser_key。认证失败返回 401

1. 健康检查

GET /health

该接口不需要 user_iduser_key,用于确认 Gateway API 是否可响应,以及上游 EverOS 是否可访问。

请求示例:

curl http://127.0.0.1:8010/health

EverOS 正常时响应示例:

{
  "status": "ok",
  "api": {
    "status": "ok"
  },
  "everos": {
    "status": "ok",
    "base_url": "http://127.0.0.1:1995",
    "data": {
      "status": "ok"
    }
  }
}

EverOS 不可访问时仍返回 HTTP 200status 会变成 degraded便于区分“Gateway API 活着”和“上游 EverOS 故障”:

{
  "status": "degraded",
  "api": {
    "status": "ok"
  },
  "everos": {
    "status": "unavailable",
    "base_url": "http://127.0.0.1:1995",
    "error": "Connection refused"
  }
}

2. 创建用户

POST /users
Content-Type: application/json

请求参数:

参数 类型 必填 说明
user_id string 用户 ID

请求示例:

curl -X POST http://127.0.0.1:8010/users \
  -H 'Content-Type: application/json' \
  -d '{"user_id":"u_123"}'

响应示例:

{
  "user_id": "u_123",
  "user_key": "uk_xxx",
  "created_at": "2026-06-10T10:00:00+00:00"
}

user_key 需要由调用方保存,后续上传、查询、搜索、修改和删除都要传入。如果同一个 user_id 已存在,接口会返回已有 user_key

3. 上传资源

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. 计算 sha256size_bytes
  3. 生成 resource_id
  4. 生成 session_id = resource:{user_id}:{resource_id}
  5. 写入 user_resources,状态为 ingesting
  6. 根据 MIME 类型映射 EverOS content type。
  7. 构造 EverOS content item文本类上传以内联 text 发送,非文本上传以内联 base64 发送,不要求 EverOS 访问 Gateway 本地 file:// 路径。
  8. 调用 EverOS /api/v1/memory/add
  9. 调用 EverOS /api/v1/memory/flush
  10. 成功后状态改为 extracted,失败后状态改为 failed

上传策略:

  • 文件会按流式方式写入磁盘,超过 MEMORY_GATEWAY_MAX_UPLOAD_BYTES 会返回 413,不会写入资源记录。
  • MIME 类型不在 MEMORY_GATEWAY_ALLOWED_MIME_TYPES 白名单内会返回 415
  • 同一用户在同一 app_idproject_id 下重复上传相同 sha256 的活跃资源,会直接返回已有资源,避免重复调用 EverOS 摄入。
  • EverOS addflush 临时失败时会分别按配置重试;单次请求受 MEMORY_GATEWAY_EVEROS_TIMEOUT_SECONDS 控制;全部失败后资源状态为 failed,并记录 error_message

content type 映射:

文件类型 EverOS content type
image/* image
audio/* audio
PDF pdf
HTML html
纯文本、Markdown、CSV、日志 text
其他文档 doc

请求示例:

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

响应示例:

{
  "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. 查询资源列表

GET /resources?user_id={user_id}&user_key={user_key}

参数:

参数 类型 必填 说明
user_id string 用户 ID
user_key string 用户 key

只返回当前用户 deleted_at IS NULL 的资源。不同用户的资源彼此隔离。

请求示例:

curl "http://127.0.0.1:8010/resources?user_id=u_123&user_key=uk_xxx"

响应示例:

{
  "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": []
}

5. 查询资源详情

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

请求示例:

curl "http://127.0.0.1:8010/resources/r_xxx?user_id=u_123&user_key=uk_xxx"

响应示例:

{
  "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、资源不存在、或资源属于其他用户返回

{
  "resources": []
}

这种设计避免通过资源 ID 探测其他用户的数据。uri 同样只返回公开 resource://{user_id}/{resource_id},不会泄露内部 URI。

6. 删除资源

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 下保存的原始上传文件。
  • 不物理删除 EverOS 内部记忆或索引。

请求示例:

curl -X DELETE "http://127.0.0.1:8010/resources/r_xxx?user_id=u_123&user_key=uk_xxx"

响应示例:

{
  "resource_id": "r_xxx",
  "session_id": "resource:u_123:r_xxx",
  "uri": "resource://u_123/r_xxx",
  "status": "deleted"
}

7. 搜索记忆

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 过滤

请求示例:

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_idsession_id
  6. 应用 active memory_overrides,把 text 替换为 override_text

响应示例:

{
  "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 返回内容"
      }
    }
  ]
}

8. 修改记忆

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 原始文件。写入前会校验 session_id 属于当前用户:当前版本支持当前用户的 resource:{user_id}:{resource_id}memory_edit:{user_id}。后续搜索结果命中该 memory_id 时,返回的 text 会替换为 override_text,但保留原始 memory id。

请求示例:

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": "修正后的记忆内容"
  }'

响应示例:

{
  "memory_id": "mem_abc",
  "override_id": "o_xxx",
  "status": "active"
}

9. 删除记忆

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 原始文件。写入前会校验 session_id 属于当前用户:当前版本支持当前用户的 resource:{user_id}:{resource_id}memory_edit:{user_id}。后续搜索结果如果命中 tombstone 的 memory_idsession_id,会被过滤。

请求示例:

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": "用户手动删除"
  }'

响应示例:

{
  "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
  • health_check() -> GET /health

AI Agent Skill

项目提供可供 AI Agent 使用的 Skill

skill/memory-gateway-agent

其中 SKILL.md 定义 Agent 工作流,scripts/memory_gateway.py 提供无额外依赖的命令行客户端,references/api.md 提供完整参数说明。使用前设置:

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

运行测试

cd /home/tom/memory-gateway
.venv/bin/python -B -m pytest -q -p no:cacheprovider

默认测试不会访问真实 EverOS。若要对已部署的 EverOS 做 health 集成验证,先确认 EverOS 正在监听 0.0.0.0:1995,然后从 Gateway 所在机器用客户端可访问地址访问:

cd /home/tom/memory-gateway
RUN_EVEROS_INTEGRATION=1 \
EVEROS_BASE_URL=http://10.6.80.123:1995 \
.venv/bin/python -B -m pytest -q tests/test_everos_integration.py -p no:cacheprovider

真实 add/flush 上传会写入 EverOS且可能受上游解析、LLM、embedding 服务耗时影响。需要验证完整摄入链路时再打开第二层开关:

cd /home/tom/memory-gateway
RUN_EVEROS_INTEGRATION=1 \
RUN_EVEROS_INGEST_INTEGRATION=1 \
EVEROS_BASE_URL=http://10.6.80.123:1995 \
.venv/bin/python -B -m pytest -q tests/test_everos_integration.py -p no:cacheprovider