Refactor OpenViking Memory API and User Management

- Updated API authentication headers to use `X-API-Key` for both admin and user APIs.
- Modified the account creation process to directly create user-specific accounts without requiring an admin workspace.
- Enhanced user creation to return account-specific details, including `admin_user_id`.
- Introduced new endpoints for retrieving task status and user profiles, allowing for more flexible user data management.
- Updated search functionality to include additional parameters such as `level` and `score_threshold`.
- Improved the handling of user keys in the storage layer to associate them with specific accounts.
- Added tests to validate the new user account creation process and search functionalities, ensuring proper integration with the OpenViking service.
- Included new documentation to reflect changes in API usage and expected request/response formats.
This commit is contained in:
2026-05-27 16:09:28 +08:00
parent a89807b174
commit 70cda923b2
13 changed files with 543 additions and 165 deletions

113
README.md
View File

@ -5,7 +5,7 @@ Memory Gateway 是一个轻量级记忆网关项目,用统一的 HTTP API 把
- OpenViking负责会话归档、长期记忆抽取、按 `user_id / session_id` 隔离的语义检索。
- EverOS负责用户画像、episodic memory、profile 查询等补充记忆能力。
当前重点模块是 `memory_system_api`。它不是 OpenViking 的替代品,而是一个更窄的业务网关:上层只面对 user、user key、session 和 message网关内部固定使用 OpenViking `admin` 工作区
当前重点模块是 `memory_system_api`。它不是 OpenViking 的替代品,而是一个更窄的业务网关:上层只面对 user、user key、session 和 message网关内部为每个业务用户创建独立的 OpenViking admin account
## 核心模型
@ -28,21 +28,14 @@ Memory Gateway 是一个轻量级记忆网关项目,用统一的 HTTP API 把
如果没有先创建 user或者 `user_key` 与数据库里的 `user_id` 不匹配,`messages``commit``extract``tasks``search``profile` 都会返回 `401`
第一次创建业务 user 时,如果 SQLite 里还没有 OpenViking `admin` 工作区记录,网关会先调用 OpenViking
创建业务 user 时,网关直接为该用户创建一个独立 OpenViking account。默认 `account_id``<user_id>_account``admin_user_id` 是传入的 `user_id`
```http
POST /api/v1/admin/accounts
{"account_id": "admin", "admin_user_id": "admin"}
{"account_id": "<user_id>_account", "admin_user_id": "<user_id>"}
```
随后创建具体用户:
```http
POST /api/v1/admin/accounts/admin/users
{"user_id": "<user_id>", "role": "user"}
```
后续创建第二、第三个用户时,网关直接调用 `/api/v1/admin/accounts/admin/users`。所有返回的 `user_id / user_key`、创建过的 `session_id`、commit 返回的 `task_id / archive_uri` 都写入 SQLite不放在进程内缓存里。
网关不再调用 `/api/v1/admin/accounts/admin/users`。所有返回的 `admin_user_id / user_key`、创建过的 `session_id`、commit 返回的 `task_id / archive_uri` 都写入 SQLite不放在进程内缓存里。
### 鉴权
@ -59,12 +52,12 @@ OpenViking 内部调用遵循:
| OpenViking 场景 | 鉴权 |
|---|---|
| Admin API例如创建 account/user | `X-API-Key: <openviking root key>` |
| 普通用户 API例如 session/message/commit/task/search | `Authorization: Bearer <openviking user key>` |
| Admin API例如创建 account | `X-API-Key: <openviking root key>` |
| 普通用户 API例如 session/message/commit/task/search | `X-API-Key: <openviking user key>` |
### Session 与搜索范围
OpenViking session 由请求里的 `session_id` 创建和提交。普通向量搜索使用显式用户 memory 路径 `viking://user/<user_id>/memories/``use_llm=true` 的智能搜索会同时传 `session_id``target_uri: viking://user/<user_id>/<session_id>`,用于结合当前 session context
OpenViking session 由请求里的 `session_id` 创建和提交。`/memory-system/search` 的 OpenViking 分支固定调用 OpenViking `/api/v1/search/search``target_uri` 可选,默认是 `viking://user/memories`,并默认传 `level: 2``score_threshold: 0.8`
## 安装
@ -127,7 +120,7 @@ storage:
先启动 OpenViking 和 EverOS再启动 Memory System API
```bash
python -m memory_system_api.server --config config.yaml --host 127.0.0.1 --port 1934
python -m memory_system_api.server --config config.yaml --host 0.0.0.0 --port 1934
```
如果 `server.api_key` 非空,所有请求还要加:
@ -144,6 +137,12 @@ Base URL
http://127.0.0.1:1934/memory-system
```
下面的示例默认先设置:
```bash
API=http://127.0.0.1:1934/memory-system
```
| 方法 | 路径 | 作用 | User key |
|---|---|---|---|
| `GET` | `/health` | 检查 OpenViking 和 EverOS 健康状态 | 不需要 |
@ -152,9 +151,9 @@ http://127.0.0.1:1934/memory-system
| `POST` | `/sessions/{session_id}/commit` | 提交会话,触发 OpenViking commit 和 EverOS flush | 需要 |
| `POST` | `/sessions/{session_id}/extract` | 立即触发 OpenViking extract | 需要 |
| `GET/POST` | `/sessions/{session_id}/context` | 查询 OpenViking 会话上下文,并用同一 query 搜索 EverOS 记忆 | 需要 |
| `GET` | `/openviking/tasks/{task_id}` | 查询 OpenViking 后台任务状态 | 需要 |
| `GET/POST` | `/openviking/tasks/{task_id}` | 查询 OpenViking 后台任务状态 | 需要 |
| `POST` | `/search` | 同时搜索 OpenViking 和 EverOS 记忆 | 需要 |
| `GET` | `/users/{user_id}/profile` | 查询 EverOS profile | 需要 |
| `GET/POST` | `/users/{user_id}/profile` | 查询 EverOS profile,并补充 OpenViking 用户记忆搜索结果 | 需要 |
## API 详情
@ -163,7 +162,7 @@ http://127.0.0.1:1934/memory-system
检查两个后端是否可访问。
```bash
curl -sS http://127.0.0.1:1934/memory-system/health
curl -sS "$API/health"
```
返回字段:
@ -176,7 +175,7 @@ curl -sS http://127.0.0.1:1934/memory-system/health
### `POST /users`
创建新的 OpenViking user,并把返回的 `user_id / user_key` 保存到本地 SQLite。
创建新的 OpenViking admin account,并把返回的 `admin_user_id / user_key` 保存到本地 SQLite。
请求体:
@ -187,9 +186,9 @@ curl -sS http://127.0.0.1:1934/memory-system/health
示例:
```bash
curl -sS -X POST http://127.0.0.1:1934/memory-system/users \
curl -sS -X POST "$API/users" \
-H "Content-Type: application/json" \
-d '{"user_id": "userA"}'
-d '{"user_id":"userA"}'
```
返回中的 `account.result.user_key` 是后续业务调用要传的 `user_key`
@ -200,8 +199,8 @@ curl -sS -X POST http://127.0.0.1:1934/memory-system/users \
"account": {
"status": "ok",
"result": {
"account_id": "admin",
"user_id": "userA",
"account_id": "userA_account",
"admin_user_id": "userA",
"user_key": "..."
}
}
@ -229,7 +228,7 @@ curl -sS -X POST http://127.0.0.1:1934/memory-system/users \
```bash
USER_KEY="..."
curl -sS -X POST http://127.0.0.1:1934/memory-system/messages \
curl -sS -X POST "$API/messages" \
-H "Content-Type: application/json" \
-d '{
"user_id": "userA",
@ -260,7 +259,7 @@ curl -sS -X POST http://127.0.0.1:1934/memory-system/messages \
示例:
```bash
curl -sS -X POST http://127.0.0.1:1934/memory-system/sessions/sessionA1/commit \
curl -sS -X POST "$API/sessions/sessionA1/commit" \
-H "Content-Type: application/json" \
-d '{
"user_id": "userA",
@ -277,7 +276,7 @@ curl -sS -X POST http://127.0.0.1:1934/memory-system/sessions/sessionA1/commit \
参数同 commit
```bash
curl -sS -X POST http://127.0.0.1:1934/memory-system/sessions/sessionA1/extract \
curl -sS -X POST "$API/sessions/sessionA1/extract" \
-H "Content-Type: application/json" \
-d '{
"user_id": "userA",
@ -291,7 +290,7 @@ curl -sS -X POST http://127.0.0.1:1934/memory-system/sessions/sessionA1/extract
```http
GET /api/v1/sessions/{session_id}/context
Authorization: Bearer <user_key>
X-API-Key: <user_key>
```
同时用同一个 `query` 调用 EverOS `/api/v1/memories/search`,返回相关 episodic/profile/raw message 记忆。适合在回答用户问题前,把“当前 session 工作记忆”和“EverOS 相关记忆”一起取回。
@ -314,7 +313,7 @@ Authorization: Bearer <user_key>
POST 示例:
```bash
curl -sS -X POST http://127.0.0.1:1934/memory-system/sessions/sessionA1/context \
curl -sS -X POST "$API/sessions/sessionA1/context" \
-H "Content-Type: application/json" \
-d '{
"user_id": "userA",
@ -324,12 +323,6 @@ curl -sS -X POST http://127.0.0.1:1934/memory-system/sessions/sessionA1/context
}'
```
GET 示例:
```bash
curl -sS "http://127.0.0.1:1934/memory-system/sessions/sessionA1/context?user_id=userA&user_key=$USER_KEY&query=我喜欢喝什么?&limit=10"
```
返回字段:
| 字段 | 含义 |
@ -338,11 +331,11 @@ curl -sS "http://127.0.0.1:1934/memory-system/sessions/sessionA1/context?user_id
| `items` | EverOS 搜索命中的精简记忆结果,含 `source_backend: "everos"` |
| `backends` | 两个后端的精简诊断信息,不重复返回完整 OpenViking context 或 EverOS `original_data` |
### `GET /openviking/tasks/{task_id}`
### `GET/POST /openviking/tasks/{task_id}`
查询 OpenViking 后台任务状态,例如 commit 返回的任务。
Query 参数
请求体
| 参数 | 类型 | 必需 | 说明 |
|---|---|---:|---|
@ -353,7 +346,7 @@ Query 参数:
示例:
```bash
curl -sS -X POST "http://127.0.0.1:1934/memory-system/openviking/tasks/${TASK_ID}" \
curl -sS -X POST "$API/openviking/tasks/${TASK_ID}" \
-H "Content-Type: application/json" \
-d '{
"user_id": "userA",
@ -372,15 +365,18 @@ curl -sS -X POST "http://127.0.0.1:1934/memory-system/openviking/tasks/${TASK_ID
|---|---|---:|---|
| `user_id` | string | 是 | 用户 ID |
| `user_key` | string | 是 | `/users` 返回的 user key |
| `session_id` | string/null | 否 | 会话 ID`use_llm=true` 时会作为 OpenViking search 的 session context |
| `session_id` | string/null | 否 | 会话 ID用于 EverOS 过滤和鉴权身份 |
| `query` | string | 是 | 查询文本 |
| `use_llm` | bool | 否 | `false` 使用 OpenViking find + EverOS hybrid`true` 使用 OpenViking search + EverOS agentic |
| `use_llm` | bool | 否 | 只影响 EverOS 检索方式:`false` 使用 hybrid`true` 使用 agentic |
| `limit` | int | 否 | 返回条数,默认 10范围 1 到 100 |
| `level` | int | 否 | OpenViking search level默认 2 |
| `score_threshold` | float | 否 | OpenViking 最低分数阈值,默认 0.8,范围 0 到 1 |
| `target_uri` | string | 否 | OpenViking 搜索范围,默认 `viking://user/memories` |
示例:
```bash
curl -sS -X POST http://127.0.0.1:1934/memory-system/search \
curl -sS -X POST "$API/search" \
-H "Content-Type: application/json" \
-d '{
"user_id": "userA",
@ -388,7 +384,10 @@ curl -sS -X POST http://127.0.0.1:1934/memory-system/search \
"session_id": "sessionA1",
"query": "我喜欢喝什么?",
"use_llm": false,
"limit": 10
"limit": 10,
"level": 2,
"score_threshold": 0.8,
"target_uri": "viking://user/memories"
}'
```
@ -400,16 +399,34 @@ curl -sS -X POST http://127.0.0.1:1934/memory-system/search \
| `items` | 合并后的记忆结果,含 `source_backend` |
| `backends` | 两个后端的精简诊断信息例如命中数量、query plan 或查询过滤条件;不再回传完整原始命中和 `original_data` |
### `GET /users/{user_id}/profile`
### `GET/POST /users/{user_id}/profile`
读取 EverOS profile。该接口需要 `user_key`,用于确认调用方属于该 user。
读取用户画像。该接口需要 `user_key`,用于确认调用方属于该 user。网关会读取 EverOS profile并用同一个 `query` 调 OpenViking `/api/v1/search/search`,固定传 `target_uri: viking://user/memories``level`
请求体:
| 参数 | 类型 | 必需 | 说明 |
|---|---|---:|---|
| `user_key` | string | 是 | `/users` 返回的 user key |
| `query` | string | 否 | OpenViking 搜索文本,默认 `用户画像` |
| `limit` | int | 否 | OpenViking 返回条数,默认 10范围 1 到 100 |
| `level` | int | 否 | OpenViking search level默认 2 |
示例:
```bash
curl -sS "http://127.0.0.1:1934/memory-system/users/userA/profile?user_key=$USER_KEY"
curl -sS -X POST "$API/users/userA/profile" \
-H "Content-Type: application/json" \
-d '{
"user_key": "'"$USER_KEY"'",
"query": "我想喝东西",
"limit": 10,
"level": 2
}'
```
返回中的 `profile` 是 EverOS profile 原始结果,`items` 是 OpenViking 命中的用户记忆,`backends` 只保留两个后端的状态和计数诊断,不重复返回完整 EverOS profile 或 OpenViking 命中列表。
## 完整测试流程
```bash
@ -449,7 +466,13 @@ curl -sS -X POST "$API/search" \
OpenViking commit 返回 `task_id` 后,轮询到 `completed` 再搜索长期 memory
```bash
curl -sS "$API/openviking/tasks/<task_id>?user_id=userA&user_key=$USER_KEY&session_id=sessionA1"
curl -sS -X POST "$API/openviking/tasks/<task_id>" \
-H "Content-Type: application/json" \
-d '{
"user_id": "userA",
"user_key": "'"$USER_KEY"'",
"session_id": "sessionA1"
}'
```
## 开发检查

View File

@ -20,22 +20,22 @@ OpenViking 的常见鉴权方式:
| 场景 | Header |
|---|---|
| Admin API例如创建 account/user | `X-API-Key: $ROOT_KEY` |
| 普通用户 API例如 session/message/search | `Authorization: Bearer $USER_A_KEY` |
| Admin API例如创建 account | `X-API-Key: $ROOT_KEY` |
| 普通用户 API例如 session/message/search | `X-API-Key: $USER_A_KEY` |
---
## 1. 创建 admin 工作区 / account
## 1. 创建用户隔离工作区 / account
Admin API 用于多租户管理,包含 workspace/account 创建、用户注册、角色变更、key 生成等能力。创建 account 需要 root 权限
Admin API 用于多租户管理。Memory Gateway 为每个业务用户直接创建一个 admin account不再调用 `/api/v1/admin/accounts/admin/users`
```bash
curl -X POST "$OV_HOST/api/v1/admin/accounts" \
-H "X-API-Key: $ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{
"account_id": "admin",
"admin_user_id": "admin"
"account_id": "userA_account",
"admin_user_id": "userA"
}'
```
@ -45,9 +45,9 @@ curl -X POST "$OV_HOST/api/v1/admin/accounts" \
{
"status": "ok",
"result": {
"account_id": "admin",
"admin_user_id": "admin",
"user_key": "<admin-user-key>"
"account_id": "userA_account",
"admin_user_id": "userA",
"user_key": "<userA-user-key>"
},
"error": null,
"telemetry": null
@ -56,17 +56,17 @@ curl -X POST "$OV_HOST/api/v1/admin/accounts" \
---
## 2. 创建用户
## 2. 创建更多用户
### 2.1 创建 userA
```bash
curl -X POST "$OV_HOST/api/v1/admin/accounts/admin/users" \
curl -X POST "$OV_HOST/api/v1/admin/accounts" \
-H "X-API-Key: $ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_id": "userA",
"role": "user"
"account_id": "userA_account",
"admin_user_id": "userA"
}'
```
@ -79,12 +79,12 @@ export USER_A_KEY="<userA-user-key>"
### 2.2 创建 userB
```bash
curl -X POST "$OV_HOST/api/v1/admin/accounts/admin/users" \
curl -X POST "$OV_HOST/api/v1/admin/accounts" \
-H "X-API-Key: $ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_id": "userB",
"role": "user"
"account_id": "userB_account",
"admin_user_id": "userB"
}'
```
@ -106,7 +106,7 @@ Session 是会话容器用于保存消息、跟踪上下文使用、commit
```bash
curl -X POST "$OV_HOST/api/v1/sessions" \
-H "Authorization: Bearer $USER_A_KEY" \
-H "X-API-Key: $USER_A_KEY" \
-H "Content-Type: application/json" \
-d '{
"session_id": "sessionA1"
@ -121,7 +121,7 @@ curl -X POST "$OV_HOST/api/v1/sessions" \
"result": {
"session_id": "sessionA1",
"user": {
"account_id": "admin",
"account_id": "userA_account",
"user_id": "userA",
"agent_id": "default"
}
@ -135,7 +135,7 @@ curl -X POST "$OV_HOST/api/v1/sessions" \
```bash
curl -X POST "$OV_HOST/api/v1/sessions" \
-H "Authorization: Bearer $USER_B_KEY" \
-H "X-API-Key: $USER_B_KEY" \
-H "Content-Type: application/json" \
-d '{
"session_id": "sessionB1"
@ -152,7 +152,7 @@ HTTP API 支持简单文本模式:`role + content`。`role` 通常为 `user`
```bash
curl -X POST "$OV_HOST/api/v1/sessions/sessionA1/messages" \
-H "Authorization: Bearer $USER_A_KEY" \
-H "X-API-Key: $USER_A_KEY" \
-H "Content-Type: application/json" \
-d '{
"role": "user",
@ -162,7 +162,7 @@ curl -X POST "$OV_HOST/api/v1/sessions/sessionA1/messages" \
```bash
curl -X POST "$OV_HOST/api/v1/sessions/sessionA1/messages" \
-H "Authorization: Bearer $USER_A_KEY" \
-H "X-API-Key: $USER_A_KEY" \
-H "Content-Type: application/json" \
-d '{
"role": "assistant",
@ -174,7 +174,7 @@ curl -X POST "$OV_HOST/api/v1/sessions/sessionA1/messages" \
```bash
curl -X POST "$OV_HOST/api/v1/sessions/sessionB1/messages" \
-H "Authorization: Bearer $USER_B_KEY" \
-H "X-API-Key: $USER_B_KEY" \
-H "Content-Type: application/json" \
-d '{
"role": "user",
@ -184,7 +184,7 @@ curl -X POST "$OV_HOST/api/v1/sessions/sessionB1/messages" \
```bash
curl -X POST "$OV_HOST/api/v1/sessions/sessionB1/messages" \
-H "Authorization: Bearer $USER_B_KEY" \
-H "X-API-Key: $USER_B_KEY" \
-H "Content-Type: application/json" \
-d '{
"role": "assistant",
@ -207,7 +207,7 @@ curl -X POST "$OV_HOST/api/v1/sessions/sessionB1/messages" \
```bash
curl -X POST "$OV_HOST/api/v1/sessions/sessionA1/commit" \
-H "Authorization: Bearer $USER_A_KEY" \
-H "X-API-Key: $USER_A_KEY" \
-H "Content-Type: application/json" \
-d '{
"keep_recent_count": 0
@ -233,7 +233,7 @@ curl -X POST "$OV_HOST/api/v1/sessions/sessionA1/commit" \
```bash
curl -X POST "$OV_HOST/api/v1/sessions/sessionB1/commit" \
-H "Authorization: Bearer $USER_B_KEY" \
-H "X-API-Key: $USER_B_KEY" \
-H "Content-Type: application/json" \
-d '{
"keep_recent_count": 0
@ -246,7 +246,7 @@ curl -X POST "$OV_HOST/api/v1/sessions/sessionB1/commit" \
```bash
curl -s "$OV_HOST/api/v1/tasks/<task_id>" \
-H "Authorization: Bearer $USER_A_KEY" | jq .
-H "X-API-Key: $USER_A_KEY" | jq .
```
成功完成后类似:
@ -302,7 +302,7 @@ curl -s "$OV_HOST/api/v1/tasks/<task_id>" \
```bash
curl -s -X POST "$OV_HOST/api/v1/search/find" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $USER_A_KEY" \
-H "X-API-Key: $USER_A_KEY" \
-d '{
"query": "我之前说了什么",
"target_uri": "viking://user/userA/memories/",
@ -341,7 +341,7 @@ curl -s -X POST "$OV_HOST/api/v1/search/find" \
```bash
curl -s -X POST "$OV_HOST/api/v1/search/search" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $USER_A_KEY" \
-H "X-API-Key: $USER_A_KEY" \
-d '{
"query": "我正在做什么",
"target_uri": "viking://user/userA/sessionA1", #
@ -388,4 +388,3 @@ curl -s -X POST "$OV_HOST/api/v1/search/search" \
1. **验证 memory 是否已经写入**:先用 `tasks/{task_id}` 确认 `completed`
2. **确认 user 长期 memory 是否可召回**:用 `/api/v1/search/find`query 写得贴近 memory 内容。
3. **需要结合当前会话上下文**:再用 `/api/v1/search/search``session_id`

View File

@ -6,9 +6,11 @@ from fastapi import APIRouter, Depends, HTTPException, Query, status
from .auth import verify_api_key
from .schemas import (
MessageIngestRequest,
ProfileRequest,
SearchRequest,
SessionContextRequest,
SessionUserRequest,
TaskStatusRequest,
UserCreateRequest,
)
from .service import MemorySystemService
@ -123,6 +125,23 @@ async def get_openviking_task(
raise user_auth_error(exc) from exc
@router.post("/openviking/tasks/{task_id}")
async def get_openviking_task_from_body(
task_id: str,
request: TaskStatusRequest,
service: MemorySystemService = Depends(get_service),
):
try:
return await service.get_openviking_task(
request.user_id,
request.user_key,
task_id,
session_id=request.session_id,
)
except PermissionError as exc:
raise user_auth_error(exc) from exc
@router.post("/search")
async def search(
request: SearchRequest,
@ -134,14 +153,34 @@ async def search(
raise user_auth_error(exc) from exc
@router.post("/users/{user_id}/profile")
async def get_profile_from_body(
user_id: str,
request: ProfileRequest,
service: MemorySystemService = Depends(get_service),
):
try:
return await service.get_profile(
user_id,
request.user_key,
query=request.query,
limit=request.limit,
level=request.level,
)
except PermissionError as exc:
raise user_auth_error(exc) from exc
@router.get("/users/{user_id}/profile")
async def get_profile(
user_id: str,
user_key: str = Query(min_length=1),
query: str = Query(default="用户画像", min_length=1),
limit: int = Query(default=10, ge=1, le=100),
level: int = Query(default=2, ge=0),
service: MemorySystemService = Depends(get_service),
):
try:
service.openviking.credential_for_user(user_id, user_key)
return await service.get_profile(user_id, user_key, query=query, limit=limit, level=level)
except PermissionError as exc:
raise user_auth_error(exc) from exc
return await service.get_profile(user_id)

View File

@ -49,36 +49,24 @@ class OpenVikingMemorySystemClient:
user_key = self._extract_user_key(data)
if user_key:
self.store.save_account_key(account_id, admin_user_id, user_key)
self.store.save_user_key(admin_user_id, user_key)
self.store.save_user_key(admin_user_id, user_key, account_id=account_id)
return data
async def ensure_admin_workspace(self) -> None:
if self.store.get_account_key(ADMIN_ACCOUNT_ID):
return
await self.create_account(ADMIN_ACCOUNT_ID, ADMIN_USER_ID)
async def create_user(self, user_id: str, role: str = "user") -> dict[str, Any]:
existing = self.store.get_user_key(user_id)
account_id = self._account_id_for_user(user_id)
if existing:
return {"status": "ok", "result": {"account_id": ADMIN_ACCOUNT_ID, "user_id": user_id, "user_key": existing}}
return {
"status": "ok",
"result": {
"account_id": account_id,
"admin_user_id": user_id,
"user_id": user_id,
"user_key": existing,
},
}
await self.ensure_admin_workspace()
if user_id == ADMIN_USER_ID:
user_key = self.store.get_user_key(user_id)
return {"status": "ok", "result": {"account_id": ADMIN_ACCOUNT_ID, "user_id": user_id, "user_key": user_key}}
async with self._client(self.root_key) as client:
response = await client.post(
f"/api/v1/admin/accounts/{ADMIN_ACCOUNT_ID}/users",
json={"user_id": user_id, "role": role},
)
response.raise_for_status()
data = response.json()
user_key = self._extract_user_key(data)
if user_key:
self.store.save_user_key(user_id, user_key)
return data
return await self.create_account(account_id, user_id)
def credential_for_user(
self,
@ -98,7 +86,7 @@ class OpenVikingMemorySystemClient:
) -> OpenVikingCredential:
return OpenVikingCredential(
api_key=user_key,
account_id=ADMIN_ACCOUNT_ID,
account_id=self._account_id_for_user(user_id),
user_id=user_id,
agent_id=agent_id,
user_key_auth=True,
@ -164,22 +152,46 @@ class OpenVikingMemorySystemClient:
return response.json()
async def search(
self, credential: OpenVikingCredential | str, session_id: str | None, query: str, limit: int
self,
credential: OpenVikingCredential | str,
query: str,
limit: int,
level: int = 2,
score_threshold: float = 0.8,
target_uri: str = "viking://user/memories",
) -> dict[str, Any]:
payload: dict[str, Any] = {"query": query, "limit": limit}
if session_id:
payload["session_id"] = session_id
if isinstance(credential, OpenVikingCredential) and credential.user_id:
payload["target_uri"] = (
f"viking://user/{credential.user_id}/{session_id}"
if session_id
else f"viking://user/{credential.user_id}/memories/"
)
payload: dict[str, Any] = {
"query": query,
"target_uri": target_uri,
"limit": limit,
"level": level,
"score_threshold": score_threshold,
}
async with self._credential_client(credential) as client:
response = await client.post("/api/v1/search/search", json=payload)
response.raise_for_status()
return response.json()
async def search_profile_memories(
self,
credential: OpenVikingCredential | str,
query: str,
limit: int,
level: int,
) -> dict[str, Any]:
async with self._credential_client(credential) as client:
response = await client.post(
"/api/v1/search/search",
json={
"query": query,
"limit": limit,
"level": level,
"target_uri": "viking://user/memories",
},
)
response.raise_for_status()
return response.json()
async def get_session_context(self, credential: OpenVikingCredential | str, session_id: str) -> dict[str, Any]:
async with self._credential_client(credential) as client:
response = await client.get(f"/api/v1/sessions/{session_id}/context")
@ -201,11 +213,7 @@ class OpenVikingMemorySystemClient:
return self._client(credential.api_key, headers)
def _client(self, api_key: str, extra_headers: dict[str, str] | None = None) -> httpx.AsyncClient:
headers = {"Content-Type": "application/json"}
if api_key == self.root_key:
headers["X-API-Key"] = api_key
else:
headers["Authorization"] = f"Bearer {api_key}"
headers = {"Content-Type": "application/json", "X-API-Key": api_key}
if extra_headers:
headers.update(extra_headers)
return httpx.AsyncClient(
@ -226,6 +234,9 @@ class OpenVikingMemorySystemClient:
"admin_user_id": admin_user_id,
}
def _account_id_for_user(self, user_id: str) -> str:
return f"{user_id}_account"
def _save_session(self, credential: OpenVikingCredential | str, session_id: str) -> None:
if isinstance(credential, OpenVikingCredential) and credential.user_id:
self.store.save_session(credential.user_id, session_id)

View File

@ -24,6 +24,10 @@ class SessionUserRequest(BaseModel):
user_key: str = Field(min_length=1)
class TaskStatusRequest(SessionUserRequest):
session_id: str | None = Field(default=None, min_length=1)
class SearchRequest(BaseModel):
user_id: str = Field(min_length=1)
user_key: str = Field(min_length=1)
@ -31,6 +35,9 @@ class SearchRequest(BaseModel):
query: str = Field(min_length=1)
use_llm: bool = False
limit: int = Field(default=10, ge=1, le=100)
level: int = Field(default=2, ge=0)
score_threshold: float = Field(default=0.8, ge=0, le=1)
target_uri: str = Field(default="viking://user/memories", min_length=1)
class SessionContextRequest(BaseModel):
@ -40,6 +47,13 @@ class SessionContextRequest(BaseModel):
limit: int = Field(default=10, ge=1, le=100)
class ProfileRequest(BaseModel):
user_key: str = Field(min_length=1)
query: str = Field(default="用户画像", min_length=1)
limit: int = Field(default=10, ge=1, le=100)
level: int = Field(default=2, ge=0)
class BackendStatus(BaseModel):
status: OperationStatus
result: Any = None
@ -88,4 +102,5 @@ class SessionContextResponse(BaseModel):
class ProfileResponse(BaseModel):
status: OperationStatus
profile: Any = None
items: list[dict[str, Any]] = Field(default_factory=list)
backends: dict[str, BackendStatus]

View File

@ -103,9 +103,14 @@ class MemorySystemService:
everos_method = "agentic" if request.use_llm else "hybrid"
async def search_openviking() -> dict[str, Any]:
if request.use_llm:
return await self.openviking.search(credential, request.session_id, request.query, request.limit)
return await self.openviking.find(credential, request.query, request.limit)
return await self.openviking.search(
credential,
request.query,
request.limit,
request.level,
request.score_threshold,
request.target_uri,
)
async def search_everos() -> dict[str, Any]:
return await self.everos.search(
@ -160,10 +165,32 @@ class MemorySystemService:
backends=self._compact_session_context_backends(backends),
)
async def get_profile(self, user_id: str) -> ProfileResponse:
backends = {"everos": await self._capture(lambda: self.everos.get_profile(user_id))}
async def get_profile(
self,
user_id: str,
user_key: str,
query: str = "用户画像",
limit: int = 10,
level: int = 2,
) -> ProfileResponse:
credential = self.openviking.credential_for_user(user_id, user_key)
backends = await self._run_backends(
everos=lambda: self.everos.get_profile(user_id),
openviking=lambda: self.openviking.search_profile_memories(credential, query, limit, level),
)
backends = self._remove_vectors_from_backends(backends)
profile = backends["everos"].result if backends["everos"].status == "success" else None
return ProfileResponse(status=self._aggregate_status(backends), profile=profile, backends=backends)
items = (
self._items_from_backend_result("openviking", backends["openviking"].result)[:limit]
if backends["openviking"].status == "success"
else []
)
return ProfileResponse(
status=self._aggregate_status(backends),
profile=profile,
items=items,
backends=self._compact_profile_backends(backends),
)
async def health(self) -> dict[str, Any]:
backends = await self._run_backends(openviking=self.openviking.health, everos=self.everos.health)
@ -323,6 +350,31 @@ class MemorySystemService:
return {key: value for key, value in compact.items() if value is not None}
return self._compact_backend_result(backend_name, result)
def _compact_profile_backends(self, backends: dict[str, BackendStatus]) -> dict[str, BackendStatus]:
return {
name: backend.model_copy(update={"result": self._compact_profile_backend_result(name, backend.result)})
for name, backend in backends.items()
}
def _compact_profile_backend_result(self, backend_name: str, result: Any) -> Any:
if backend_name == "openviking":
return self._compact_backend_result("openviking", result)
if backend_name == "everos":
data = result.get("data") if isinstance(result, dict) and isinstance(result.get("data"), dict) else result
if not isinstance(data, dict):
return result
compact: dict[str, Any] = {}
for key in ("total_count", "count"):
if key in data:
compact[key] = data[key]
compact["counts"] = {
key: len(data.get(key) or [])
for key in ("episodes", "profiles", "agent_cases", "agent_skills")
if isinstance(data.get(key), list)
}
return compact
return result
def _remove_vectors_from_backends(self, backends: dict[str, BackendStatus]) -> dict[str, BackendStatus]:
return {
name: backend.model_copy(update={"result": self._remove_vectors(backend.result)})

View File

@ -65,7 +65,7 @@ class OpenVikingUserKeyStore:
).fetchone()
return str(row[0]) if row else None
def save_user_key(self, user_id: str, user_key: str) -> None:
def save_user_key(self, user_id: str, user_key: str, account_id: str = ADMIN_ACCOUNT_ID) -> None:
now = datetime.now(timezone.utc).isoformat()
with self._connect() as conn:
conn.execute(
@ -76,7 +76,7 @@ class OpenVikingUserKeyStore:
user_key = excluded.user_key,
updated_at = excluded.updated_at
""",
(user_id, ADMIN_ACCOUNT_ID, user_key, now, now),
(user_id, account_id, user_key, now, now),
)
def user_key_matches(self, user_id: str, user_key: str) -> bool:

View File

@ -147,7 +147,10 @@ curl -s -X POST <MEMORY_SYSTEM_BASE_URL>/memory-system/search \
"session_id": "<SESSION_ID>",
"query": "我喜欢喝什么咖啡?",
"use_llm": false,
"limit": 10
"limit": 10,
"level": 2,
"score_threshold": 0.8,
"target_uri": "viking://user/memories"
}'
```
@ -162,11 +165,14 @@ curl -s -X POST <MEMORY_SYSTEM_BASE_URL>/memory-system/search \
"session_id": "<SESSION_ID>",
"query": "我的偏好是什么?",
"use_llm": true,
"limit": 10
"limit": 10,
"level": 2,
"score_threshold": 0.8,
"target_uri": "viking://user/memories"
}'
```
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.
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.
The search response shape should now look more like:
@ -219,9 +225,15 @@ Use the compact `text` fields for answering. Only inspect raw `backends` when de
## Profile
```bash
curl -s "<MEMORY_SYSTEM_BASE_URL>/memory-system/users/<USER_ID>/profile?user_key=<USER_KEY>"
curl -sS --get "<MEMORY_SYSTEM_BASE_URL>/memory-system/users/<USER_ID>/profile" \
--data-urlencode "user_key=<USER_KEY>" \
--data-urlencode "query=我想喝东西" \
--data-urlencode "limit=10" \
--data-urlencode "level=2"
```
This combines EverOS profile with OpenViking memory recall. The OpenViking request uses `/api/v1/search/search` with `target_uri: viking://user/memories`, `limit`, and `level`.
## Response Interpretation
Inspect backend status:

BIN
tests/Prompt Engineer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 KiB

View File

@ -13,7 +13,7 @@ class FakeStore:
def get_user_key(self, user_id: str) -> str | None:
return self.users.get(user_id)
def save_user_key(self, user_id: str, user_key: str) -> None:
def save_user_key(self, user_id: str, user_key: str, account_id: str = "admin") -> None:
self.users[user_id] = user_key
def get_account_key(self, account_id: str) -> str | None:
@ -91,12 +91,24 @@ def test_openviking_accepts_matching_user_credentials():
credential = client.credential_for_user("tom", "tom-key", agent_id="sess-1")
assert credential.api_key == "tom-key"
assert credential.account_id == "admin"
assert credential.account_id == "tom_account"
assert credential.user_id == "tom"
assert credential.agent_id == "sess-1"
def test_openviking_create_user_initializes_admin_workspace_first():
def test_openviking_client_uses_x_api_key_for_user_keys():
client = OpenVikingMemorySystemClient(store=FakeStore())
client.root_key = "root-key"
http_client = client._client("tom-key")
try:
assert http_client.headers["X-API-Key"] == "tom-key"
assert "Authorization" not in http_client.headers
finally:
asyncio.run(http_client.aclose())
def test_openviking_create_user_creates_isolated_admin_account():
store = FakeStore()
client = OpenVikingMemorySystemClient(store=store)
client.root_key = "root-key"
@ -104,11 +116,14 @@ def test_openviking_create_user_initializes_admin_workspace_first():
responses = [
FakeResponse(
200,
{"status": "ok", "result": {"account_id": "admin", "admin_user_id": "admin", "user_key": "admin-key"}},
),
FakeResponse(
200,
{"status": "ok", "result": {"account_id": "admin", "user_id": "userA", "user_key": "userA-key"}},
{
"status": "ok",
"result": {
"account_id": "userA_account",
"admin_user_id": "userA",
"user_key": "userA-key",
},
},
),
]
client._client = lambda api_key, extra_headers=None: FakeAsyncClient( # type: ignore[method-assign]
@ -120,28 +135,28 @@ def test_openviking_create_user_initializes_admin_workspace_first():
result = asyncio.run(client.create_user("userA"))
assert result == {"status": "ok", "result": {"account_id": "admin", "user_id": "userA", "user_key": "userA-key"}}
assert store.accounts == {"admin": "admin-key"}
assert store.users == {"admin": "admin-key", "userA": "userA-key"}
assert result == {
"status": "ok",
"result": {
"account_id": "userA_account",
"admin_user_id": "userA",
"user_key": "userA-key",
},
}
assert store.accounts == {"userA_account": "userA-key"}
assert store.users == {"userA": "userA-key"}
assert calls == [
(
"post",
"root-key",
{},
"/api/v1/admin/accounts",
{"account_id": "admin", "admin_user_id": "admin"},
),
(
"post",
"root-key",
{},
"/api/v1/admin/accounts/admin/users",
{"user_id": "userA", "role": "user"},
{"account_id": "userA_account", "admin_user_id": "userA"},
),
]
def test_openviking_create_user_reuses_existing_admin_workspace():
def test_openviking_create_user_creates_account_even_when_admin_workspace_exists():
store = FakeStore()
store.save_account_key("admin", "admin", "admin-key")
client = OpenVikingMemorySystemClient(store=store)
@ -150,7 +165,14 @@ def test_openviking_create_user_reuses_existing_admin_workspace():
responses = [
FakeResponse(
200,
{"status": "ok", "result": {"account_id": "admin", "user_id": "userB", "user_key": "userB-key"}},
{
"status": "ok",
"result": {
"account_id": "userB_account",
"admin_user_id": "userB",
"user_key": "userB-key",
},
},
)
]
client._client = lambda api_key, extra_headers=None: FakeAsyncClient( # type: ignore[method-assign]
@ -162,15 +184,23 @@ def test_openviking_create_user_reuses_existing_admin_workspace():
result = asyncio.run(client.create_user("userB"))
assert result == {"status": "ok", "result": {"account_id": "admin", "user_id": "userB", "user_key": "userB-key"}}
assert result == {
"status": "ok",
"result": {
"account_id": "userB_account",
"admin_user_id": "userB",
"user_key": "userB-key",
},
}
assert store.accounts == {"admin": "admin-key", "userB_account": "userB-key"}
assert store.users == {"userB": "userB-key"}
assert calls == [
(
"post",
"root-key",
{},
"/api/v1/admin/accounts/admin/users",
{"user_id": "userB", "role": "user"},
"/api/v1/admin/accounts",
{"account_id": "userB_account", "admin_user_id": "userB"},
)
]
@ -229,7 +259,7 @@ def test_openviking_find_uses_current_identity_memory_scope():
]
def test_openviking_search_uses_session_target_uri():
def test_openviking_search_uses_fixed_user_memory_target_with_level_and_score_threshold():
client = OpenVikingMemorySystemClient(store=FakeStore())
calls = []
responses = [FakeResponse(200, {"status": "ok", "result": {"memories": []}})]
@ -241,7 +271,7 @@ def test_openviking_search_uses_session_target_uri():
)
credential = client.user_credential("tom-key", "tom", agent_id="sess-1")
result = asyncio.run(client.search(credential, "sess-1", "咖啡", 5))
result = asyncio.run(client.search(credential, "咖啡", 5, level=3, score_threshold=0.7))
assert result == {"status": "ok", "result": {"memories": []}}
assert calls == [
@ -250,7 +280,83 @@ def test_openviking_search_uses_session_target_uri():
"tom-key",
{},
"/api/v1/search/search",
{"query": "咖啡", "limit": 5, "session_id": "sess-1", "target_uri": "viking://user/tom/sess-1"},
{
"query": "咖啡",
"target_uri": "viking://user/memories",
"limit": 5,
"level": 3,
"score_threshold": 0.7,
},
)
]
def test_openviking_search_accepts_custom_target_uri():
client = OpenVikingMemorySystemClient(store=FakeStore())
calls = []
responses = [FakeResponse(200, {"status": "ok", "result": {"memories": []}})]
client._client = lambda api_key, extra_headers=None: FakeAsyncClient( # type: ignore[method-assign]
calls,
responses,
api_key,
extra_headers or {},
)
credential = client.user_credential("tom-key", "tom", agent_id="sess-1")
result = asyncio.run(client.search(
credential,
"咖啡",
5,
level=3,
score_threshold=0.7,
target_uri="viking://user/custom/memories",
))
assert result == {"status": "ok", "result": {"memories": []}}
assert calls == [
(
"post",
"tom-key",
{},
"/api/v1/search/search",
{
"query": "咖啡",
"target_uri": "viking://user/custom/memories",
"limit": 5,
"level": 3,
"score_threshold": 0.7,
},
)
]
def test_openviking_profile_search_uses_user_memory_target_and_level():
client = OpenVikingMemorySystemClient(store=FakeStore())
calls = []
responses = [FakeResponse(200, {"status": "ok", "result": {"memories": []}})]
client._client = lambda api_key, extra_headers=None: FakeAsyncClient( # type: ignore[method-assign]
calls,
responses,
api_key,
extra_headers or {},
)
credential = client.user_credential("tom-key", "tom")
result = asyncio.run(client.search_profile_memories(credential, "我想喝东西", 10, 2))
assert result == {"status": "ok", "result": {"memories": []}}
assert calls == [
(
"post",
"tom-key",
{},
"/api/v1/search/search",
{
"query": "我想喝东西",
"limit": 10,
"level": 2,
"target_uri": "viking://user/memories",
},
)
]

View File

@ -14,6 +14,20 @@ def test_memory_system_server_exposes_routes():
assert {"GET", "POST"} <= context_methods
assert "/memory-system/search" in paths
assert "/memory-system/users/{user_id}/profile" in paths
task_methods = {
method
for route in app.routes
if getattr(route, "path", "") == "/memory-system/openviking/tasks/{task_id}"
for method in getattr(route, "methods", set())
}
profile_methods = {
method
for route in app.routes
if getattr(route, "path", "") == "/memory-system/users/{user_id}/profile"
for method in getattr(route, "methods", set())
}
assert {"GET", "POST"} <= task_methods
assert {"GET", "POST"} <= profile_methods
def test_memory_system_messages_does_not_require_account_key_header():

View File

@ -11,7 +11,7 @@ class FakeOpenViking:
async def create_user(self, user_id: str) -> dict:
self.calls.append(("create_user", user_id))
return {"account_id": "admin", "user_id": user_id, "user_key": f"{user_id}-key"}
return {"account_id": f"{user_id}_account", "admin_user_id": user_id, "user_key": f"{user_id}-key"}
def credential_for_user(
self,
@ -39,11 +39,39 @@ class FakeOpenViking:
await asyncio.sleep(0.01)
return {"items": [{"source": "openviking-find"}]}
async def search(self, user_key: str, session_id: str | None, query: str, limit: int) -> dict:
self.calls.append(("search", user_key, session_id, query, limit))
async def search(
self,
user_key: str,
query: str,
limit: int,
level: int = 2,
score_threshold: float = 0.8,
target_uri: str = "viking://user/memories",
) -> dict:
self.calls.append(("search", user_key, query, limit, level, score_threshold, target_uri))
await asyncio.sleep(0.01)
return {"items": [{"source": "openviking-search"}]}
async def search_profile_memories(self, user_key: str, query: str, limit: int, level: int) -> dict:
self.calls.append(("search_profile_memories", user_key, query, limit, level))
return {
"status": "ok",
"result": {
"memories": [
{
"context_type": "memory",
"uri": "viking://user/tom/memories/preferences/coffee.md",
"level": 2,
"score": 0.91,
"abstract": "用户喜欢喝咖啡。",
}
],
"resources": [],
"skills": [],
"total": 1,
},
}
async def get_session_context(self, user_key: str, session_id: str) -> dict:
self.calls.append(("get_session_context", user_key, session_id))
return {
@ -82,6 +110,25 @@ class FakeEverOS:
self.calls.append(("flush", user_id, session_id))
return {"status": "flushed"}
async def get_profile(self, user_id: str) -> dict:
self.calls.append(("get_profile", user_id))
return {
"data": {
"episodes": [],
"profiles": [
{
"id": "profile-1",
"user_id": user_id,
"profile_data": {"summary": "喜欢咖啡"},
}
],
"agent_cases": [],
"agent_skills": [],
"total_count": 1,
"count": 1,
}
}
class FakeEverOSWithVector(FakeEverOS):
async def search(self, user_id: str, session_id: str | None, query: str, method: str, limit: int) -> dict:
@ -166,7 +213,7 @@ def test_create_user_delegates_to_openviking_only():
response = asyncio.run(service.create_user("alice"))
assert response.status == "success"
assert response.account == {"account_id": "admin", "user_id": "alice", "user_key": "alice-key"}
assert response.account == {"account_id": "alice_account", "admin_user_id": "alice", "user_key": "alice-key"}
assert openviking.calls == [("create_user", "alice")]
assert everos.calls == []
@ -179,7 +226,7 @@ def test_search_removes_vectors_from_items_and_backend_results():
))
assert response.items == [
{"source_backend": "openviking", "source": "openviking-find"},
{"source_backend": "openviking", "source": "openviking-search"},
{"source_backend": "everos", "memory_type": "episode", "id": "episode-1"},
]
assert not _has_key(response.backends["everos"].result, "vector")
@ -253,6 +300,56 @@ def test_session_context_combines_openviking_context_and_everos_search_items():
assert ("search", "tom", "sess-1", "我喜欢喝什么?", "hybrid", 5) in everos.calls
def test_profile_combines_everos_profile_and_openviking_memory_search():
openviking = FakeOpenViking()
everos = FakeEverOS()
service = MemorySystemService(openviking=openviking, everos=everos)
response = asyncio.run(service.get_profile("tom", "tom-key", query="我想喝东西", limit=10, level=2))
assert response.status == "success"
assert response.profile == {
"data": {
"episodes": [],
"profiles": [
{
"id": "profile-1",
"user_id": "tom",
"profile_data": {"summary": "喜欢咖啡"},
}
],
"agent_cases": [],
"agent_skills": [],
"total_count": 1,
"count": 1,
}
}
assert response.items == [
{
"source_backend": "openviking",
"context_type": "memory",
"uri": "viking://user/tom/memories/preferences/coffee.md",
"level": 2,
"score": 0.91,
"abstract": "用户喜欢喝咖啡。",
}
]
assert response.backends["everos"].result == {
"total_count": 1,
"count": 1,
"counts": {
"episodes": 0,
"profiles": 1,
"agent_cases": 0,
"agent_skills": 0,
},
}
assert "profiles" not in response.backends["everos"].result
assert ("credential_for_user", "tom", "tom-key", None) in openviking.calls
assert ("search_profile_memories", "key-tom", "我想喝东西", 10, 2) in openviking.calls
assert everos.calls == [("get_profile", "tom")]
def _has_key(value, key: str) -> bool:
if isinstance(value, dict):
return key in value or any(_has_key(item, key) for item in value.values())
@ -332,22 +429,32 @@ def test_commit_uses_user_key_without_account_id():
assert everos.calls == [("flush", "tom", "sess-1")]
def test_search_uses_find_and_hybrid_without_llm():
def test_search_uses_openviking_search_and_hybrid_without_llm():
openviking = FakeOpenViking()
everos = FakeEverOS()
service = MemorySystemService(openviking=openviking, everos=everos)
response = asyncio.run(service.search(
SearchRequest(user_id="tom", user_key="tom-key", session_id="sess-1", query="咖啡偏好", use_llm=False, limit=5),
SearchRequest(
user_id="tom",
user_key="tom-key",
session_id="sess-1",
query="咖啡偏好",
use_llm=False,
limit=5,
level=3,
score_threshold=0.7,
target_uri="viking://user/custom/memories",
),
))
assert response.status == "success"
assert response.items == [
{"source_backend": "openviking", "source": "openviking-find"},
{"source_backend": "openviking", "source": "openviking-search"},
{"source_backend": "everos", "source": "everos-hybrid"},
]
assert ("credential_for_user", "tom", "tom-key", "sess-1") in openviking.calls
assert ("find", "key-tom", "咖啡偏好", 5) in openviking.calls
assert ("search", "key-tom", "咖啡偏好", 5, 3, 0.7, "viking://user/custom/memories") in openviking.calls
assert ("search", "tom", "sess-1", "咖啡偏好", "hybrid", 5) in everos.calls
@ -366,5 +473,5 @@ def test_search_uses_search_and_agentic_with_llm():
{"source_backend": "everos", "source": "everos-agentic"},
]
assert ("credential_for_user", "tom", "tom-key", "sess-1") in openviking.calls
assert ("search", "key-tom", "sess-1", "咖啡偏好", 5) in openviking.calls
assert ("search", "key-tom", "咖啡偏好", 5, 2, 0.8, "viking://user/memories") in openviking.calls
assert ("search", "tom", "sess-1", "咖啡偏好", "agentic", 5) in everos.calls

Binary file not shown.