From 000415404b1a528d165405ac2f0ca2961e397778 Mon Sep 17 00:00:00 2001 From: tomtan Date: Wed, 10 Jun 2026 10:47:36 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=BC=E5=AE=B9=E6=96=B0=E7=89=88=20EverOS?= =?UTF-8?q?=20memory=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 69 ++++++++----- config.example.yaml | 2 +- memory_system_api/clients.py | 34 +++--- skills/memory-system-api/references/api.md | 4 +- tests/test_memory_system_clients.py | 114 +++++++++++++++++++++ 5 files changed, 177 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 4ce9a76..d40c23b 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ Memory Gateway 需要先连上两个后端服务。默认本地端口约定: | 服务 | 默认地址 | 作用 | |---|---|---| | OpenViking | `http://127.0.0.1:1933` | session、archive、long-term memory、resources、semantic search | -| EverOS EverCore | `http://127.0.0.1:1995` | profile、episodic memory、recall | +| EverOS | `http://127.0.0.1:1995` | profile、episodic memory、recall | | Memory Gateway | `http://127.0.0.1:1934` | 对业务方暴露统一 `/memory-system` API | ### 安装 OpenViking @@ -82,23 +82,28 @@ pip install openviking --upgrade --force-reinstall 如果本机已经有 OpenViking 源码或已安装的 `openviking-server`,可以跳过安装,直接确认 `openviking-server --help` 可用。 -### 安装 EverOS / EverCore +### 安装 EverOS -Memory Gateway 使用 EverOS 里的 EverCore HTTP 服务。按 [EverOS / EverCore Quick Start](https://github.com/EverMind-AI/EverOS#evercore): +Memory Gateway 使用当前 EverOS HTTP 服务: ```bash git clone https://github.com/EverMind-AI/EverOS.git /Users/tom/projects/EverOS -cd /Users/tom/projects/EverOS/methods/EverCore +cd /Users/tom/projects/EverOS -# 需要 Docker 和 Python 3.12。 -cp /Users/tom/projects/memory-gateway/everos.env.example .env -# 编辑 .env,至少填入实际可用的 LLM_API_KEY 和 VECTORIZE_API_KEY。 - -docker compose up -d -uv sync +# 编辑 .env,至少配置 LLM、multimodal、embedding、rerank 的 model/api_key/base_url。 +uv sync --extra dev ``` -如果 EverOS 已经在本机存在,只需要进入 `methods/EverCore` 并补齐 `.env`。 +如果 EverOS 已经在本机存在,只需要进入 EverOS 根目录并补齐 `.env`。 + +当前 Memory Gateway 兼容的 EverOS v1 记忆接口是: + +```text +POST /api/v1/memory/add +POST /api/v1/memory/flush +POST /api/v1/memory/search +POST /api/v1/memory/get +``` ### 安装 Memory Gateway @@ -149,16 +154,25 @@ openviking-server doctor `root_api_key` 只放在 OpenViking 和 Memory Gateway 的服务端配置里。业务调用方不要传 root key。 -### EverOS / EverCore 配置 +### EverOS 配置 -Memory Gateway 仓库内也提供 EverOS / EverCore 的 `.env` 模板。复制到 EverCore 目录: +在 EverOS 根目录创建或编辑 `.env`: ```bash -cd /Users/tom/projects/EverOS/methods/EverCore -cp /Users/tom/projects/memory-gateway/everos.env.example .env +cd /Users/tom/projects/EverOS +$EDITOR .env ``` -然后编辑 `.env`,至少填入 `LLM_API_KEY`、`LLM_MODEL`、`VECTORIZE_API_KEY`、`EMBEDDING_MODEL` 和 `RERANK_MODEL`。Docker 依赖地址默认使用模板里的本地 compose 端口;如果改过 EverCore 的 compose 配置,需要同步修改 `.env`。 +至少需要配置: + +```text +EVEROS_LLM__MODEL / EVEROS_LLM__API_KEY / EVEROS_LLM__BASE_URL +EVEROS_MULTIMODAL__MODEL / EVEROS_MULTIMODAL__API_KEY / EVEROS_MULTIMODAL__BASE_URL +EVEROS_EMBEDDING__MODEL / EVEROS_EMBEDDING__API_KEY / EVEROS_EMBEDDING__BASE_URL +EVEROS_RERANK__MODEL / EVEROS_RERANK__API_KEY / EVEROS_RERANK__BASE_URL +``` + +大模型较慢时,建议同时设置 `EVEROS_LLM__TIMEOUT_SECONDS` 和 `EVEROS_MULTIMODAL__TIMEOUT_SECONDS`。 ### Memory Gateway 配置 @@ -220,12 +234,11 @@ openviking-server --config ~/.openviking/ov.conf 如果使用 Docker 方式运行 OpenViking,确认对 Memory Gateway 暴露的是 `1933` 端口,避免和 Memory Gateway 的 `1934` 端口冲突。 -终端 2:启动 EverOS / EverCore。 +终端 2:启动 EverOS。 ```bash -cd /Users/tom/projects/EverOS/methods/EverCore -docker compose up -d -uv run python src/run.py --port 1995 +cd /Users/tom/projects/EverOS +uv run everos server start ``` 终端 3:启动 Memory Gateway。 @@ -335,7 +348,7 @@ curl -sS -X POST "$API/users" \ ### `POST /messages` -写入用户消息和/或助手消息。至少要传 `user_message` 或 `assistant_message` 其中一个。网关会先确保 OpenViking user/session 存在,再把消息写入 OpenViking session,同时写入 EverOS。 +写入用户消息和/或助手消息。至少要传 `user_message` 或 `assistant_message` 其中一个。网关会先确保 OpenViking user/session 存在,再把消息写入 OpenViking session,同时调用 EverOS `POST /api/v1/memory/add`。 请求体: @@ -367,7 +380,7 @@ curl -sS -X POST "$API/messages" \ ### `POST /sessions/{session_id}/commit` -提交会话。OpenViking 会归档 session 并异步抽取长期记忆,通常会返回 `task_id`;EverOS 会 flush 当前 session。 +提交会话。OpenViking 会归档 session 并异步抽取长期记忆,通常会返回 `task_id`;EverOS 会调用 `POST /api/v1/memory/flush` flush 当前 session。 路径参数: @@ -419,7 +432,7 @@ GET /api/v1/sessions/{session_id}/context X-API-Key: ``` -同时用同一个 `query` 调用 EverOS `/api/v1/memories/search`,返回相关 episodic/profile/raw message 记忆。适合在回答用户问题前,把“当前 session 工作记忆”和“EverOS 相关记忆”一起取回。 +同时用同一个 `query` 调用 EverOS `POST /api/v1/memory/search`,请求体使用顶层 `user_id`,并把 `session_id` 放在 `filters.session_id` 中。适合在回答用户问题前,把“当前 session 工作记忆”和“EverOS 相关记忆”一起取回。 路径参数: @@ -455,7 +468,7 @@ curl -sS -X POST "$API/sessions/sessionA1/context" \ |---|---| | `context` | OpenViking `result`,包含 `latest_archive_overview`、`pre_archive_abstracts`、`messages`、`stats` | | `items` | EverOS 搜索命中的精简记忆结果,含 `source_backend: "everos"` | -| `backends` | 两个后端的精简诊断信息,不重复返回完整 OpenViking context 或 EverOS `original_data` | +| `backends` | 两个后端的精简诊断信息,不重复返回完整 OpenViking context 或 EverOS 原始大 payload | ### `GET/POST /openviking/tasks/{task_id}` @@ -667,7 +680,7 @@ curl -sS -X DELETE -G "$API/resources" \ ### `POST /search` -同时查询 OpenViking 和 EverOS,并合并结果。 +同时查询 OpenViking 和 EverOS,并合并结果。EverOS 分支调用 `POST /api/v1/memory/search`,请求体形如 `{"user_id": "...", "query": "...", "method": "hybrid|agentic", "top_k": 10, "include_profile": true, "filters": {"session_id": "..."}}`。 请求体: @@ -675,7 +688,7 @@ curl -sS -X DELETE -G "$API/resources" \ |---|---|---:|---| | `user_id` | string | 是 | 用户 ID | | `user_key` | string | 是 | `/users` 返回的 user key | -| `session_id` | string/null | 否 | 会话 ID;用于 EverOS 过滤和鉴权身份 | +| `session_id` | string/null | 否 | 会话 ID;用于 EverOS `filters.session_id` 和 OpenViking agent identity | | `query` | string | 是 | 查询文本 | | `use_llm` | bool | 否 | 只影响 EverOS 检索方式:`false` 使用 hybrid,`true` 使用 agentic | | `limit` | int | 否 | 返回条数,默认 10,范围 1 到 100 | @@ -707,11 +720,11 @@ curl -sS -X POST "$API/search" \ |---|---| | `status` | 总体状态 | | `items` | 合并后的记忆结果,含 `source_backend` | -| `backends` | 两个后端的精简诊断信息,例如命中数量、query plan 或查询过滤条件;不再回传完整原始命中和 `original_data` | +| `backends` | 两个后端的精简诊断信息,例如命中数量、query plan 或查询过滤条件;不再回传完整原始命中 | ### `GET/POST /users/{user_id}/profile` -读取用户画像。该接口需要 `user_key`,用于确认调用方属于该 user。网关会读取 EverOS profile,并用同一个 `query` 调 OpenViking `/api/v1/search/search`,固定传 `target_uri: viking://user/memories` 和 `level`。 +读取用户画像。该接口需要 `user_key`,用于确认调用方属于该 user。网关会调用 EverOS `POST /api/v1/memory/get`,请求体为 `{"user_id": "...", "memory_type": "profile", "page": 1, "page_size": 20}`;同时用同一个 `query` 调 OpenViking `/api/v1/search/search`,固定传 `target_uri: viking://user/memories` 和 `level`。 请求体: diff --git a/config.example.yaml b/config.example.yaml index 84ca611..d0f9e74 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -13,7 +13,7 @@ openviking: api_key: "" everos: - # EverOS EverCore HTTP server. + # EverOS HTTP server exposing /api/v1/memory/*. url: "http://127.0.0.1:1995" storage: diff --git a/memory_system_api/clients.py b/memory_system_api/clients.py index d39818c..87fbb81 100644 --- a/memory_system_api/clients.py +++ b/memory_system_api/clients.py @@ -403,7 +403,7 @@ class EverOSMemorySystemClient: async def append_message(self, user_id: str, session_id: str, role: str, content: str) -> dict[str, Any]: async with self._client() as client: response = await client.post( - "/api/v1/memories", + "/api/v1/memory/add", json=self.build_message_payload(user_id=user_id, session_id=session_id, role=role, content=content), ) response.raise_for_status() @@ -414,7 +414,6 @@ class EverOSMemorySystemClient: sender_id = "assistant" if everos_role == "assistant" else user_id timestamp = int(datetime.now(timezone.utc).timestamp() * 1000) return { - "user_id": user_id, "session_id": session_id, "messages": [ { @@ -430,25 +429,30 @@ class EverOSMemorySystemClient: async def flush(self, user_id: str, session_id: str) -> dict[str, Any]: async with self._client() as client: - response = await client.post("/api/v1/memories/flush", json={"user_id": user_id, "session_id": session_id}) + response = await client.post( + "/api/v1/memory/flush", + json={"session_id": session_id}, + ) response.raise_for_status() return response.json() async def search(self, user_id: str, session_id: str | None, query: str, method: str, limit: int) -> dict[str, Any]: - filters: dict[str, Any] = {"user_id": user_id} + filters: dict[str, Any] = {} if session_id: filters["session_id"] = session_id + payload: dict[str, Any] = { + "user_id": user_id, + "query": query, + "method": method, + "top_k": limit, + "include_profile": True, + } + if filters: + payload["filters"] = filters async with self._client() as client: response = await client.post( - "/api/v1/memories/search", - json={ - "query": query, - "method": method, - "memory_types": ["episodic_memory", "profile", "raw_message"], - "filters": filters, - "top_k": limit, - "include_original_data": True, - }, + "/api/v1/memory/search", + json=payload, ) response.raise_for_status() return response.json() @@ -456,10 +460,10 @@ class EverOSMemorySystemClient: async def get_profile(self, user_id: str) -> dict[str, Any]: async with self._client() as client: response = await client.post( - "/api/v1/memories/get", + "/api/v1/memory/get", json={ + "user_id": user_id, "memory_type": "profile", - "filters": {"user_id": user_id}, "page": 1, "page_size": 20, }, diff --git a/skills/memory-system-api/references/api.md b/skills/memory-system-api/references/api.md index aa78535..d31d64d 100644 --- a/skills/memory-system-api/references/api.md +++ b/skills/memory-system-api/references/api.md @@ -264,7 +264,7 @@ curl -s -X POST /memory-system/search \ }' ``` -Raw API search responses include merged `items` plus compact backend diagnostics. Fields named `vector` are stripped recursively before the API returns JSON. The API does not return EverOS `original_data` or full episode payloads inside `backends` anymore. OpenViking search uses `/api/v1/search/search`; `target_uri`, `level`, and `score_threshold` are optional request fields. +Raw API search responses include merged `items` plus compact backend diagnostics. Fields named `vector` are stripped recursively before the API returns JSON. The API calls current EverOS `POST /api/v1/memory/search` with top-level `user_id`, `query`, `method`, `top_k`, `include_profile`, and optional `filters.session_id`; it does not return full EverOS episode/profile payloads inside `backends`. OpenViking search uses `/api/v1/search/search`; `target_uri`, `level`, and `score_threshold` are optional request fields. The search response shape should now look more like: @@ -287,7 +287,7 @@ The search response shape should now look more like: "everos": { "status": "success", "result": { - "counts": {"episodes": 1, "profiles": 0, "raw_messages": 0}, + "counts": {"episodes": 1, "profiles": 0}, "query": {"text": "我喜欢喝什么?", "method": "agentic"} } } diff --git a/tests/test_memory_system_clients.py b/tests/test_memory_system_clients.py index afef1d0..e48b345 100644 --- a/tests/test_memory_system_clients.py +++ b/tests/test_memory_system_clients.py @@ -732,3 +732,117 @@ def test_everos_user_payload_uses_user_id_as_sender(): assert message["role"] == "user" assert message["sender_id"] == "tom" assert message["sender_name"] == "tom" + + +def test_everos_append_message_posts_current_memory_add_contract(): + client = EverOSMemorySystemClient() + calls = [] + responses = [FakeResponse(200, {"request_id": "req-1", "data": {"status": "accumulated", "message_count": 1}})] + client._client = lambda: FakeAsyncClient( # type: ignore[method-assign] + calls, + responses, + client.api_key or "", + {}, + ) + + result = asyncio.run(client.append_message("tom", "sess-1", "user", "我喜欢拿铁")) + + assert result == {"request_id": "req-1", "data": {"status": "accumulated", "message_count": 1}} + assert calls[0][0] == "post" + assert calls[0][3] == "/api/v1/memory/add" + payload = calls[0][4] + assert payload["session_id"] == "sess-1" + assert "user_id" not in payload + assert payload["messages"][0]["sender_id"] == "tom" + assert payload["messages"][0]["role"] == "user" + assert payload["messages"][0]["content"] == "我喜欢拿铁" + + +def test_everos_flush_posts_current_memory_flush_contract(): + client = EverOSMemorySystemClient() + calls = [] + responses = [FakeResponse(200, {"request_id": "req-1", "data": {"status": "extracted"}})] + client._client = lambda: FakeAsyncClient( # type: ignore[method-assign] + calls, + responses, + client.api_key or "", + {}, + ) + + result = asyncio.run(client.flush("tom", "sess-1")) + + assert result == {"request_id": "req-1", "data": {"status": "extracted"}} + assert calls == [ + ( + "post", + client.api_key or "", + {}, + "/api/v1/memory/flush", + {"session_id": "sess-1"}, + None, + ) + ] + + +def test_everos_search_posts_current_memory_search_contract(): + client = EverOSMemorySystemClient() + calls = [] + responses = [FakeResponse(200, {"request_id": "req-1", "data": {"episodes": []}})] + client._client = lambda: FakeAsyncClient( # type: ignore[method-assign] + calls, + responses, + client.api_key or "", + {}, + ) + + result = asyncio.run(client.search("tom", "sess-1", "牛奶在哪里", "hybrid", 7)) + + assert result == {"request_id": "req-1", "data": {"episodes": []}} + assert calls == [ + ( + "post", + client.api_key or "", + {}, + "/api/v1/memory/search", + { + "user_id": "tom", + "query": "牛奶在哪里", + "method": "hybrid", + "top_k": 7, + "include_profile": True, + "filters": {"session_id": "sess-1"}, + }, + None, + ) + ] + + +def test_everos_get_profile_posts_current_memory_get_contract(): + client = EverOSMemorySystemClient() + calls = [] + responses = [FakeResponse(200, {"request_id": "req-1", "data": {"profiles": []}})] + client._client = lambda: FakeAsyncClient( # type: ignore[method-assign] + calls, + responses, + client.api_key or "", + {}, + ) + + result = asyncio.run(client.get_profile("tom")) + + assert result == {"request_id": "req-1", "data": {"profiles": []}} + assert calls == [ + ( + "post", + client.api_key or "", + {}, + "/api/v1/memory/get", + { + "user_id": "tom", + "memory_type": "profile", + "page": 1, + "page_size": 20, + }, + None, + ) + ]