neutralize upstream service branding

This commit is contained in:
2026-06-12 16:31:08 +08:00
parent 126ae4eafa
commit 42de7f9da0
18 changed files with 340 additions and 256 deletions

View File

@ -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
View File

@ -1,6 +1,6 @@
# Local environment files
.env
everos.env
backend.env
*.env.local
# Gateway runtime data

View File

@ -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
```

View File

@ -1,4 +1,4 @@
"""Lightweight Memory Gateway for EverOS."""
"""Lightweight user resource memory gateway."""
from __future__ import annotations

View File

@ -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,
},
}

View File

@ -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

View File

@ -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")
),
)

View File

@ -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,

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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",
]

View File

@ -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.

View File

@ -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

View File

@ -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
View 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 == []

View File

@ -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"}

View File

@ -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",