Files
memory-gateway/README.md

21 KiB
Raw Blame History

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

目录结构

/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

环境配置

复制示例配置:

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 上游记忆服务 addflush 各自最多重试次数
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_PATHMEMORY_GATEWAY_STORAGE_DIR 是 Gateway 自己的存储位置,不要配置成上游记忆服务的内部存储目录。

安装依赖

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 是否可响应,以及上游记忆服务是否可访问。

请求示例:

curl http://127.0.0.1:8010/health

上游记忆服务正常时响应示例:

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

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

{
  "status": "degraded",
  "api": {
    "status": "ok"
  },
  "backend": {
    "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 上游记忆服务 app scope
project_id string default 上游记忆服务 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 类型映射上游记忆服务 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_idproject_id 下重复上传相同 sha256 的活跃资源,会直接返回已有资源,避免重复调用上游记忆服务摄入。
  • 上游记忆服务的 addflush 临时失败时会分别按配置重试;单次请求受 MEMORY_GATEWAY_BACKEND_TIMEOUT_SECONDS 控制;全部失败后资源状态为 failed,并记录 error_message

content type 映射:

文件类型 上游记忆服务 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}。按文件名 命中的记忆搜索结果会另外通过 attachments[].internal_uri 返回真实 URI。

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

请求示例:

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
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 搜索方法:keywordvectorhybridagentic
top_k integer 8 每次上游搜索返回数量,支持 -11..100-1 表示使用上游默认数量
radius number null 相似度半径,范围 0..1;未提供时不发送给上游
include_profile boolean true 是否同时获取用户 profileagent 查询由上游决定是否忽略该参数
enable_llm_rerank boolean true 是否启用上游 LLM rerank具体生效范围由搜索方法和记忆类型决定
filters object null 上游过滤 DSL支持字段条件以及嵌套 ANDOR
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 过滤

请求示例:

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_idsession_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调用 该接口的客户端必须被视为可信客户端。

响应示例:

{
  "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. 修改记忆

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。

请求示例:

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,不会修改上游记忆服务原始文件。写入前会校验 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"
}

上游记忆服务客户端封装

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

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

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

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 服务耗时影响。需要验证完整摄入链路时再打开第二层开关:

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