diff --git a/README.md b/README.md index 5a8690a..692fc47 100644 --- a/README.md +++ b/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` 是 `_account`,`admin_user_id` 是传入的 `user_id`: ```http POST /api/v1/admin/accounts -{"account_id": "admin", "admin_user_id": "admin"} +{"account_id": "_account", "admin_user_id": ""} ``` -随后创建具体用户: - -```http -POST /api/v1/admin/accounts/admin/users -{"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: ` | -| 普通用户 API,例如 session/message/commit/task/search | `Authorization: Bearer ` | +| Admin API,例如创建 account | `X-API-Key: ` | +| 普通用户 API,例如 session/message/commit/task/search | `X-API-Key: ` | ### Session 与搜索范围 -OpenViking session 由请求里的 `session_id` 创建和提交。普通向量搜索使用显式用户 memory 路径 `viking://user//memories/`;`use_llm=true` 的智能搜索会同时传 `session_id` 和 `target_uri: viking://user//`,用于结合当前 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 +X-API-Key: ``` 同时用同一个 `query` 调用 EverOS `/api/v1/memories/search`,返回相关 episodic/profile/raw message 记忆。适合在回答用户问题前,把“当前 session 工作记忆”和“EverOS 相关记忆”一起取回。 @@ -314,7 +313,7 @@ Authorization: Bearer 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/?user_id=userA&user_key=$USER_KEY&session_id=sessionA1" +curl -sS -X POST "$API/openviking/tasks/" \ + -H "Content-Type: application/json" \ + -d '{ + "user_id": "userA", + "user_key": "'"$USER_KEY"'", + "session_id": "sessionA1" + }' ``` ## 开发检查 diff --git a/docs/openviking_memory_api_flow.md b/docs/openviking_memory_api_flow.md index 93a73f2..aafc79c 100644 --- a/docs/openviking_memory_api_flow.md +++ b/docs/openviking_memory_api_flow.md @@ -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": "" + "account_id": "userA_account", + "admin_user_id": "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="" ### 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/" \ - -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/" \ ```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`。 - diff --git a/memory_system_api/api.py b/memory_system_api/api.py index 71b5318..68021b7 100644 --- a/memory_system_api/api.py +++ b/memory_system_api/api.py @@ -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) diff --git a/memory_system_api/clients.py b/memory_system_api/clients.py index 5d191cc..00d2a7d 100644 --- a/memory_system_api/clients.py +++ b/memory_system_api/clients.py @@ -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) diff --git a/memory_system_api/schemas.py b/memory_system_api/schemas.py index bd2003b..c04dc64 100644 --- a/memory_system_api/schemas.py +++ b/memory_system_api/schemas.py @@ -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] diff --git a/memory_system_api/service.py b/memory_system_api/service.py index 365543a..41fad29 100644 --- a/memory_system_api/service.py +++ b/memory_system_api/service.py @@ -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)}) diff --git a/memory_system_api/store.py b/memory_system_api/store.py index 1f6fa53..c08c77f 100644 --- a/memory_system_api/store.py +++ b/memory_system_api/store.py @@ -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: diff --git a/skills/memory-system-api/references/api.md b/skills/memory-system-api/references/api.md index 32b3070..fdf6f5a 100644 --- a/skills/memory-system-api/references/api.md +++ b/skills/memory-system-api/references/api.md @@ -147,7 +147,10 @@ curl -s -X POST /memory-system/search \ "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/search \ "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/users//profile?user_key=" +curl -sS --get "/memory-system/users//profile" \ + --data-urlencode "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: diff --git a/tests/Prompt Engineer.png b/tests/Prompt Engineer.png new file mode 100644 index 0000000..70f5f5b Binary files /dev/null and b/tests/Prompt Engineer.png differ diff --git a/tests/test_memory_system_clients.py b/tests/test_memory_system_clients.py index a6859b9..ce9baa1 100644 --- a/tests/test_memory_system_clients.py +++ b/tests/test_memory_system_clients.py @@ -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", + }, ) ] diff --git a/tests/test_memory_system_server.py b/tests/test_memory_system_server.py index ca746c6..1f39a42 100644 --- a/tests/test_memory_system_server.py +++ b/tests/test_memory_system_server.py @@ -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(): diff --git a/tests/test_memory_system_service.py b/tests/test_memory_system_service.py index 59604bc..7c15dc5 100644 --- a/tests/test_memory_system_service.py +++ b/tests/test_memory_system_service.py @@ -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 diff --git a/tests/大语言模型应用.pdf b/tests/大语言模型应用.pdf new file mode 100644 index 0000000..5dd9b8e Binary files /dev/null and b/tests/大语言模型应用.pdf differ