Files
memory-gateway/README.md

20 KiB
Raw Blame History

Memory Gateway

Memory Gateway 是一个轻量级 FastAPI 服务,位于调用方和上游 memory service 之间。它负责用户鉴权、文件接收、资源元数据管理、 附件映射、软删除、手动覆盖,以及按范围编排记忆搜索。

Gateway 不直接修改上游记忆服务的 Markdown、SQLite 或向量索引文件。它通过 上游现有 API 完成记忆写入、flush 和搜索:

  • POST /api/v1/memory/add
  • POST /api/v1/memory/flush
  • POST /api/v1/memory/search

适用场景

  • 客户端要把聊天消息写入记忆。
  • 客户端要在同一个聊天 session 中上传图片、音频或文件,但不能自行转 base64。
  • 用户要上传独立资源如图片、音频、PDF、HTML、文档、文本文件。
  • 文件已经在外部存储中,需要登记长期 URI 和本次摄入 URI。
  • 搜索时需要同时覆盖当前聊天、资源记忆、全部用户记忆。
  • 需要在 Gateway 层实现用户隔离、资源软删除、记忆 tombstone 和人工覆盖。

关键原则

file:// 不是上传协议。它只表示“某台机器上的本地路径”。如果客户端和 Gateway 不在同一台机器、同一个容器、或同一个挂载路径下,直接把客户端本机 file:// 传给 Gateway 或上游服务会失败。

正确选择:

需求 推荐接口
聊天消息里带本地文件,且客户端不能转 base64 POST /memories/add/multipart
上传一个独立、长期可检索资源 POST /resources
文件已经在 MinIO/S3/Beaver user_files 等外部存储 POST /resources/external
调用方已经有 base64 或上游可访问 URI POST /memories/add

项目结构

memory-gateway/
├── core/
│   ├── api.py              # FastAPI 路由、请求校验、HTTP 错误映射
│   ├── backend_client.py   # 上游 memory service HTTP client
│   ├── config.py           # 环境变量配置
│   ├── db.py               # SQLite schema 初始化
│   ├── repository.py       # SQLite 读写
│   └── service.py          # 业务编排、上传、附件、搜索、软删除
├── main.py                 # uvicorn 启动入口
├── skill/                  # Memory Gateway agent skill 和 CLI
├── tests/                  # 单元、集成、实测命令记录
├── pyproject.toml
└── uv.lock

安装和启动

安装依赖:

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

启动 API

python main.py

默认服务地址:

http://127.0.0.1:8010

健康检查:

curl http://127.0.0.1:8010/health

健康响应中:

  • status: ok 表示 Gateway 和上游服务都可访问。
  • status: degraded 表示 Gateway 可访问,但上游服务不可访问。

配置

配置来自环境变量。常用 .env 示例:

MEMORY_GATEWAY_BACKEND_BASE_URL=http://127.0.0.1:1995
MEMORY_GATEWAY_DB_PATH=./data/memory_gateway.sqlite3
MEMORY_GATEWAY_STORAGE_DIR=./data/storage
MEMORY_GATEWAY_RESOURCE_SEARCH_BATCH_SIZE=50
MEMORY_GATEWAY_MAX_UPLOAD_BYTES=26214400
MEMORY_GATEWAY_ALLOWED_MIME_TYPES=image/*,audio/*,application/pdf,text/html,application/xhtml+xml,text/plain,text/markdown,text/csv,application/json,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-powerpoint,application/vnd.openxmlformats-officedocument.presentationml.presentation
MEMORY_GATEWAY_BACKEND_INGEST_ATTEMPTS=3
MEMORY_GATEWAY_BACKEND_RETRY_DELAY_SECONDS=0.25
MEMORY_GATEWAY_BACKEND_TIMEOUT_SECONDS=120
MEMORY_GATEWAY_HOST=127.0.0.1
MEMORY_GATEWAY_PORT=8010
MEMORY_GATEWAY_RELOAD=false

配置说明:

变量 默认值 说明
MEMORY_GATEWAY_BACKEND_BASE_URL http://127.0.0.1:1995 上游 memory service 地址
MEMORY_GATEWAY_DB_PATH ./data/memory_gateway.sqlite3 Gateway SQLite 数据库
MEMORY_GATEWAY_STORAGE_DIR ./data/storage Gateway 保存上传文件的位置
MEMORY_GATEWAY_RESOURCE_SEARCH_BATCH_SIZE 50 resources 搜索每批 session 数量
MEMORY_GATEWAY_MAX_UPLOAD_BYTES 26214400 单文件上传大小限制,默认 25 MiB
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 上游请求超时
MEMORY_GATEWAY_HOST 127.0.0.1 Gateway 监听地址
MEMORY_GATEWAY_PORT 8010 Gateway 监听端口
MEMORY_GATEWAY_RELOAD false 是否启用 uvicorn reload

不要把 Gateway 的数据库或 storage 配到上游服务内部目录。Gateway 只管理自己的状态。

数据模型

Gateway 启动时自动创建 SQLite 表:

用途
users 用户 ID 和 Gateway 生成的 user_key
user_resources 独立资源元数据、状态、内部 URI、软删除时间
memory_attachments 聊天/资源 session 到真实附件 URI 的映射
memory_tombstones 用户删除的 memory id 或 session_id
memory_overrides 用户人工修正后的 memory 文本

附件来源 source

source 来源
resource_upload /resources 上传
external_resource /resources/external 登记
memory_add_uri /memories/add 中的 URI item
memory_add_base64 /memories/add 中的 base64 item
memory_add_upload /memories/add/multipart 中的上传文件

Session 约定

场景 session_id
普通聊天 chat:{conversation_id}
独立上传资源 resource:{user_id}:{resource_id}
手动修正 memory_edit:{user_id}

/resources 会自动生成资源 session。/memories/add/memories/add/multipart 使用调用方传入的 session。

鉴权

POST /usersGET /health 外,所有业务接口都需要 user_iduser_key。认证失败返回 401

创建用户:

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-22T06:54:35.823262+00:00"
}

同一个 user_id 重复创建会返回已有用户记录。

API 概览

方法 路径 说明
GET /health Gateway 和上游健康检查
POST /users 创建或读取用户 key
POST /resources multipart 上传独立资源
POST /resources/external 登记外部资源
GET /resources 列出用户资源
GET /resources/{resource_id} 读取资源详情
DELETE /resources/{resource_id} 软删除资源
POST /memories/add JSON 追加记忆消息
POST /memories/add/multipart multipart 追加消息并上传附件
POST /memories/flush flush 指定 session
POST /memories/search 编排搜索
PATCH /memories/{memory_id} 人工覆盖 memory 文本
DELETE /memories/{memory_id} tombstone 删除 memory

上传聊天附件:/memories/add/multipart

用于“同一个聊天 session 中发送消息并上传附件”。客户端不需要转 base64也不需要 传 file://

请求类型:

POST /memories/add/multipart
Content-Type: multipart/form-data

表单字段:

字段 类型 必填 说明
user_id string 用户 ID
user_key string 用户 key
session_id string 通常是 chat:{conversation_id}
app_id string 默认 default
project_id string 默认 default
messages string 或 JSON file JSON array结构同 /memories/add
动态文件字段 file 条件必填 如果 content item 包含 upload_id,必须上传同名文件字段

upload_id 由调用方自定义。Gateway 不会随机生成,也不要求固定格式 (例如 user_id_filetype_number)。它只需要在同一次请求里非空、唯一,并且和 multipart 文件字段名一致。推荐使用简单可读的值,如 image_1audio_1doc_1

messages 示例:

[
  {
    "sender_id": "u_123",
    "role": "user",
    "timestamp": 1782111275810,
    "content": [
      {"type": "text", "text": "请记住这张图片和音频"},
      {
        "type": "image",
        "upload_id": "image_1",
        "name": "simple-multimodal-image.png",
        "ext": "png"
      },
      {
        "type": "audio",
        "upload_id": "audio_1",
        "name": "simple-tone.wav",
        "ext": "wav"
      }
    ]
  }
]

curl 示例:

curl -X POST http://127.0.0.1:8010/memories/add/multipart \
  -F user_id=u_123 \
  -F user_key=uk_xxx \
  -F session_id=chat:c_456 \
  -F app_id=default \
  -F project_id=default \
  -F 'messages=@messages.json;type=application/json' \
  -F 'image_1=@tests/simple-multimodal-image.png;type=image/png' \
  -F 'audio_1=@tests/simple-tone.wav;type=audio/wav'

处理流程:

  1. Gateway 校验用户。
  2. 解析 messages
  3. 找到 content item 中的 upload_id
  4. 从 multipart 中读取同名文件字段。
  5. 校验 MIME 和大小。
  6. 保存文件到 MEMORY_GATEWAY_STORAGE_DIR/{user_id}/memory_attachments/{sha256}/
  7. 将 item 转成上游可消费的 textbase64,不透传客户端本机路径。
  8. 调用上游 memory add。
  9. 上游 add 成功后写入 memory_attachmentssource = memory_add_upload

该接口只追加消息,不自动 flush。需要抽取和索引时继续调用 /memories/flush

追加 JSON 记忆:/memories/add

用于调用方已经有纯文本、base64或明确有上游可读取 URI 的场景。

请求:

POST /memories/add
Content-Type: application/json

示例:

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": 1782111275810,
        "content": [
          {"type": "text", "text": "用户喜欢简洁的中文回答"},
          {
            "type": "audio",
            "base64": "BASE64_DATA",
            "ext": "wav",
            "name": "tone.wav"
          }
        ]
      }
    ]
  }'

/memories/add 中:

  • uri item 会原样转发给上游,并登记附件映射。
  • base64 item 会保存到 Gateway storage并登记生成的内部 file://
  • 如果 URI 是客户端本机路径,且上游读不到该路径,请改用 /memories/add/multipart

Flush/memories/flush

curl -X POST http://127.0.0.1:8010/memories/flush \
  -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"
  }'

响应示例:

{
  "session_id": "chat:c_456",
  "backend": {
    "request_id": "request_id",
    "data": {
      "status": "extracted"
    }
  }
}

上传独立资源:/resources

用于把文件作为长期资源纳入 resources 搜索范围。

请求类型:

POST /resources
Content-Type: multipart/form-data

表单字段:

字段 类型 必填 说明
user_id string 用户 ID
user_key string 用户 key
app_id string 默认 default
project_id string 默认 default
title string 资源标题
description string 资源描述
file file 上传文件

示例:

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="Gateway image resource" \
  -F description="Example image resource" \
  -F 'file=@tests/simple-multimodal-image.png;type=image/png'

响应:

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

/resources 会:

  1. 保存文件到 Gateway storage。
  2. 写入 user_resources
  3. 写入 memory_attachments
  4. 构造上游 content item文本用 text,二进制用 base64
  5. 自动调用上游 add 和 flush。
  6. 成功后状态为 extracted

同一用户、同一 app/project 下相同 sha256 的活跃资源会复用已有资源。

登记外部资源:/resources/external

用于文件已经在外部存储中Gateway 只保存元数据和 URI 映射。

示例:

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://bucket/users/u_123/chart.png",
    "ingest_uri": "https://minio.example/presigned/chart.png",
    "title": "chart.png",
    "description": "External image"
  }'

字段含义:

  • source_uri:长期保存的真实映射 URI。
  • ingest_uri:上游本次摄入可读取的 URI通常是短期 presigned URL。

Gateway 不会下载 source_uriingest_uri

资源读取和删除

列出资源:

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

读取资源详情:

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

软删除资源:

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

删除资源会:

  • 设置资源 status = deleted
  • 设置资源 deleted_at
  • 软删除相关附件映射。
  • 清理 Gateway storage 中属于该资源的本地文件。
  • 不直接删除上游记忆服务内部文件或索引。

搜索:/memories/search

请求:

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,
    "app_id": "default",
    "project_id": "default"
  }'

搜索 scope

scope 行为
current_chat 搜索 chat:{conversation_id},需要传 conversation_id
resources 搜索当前用户已提取且未删除的资源 session
all_user_memory 搜索用户全部记忆,不加 session 过滤

返回结果会标准化为:

{
  "results": [
    {
      "id": "memory_id",
      "session_id": "chat:c_456",
      "text": "memory text",
      "score": 0.61,
      "source_scope": "current_chat",
      "resource_id": null,
      "resource_uri": null,
      "attachments": [
        {
          "type": "image",
          "name": "image.png",
          "internal_uri": "file:///..."
        }
      ],
      "raw": {}
    }
  ]
}

附件匹配规则:

  • Gateway 只返回当前用户、当前 session 的附件映射。
  • Gateway 根据搜索结果 raw/text 中出现的文件名匹配附件。
  • base64 字段内容不会参与匹配。
  • 未匹配到附件时不返回 attachments

手动覆盖和删除记忆

覆盖 memory 文本:

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": "chat:c_456",
    "override_text": "修正后的记忆文本"
  }'

删除 memory

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": "chat:c_456",
    "reason": "用户要求删除"
  }'

Gateway 会校验 session 归属,防止用户覆盖或删除其他用户资源 session 中的记忆。

/memories/add/multipart/resources 的区别

对比项 /memories/add/multipart /resources
目标 聊天 session 中的消息附件 独立长期资源
session 调用方传入,如 chat:c_456 Gateway 生成 resource:{user_id}:{resource_id}
文件字段 动态字段名匹配 upload_id 固定字段 file
元数据表 不写 user_resources user_resources
附件 source memory_add_upload resource_upload
上游调用 只 add调用方按需 flush Gateway 自动 add + flush
常用搜索 current_chatall_user_memory resources

错误码

状态码 常见原因
400 messages 不是合法 JSON array
401 user_iduser_key 无效
403 操作的 session 不属于当前用户
404 删除资源时资源不存在
413 上传文件超过 MEMORY_GATEWAY_MAX_UPLOAD_BYTES
415 MIME 类型不在白名单
422 请求字段缺失、upload_id 缺文件、重复文件字段、无效 base64

API 日志

Gateway 使用 memory_gateway.api logger 输出 JSON 日志:

  • 请求时间、耗时、方法、路径、URL、客户端。
  • Query 参数和小型请求体。
  • user_key、token、password、secret、API key 等敏感字段会遮蔽。
  • multipart 请求只记录 content type 和大小,不记录文件内容。
  • 响应体会按同样规则遮蔽敏感字段。

Agent CLI

项目包含 Memory Gateway agent skill CLI

python skill/memory-gateway-agent/scripts/memory_gateway.py --help

常用环境变量:

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

示例:

CLI="python skill/memory-gateway-agent/scripts/memory_gateway.py"

$CLI health
$CLI create-user u_123
$CLI upload-resource ./document.pdf --title "Document"
$CLI search "payment terms" --scope resources
$CLI add-memory --session-id chat:c_456 --messages ./messages.json
$CLI flush-memory --session-id chat:c_456

CLI 的 upload-resource 使用 multipart不会暴露本地 file:// 路径。

测试

运行自动化测试:

uv run pytest -q

当前验证结果:

56 passed, 2 skipped

运行聚焦测试:

uv run pytest tests/test_gateway.py -k "add_memory or upload_resource or attachment" -q

当前验证结果:

16 passed, 33 deselected

真实部署命令记录:

tests/test_command.md

该文件记录了 2026-06-22 对已部署 Gateway 和上游 memory service 执行的真实 curl 命令,包括:

  • GET /health
  • POST /memories/add/multipart
  • POST /memories/flush
  • POST /memories/search
  • POST /resources
  • GET /resources
  • resources scope 搜索

开发注意事项

  • 保持 Gateway 与上游服务职责分离;不要直接写上游内部文件。
  • 新上传入口应优先使用 multipart 或外部存储 URI避免要求客户端构造 base64。
  • 不要在响应中泄露内部 file://,除搜索附件映射 attachments[].internal_uri 外。
  • 资源详情和列表返回公开 resource://{user_id}/{resource_id}
  • 删除资源只做 Gateway 层软删除和本地文件清理,不直接删除上游记忆。
  • 搜索必须保持用户隔离,尤其是附件映射和资源 session。