兼容新版 EverOS memory API

This commit is contained in:
2026-06-10 10:47:36 +08:00
parent 64e3ea9631
commit 000415404b
5 changed files with 177 additions and 46 deletions

View File

@ -66,7 +66,7 @@ Memory Gateway 需要先连上两个后端服务。默认本地端口约定:
| 服务 | 默认地址 | 作用 | | 服务 | 默认地址 | 作用 |
|---|---|---| |---|---|---|
| OpenViking | `http://127.0.0.1:1933` | session、archive、long-term memory、resources、semantic search | | 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 | | Memory Gateway | `http://127.0.0.1:1934` | 对业务方暴露统一 `/memory-system` API |
### 安装 OpenViking ### 安装 OpenViking
@ -82,23 +82,28 @@ pip install openviking --upgrade --force-reinstall
如果本机已经有 OpenViking 源码或已安装的 `openviking-server`,可以跳过安装,直接确认 `openviking-server --help` 可用。 如果本机已经有 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 ```bash
git clone https://github.com/EverMind-AI/EverOS.git /Users/tom/projects/EverOS 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 # 编辑 .env至少配置 LLM、multimodal、embedding、rerank 的 model/api_key/base_url
cp /Users/tom/projects/memory-gateway/everos.env.example .env uv sync --extra dev
# 编辑 .env至少填入实际可用的 LLM_API_KEY 和 VECTORIZE_API_KEY。
docker compose up -d
uv sync
``` ```
如果 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 ### 安装 Memory Gateway
@ -149,16 +154,25 @@ openviking-server doctor
`root_api_key` 只放在 OpenViking 和 Memory Gateway 的服务端配置里。业务调用方不要传 root key。 `root_api_key` 只放在 OpenViking 和 Memory Gateway 的服务端配置里。业务调用方不要传 root key。
### EverOS / EverCore 配置 ### EverOS 配置
Memory Gateway 仓库内也提供 EverOS / EverCore 的 `.env` 模板。复制到 EverCore 目录 在 EverOS 根目录创建或编辑 `.env`
```bash ```bash
cd /Users/tom/projects/EverOS/methods/EverCore cd /Users/tom/projects/EverOS
cp /Users/tom/projects/memory-gateway/everos.env.example .env $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 配置 ### Memory Gateway 配置
@ -220,12 +234,11 @@ openviking-server --config ~/.openviking/ov.conf
如果使用 Docker 方式运行 OpenViking确认对 Memory Gateway 暴露的是 `1933` 端口,避免和 Memory Gateway 的 `1934` 端口冲突。 如果使用 Docker 方式运行 OpenViking确认对 Memory Gateway 暴露的是 `1933` 端口,避免和 Memory Gateway 的 `1934` 端口冲突。
终端 2启动 EverOS / EverCore 终端 2启动 EverOS。
```bash ```bash
cd /Users/tom/projects/EverOS/methods/EverCore cd /Users/tom/projects/EverOS
docker compose up -d uv run everos server start
uv run python src/run.py --port 1995
``` ```
终端 3启动 Memory Gateway。 终端 3启动 Memory Gateway。
@ -335,7 +348,7 @@ curl -sS -X POST "$API/users" \
### `POST /messages` ### `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` ### `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: <user_key> X-API-Key: <user_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` | | `context` | OpenViking `result`,包含 `latest_archive_overview``pre_archive_abstracts``messages``stats` |
| `items` | EverOS 搜索命中的精简记忆结果,含 `source_backend: "everos"` | | `items` | EverOS 搜索命中的精简记忆结果,含 `source_backend: "everos"` |
| `backends` | 两个后端的精简诊断信息,不重复返回完整 OpenViking context 或 EverOS `original_data` | | `backends` | 两个后端的精简诊断信息,不重复返回完整 OpenViking context 或 EverOS 原始大 payload |
### `GET/POST /openviking/tasks/{task_id}` ### `GET/POST /openviking/tasks/{task_id}`
@ -667,7 +680,7 @@ curl -sS -X DELETE -G "$API/resources" \
### `POST /search` ### `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_id` | string | 是 | 用户 ID |
| `user_key` | string | 是 | `/users` 返回的 user key | | `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 | 是 | 查询文本 | | `query` | string | 是 | 查询文本 |
| `use_llm` | bool | 否 | 只影响 EverOS 检索方式:`false` 使用 hybrid`true` 使用 agentic | | `use_llm` | bool | 否 | 只影响 EverOS 检索方式:`false` 使用 hybrid`true` 使用 agentic |
| `limit` | int | 否 | 返回条数,默认 10范围 1 到 100 | | `limit` | int | 否 | 返回条数,默认 10范围 1 到 100 |
@ -707,11 +720,11 @@ curl -sS -X POST "$API/search" \
|---|---| |---|---|
| `status` | 总体状态 | | `status` | 总体状态 |
| `items` | 合并后的记忆结果,含 `source_backend` | | `items` | 合并后的记忆结果,含 `source_backend` |
| `backends` | 两个后端的精简诊断信息例如命中数量、query plan 或查询过滤条件;不再回传完整原始命中`original_data` | | `backends` | 两个后端的精简诊断信息例如命中数量、query plan 或查询过滤条件;不再回传完整原始命中 |
### `GET/POST /users/{user_id}/profile` ### `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`
请求体: 请求体:

View File

@ -13,7 +13,7 @@ openviking:
api_key: "<OPENVIKING_ROOT_KEY>" api_key: "<OPENVIKING_ROOT_KEY>"
everos: everos:
# EverOS EverCore HTTP server. # EverOS HTTP server exposing /api/v1/memory/*.
url: "http://127.0.0.1:1995" url: "http://127.0.0.1:1995"
storage: storage:

View File

@ -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 def append_message(self, user_id: str, session_id: str, role: str, content: str) -> dict[str, Any]:
async with self._client() as client: async with self._client() as client:
response = await client.post( 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), json=self.build_message_payload(user_id=user_id, session_id=session_id, role=role, content=content),
) )
response.raise_for_status() response.raise_for_status()
@ -414,7 +414,6 @@ class EverOSMemorySystemClient:
sender_id = "assistant" if everos_role == "assistant" else user_id sender_id = "assistant" if everos_role == "assistant" else user_id
timestamp = int(datetime.now(timezone.utc).timestamp() * 1000) timestamp = int(datetime.now(timezone.utc).timestamp() * 1000)
return { return {
"user_id": user_id,
"session_id": session_id, "session_id": session_id,
"messages": [ "messages": [
{ {
@ -430,25 +429,30 @@ class EverOSMemorySystemClient:
async def flush(self, user_id: str, session_id: str) -> dict[str, Any]: async def flush(self, user_id: str, session_id: str) -> dict[str, Any]:
async with self._client() as client: 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() response.raise_for_status()
return response.json() return response.json()
async def search(self, user_id: str, session_id: str | None, query: str, method: str, limit: int) -> dict[str, Any]: 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: if session_id:
filters["session_id"] = session_id filters["session_id"] = session_id
async with self._client() as client: payload: dict[str, Any] = {
response = await client.post( "user_id": user_id,
"/api/v1/memories/search",
json={
"query": query, "query": query,
"method": method, "method": method,
"memory_types": ["episodic_memory", "profile", "raw_message"],
"filters": filters,
"top_k": limit, "top_k": limit,
"include_original_data": True, "include_profile": True,
}, }
if filters:
payload["filters"] = filters
async with self._client() as client:
response = await client.post(
"/api/v1/memory/search",
json=payload,
) )
response.raise_for_status() response.raise_for_status()
return response.json() return response.json()
@ -456,10 +460,10 @@ class EverOSMemorySystemClient:
async def get_profile(self, user_id: str) -> dict[str, Any]: async def get_profile(self, user_id: str) -> dict[str, Any]:
async with self._client() as client: async with self._client() as client:
response = await client.post( response = await client.post(
"/api/v1/memories/get", "/api/v1/memory/get",
json={ json={
"user_id": user_id,
"memory_type": "profile", "memory_type": "profile",
"filters": {"user_id": user_id},
"page": 1, "page": 1,
"page_size": 20, "page_size": 20,
}, },

View File

@ -264,7 +264,7 @@ curl -s -X POST <MEMORY_SYSTEM_BASE_URL>/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: The search response shape should now look more like:
@ -287,7 +287,7 @@ The search response shape should now look more like:
"everos": { "everos": {
"status": "success", "status": "success",
"result": { "result": {
"counts": {"episodes": 1, "profiles": 0, "raw_messages": 0}, "counts": {"episodes": 1, "profiles": 0},
"query": {"text": "我喜欢喝什么?", "method": "agentic"} "query": {"text": "我喜欢喝什么?", "method": "agentic"}
} }
} }

View File

@ -732,3 +732,117 @@ def test_everos_user_payload_uses_user_id_as_sender():
assert message["role"] == "user" assert message["role"] == "user"
assert message["sender_id"] == "tom" assert message["sender_id"] == "tom"
assert message["sender_name"] == "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,
)
]