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:
113
README.md
113
README.md
@ -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"
|
||||
}'
|
||||
```
|
||||
|
||||
## 开发检查
|
||||
|
||||
@ -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`。
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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)})
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
BIN
tests/Prompt Engineer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 548 KiB |
@ -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",
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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
|
||||
|
||||
BIN
tests/大语言模型应用.pdf
Normal file
BIN
tests/大语言模型应用.pdf
Normal file
Binary file not shown.
Reference in New Issue
Block a user