neutralize upstream service branding
This commit is contained in:
18
.env.example
18
.env.example
@ -1,13 +1,13 @@
|
||||
# EverOS HTTP server used by the gateway client.
|
||||
EVEROS_BASE_URL=http://127.0.0.1:1995
|
||||
# Upstream memory service used by the gateway client.
|
||||
MEMORY_GATEWAY_BACKEND_BASE_URL=http://127.0.0.1:1995
|
||||
|
||||
# Gateway-owned SQLite database. This does not point at EverOS internal storage.
|
||||
# Gateway-owned SQLite database. This does not point at upstream internal storage.
|
||||
MEMORY_GATEWAY_DB_PATH=./data/memory_gateway.sqlite3
|
||||
|
||||
# Raw uploaded files are stored here before being passed to EverOS by file URI.
|
||||
# Raw uploaded files are stored here for gateway-managed ingestion.
|
||||
MEMORY_GATEWAY_STORAGE_DIR=./data/storage
|
||||
|
||||
# Number of resource session IDs sent per EverOS search request.
|
||||
# Number of resource session IDs sent per upstream search request.
|
||||
MEMORY_GATEWAY_RESOURCE_SEARCH_BATCH_SIZE=50
|
||||
|
||||
# Max upload size in bytes. Default here is 25 MiB.
|
||||
@ -16,10 +16,10 @@ MEMORY_GATEWAY_MAX_UPLOAD_BYTES=26214400
|
||||
# Comma-separated MIME allowlist. Prefix wildcards such as image/* are supported.
|
||||
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
|
||||
|
||||
# EverOS add/flush retry policy during resource ingestion.
|
||||
MEMORY_GATEWAY_EVEROS_INGEST_ATTEMPTS=3
|
||||
MEMORY_GATEWAY_EVEROS_RETRY_DELAY_SECONDS=0.25
|
||||
MEMORY_GATEWAY_EVEROS_TIMEOUT_SECONDS=120
|
||||
# Upstream add/flush retry policy during resource ingestion.
|
||||
MEMORY_GATEWAY_BACKEND_INGEST_ATTEMPTS=3
|
||||
MEMORY_GATEWAY_BACKEND_RETRY_DELAY_SECONDS=0.25
|
||||
MEMORY_GATEWAY_BACKEND_TIMEOUT_SECONDS=120
|
||||
|
||||
# API server settings used by python main.py.
|
||||
MEMORY_GATEWAY_HOST=0.0.0.0
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,6 +1,6 @@
|
||||
# Local environment files
|
||||
.env
|
||||
everos.env
|
||||
backend.env
|
||||
*.env.local
|
||||
|
||||
# Gateway runtime data
|
||||
|
||||
90
README.md
90
README.md
@ -1,18 +1,18 @@
|
||||
# Memory Gateway
|
||||
|
||||
Memory Gateway 是一个轻量级 FastAPI 服务,用于在 EverOS 现有
|
||||
Memory Gateway 是一个轻量级 FastAPI 服务,用于在上游记忆服务现有
|
||||
`/api/v1/memory/add`、`/api/v1/memory/flush`、`/api/v1/memory/search`
|
||||
能力之上构建用户资源记忆层。
|
||||
|
||||
它只维护 Gateway 自己的 SQLite 元数据表、软删除记录和手动覆盖记录,
|
||||
不会直接修改 EverOS 的 Markdown、SQLite 或 LanceDB 内部文件。
|
||||
不会直接修改上游记忆服务的 Markdown、SQLite 或 LanceDB 内部文件。
|
||||
|
||||
## 功能范围
|
||||
|
||||
- 上传用户资源:文件、图片、音频、PDF、HTML、普通文档、纯文本。
|
||||
- 保存资源元数据到 SQLite。
|
||||
- 为每个资源生成独立 EverOS `session_id`。
|
||||
- 调用 EverOS `add` 和 `flush` 完成资源记忆摄入,并对临时失败做轻量重试。
|
||||
- 为每个资源生成独立的上游记忆服务 `session_id`。
|
||||
- 调用上游记忆服务的 `add` 和 `flush` 完成资源记忆摄入,并对临时失败做轻量重试。
|
||||
- 提供资源列表、详情、软删除。
|
||||
- 支持上传大小限制、MIME 白名单、同用户同 app/project 下按 sha256 幂等复用资源。
|
||||
- 编排记忆搜索,支持当前聊天、资源记忆、全部用户记忆。
|
||||
@ -28,7 +28,7 @@ Memory Gateway 是一个轻量级 FastAPI 服务,用于在 EverOS 现有
|
||||
│ ├── api.py # FastAPI 路由
|
||||
│ ├── config.py # 环境变量配置
|
||||
│ ├── db.py # SQLite schema 初始化
|
||||
│ ├── everos_client.py # EverOS HTTP client
|
||||
│ ├── backend_client.py # 上游记忆服务 HTTP client
|
||||
│ ├── repository.py # SQLite 读写
|
||||
│ └── service.py # 业务编排
|
||||
├── main.py # Python 启动入口
|
||||
@ -50,21 +50,21 @@ 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_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_EVEROS_INGEST_ATTEMPTS` | `3` | EverOS `add` 和 `flush` 各自最多重试次数 |
|
||||
| `MEMORY_GATEWAY_EVEROS_RETRY_DELAY_SECONDS` | `0.25` | EverOS 摄入重试间隔秒数 |
|
||||
| `MEMORY_GATEWAY_EVEROS_TIMEOUT_SECONDS` | `120` | 单次 EverOS HTTP 请求超时秒数 |
|
||||
| `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
|
||||
自己的存储位置,不要配置成 EverOS 的内部存储目录。
|
||||
自己的存储位置,不要配置成上游记忆服务的内部存储目录。
|
||||
|
||||
## 安装依赖
|
||||
|
||||
@ -133,7 +133,7 @@ Gateway 会通过 `memory_gateway.api` logger 为每个 API 请求输出一条 J
|
||||
GET /health
|
||||
```
|
||||
|
||||
该接口不需要 `user_id` 或 `user_key`,用于确认 Gateway API 是否可响应,以及上游 EverOS 是否可访问。
|
||||
该接口不需要 `user_id` 或 `user_key`,用于确认 Gateway API 是否可响应,以及上游记忆服务是否可访问。
|
||||
|
||||
请求示例:
|
||||
|
||||
@ -141,7 +141,7 @@ GET /health
|
||||
curl http://127.0.0.1:8010/health
|
||||
```
|
||||
|
||||
EverOS 正常时响应示例:
|
||||
上游记忆服务正常时响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
@ -149,7 +149,7 @@ EverOS 正常时响应示例:
|
||||
"api": {
|
||||
"status": "ok"
|
||||
},
|
||||
"everos": {
|
||||
"backend": {
|
||||
"status": "ok",
|
||||
"base_url": "http://127.0.0.1:1995",
|
||||
"data": {
|
||||
@ -159,7 +159,7 @@ EverOS 正常时响应示例:
|
||||
}
|
||||
```
|
||||
|
||||
EverOS 不可访问时仍返回 HTTP 200,但 `status` 会变成 `degraded`,便于区分“Gateway API 活着”和“上游 EverOS 故障”:
|
||||
上游记忆服务不可访问时仍返回 HTTP 200,但 `status` 会变成 `degraded`,便于区分“Gateway API 活着”和“上游记忆服务故障”:
|
||||
|
||||
```json
|
||||
{
|
||||
@ -167,7 +167,7 @@ EverOS 不可访问时仍返回 HTTP 200,但 `status` 会变成 `degraded`,
|
||||
"api": {
|
||||
"status": "ok"
|
||||
},
|
||||
"everos": {
|
||||
"backend": {
|
||||
"status": "unavailable",
|
||||
"base_url": "http://127.0.0.1:1995",
|
||||
"error": "Connection refused"
|
||||
@ -221,8 +221,8 @@ 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 |
|
||||
| `app_id` | string | 否 | `default` | 上游记忆服务 app scope |
|
||||
| `project_id` | string | 否 | `default` | 上游记忆服务 project scope |
|
||||
| `file` | file | 是 | 无 | 上传资源文件 |
|
||||
| `title` | string | 否 | `null` | 资源标题 |
|
||||
| `description` | string | 否 | `null` | 资源描述 |
|
||||
@ -234,22 +234,22 @@ Content-Type: multipart/form-data
|
||||
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`。
|
||||
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 的活跃资源,会直接返回已有资源,避免重复调用 EverOS 摄入。
|
||||
- EverOS `add` 和 `flush` 临时失败时会分别按配置重试;单次请求受 `MEMORY_GATEWAY_EVEROS_TIMEOUT_SECONDS` 控制;全部失败后资源状态为 `failed`,并记录 `error_message`。
|
||||
- 同一用户在同一 `app_id`、`project_id` 下重复上传相同 sha256 的活跃资源,会直接返回已有资源,避免重复调用上游记忆服务摄入。
|
||||
- 上游记忆服务的 `add` 和 `flush` 临时失败时会分别按配置重试;单次请求受 `MEMORY_GATEWAY_BACKEND_TIMEOUT_SECONDS` 控制;全部失败后资源状态为 `failed`,并记录 `error_message`。
|
||||
|
||||
content type 映射:
|
||||
|
||||
| 文件类型 | EverOS content type |
|
||||
| 文件类型 | 上游记忆服务 content type |
|
||||
|---|---|
|
||||
| `image/*` | `image` |
|
||||
| `audio/*` | `audio` |
|
||||
@ -406,7 +406,7 @@ DELETE /resources/{resource_id}?user_id={user_id}&user_key={user_key}
|
||||
- 设置 `status = deleted`。
|
||||
- 后续 `resources` scope 搜索会排除该资源的 `session_id`。
|
||||
- 清理 Gateway 自己在 `MEMORY_GATEWAY_STORAGE_DIR` 下保存的原始上传文件。
|
||||
- 不物理删除 EverOS 内部记忆或索引。
|
||||
- 不物理删除上游记忆服务内部记忆或索引。
|
||||
|
||||
请求示例:
|
||||
|
||||
@ -441,9 +441,9 @@ Content-Type: application/json
|
||||
| `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 |
|
||||
| `top_k` | integer | 否 | `8` | 每次上游记忆服务搜索返回数量,范围 `1..100` |
|
||||
| `app_id` | string | 否 | `default` | 上游记忆服务 app scope |
|
||||
| `project_id` | string | 否 | `default` | 上游记忆服务 project scope |
|
||||
|
||||
`scope` 支持:
|
||||
|
||||
@ -472,9 +472,9 @@ curl -X POST http://127.0.0.1:8010/memories/search \
|
||||
|
||||
搜索编排逻辑:
|
||||
|
||||
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` 过滤。
|
||||
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. 合并结果。
|
||||
5. 过滤 `memory_tombstones` 命中的 `memory_id` 或 `session_id`。
|
||||
6. 应用 active `memory_overrides`,把 `text` 替换为 `override_text`。
|
||||
@ -495,7 +495,7 @@ curl -X POST http://127.0.0.1:8010/memories/search \
|
||||
"raw": {
|
||||
"id": "mem_abc",
|
||||
"session_id": "resource:u_123:r_xxx",
|
||||
"episode": "原始 EverOS 返回内容"
|
||||
"episode": "原始上游记忆服务返回内容"
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -518,7 +518,7 @@ Content-Type: application/json
|
||||
| `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。
|
||||
该接口只写入或更新 `memory_overrides`,不会修改上游记忆服务原始文件。写入前会校验 `session_id` 属于当前用户:当前版本支持当前用户的 `resource:{user_id}:{resource_id}` 和 `memory_edit:{user_id}`。后续搜索结果命中该 `memory_id` 时,返回的 `text` 会替换为 `override_text`,但保留原始 memory id。
|
||||
|
||||
请求示例:
|
||||
|
||||
@ -559,7 +559,7 @@ Content-Type: application/json
|
||||
| `session_id` | string | 是 | memory 所属 session,必须属于当前用户 |
|
||||
| `reason` | string | 否 | 删除原因 |
|
||||
|
||||
该接口只写入 `memory_tombstones`,不会修改 EverOS 原始文件。写入前会校验 `session_id` 属于当前用户:当前版本支持当前用户的 `resource:{user_id}:{resource_id}` 和 `memory_edit:{user_id}`。后续搜索结果如果命中 tombstone 的 `memory_id` 或 `session_id`,会被过滤。
|
||||
该接口只写入 `memory_tombstones`,不会修改上游记忆服务原始文件。写入前会校验 `session_id` 属于当前用户:当前版本支持当前用户的 `resource:{user_id}:{resource_id}` 和 `memory_edit:{user_id}`。后续搜索结果如果命中 tombstone 的 `memory_id` 或 `session_id`,会被过滤。
|
||||
|
||||
请求示例:
|
||||
|
||||
@ -584,9 +584,9 @@ curl -X DELETE http://127.0.0.1:8010/memories/mem_abc \
|
||||
}
|
||||
```
|
||||
|
||||
## EverOS client 封装
|
||||
## 上游记忆服务客户端封装
|
||||
|
||||
Gateway 内部通过 `core/everos_client.py` 调用 EverOS:
|
||||
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`
|
||||
@ -619,21 +619,21 @@ 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 所在机器用客户端可访问地址访问:
|
||||
默认测试不会访问真实上游记忆服务。若要对已部署的上游记忆服务做 health 集成验证,先确认上游记忆服务正在监听 `0.0.0.0:1995`,然后从 Gateway 所在机器用客户端可访问地址访问:
|
||||
|
||||
```bash
|
||||
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
|
||||
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 上传会写入 EverOS,且可能受上游解析、LLM、embedding 服务耗时影响。需要验证完整摄入链路时再打开第二层开关:
|
||||
真实 add/flush 上传会写入上游记忆服务,且可能受上游解析、LLM、embedding 服务耗时影响。需要验证完整摄入链路时再打开第二层开关:
|
||||
|
||||
```bash
|
||||
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
|
||||
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
|
||||
```
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""Lightweight Memory Gateway for EverOS."""
|
||||
"""Lightweight user resource memory gateway."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
24
core/api.py
24
core/api.py
@ -13,7 +13,7 @@ from starlette.responses import Response
|
||||
|
||||
from .config import GatewayConfig
|
||||
from .db import init_db
|
||||
from .everos_client import EverOSClient
|
||||
from .backend_client import BackendClient
|
||||
from .repository import MemoryRepository
|
||||
from .service import MemoryGatewayService, UnsupportedContentType, UploadTooLarge
|
||||
|
||||
@ -181,21 +181,21 @@ def _body_for_log(body: bytes, content_type: str | None) -> Any:
|
||||
def create_app(
|
||||
*,
|
||||
config: GatewayConfig | None = None,
|
||||
everos_client: Any | None = None,
|
||||
backend_client: Any | None = None,
|
||||
) -> FastAPI:
|
||||
cfg = config or GatewayConfig.from_env()
|
||||
init_db(cfg.database_path)
|
||||
repository = MemoryRepository(cfg.database_path)
|
||||
client = everos_client or EverOSClient(
|
||||
cfg.everos_base_url,
|
||||
timeout=cfg.everos_timeout_seconds,
|
||||
client = backend_client or BackendClient(
|
||||
cfg.backend_base_url,
|
||||
timeout=cfg.backend_timeout_seconds,
|
||||
)
|
||||
service = MemoryGatewayService(cfg, repository, client)
|
||||
|
||||
app = FastAPI(title="memory-gateway", version="0.1.0")
|
||||
app.state.config = cfg
|
||||
app.state.repository = repository
|
||||
app.state.everos_client = client
|
||||
app.state.backend_client = client
|
||||
app.state.gateway_service = service
|
||||
|
||||
router = APIRouter()
|
||||
@ -278,24 +278,24 @@ def create_app(
|
||||
@router.get("/health")
|
||||
async def health() -> dict[str, Any]:
|
||||
try:
|
||||
everos_health = await client.health_check()
|
||||
backend_health = await client.health_check()
|
||||
except Exception as exc:
|
||||
return {
|
||||
"status": "degraded",
|
||||
"api": {"status": "ok"},
|
||||
"everos": {
|
||||
"backend": {
|
||||
"status": "unavailable",
|
||||
"base_url": cfg.everos_base_url,
|
||||
"base_url": cfg.backend_base_url,
|
||||
"error": str(exc),
|
||||
},
|
||||
}
|
||||
return {
|
||||
"status": "ok",
|
||||
"api": {"status": "ok"},
|
||||
"everos": {
|
||||
"backend": {
|
||||
"status": "ok",
|
||||
"base_url": cfg.everos_base_url,
|
||||
"data": everos_health,
|
||||
"base_url": cfg.backend_base_url,
|
||||
"data": backend_health,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ from typing import Any
|
||||
import httpx
|
||||
|
||||
|
||||
class EverOSClient:
|
||||
class BackendClient:
|
||||
def __init__(self, base_url: str, timeout: float = 120.0) -> None:
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.timeout = timeout
|
||||
@ -27,15 +27,15 @@ _DEFAULT_ALLOWED_MIME_TYPES = (
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GatewayConfig:
|
||||
everos_base_url: str = "http://127.0.0.1:1995"
|
||||
backend_base_url: str = "http://127.0.0.1:1995"
|
||||
database_path: Path = _PROJECT_ROOT / "data" / "memory_gateway.sqlite3"
|
||||
storage_dir: Path = _PROJECT_ROOT / "data" / "storage"
|
||||
resource_search_batch_size: int = 50
|
||||
max_upload_bytes: int = 25 * 1024 * 1024
|
||||
allowed_mime_types: tuple[str, ...] = _DEFAULT_ALLOWED_MIME_TYPES
|
||||
everos_ingest_attempts: int = 3
|
||||
everos_retry_delay_seconds: float = 0.25
|
||||
everos_timeout_seconds: float = 120.0
|
||||
backend_ingest_attempts: int = 3
|
||||
backend_retry_delay_seconds: float = 0.25
|
||||
backend_timeout_seconds: float = 120.0
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> GatewayConfig:
|
||||
@ -48,8 +48,8 @@ class GatewayConfig:
|
||||
if item.strip()
|
||||
)
|
||||
return cls(
|
||||
everos_base_url=os.environ.get(
|
||||
"EVEROS_BASE_URL",
|
||||
backend_base_url=os.environ.get(
|
||||
"MEMORY_GATEWAY_BACKEND_BASE_URL",
|
||||
"http://127.0.0.1:1995",
|
||||
).rstrip("/"),
|
||||
database_path=Path(
|
||||
@ -71,13 +71,13 @@ class GatewayConfig:
|
||||
os.environ.get("MEMORY_GATEWAY_MAX_UPLOAD_BYTES", str(25 * 1024 * 1024))
|
||||
),
|
||||
allowed_mime_types=allowed_mime_types,
|
||||
everos_ingest_attempts=int(
|
||||
os.environ.get("MEMORY_GATEWAY_EVEROS_INGEST_ATTEMPTS", "3")
|
||||
backend_ingest_attempts=int(
|
||||
os.environ.get("MEMORY_GATEWAY_BACKEND_INGEST_ATTEMPTS", "3")
|
||||
),
|
||||
everos_retry_delay_seconds=float(
|
||||
os.environ.get("MEMORY_GATEWAY_EVEROS_RETRY_DELAY_SECONDS", "0.25")
|
||||
backend_retry_delay_seconds=float(
|
||||
os.environ.get("MEMORY_GATEWAY_BACKEND_RETRY_DELAY_SECONDS", "0.25")
|
||||
),
|
||||
everos_timeout_seconds=float(
|
||||
os.environ.get("MEMORY_GATEWAY_EVEROS_TIMEOUT_SECONDS", "120")
|
||||
backend_timeout_seconds=float(
|
||||
os.environ.get("MEMORY_GATEWAY_BACKEND_TIMEOUT_SECONDS", "120")
|
||||
),
|
||||
)
|
||||
|
||||
@ -127,11 +127,11 @@ class MemoryGatewayService:
|
||||
self,
|
||||
config: GatewayConfig,
|
||||
repository: MemoryRepository,
|
||||
everos_client: Any,
|
||||
backend_client: Any,
|
||||
) -> None:
|
||||
self.config = config
|
||||
self.repository = repository
|
||||
self.everos_client = everos_client
|
||||
self.backend_client = backend_client
|
||||
|
||||
def create_user(self, user_id: str) -> dict[str, Any]:
|
||||
user_key = f"uk_{secrets.token_urlsafe(32)}"
|
||||
@ -204,8 +204,8 @@ class MemoryGatewayService:
|
||||
)
|
||||
|
||||
try:
|
||||
await self._retry_everos_call(
|
||||
lambda: self.everos_client.add_memory(
|
||||
await self._retry_backend_call(
|
||||
lambda: self.backend_client.add_memory(
|
||||
self._build_add_payload(
|
||||
resource=resource,
|
||||
user_id=user_id,
|
||||
@ -215,8 +215,8 @@ class MemoryGatewayService:
|
||||
)
|
||||
)
|
||||
)
|
||||
await self._retry_everos_call(
|
||||
lambda: self.everos_client.flush_memory(session_id, app_id, project_id)
|
||||
await self._retry_backend_call(
|
||||
lambda: self.backend_client.flush_memory(session_id, app_id, project_id)
|
||||
)
|
||||
except Exception as exc:
|
||||
failed = self.repository.update_resource_status(
|
||||
@ -229,8 +229,8 @@ class MemoryGatewayService:
|
||||
extracted = self.repository.update_resource_status(resource_id, "extracted")
|
||||
return self._resource_summary(extracted or resource)
|
||||
|
||||
async def _retry_everos_call(self, operation: Any) -> Any:
|
||||
attempts = max(1, self.config.everos_ingest_attempts)
|
||||
async def _retry_backend_call(self, operation: Any) -> Any:
|
||||
attempts = max(1, self.config.backend_ingest_attempts)
|
||||
last_error: Exception | None = None
|
||||
for attempt in range(attempts):
|
||||
try:
|
||||
@ -239,11 +239,11 @@ class MemoryGatewayService:
|
||||
last_error = exc
|
||||
if attempt == attempts - 1:
|
||||
break
|
||||
delay = self.config.everos_retry_delay_seconds
|
||||
delay = self.config.backend_retry_delay_seconds
|
||||
if delay > 0:
|
||||
await asyncio.sleep(delay)
|
||||
if last_error is None:
|
||||
raise RuntimeError("EverOS operation failed")
|
||||
raise RuntimeError("upstream memory service operation failed")
|
||||
raise last_error
|
||||
|
||||
def _build_add_payload(
|
||||
@ -367,7 +367,7 @@ class MemoryGatewayService:
|
||||
)
|
||||
results.extend(
|
||||
self._extract_results(
|
||||
await self.everos_client.search_memory(payload),
|
||||
await self.backend_client.search_memory(payload),
|
||||
source_scope="current_chat",
|
||||
session_resource_map=session_resource_map,
|
||||
user_id=user_id,
|
||||
@ -393,7 +393,7 @@ class MemoryGatewayService:
|
||||
)
|
||||
results.extend(
|
||||
self._extract_results(
|
||||
await self.everos_client.search_memory(payload),
|
||||
await self.backend_client.search_memory(payload),
|
||||
source_scope="resources",
|
||||
session_resource_map=session_resource_map,
|
||||
user_id=user_id,
|
||||
@ -411,7 +411,7 @@ class MemoryGatewayService:
|
||||
)
|
||||
results.extend(
|
||||
self._extract_results(
|
||||
await self.everos_client.search_memory(payload),
|
||||
await self.backend_client.search_memory(payload),
|
||||
source_scope="all_user_memory",
|
||||
session_resource_map=session_resource_map,
|
||||
user_id=user_id,
|
||||
@ -438,7 +438,7 @@ class MemoryGatewayService:
|
||||
}
|
||||
return {
|
||||
"session_id": session_id,
|
||||
"everos": await self.everos_client.add_memory(payload),
|
||||
"backend": await self.backend_client.add_memory(payload),
|
||||
}
|
||||
|
||||
async def flush_memory(
|
||||
@ -450,7 +450,7 @@ class MemoryGatewayService:
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"session_id": session_id,
|
||||
"everos": await self.everos_client.flush_memory(
|
||||
"backend": await self.backend_client.flush_memory(
|
||||
session_id,
|
||||
app_id,
|
||||
project_id,
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
# Upstream Brand Neutralization Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Remove the upstream product identity from the current Memory Gateway files without changing the upstream memory HTTP protocol.
|
||||
|
||||
**Architecture:** Replace product-specific names with `backend` terminology at configuration, client, service, API, test, and documentation boundaries. Enforce the result with a repository-level regression test that scans both file names and text content.
|
||||
|
||||
**Tech Stack:** Python 3, FastAPI, pytest, Markdown, environment configuration.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add the neutral-brand regression test
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/test_branding.py`
|
||||
|
||||
- [x] Add a test that constructs the forbidden token from separate string fragments.
|
||||
- [x] Scan non-generated project file names and UTF-8 text contents.
|
||||
- [x] Run the test and verify it fails against the existing product-specific names.
|
||||
|
||||
### Task 2: Rename runtime boundaries
|
||||
|
||||
**Files:**
|
||||
- Rename: `core/backend_client.py` to `core/backend_client.py`
|
||||
- Modify: `core/api.py`
|
||||
- Modify: `core/config.py`
|
||||
- Modify: `core/service.py`
|
||||
- Modify: `core/__init__.py`
|
||||
- Modify: `.env.example`
|
||||
- Modify: `.gitignore`
|
||||
- Delete: `backend.env.example`
|
||||
|
||||
- [x] Rename the client class, dependency attributes, retry helpers, and configuration fields to `backend` terminology.
|
||||
- [x] Rename environment variables to `MEMORY_GATEWAY_BACKEND_*`.
|
||||
- [x] Rename health and direct add/flush response fields to `backend`.
|
||||
- [x] Preserve all `/api/v1/memory/*` paths.
|
||||
|
||||
### Task 3: Rename tests and public documentation
|
||||
|
||||
**Files:**
|
||||
- Rename: `tests/test_backend_integration.py` to `tests/test_backend_integration.py`
|
||||
- Modify: `tests/test_gateway.py`
|
||||
- Modify: `tests/test_command.md`
|
||||
- Modify: `README.md`
|
||||
- Modify: `pyproject.toml`
|
||||
- Modify: `skill/memory-gateway-agent/SKILL.md`
|
||||
- Modify: `skill/memory-gateway-agent/references/api.md`
|
||||
|
||||
- [x] Rename fixtures, tests, environment flags, examples, and expected JSON fields.
|
||||
- [x] Describe the dependency only as an upstream memory service.
|
||||
- [x] Update integration commands and package metadata.
|
||||
|
||||
### Task 4: Verify the current working tree
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/superpowers/plans/2026-06-12-upstream-brand-neutralization.md`
|
||||
|
||||
- [x] Remove generated bytecode caches.
|
||||
- [x] Run the branding regression test.
|
||||
- [x] Run the full test suite and Python compilation.
|
||||
- [x] Run a final case-insensitive content and file-name scan, excluding `.git` history.
|
||||
@ -0,0 +1,23 @@
|
||||
# Upstream Brand Neutralization Design
|
||||
|
||||
## Goal
|
||||
|
||||
Remove the upstream product name from the current Memory Gateway working tree while preserving the upstream HTTP protocol and application behavior.
|
||||
|
||||
## Scope
|
||||
|
||||
- Rename the upstream client module, class, configuration fields, environment variables, state attributes, response fields, tests, and integration test file to neutral `backend` terminology.
|
||||
- Rewrite README, Skill documentation, examples, package metadata, and test records to describe an "upstream memory service".
|
||||
- Remove the upstream-specific environment example because its variable names identify the product.
|
||||
- Preserve `/api/v1/memory/add`, `/api/v1/memory/flush`, and `/api/v1/memory/search` paths.
|
||||
- Do not rewrite Git history.
|
||||
|
||||
## Compatibility
|
||||
|
||||
This is an intentional configuration and response-schema rename. Deployments must move to `MEMORY_GATEWAY_BACKEND_*` variables, and health/add/flush consumers must read the `backend` field. No legacy aliases are retained because they would defeat the neutralization requirement.
|
||||
|
||||
## Verification
|
||||
|
||||
- Add an automated repository scan that rejects the forbidden upstream token in current files and file names.
|
||||
- Run the full unit suite and compilation checks.
|
||||
- Run a final case-insensitive repository scan excluding `.git`, virtual environments, runtime data, and generated bytecode.
|
||||
@ -1,36 +0,0 @@
|
||||
# EverOS server settings used by the upstream EverOS process.
|
||||
# Copy this file to everos.env or to the EverOS project .env, then fill secrets.
|
||||
|
||||
# API listener. The Memory Gateway should point EVEROS_BASE_URL at this host/port.
|
||||
EVEROS_API__HOST=127.0.0.1
|
||||
EVEROS_API__PORT=8000
|
||||
|
||||
# Logging
|
||||
EVEROS_LOG_LEVEL=INFO
|
||||
EVEROS_LOG_FORMAT=console
|
||||
TZ=Asia/Shanghai
|
||||
|
||||
# LLM provider
|
||||
EVEROS_LLM__BASE_URL=https://api.openai.com/v1
|
||||
EVEROS_LLM__API_KEY=replace-with-llm-api-key
|
||||
EVEROS_LLM__MODEL=gpt-4o-mini
|
||||
EVEROS_LLM__TIMEOUT_SECONDS=120
|
||||
|
||||
# Embedding provider
|
||||
EVEROS_EMBEDDING__BASE_URL=https://api.openai.com/v1
|
||||
EVEROS_EMBEDDING__API_KEY=replace-with-embedding-api-key
|
||||
EVEROS_EMBEDDING__MODEL=text-embedding-3-small
|
||||
EVEROS_EMBEDDING__TIMEOUT_SECONDS=120
|
||||
|
||||
# Rerank provider
|
||||
EVEROS_RERANK__BASE_URL=https://api.example.com/v1
|
||||
EVEROS_RERANK__API_KEY=replace-with-rerank-api-key
|
||||
EVEROS_RERANK__MODEL=replace-with-rerank-model
|
||||
EVEROS_RERANK__TIMEOUT_SECONDS=120
|
||||
|
||||
# Multimodal parsing provider
|
||||
EVEROS_MULTIMODAL__BASE_URL=https://api.openai.com/v1
|
||||
EVEROS_MULTIMODAL__API_KEY=replace-with-multimodal-api-key
|
||||
EVEROS_MULTIMODAL__MODEL=gpt-4o-mini
|
||||
EVEROS_MULTIMODAL__TIMEOUT_SECONDS=120
|
||||
EVEROS_MULTIMODAL__RESIZE_IMAGES_FOR_VLM=true
|
||||
@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "memory-gateway"
|
||||
version = "0.1.0"
|
||||
description = "Lightweight Memory Gateway for EverOS user resources"
|
||||
description = "Lightweight user resource memory gateway"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"fastapi>=0.104.0",
|
||||
@ -31,5 +31,5 @@ testpaths = ["tests"]
|
||||
python_files = ["test_*.py"]
|
||||
asyncio_mode = "auto"
|
||||
markers = [
|
||||
"integration: tests that call a real EverOS service",
|
||||
"integration: tests that call a real upstream memory service",
|
||||
]
|
||||
|
||||
@ -23,7 +23,7 @@ Do not write a real `user_key` into source files, prompts, logs, or committed do
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Run `$CLI health` when connectivity or EverOS availability is uncertain.
|
||||
1. Run `$CLI health` when connectivity or upstream memory service availability is uncertain.
|
||||
2. Use an existing `user_id` and `user_key`. Run `create-user` only when the user explicitly needs a new Gateway identity.
|
||||
3. Choose the operation:
|
||||
- Upload durable user files with `upload-resource`.
|
||||
@ -58,7 +58,7 @@ Read [references/api.md](references/api.md) when choosing scopes, constructing m
|
||||
|
||||
- Do not expose internal file paths. Return the Gateway's `resource://{user_id}/{resource_id}` URI to users.
|
||||
- Do not claim ingestion succeeded unless the upload status is `extracted` or flush reports success.
|
||||
- Treat `health.status = degraded` as Gateway available but EverOS unavailable.
|
||||
- Resource deletion is soft deletion in Gateway search scope and removes the Gateway upload copy; it does not delete EverOS internal indexes.
|
||||
- Treat `health.status = degraded` as Gateway available but upstream memory service unavailable.
|
||||
- Resource deletion is soft deletion in Gateway search scope and removes the Gateway upload copy; it does not delete upstream memory service internal indexes.
|
||||
- Memory override and deletion require an owned `resource:{user_id}:{resource_id}` or `memory_edit:{user_id}` session.
|
||||
- Ask for confirmation before destructive deletion unless the user's current request explicitly instructs deletion.
|
||||
|
||||
@ -46,7 +46,7 @@ CLI="python skill/memory-gateway-agent/scripts/memory_gateway.py"
|
||||
$CLI health
|
||||
```
|
||||
|
||||
No credentials required. HTTP 200 may contain `"status": "degraded"` when EverOS is unavailable.
|
||||
No credentials required. HTTP 200 may contain `"status": "degraded"` when upstream memory service is unavailable.
|
||||
|
||||
### Create User
|
||||
|
||||
@ -77,7 +77,7 @@ Requires credentials. Supported resources depend on the server MIME allowlist. S
|
||||
}
|
||||
```
|
||||
|
||||
`failed` means the record exists but EverOS ingestion failed. Identical active content for the same user/app/project may return the existing resource.
|
||||
`failed` means the record exists but upstream memory service ingestion failed. Identical active content for the same user/app/project may return the existing resource.
|
||||
|
||||
### List, Get, and Delete Resources
|
||||
|
||||
@ -108,7 +108,7 @@ $CLI search "query" --scope current_chat --scope resources \
|
||||
|
||||
When no scope is provided, the CLI searches `resources` only unless a conversation ID is supplied; with a conversation ID it searches `current_chat` and `resources`. Explicit `current_chat` scope requires `--conversation-id`.
|
||||
|
||||
Each result includes normalized `id`, `session_id`, `text`, `source_scope`, and optional resource metadata. The `raw` field preserves the EverOS response.
|
||||
Each result includes normalized `id`, `session_id`, `text`, `source_scope`, and optional resource metadata. The `raw` field preserves the upstream memory service response.
|
||||
|
||||
### Add and Flush Memory
|
||||
|
||||
@ -150,7 +150,7 @@ $CLI delete-memory mem_abc \
|
||||
--reason "User requested deletion"
|
||||
```
|
||||
|
||||
These operations write Gateway overrides or tombstones. They do not modify EverOS files directly. The server rejects sessions not owned by the authenticated user.
|
||||
These operations write Gateway overrides or tombstones. They do not modify upstream memory service files directly. The server rejects sessions not owned by the authenticated user.
|
||||
|
||||
## Message Format
|
||||
|
||||
@ -161,7 +161,7 @@ Each message requires:
|
||||
| `sender_id` | string | Usually the current user ID |
|
||||
| `role` | string | `user`, `assistant`, or `tool` |
|
||||
| `timestamp` | integer | Unix milliseconds, greater than zero |
|
||||
| `content` | string or array | Text or EverOS content items |
|
||||
| `content` | string or array | Text or upstream memory service content items |
|
||||
|
||||
Common content items:
|
||||
|
||||
@ -187,7 +187,7 @@ Common content items:
|
||||
}
|
||||
```
|
||||
|
||||
Prefer base64 for local binary files. A `file://` URI is only usable when EverOS can access the same filesystem path.
|
||||
Prefer base64 for local binary files. A `file://` URI is only usable when upstream memory service can access the same filesystem path.
|
||||
|
||||
## Search Scopes
|
||||
|
||||
|
||||
@ -9,31 +9,31 @@ import pytest
|
||||
|
||||
from core.api import create_app
|
||||
from core.config import GatewayConfig
|
||||
from core.everos_client import EverOSClient
|
||||
from core.backend_client import BackendClient
|
||||
|
||||
|
||||
pytestmark = pytest.mark.integration
|
||||
|
||||
|
||||
def _integration_enabled() -> bool:
|
||||
return os.environ.get("RUN_EVEROS_INTEGRATION") == "1"
|
||||
return os.environ.get("RUN_BACKEND_INTEGRATION") == "1"
|
||||
|
||||
|
||||
def _ingest_integration_enabled() -> bool:
|
||||
return os.environ.get("RUN_EVEROS_INGEST_INTEGRATION") == "1"
|
||||
return os.environ.get("RUN_BACKEND_INGEST_INTEGRATION") == "1"
|
||||
|
||||
|
||||
def _everos_base_url() -> str:
|
||||
return os.environ.get("EVEROS_BASE_URL", "http://127.0.0.1:1995")
|
||||
def _backend_base_url() -> str:
|
||||
return os.environ.get("MEMORY_GATEWAY_BACKEND_BASE_URL", "http://127.0.0.1:1995")
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not _integration_enabled(),
|
||||
reason="set RUN_EVEROS_INTEGRATION=1 to run against a real EverOS service",
|
||||
reason="set RUN_BACKEND_INTEGRATION=1 to run against an upstream memory service",
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_real_everos_health_check() -> None:
|
||||
client = EverOSClient(_everos_base_url(), timeout=10)
|
||||
async def test_real_backend_health_check() -> None:
|
||||
client = BackendClient(_backend_base_url(), timeout=10)
|
||||
|
||||
health = await client.health_check()
|
||||
|
||||
@ -43,17 +43,17 @@ async def test_real_everos_health_check() -> None:
|
||||
@pytest.mark.skipif(
|
||||
not _ingest_integration_enabled(),
|
||||
reason=(
|
||||
"set RUN_EVEROS_INGEST_INTEGRATION=1 to run real EverOS add/flush ingestion"
|
||||
"set RUN_BACKEND_INGEST_INTEGRATION=1 to run upstream add/flush ingestion"
|
||||
),
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_gateway_uploads_text_resource_to_real_everos(tmp_path: Path) -> None:
|
||||
async def test_gateway_uploads_text_resource_to_real_backend(tmp_path: Path) -> None:
|
||||
config = GatewayConfig(
|
||||
everos_base_url=_everos_base_url(),
|
||||
backend_base_url=_backend_base_url(),
|
||||
database_path=tmp_path / "gateway.sqlite3",
|
||||
storage_dir=tmp_path / "storage",
|
||||
everos_ingest_attempts=1,
|
||||
everos_timeout_seconds=30,
|
||||
backend_ingest_attempts=1,
|
||||
backend_timeout_seconds=30,
|
||||
)
|
||||
app = create_app(config=config)
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
@ -69,8 +69,8 @@ async def test_gateway_uploads_text_resource_to_real_everos(tmp_path: Path) -> N
|
||||
data={"user_id": user_id, "user_key": user_key},
|
||||
files={
|
||||
"file": (
|
||||
"real-everos.txt",
|
||||
b"real everos integration",
|
||||
"integration.txt",
|
||||
b"upstream memory service integration",
|
||||
"text/plain",
|
||||
)
|
||||
},
|
||||
35
tests/test_branding.py
Normal file
35
tests/test_branding.py
Normal file
@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
FORBIDDEN_TOKEN = "ever" + "os"
|
||||
SKIPPED_PARTS = {
|
||||
".git",
|
||||
".pytest_cache",
|
||||
".venv",
|
||||
"__pycache__",
|
||||
"data",
|
||||
}
|
||||
|
||||
|
||||
def test_current_project_does_not_expose_upstream_product_name() -> None:
|
||||
matches: list[str] = []
|
||||
for path in PROJECT_ROOT.rglob("*"):
|
||||
relative = path.relative_to(PROJECT_ROOT)
|
||||
if any(part in SKIPPED_PARTS for part in relative.parts):
|
||||
continue
|
||||
if FORBIDDEN_TOKEN in path.name.lower():
|
||||
matches.append(f"filename: {relative}")
|
||||
if not path.is_file():
|
||||
continue
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
for line_number, line in enumerate(text.splitlines(), start=1):
|
||||
if FORBIDDEN_TOKEN in line.lower():
|
||||
matches.append(f"content: {relative}:{line_number}")
|
||||
|
||||
assert matches == []
|
||||
@ -1,6 +1,6 @@
|
||||
# Memory Gateway multimodal API test
|
||||
|
||||
This file records a real end-to-end test through **Memory Gateway**, not direct EverOS calls.
|
||||
This file records a real end-to-end test through **Memory Gateway**, not direct upstream memory service calls.
|
||||
|
||||
Gateway URL used by curl:
|
||||
|
||||
@ -8,7 +8,7 @@ Gateway URL used by curl:
|
||||
http://127.0.0.1:8010
|
||||
```
|
||||
|
||||
Gateway upstream EverOS:
|
||||
Gateway upstream memory service:
|
||||
|
||||
```text
|
||||
http://10.6.80.123:1995
|
||||
@ -35,7 +35,7 @@ Command:
|
||||
```bash
|
||||
cd /home/tom/memory-gateway
|
||||
|
||||
EVEROS_BASE_URL=http://10.6.80.123:1995 \
|
||||
MEMORY_GATEWAY_BACKEND_BASE_URL=http://10.6.80.123:1995 \
|
||||
MEMORY_GATEWAY_DB_PATH=/tmp/memory_gateway_curl.sqlite3 \
|
||||
MEMORY_GATEWAY_STORAGE_DIR=/tmp/memory_gateway_curl_storage \
|
||||
MEMORY_GATEWAY_HOST=127.0.0.1 \
|
||||
@ -136,7 +136,7 @@ Response:
|
||||
```json
|
||||
{
|
||||
"session_id": "chat:gateway-multimodal-20260611180257",
|
||||
"everos": {
|
||||
"backend": {
|
||||
"request_id": "c9e24b8d27ee4ad08a8df70273336637",
|
||||
"data": {
|
||||
"message_count": 1,
|
||||
@ -174,7 +174,7 @@ Response:
|
||||
```json
|
||||
{
|
||||
"session_id": "chat:gateway-multimodal-20260611180257",
|
||||
"everos": {
|
||||
"backend": {
|
||||
"request_id": "8eb7d5db2d3b43f4999f445aabb813b1",
|
||||
"data": {
|
||||
"status": "extracted"
|
||||
@ -192,7 +192,7 @@ TOTAL_TIME:2.135721
|
||||
|
||||
## 4. Search through Gateway
|
||||
|
||||
EverOS indexing can lag briefly after `flush`, so this test waited about 2 seconds before searching.
|
||||
upstream memory service indexing can lag briefly after `flush`, so this test waited about 2 seconds before searching.
|
||||
|
||||
Request:
|
||||
|
||||
@ -279,7 +279,7 @@ Response:
|
||||
{
|
||||
"status": "ok",
|
||||
"api": {"status": "ok"},
|
||||
"everos": {
|
||||
"backend": {
|
||||
"status": "ok",
|
||||
"base_url": "http://10.6.80.123:1995",
|
||||
"data": {"status": "ok"}
|
||||
|
||||
@ -17,7 +17,7 @@ import core.api as api_module
|
||||
import core.service as service_module
|
||||
|
||||
|
||||
class FakeEverOSClient:
|
||||
class FakeBackendClient:
|
||||
def __init__(
|
||||
self,
|
||||
search_results: list[dict[str, Any]] | None = None,
|
||||
@ -67,7 +67,7 @@ class FakeEverOSClient:
|
||||
@pytest.fixture
|
||||
def config(tmp_path: Path) -> GatewayConfig:
|
||||
return GatewayConfig(
|
||||
everos_base_url="http://everos.test",
|
||||
backend_base_url="http://backend.test",
|
||||
database_path=tmp_path / "gateway.sqlite3",
|
||||
storage_dir=tmp_path / "storage",
|
||||
)
|
||||
@ -81,9 +81,9 @@ def repo(config: GatewayConfig) -> MemoryRepository:
|
||||
|
||||
def app_client(
|
||||
config: GatewayConfig,
|
||||
everos_client: FakeEverOSClient,
|
||||
backend_client: FakeBackendClient,
|
||||
) -> httpx.AsyncClient:
|
||||
app = create_app(config=config, everos_client=everos_client)
|
||||
app = create_app(config=config, backend_client=backend_client)
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
return httpx.AsyncClient(transport=transport, base_url="http://test")
|
||||
|
||||
@ -124,20 +124,20 @@ async def create_user(client: httpx.AsyncClient, user_id: str = "u_123") -> str:
|
||||
return body["user_key"]
|
||||
|
||||
|
||||
def test_create_app_uses_configured_everos_timeout(config: GatewayConfig) -> None:
|
||||
def test_create_app_uses_configured_backend_timeout(config: GatewayConfig) -> None:
|
||||
config = GatewayConfig(
|
||||
everos_base_url=config.everos_base_url,
|
||||
backend_base_url=config.backend_base_url,
|
||||
database_path=config.database_path,
|
||||
storage_dir=config.storage_dir,
|
||||
everos_timeout_seconds=7.5,
|
||||
backend_timeout_seconds=7.5,
|
||||
)
|
||||
app = create_app(config=config)
|
||||
|
||||
assert app.state.everos_client.timeout == 7.5
|
||||
assert app.state.backend_client.timeout == 7.5
|
||||
|
||||
|
||||
def test_create_app_uses_project_name(config: GatewayConfig) -> None:
|
||||
app = create_app(config=config, everos_client=FakeEverOSClient())
|
||||
app = create_app(config=config, backend_client=FakeBackendClient())
|
||||
|
||||
assert app.title == "memory-gateway"
|
||||
|
||||
@ -160,8 +160,8 @@ async def test_api_logs_request_time_address_input_and_output(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
caplog.set_level(logging.INFO, logger="memory_gateway.api")
|
||||
everos = FakeEverOSClient()
|
||||
async with app_client(config, everos) as client:
|
||||
backend = FakeBackendClient()
|
||||
async with app_client(config, backend) as client:
|
||||
user_key = await create_user(client, "u_log")
|
||||
response = await client.get(
|
||||
"/resources",
|
||||
@ -192,8 +192,8 @@ async def test_api_logs_do_not_expose_secrets_from_large_json_bodies(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
caplog.set_level(logging.INFO, logger="memory_gateway.api")
|
||||
everos = FakeEverOSClient()
|
||||
async with app_client(config, everos) as client:
|
||||
backend = FakeBackendClient()
|
||||
async with app_client(config, backend) as client:
|
||||
user_key = await create_user(client, "u_large_log")
|
||||
caplog.clear()
|
||||
response = await client.post(
|
||||
@ -224,40 +224,40 @@ async def test_api_logs_do_not_expose_secrets_from_large_json_bodies(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_reports_api_and_everos_ok(
|
||||
async def test_health_reports_api_and_backend_ok(
|
||||
config: GatewayConfig,
|
||||
) -> None:
|
||||
everos = FakeEverOSClient()
|
||||
async with app_client(config, everos) as client:
|
||||
backend = FakeBackendClient()
|
||||
async with app_client(config, backend) as client:
|
||||
response = await client.get("/health")
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
"status": "ok",
|
||||
"api": {"status": "ok"},
|
||||
"everos": {
|
||||
"backend": {
|
||||
"status": "ok",
|
||||
"base_url": "http://everos.test",
|
||||
"base_url": "http://backend.test",
|
||||
"data": {"status": "ok"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_reports_degraded_when_everos_fails(
|
||||
async def test_health_reports_degraded_when_backend_fails(
|
||||
config: GatewayConfig,
|
||||
) -> None:
|
||||
everos = FakeEverOSClient(health_error=RuntimeError("everos down"))
|
||||
async with app_client(config, everos) as client:
|
||||
backend = FakeBackendClient(health_error=RuntimeError("backend down"))
|
||||
async with app_client(config, backend) as client:
|
||||
response = await client.get("/health")
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
body = response.json()
|
||||
assert body["status"] == "degraded"
|
||||
assert body["api"] == {"status": "ok"}
|
||||
assert body["everos"]["status"] == "unavailable"
|
||||
assert body["everos"]["base_url"] == "http://everos.test"
|
||||
assert body["everos"]["error"] == "everos down"
|
||||
assert body["backend"]["status"] == "unavailable"
|
||||
assert body["backend"]["base_url"] == "http://backend.test"
|
||||
assert body["backend"]["error"] == "backend down"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@ -265,8 +265,8 @@ async def test_create_user_generates_and_persists_user_key(
|
||||
config: GatewayConfig,
|
||||
repo: MemoryRepository,
|
||||
) -> None:
|
||||
everos = FakeEverOSClient()
|
||||
async with app_client(config, everos) as client:
|
||||
backend = FakeBackendClient()
|
||||
async with app_client(config, backend) as client:
|
||||
user_key = await create_user(client, "u_123")
|
||||
|
||||
user = repo.get_user("u_123")
|
||||
@ -275,11 +275,11 @@ async def test_create_user_generates_and_persists_user_key(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upload_resource_creates_record_and_calls_everos(
|
||||
async def test_upload_resource_creates_record_and_calls_backend(
|
||||
config: GatewayConfig,
|
||||
) -> None:
|
||||
everos = FakeEverOSClient()
|
||||
async with app_client(config, everos) as client:
|
||||
backend = FakeBackendClient()
|
||||
async with app_client(config, backend) as client:
|
||||
user_key = await create_user(client)
|
||||
response = await client.post(
|
||||
"/resources",
|
||||
@ -304,15 +304,15 @@ async def test_upload_resource_creates_record_and_calls_everos(
|
||||
assert resource["size_bytes"] == len(b"pay in 30 days")
|
||||
assert not resource["uri"].startswith("resource://")
|
||||
|
||||
assert len(everos.add_calls) == 1
|
||||
add_payload = everos.add_calls[0]
|
||||
assert len(backend.add_calls) == 1
|
||||
add_payload = backend.add_calls[0]
|
||||
assert add_payload["session_id"] == f"resource:u_123:{resource_id}"
|
||||
content = add_payload["messages"][0]["content"][0]
|
||||
assert content["type"] == "text"
|
||||
assert content["text"] == "pay in 30 days"
|
||||
assert "uri" not in content
|
||||
assert content["extras"] == {"resource_id": resource_id, "source": "user_upload"}
|
||||
assert everos.flush_calls == [
|
||||
assert backend.flush_calls == [
|
||||
{
|
||||
"session_id": f"resource:u_123:{resource_id}",
|
||||
"app_id": "default",
|
||||
@ -322,11 +322,11 @@ async def test_upload_resource_creates_record_and_calls_everos(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upload_binary_resource_sends_base64_content_to_everos(
|
||||
async def test_upload_binary_resource_sends_base64_content_to_backend(
|
||||
config: GatewayConfig,
|
||||
) -> None:
|
||||
everos = FakeEverOSClient()
|
||||
async with app_client(config, everos) as client:
|
||||
backend = FakeBackendClient()
|
||||
async with app_client(config, backend) as client:
|
||||
user_key = await create_user(client)
|
||||
response = await client.post(
|
||||
"/resources",
|
||||
@ -335,7 +335,7 @@ async def test_upload_binary_resource_sends_base64_content_to_everos(
|
||||
)
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
content = everos.add_calls[0]["messages"][0]["content"][0]
|
||||
content = backend.add_calls[0]["messages"][0]["content"][0]
|
||||
assert content["type"] == "pdf"
|
||||
assert content["base64"] == base64.b64encode(b"%PDF bytes").decode("ascii")
|
||||
assert content["ext"] == "pdf"
|
||||
@ -354,8 +354,8 @@ async def test_upload_resource_uses_current_timestamp(
|
||||
lambda: 1234567890123,
|
||||
raising=False,
|
||||
)
|
||||
everos = FakeEverOSClient()
|
||||
async with app_client(config, everos) as client:
|
||||
backend = FakeBackendClient()
|
||||
async with app_client(config, backend) as client:
|
||||
user_key = await create_user(client)
|
||||
response = await client.post(
|
||||
"/resources",
|
||||
@ -364,22 +364,22 @@ async def test_upload_resource_uses_current_timestamp(
|
||||
)
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
assert everos.add_calls[0]["messages"][0]["timestamp"] == 1234567890123
|
||||
assert backend.add_calls[0]["messages"][0]["timestamp"] == 1234567890123
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upload_retries_transient_everos_failure(
|
||||
async def test_upload_retries_transient_backend_failure(
|
||||
config: GatewayConfig,
|
||||
) -> None:
|
||||
config = GatewayConfig(
|
||||
everos_base_url=config.everos_base_url,
|
||||
backend_base_url=config.backend_base_url,
|
||||
database_path=config.database_path,
|
||||
storage_dir=config.storage_dir,
|
||||
everos_ingest_attempts=2,
|
||||
everos_retry_delay_seconds=0,
|
||||
backend_ingest_attempts=2,
|
||||
backend_retry_delay_seconds=0,
|
||||
)
|
||||
everos = FakeEverOSClient(add_failures=1, flush_failures=1)
|
||||
async with app_client(config, everos) as client:
|
||||
backend = FakeBackendClient(add_failures=1, flush_failures=1)
|
||||
async with app_client(config, backend) as client:
|
||||
user_key = await create_user(client)
|
||||
response = await client.post(
|
||||
"/resources",
|
||||
@ -389,16 +389,16 @@ async def test_upload_retries_transient_everos_failure(
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json()["status"] == "extracted"
|
||||
assert len(everos.add_calls) == 2
|
||||
assert len(everos.flush_calls) == 2
|
||||
assert len(backend.add_calls) == 2
|
||||
assert len(backend.flush_calls) == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upload_duplicate_resource_is_idempotent_for_same_user(
|
||||
config: GatewayConfig,
|
||||
) -> None:
|
||||
everos = FakeEverOSClient()
|
||||
async with app_client(config, everos) as client:
|
||||
backend = FakeBackendClient()
|
||||
async with app_client(config, backend) as client:
|
||||
user_key = await create_user(client)
|
||||
first = await client.post(
|
||||
"/resources",
|
||||
@ -414,8 +414,8 @@ async def test_upload_duplicate_resource_is_idempotent_for_same_user(
|
||||
assert first.status_code == 200, first.text
|
||||
assert second.status_code == 200, second.text
|
||||
assert second.json()["resource_id"] == first.json()["resource_id"]
|
||||
assert len(everos.add_calls) == 1
|
||||
assert len(everos.flush_calls) == 1
|
||||
assert len(backend.add_calls) == 1
|
||||
assert len(backend.flush_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@ -424,13 +424,13 @@ async def test_upload_rejects_file_larger_than_configured_limit(
|
||||
repo: MemoryRepository,
|
||||
) -> None:
|
||||
config = GatewayConfig(
|
||||
everos_base_url=config.everos_base_url,
|
||||
backend_base_url=config.backend_base_url,
|
||||
database_path=config.database_path,
|
||||
storage_dir=config.storage_dir,
|
||||
max_upload_bytes=4,
|
||||
)
|
||||
everos = FakeEverOSClient()
|
||||
async with app_client(config, everos) as client:
|
||||
backend = FakeBackendClient()
|
||||
async with app_client(config, backend) as client:
|
||||
user_key = await create_user(client)
|
||||
response = await client.post(
|
||||
"/resources",
|
||||
@ -441,7 +441,7 @@ async def test_upload_rejects_file_larger_than_configured_limit(
|
||||
assert response.status_code == 413, response.text
|
||||
assert repo.list_resources("u_123") == []
|
||||
assert not any(config.storage_dir.rglob("*"))
|
||||
assert everos.add_calls == []
|
||||
assert backend.add_calls == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@ -449,8 +449,8 @@ async def test_upload_rejects_unsupported_mime_type(
|
||||
config: GatewayConfig,
|
||||
repo: MemoryRepository,
|
||||
) -> None:
|
||||
everos = FakeEverOSClient()
|
||||
async with app_client(config, everos) as client:
|
||||
backend = FakeBackendClient()
|
||||
async with app_client(config, backend) as client:
|
||||
user_key = await create_user(client)
|
||||
response = await client.post(
|
||||
"/resources",
|
||||
@ -460,15 +460,15 @@ async def test_upload_rejects_unsupported_mime_type(
|
||||
|
||||
assert response.status_code == 415, response.text
|
||||
assert repo.list_resources("u_123") == []
|
||||
assert everos.add_calls == []
|
||||
assert backend.add_calls == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resource_detail_does_not_leak_internal_uri(
|
||||
config: GatewayConfig,
|
||||
) -> None:
|
||||
everos = FakeEverOSClient()
|
||||
async with app_client(config, everos) as client:
|
||||
backend = FakeBackendClient()
|
||||
async with app_client(config, backend) as client:
|
||||
user_key = await create_user(client)
|
||||
created = await client.post(
|
||||
"/resources",
|
||||
@ -492,8 +492,8 @@ async def test_resource_detail_does_not_leak_internal_uri(
|
||||
async def test_resource_detail_returns_empty_when_user_has_no_resource(
|
||||
config: GatewayConfig,
|
||||
) -> None:
|
||||
everos = FakeEverOSClient()
|
||||
async with app_client(config, everos) as client:
|
||||
backend = FakeBackendClient()
|
||||
async with app_client(config, backend) as client:
|
||||
user_key = await create_user(client, "u_empty")
|
||||
response = await client.get(
|
||||
"/resources/r_missing",
|
||||
@ -508,8 +508,8 @@ async def test_resource_detail_returns_empty_when_user_has_no_resource(
|
||||
async def test_resources_are_isolated_by_user_key(
|
||||
config: GatewayConfig,
|
||||
) -> None:
|
||||
everos = FakeEverOSClient()
|
||||
async with app_client(config, everos) as client:
|
||||
backend = FakeBackendClient()
|
||||
async with app_client(config, backend) as client:
|
||||
alice_key = await create_user(client, "alice")
|
||||
bob_key = await create_user(client, "bob")
|
||||
created = await client.post(
|
||||
@ -537,8 +537,8 @@ async def test_resources_are_isolated_by_user_key(
|
||||
async def test_resource_api_rejects_invalid_user_key(
|
||||
config: GatewayConfig,
|
||||
) -> None:
|
||||
everos = FakeEverOSClient()
|
||||
async with app_client(config, everos) as client:
|
||||
backend = FakeBackendClient()
|
||||
async with app_client(config, backend) as client:
|
||||
await create_user(client, "u_123")
|
||||
response = await client.get(
|
||||
"/resources",
|
||||
@ -549,10 +549,10 @@ async def test_resource_api_rejects_invalid_user_key(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_memory_forwards_multimodal_payload_to_everos(
|
||||
async def test_add_memory_forwards_multimodal_payload_to_backend(
|
||||
config: GatewayConfig,
|
||||
) -> None:
|
||||
everos = FakeEverOSClient()
|
||||
backend = FakeBackendClient()
|
||||
audio = base64.b64encode(b"wav bytes").decode("ascii")
|
||||
content = [
|
||||
{"type": "text", "text": "remember the picture and audio"},
|
||||
@ -564,7 +564,7 @@ async def test_add_memory_forwards_multimodal_payload_to_everos(
|
||||
"name": "simple-multimodal-image.png",
|
||||
},
|
||||
]
|
||||
async with app_client(config, everos) as client:
|
||||
async with app_client(config, backend) as client:
|
||||
user_key = await create_user(client)
|
||||
response = await client.post(
|
||||
"/memories/add",
|
||||
@ -588,9 +588,9 @@ async def test_add_memory_forwards_multimodal_payload_to_everos(
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
"session_id": "chat:c_multimodal",
|
||||
"everos": {"request_id": "add", "data": {"status": "accumulated"}},
|
||||
"backend": {"request_id": "add", "data": {"status": "accumulated"}},
|
||||
}
|
||||
assert everos.add_calls == [
|
||||
assert backend.add_calls == [
|
||||
{
|
||||
"session_id": "chat:c_multimodal",
|
||||
"app_id": "default",
|
||||
@ -608,11 +608,11 @@ async def test_add_memory_forwards_multimodal_payload_to_everos(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_flush_memory_forwards_request_to_everos(
|
||||
async def test_flush_memory_forwards_request_to_backend(
|
||||
config: GatewayConfig,
|
||||
) -> None:
|
||||
everos = FakeEverOSClient()
|
||||
async with app_client(config, everos) as client:
|
||||
backend = FakeBackendClient()
|
||||
async with app_client(config, backend) as client:
|
||||
user_key = await create_user(client)
|
||||
response = await client.post(
|
||||
"/memories/flush",
|
||||
@ -628,9 +628,9 @@ async def test_flush_memory_forwards_request_to_everos(
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
"session_id": "chat:c_multimodal",
|
||||
"everos": {"request_id": "flush", "data": {"status": "extracted"}},
|
||||
"backend": {"request_id": "flush", "data": {"status": "extracted"}},
|
||||
}
|
||||
assert everos.flush_calls == [
|
||||
assert backend.flush_calls == [
|
||||
{
|
||||
"session_id": "chat:c_multimodal",
|
||||
"app_id": "default",
|
||||
@ -643,10 +643,10 @@ async def test_flush_memory_forwards_request_to_everos(
|
||||
async def test_deleted_resource_is_excluded_from_resource_scope_search(
|
||||
config: GatewayConfig,
|
||||
) -> None:
|
||||
everos = FakeEverOSClient(
|
||||
backend = FakeBackendClient(
|
||||
[{"id": "mem_1", "session_id": "resource:u_123:r_live", "episode": "live"}]
|
||||
)
|
||||
async with app_client(config, everos) as client:
|
||||
async with app_client(config, backend) as client:
|
||||
user_key = await create_user(client)
|
||||
created = await client.post(
|
||||
"/resources",
|
||||
@ -671,7 +671,7 @@ async def test_deleted_resource_is_excluded_from_resource_scope_search(
|
||||
|
||||
assert delete_response.status_code == 200
|
||||
assert search_response.status_code == 200
|
||||
assert everos.search_calls == []
|
||||
assert backend.search_calls == []
|
||||
assert search_response.json()["results"] == []
|
||||
repo = MemoryRepository(config.database_path)
|
||||
deleted = repo.get_resource(resource_id)
|
||||
@ -691,14 +691,14 @@ async def test_tombstone_filters_search_results(
|
||||
session_id=None,
|
||||
reason="user deleted",
|
||||
)
|
||||
everos = FakeEverOSClient(
|
||||
backend = FakeBackendClient(
|
||||
[
|
||||
{"id": "mem_deleted", "session_id": "resource:u_123:r_1", "episode": "x"},
|
||||
{"id": "mem_live", "session_id": "resource:u_123:r_1", "episode": "y"},
|
||||
]
|
||||
)
|
||||
|
||||
async with app_client(config, everos) as client:
|
||||
async with app_client(config, backend) as client:
|
||||
user_key = await create_user(client)
|
||||
response = await client.post(
|
||||
"/memories/search",
|
||||
@ -721,10 +721,10 @@ async def test_override_replaces_search_result_text(
|
||||
repo: MemoryRepository,
|
||||
) -> None:
|
||||
create_test_resource(repo, resource_id="r_1", user_id="u_123")
|
||||
everos = FakeEverOSClient(
|
||||
backend = FakeBackendClient(
|
||||
[{"id": "mem_1", "session_id": "resource:u_123:r_1", "episode": "old text"}]
|
||||
)
|
||||
async with app_client(config, everos) as client:
|
||||
async with app_client(config, backend) as client:
|
||||
user_key = await create_user(client)
|
||||
patch_response = await client.patch(
|
||||
"/memories/mem_1",
|
||||
@ -758,8 +758,8 @@ async def test_memory_override_rejects_session_owned_by_another_user(
|
||||
repo: MemoryRepository,
|
||||
) -> None:
|
||||
create_test_resource(repo, resource_id="r_bob", user_id="bob")
|
||||
everos = FakeEverOSClient()
|
||||
async with app_client(config, everos) as client:
|
||||
backend = FakeBackendClient()
|
||||
async with app_client(config, backend) as client:
|
||||
user_key = await create_user(client, "alice")
|
||||
response = await client.patch(
|
||||
"/memories/mem_1",
|
||||
@ -780,8 +780,8 @@ async def test_memory_delete_requires_owned_session(
|
||||
config: GatewayConfig,
|
||||
repo: MemoryRepository,
|
||||
) -> None:
|
||||
everos = FakeEverOSClient()
|
||||
async with app_client(config, everos) as client:
|
||||
backend = FakeBackendClient()
|
||||
async with app_client(config, backend) as client:
|
||||
user_key = await create_user(client, "u_123")
|
||||
response = await client.request(
|
||||
"DELETE",
|
||||
@ -802,8 +802,8 @@ async def test_memory_delete_requires_owned_session(
|
||||
async def test_list_resources_returns_only_not_deleted(
|
||||
config: GatewayConfig,
|
||||
) -> None:
|
||||
everos = FakeEverOSClient()
|
||||
async with app_client(config, everos) as client:
|
||||
backend = FakeBackendClient()
|
||||
async with app_client(config, backend) as client:
|
||||
user_key = await create_user(client)
|
||||
first = await client.post(
|
||||
"/resources",
|
||||
@ -837,8 +837,8 @@ async def test_delete_memory_writes_tombstone(
|
||||
repo: MemoryRepository,
|
||||
) -> None:
|
||||
create_test_resource(repo, resource_id="r_1", user_id="u_123")
|
||||
everos = FakeEverOSClient()
|
||||
async with app_client(config, everos) as client:
|
||||
backend = FakeBackendClient()
|
||||
async with app_client(config, backend) as client:
|
||||
user_key = await create_user(client)
|
||||
response = await client.request(
|
||||
"DELETE",
|
||||
|
||||
Reference in New Issue
Block a user