From 68b2513043b060a43d2e59338ba70af02c09b086 Mon Sep 17 00:00:00 2001 From: tomtan Date: Fri, 29 May 2026 16:38:57 +0800 Subject: [PATCH] Add memory management APIs for OpenViking: list, read, write, and delete memories --- README.md | 89 +++++++++++++++++++ memory_system_api/api.py | 53 +++++++++++ memory_system_api/clients.py | 56 ++++++++++++ memory_system_api/schemas.py | 16 ++++ memory_system_api/service.py | 59 ++++++++++++ skills/memory-system-api/SKILL.md | 48 ++++++++++ tests/test_memory_system_clients.py | 133 +++++++++++++++++++++++++++- tests/test_memory_system_server.py | 16 ++++ tests/test_memory_system_service.py | 111 ++++++++++++++++++++++- 9 files changed, 578 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 21fcce9..44f81a2 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,10 @@ API=http://127.0.0.1:1934/memory-system | `POST` | `/sessions/{session_id}/extract` | 立即触发 OpenViking extract | 需要 | | `GET/POST` | `/sessions/{session_id}/context` | 查询 OpenViking 会话上下文,并用同一 query 搜索 EverOS 记忆 | 需要 | | `GET/POST` | `/openviking/tasks/{task_id}` | 查询 OpenViking 后台任务状态 | 需要 | +| `GET` | `/memories` | 列出 OpenViking memory URI | 需要 | +| `GET` | `/memories/content` | 读取某条 memory 内容 | 需要 | +| `POST` | `/memories` | 创建、覆盖或追加写入 memory | 需要 | +| `DELETE` | `/memories` | 删除某条 memory URI | 需要 | | `POST` | `/resources` | 上传本地文件或远程 URL 到 OpenViking resources | 需要 | | `DELETE` | `/resources` | 删除 OpenViking resource URI | 需要 | | `POST` | `/search` | 同时搜索 OpenViking 和 EverOS 记忆 | 需要 | @@ -357,6 +361,91 @@ curl -sS -X POST "$API/openviking/tasks/${TASK_ID}" \ }' ``` +### `GET /memories` + +列出 OpenViking memory URI。网关会调用 OpenViking: + +```http +GET /api/v1/fs/ls?uri=viking://user/memories&recursive=true +X-API-Key: +``` + +Query 参数: + +| 参数 | 类型 | 必需 | 说明 | +|---|---|---:|---| +| `user_id` | string | 是 | 用户 ID | +| `user_key` | string | 是 | `/users` 返回的 user key | +| `uri` | string | 否 | 要列出的 memory 根 URI,默认 `viking://user/memories` | +| `recursive` | bool | 否 | 是否递归列出,默认 `true` | + +示例: + +```bash +curl -sS -G "$API/memories" \ + --data-urlencode "user_id=userA" \ + --data-urlencode "user_key=$USER_KEY" \ + --data-urlencode "uri=viking://user/memories" \ + --data-urlencode "recursive=true" +``` + +### `GET /memories/content` + +读取某条 memory 内容。先用 `/memories` 或 `/search` 找到 `viking://user/memories/...` URI,再读取: + +```bash +curl -sS -G "$API/memories/content" \ + --data-urlencode "user_id=userA" \ + --data-urlencode "user_key=$USER_KEY" \ + --data-urlencode "uri=viking://user/memories/preferences/python.md" +``` + +### `POST /memories` + +创建、覆盖或追加写入 memory。网关会调用 OpenViking `/api/v1/content/write`,写入后由 OpenViking 刷新语义和向量索引。 + +请求体: + +| 参数 | 类型 | 必需 | 说明 | +|---|---|---:|---| +| `user_id` | string | 是 | 用户 ID | +| `user_key` | string | 是 | `/users` 返回的 user key | +| `uri` | string | 是 | 目标 memory URI,例如 `viking://user/memories/profile.md` | +| `content` | string | 是 | 要写入的 Markdown/text 内容 | +| `mode` | `create`/`replace`/`append` | 否 | 写入模式,默认 `create` | +| `wait` | bool | 否 | 是否等待索引刷新,默认 `true` | + +覆盖修改: + +```bash +curl -sS -X POST "$API/memories" \ + -H "Content-Type: application/json" \ + -d '{ + "user_id": "userA", + "user_key": "'"$USER_KEY"'", + "uri": "viking://user/memories/preferences/python.md", + "content": "# Python 偏好\n\n用户偏好使用 Python 做数据分析,常用 pandas。", + "mode": "replace", + "wait": true + }' +``` + +追加补充时把 `mode` 改为 `append`;新增 memory 时可用默认的 `create`。 + +### `DELETE /memories` + +删除某条 memory。默认非递归删除;如果 OpenViking 提示目标是目录或复合资源,再把 `recursive` 设为 `true`。 + +```bash +curl -sS -X DELETE -G "$API/memories" \ + --data-urlencode "user_id=userA" \ + --data-urlencode "user_key=$USER_KEY" \ + --data-urlencode "uri=viking://user/memories/preferences/python.md" \ + --data-urlencode "recursive=false" +``` + +返回中的 `memory` 是 OpenViking 对应接口的原始响应。 + ### `POST /resources` 上传文件资源到 OpenViking。网关只调用 OpenViking,不写 EverOS。 diff --git a/memory_system_api/api.py b/memory_system_api/api.py index aaa5188..b7968d5 100644 --- a/memory_system_api/api.py +++ b/memory_system_api/api.py @@ -5,6 +5,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, status from .auth import verify_api_key from .schemas import ( + MemoryWriteRequest, MessageIngestRequest, ProfileRequest, ResourceUploadRequest, @@ -80,6 +81,58 @@ async def delete_resource( raise user_auth_error(exc) from exc +@router.get("/memories") +async def list_memories( + user_id: str = Query(min_length=1), + user_key: str = Query(min_length=1), + uri: str = Query(default="viking://user/memories", min_length=1), + recursive: bool = Query(default=True), + service: MemorySystemService = Depends(get_service), +): + try: + return await service.list_memories(user_id, user_key, uri=uri, recursive=recursive) + except PermissionError as exc: + raise user_auth_error(exc) from exc + + +@router.get("/memories/content") +async def read_memory( + user_id: str = Query(min_length=1), + user_key: str = Query(min_length=1), + uri: str = Query(min_length=1), + service: MemorySystemService = Depends(get_service), +): + try: + return await service.read_memory(user_id, user_key, uri) + except PermissionError as exc: + raise user_auth_error(exc) from exc + + +@router.post("/memories") +async def write_memory( + request: MemoryWriteRequest, + service: MemorySystemService = Depends(get_service), +): + try: + return await service.write_memory(request) + except PermissionError as exc: + raise user_auth_error(exc) from exc + + +@router.delete("/memories") +async def delete_memory( + user_id: str = Query(min_length=1), + user_key: str = Query(min_length=1), + uri: str = Query(min_length=1), + recursive: bool = Query(default=False), + service: MemorySystemService = Depends(get_service), +): + try: + return await service.delete_memory(user_id, user_key, uri, recursive=recursive) + except PermissionError as exc: + raise user_auth_error(exc) from exc + + @router.post("/sessions/{session_id}/commit") async def commit_session( session_id: str, diff --git a/memory_system_api/clients.py b/memory_system_api/clients.py index de66a36..d39818c 100644 --- a/memory_system_api/clients.py +++ b/memory_system_api/clients.py @@ -190,6 +190,62 @@ class OpenVikingMemorySystemClient: response.raise_for_status() return response.json() + async def list_memories( + self, + credential: OpenVikingCredential | str, + uri: str = "viking://user/memories", + recursive: bool = True, + ) -> dict[str, Any]: + async with self._credential_client(credential) as client: + response = await client.get( + "/api/v1/fs/ls", + params={"uri": uri, "recursive": str(recursive).lower()}, + ) + response.raise_for_status() + return response.json() + + async def read_memory(self, credential: OpenVikingCredential | str, uri: str) -> dict[str, Any]: + async with self._credential_client(credential) as client: + response = await client.get("/api/v1/content/read", params={"uri": uri}) + response.raise_for_status() + return response.json() + + async def write_memory( + self, + credential: OpenVikingCredential | str, + *, + uri: str, + content: str, + mode: str = "create", + wait: bool = True, + ) -> dict[str, Any]: + async with self._credential_client(credential) as client: + response = await client.post( + "/api/v1/content/write", + json={ + "uri": uri, + "content": content, + "mode": mode, + "wait": wait, + }, + ) + response.raise_for_status() + return response.json() + + async def delete_memory( + self, + credential: OpenVikingCredential | str, + uri: str, + recursive: bool = False, + ) -> dict[str, Any]: + async with self._credential_client(credential) as client: + response = await client.delete( + "/api/v1/fs", + params={"uri": uri, "recursive": str(recursive).lower()}, + ) + response.raise_for_status() + return response.json() + async def find(self, credential: OpenVikingCredential | str, query: str, limit: int) -> dict[str, Any]: user_id = credential.user_id if isinstance(credential, OpenVikingCredential) else None target_uri = f"viking://user/{user_id}/memories/" if user_id else "viking://user/memories/" diff --git a/memory_system_api/schemas.py b/memory_system_api/schemas.py index 308b005..98a0728 100644 --- a/memory_system_api/schemas.py +++ b/memory_system_api/schemas.py @@ -7,6 +7,7 @@ from pydantic import BaseModel, Field OperationStatus = Literal["success", "partial_success", "failed"] +MemoryWriteMode = Literal["create", "replace", "append"] class MessageIngestRequest(BaseModel): @@ -54,6 +55,15 @@ class ProfileRequest(BaseModel): level: int = Field(default=2, ge=0) +class MemoryWriteRequest(BaseModel): + user_id: str = Field(min_length=1) + user_key: str = Field(min_length=1) + uri: str = Field(min_length=1) + content: str + mode: MemoryWriteMode = "create" + wait: bool = True + + class ResourceUploadRequest(BaseModel): user_id: str = Field(min_length=1) user_key: str = Field(min_length=1) @@ -116,6 +126,12 @@ class ProfileResponse(BaseModel): backends: dict[str, BackendStatus] +class MemoryOperationResponse(BaseModel): + status: OperationStatus + memory: Any = None + backends: dict[str, BackendStatus] + + class ResourceMutationResponse(BaseModel): status: OperationStatus resource: Any = None diff --git a/memory_system_api/service.py b/memory_system_api/service.py index b0b1aff..20f4823 100644 --- a/memory_system_api/service.py +++ b/memory_system_api/service.py @@ -11,6 +11,8 @@ from .schemas import ( BackendStatus, CommitResponse, ExtractResponse, + MemoryOperationResponse, + MemoryWriteRequest, MessageIngestRequest, MessageIngestResponse, ProfileResponse, @@ -76,6 +78,63 @@ class MemorySystemService: resource = backends["openviking"].result if backends["openviking"].status == "success" else None return ResourceMutationResponse(status=self._aggregate_status(backends), resource=resource, backends=backends) + async def list_memories( + self, + user_id: str, + user_key: str, + uri: str = "viking://user/memories", + recursive: bool = True, + ) -> MemoryOperationResponse: + credential = self.openviking.credential_for_user(user_id, user_key) + backends = { + "openviking": await self._capture(lambda: self.openviking.list_memories(credential, uri, recursive)), + } + memory = backends["openviking"].result if backends["openviking"].status == "success" else None + return MemoryOperationResponse(status=self._aggregate_status(backends), memory=memory, backends=backends) + + async def read_memory( + self, + user_id: str, + user_key: str, + uri: str, + ) -> MemoryOperationResponse: + credential = self.openviking.credential_for_user(user_id, user_key) + backends = { + "openviking": await self._capture(lambda: self.openviking.read_memory(credential, uri)), + } + memory = backends["openviking"].result if backends["openviking"].status == "success" else None + return MemoryOperationResponse(status=self._aggregate_status(backends), memory=memory, backends=backends) + + async def write_memory(self, request: MemoryWriteRequest) -> MemoryOperationResponse: + credential = self.openviking.credential_for_user(request.user_id, request.user_key) + backends = { + "openviking": await self._capture( + lambda: self.openviking.write_memory( + credential, + uri=request.uri, + content=request.content, + mode=request.mode, + wait=request.wait, + ) + ), + } + memory = backends["openviking"].result if backends["openviking"].status == "success" else None + return MemoryOperationResponse(status=self._aggregate_status(backends), memory=memory, backends=backends) + + async def delete_memory( + self, + user_id: str, + user_key: str, + uri: str, + recursive: bool = False, + ) -> MemoryOperationResponse: + credential = self.openviking.credential_for_user(user_id, user_key) + backends = { + "openviking": await self._capture(lambda: self.openviking.delete_memory(credential, uri, recursive)), + } + memory = backends["openviking"].result if backends["openviking"].status == "success" else None + return MemoryOperationResponse(status=self._aggregate_status(backends), memory=memory, backends=backends) + async def ingest_messages(self, request: MessageIngestRequest) -> MessageIngestResponse: messages = self._messages_from_request(request) if not messages: diff --git a/skills/memory-system-api/SKILL.md b/skills/memory-system-api/SKILL.md index c46880c..e562dfe 100644 --- a/skills/memory-system-api/SKILL.md +++ b/skills/memory-system-api/SKILL.md @@ -61,6 +61,10 @@ Base path: `/memory-system` | `POST` | `/sessions/{session_id}/extract` | Trigger OpenViking extract only | Yes | | `GET/POST` | `/sessions/{session_id}/context` | Read OpenViking session context plus EverOS recall | Yes | | `GET` | `/openviking/tasks/{task_id}` | Poll OpenViking task status | Yes | +| `GET` | `/memories` | List OpenViking memory URIs under a memory root | Yes | +| `GET` | `/memories/content` | Read one OpenViking memory URI | Yes | +| `POST` | `/memories` | Create, replace, or append an OpenViking memory via `content/write` | Yes | +| `DELETE` | `/memories` | Delete an OpenViking memory URI via `fs` | Yes | | `POST` | `/search` | Search OpenViking and EverOS | Yes | | `GET` | `/users/{user_id}/profile` | Read EverOS profile | Yes | @@ -147,6 +151,50 @@ GET with query parameters is also supported: curl -sS "$BASE/memory-system/sessions/sessionA1/context?user_id=userA&user_key=$USER_KEY&query=我喜欢喝什么?&limit=10" ``` +List memories: + +```bash +curl -sS -G "$BASE/memory-system/memories" \ + --data-urlencode "user_id=userA" \ + --data-urlencode "user_key=$USER_KEY" \ + --data-urlencode "uri=viking://user/memories" \ + --data-urlencode "recursive=true" +``` + +Read memory: + +```bash +curl -sS -G "$BASE/memory-system/memories/content" \ + --data-urlencode "user_id=userA" \ + --data-urlencode "user_key=$USER_KEY" \ + --data-urlencode "uri=viking://user/memories/preferences/python.md" +``` + +Create, replace, or append memory: + +```bash +curl -sS -X POST "$BASE/memory-system/memories" \ + -H "Content-Type: application/json" \ + -d '{ + "user_id": "userA", + "user_key": "'"$USER_KEY"'", + "uri": "viking://user/memories/preferences/python.md", + "content": "# Python 偏好\n\n用户偏好使用 Python 做数据分析。", + "mode": "replace", + "wait": true + }' +``` + +Delete memory: + +```bash +curl -sS -X DELETE -G "$BASE/memory-system/memories" \ + --data-urlencode "user_id=userA" \ + --data-urlencode "user_key=$USER_KEY" \ + --data-urlencode "uri=viking://user/memories/preferences/python.md" \ + --data-urlencode "recursive=false" +``` + ## Response Handling Top-level `status` is one of: diff --git a/tests/test_memory_system_clients.py b/tests/test_memory_system_clients.py index fd6b84a..afef1d0 100644 --- a/tests/test_memory_system_clients.py +++ b/tests/test_memory_system_clients.py @@ -68,8 +68,8 @@ class FakeAsyncClient: self.calls.append(("post", self.api_key, self.headers, path, json, files)) return self.responses.pop(0) - async def get(self, path: str) -> FakeResponse: - self.calls.append(("get", self.api_key, self.headers, path, None)) + async def get(self, path: str, params: dict | None = None) -> FakeResponse: + self.calls.append(("get", self.api_key, self.headers, path, params)) return self.responses.pop(0) async def delete(self, path: str, params: dict | None = None) -> FakeResponse: @@ -573,6 +573,135 @@ def test_openviking_delete_resource_sends_uri_and_recursive_flag(): ] +def test_openviking_list_memories_calls_fs_ls_with_recursive_flag(): + client = OpenVikingMemorySystemClient(store=FakeStore()) + calls = [] + responses = [FakeResponse(200, {"status": "ok", "result": {"children": []}})] + client._client = lambda api_key, extra_headers=None, json_content_type=True: FakeAsyncClient( # type: ignore[method-assign] + calls, + responses, + api_key, + extra_headers or {}, + ) + credential = client.user_credential("tom-key", "tom") + + result = asyncio.run( + client.list_memories( + credential, + uri="viking://user/memories", + recursive=True, + ) + ) + + assert result == {"status": "ok", "result": {"children": []}} + assert calls == [ + ( + "get", + "tom-key", + {}, + "/api/v1/fs/ls", + {"uri": "viking://user/memories", "recursive": "true"}, + ) + ] + + +def test_openviking_read_memory_calls_content_read(): + client = OpenVikingMemorySystemClient(store=FakeStore()) + calls = [] + responses = [FakeResponse(200, {"status": "ok", "result": {"content": "# Python"}})] + client._client = lambda api_key, extra_headers=None, json_content_type=True: FakeAsyncClient( # type: ignore[method-assign] + calls, + responses, + api_key, + extra_headers or {}, + ) + credential = client.user_credential("tom-key", "tom") + + result = asyncio.run(client.read_memory(credential, "viking://user/memories/preferences/python.md")) + + assert result == {"status": "ok", "result": {"content": "# Python"}} + assert calls == [ + ( + "get", + "tom-key", + {}, + "/api/v1/content/read", + {"uri": "viking://user/memories/preferences/python.md"}, + ) + ] + + +def test_openviking_write_memory_posts_content_write_mode_and_wait(): + client = OpenVikingMemorySystemClient(store=FakeStore()) + calls = [] + responses = [FakeResponse(200, {"status": "ok", "result": {"uri": "viking://user/memories/profile.md"}})] + client._client = lambda api_key, extra_headers=None, json_content_type=True: FakeAsyncClient( # type: ignore[method-assign] + calls, + responses, + api_key, + extra_headers or {}, + ) + credential = client.user_credential("tom-key", "tom") + + result = asyncio.run( + client.write_memory( + credential, + uri="viking://user/memories/profile.md", + content="# Profile\n\nLikes Python.", + mode="replace", + wait=True, + ) + ) + + assert result == {"status": "ok", "result": {"uri": "viking://user/memories/profile.md"}} + assert calls == [ + ( + "post", + "tom-key", + {}, + "/api/v1/content/write", + { + "uri": "viking://user/memories/profile.md", + "content": "# Profile\n\nLikes Python.", + "mode": "replace", + "wait": True, + }, + None, + ) + ] + + +def test_openviking_delete_memory_defaults_non_recursive(): + client = OpenVikingMemorySystemClient(store=FakeStore()) + calls = [] + responses = [FakeResponse(200, {"status": "ok", "result": {"estimated_deleted_count": 1}})] + client._client = lambda api_key, extra_headers=None, json_content_type=True: FakeAsyncClient( # type: ignore[method-assign] + calls, + responses, + api_key, + extra_headers or {}, + ) + credential = client.user_credential("tom-key", "tom") + + result = asyncio.run( + client.delete_memory( + credential, + uri="viking://user/memories/preferences/python.md", + ) + ) + + assert result == {"status": "ok", "result": {"estimated_deleted_count": 1}} + assert calls == [ + ( + "delete", + "tom-key", + {}, + "/api/v1/fs", + {"uri": "viking://user/memories/preferences/python.md", "recursive": "false"}, + ) + ] + + def test_everos_assistant_payload_does_not_use_user_id_as_sender(): client = EverOSMemorySystemClient() diff --git a/tests/test_memory_system_server.py b/tests/test_memory_system_server.py index 533505b..1de2ace 100644 --- a/tests/test_memory_system_server.py +++ b/tests/test_memory_system_server.py @@ -14,6 +14,8 @@ def test_memory_system_server_exposes_routes(): assert {"GET", "POST"} <= context_methods assert "/memory-system/search" in paths assert "/memory-system/resources" in paths + assert "/memory-system/memories" in paths + assert "/memory-system/memories/content" in paths assert "/memory-system/users/{user_id}/profile" in paths task_methods = { method @@ -36,6 +38,20 @@ def test_memory_system_server_exposes_routes(): for method in getattr(route, "methods", set()) } assert {"DELETE", "POST"} <= resource_methods + memory_methods = { + method + for route in app.routes + if getattr(route, "path", "") == "/memory-system/memories" + for method in getattr(route, "methods", set()) + } + memory_content_methods = { + method + for route in app.routes + if getattr(route, "path", "") == "/memory-system/memories/content" + for method in getattr(route, "methods", set()) + } + assert {"DELETE", "GET", "POST"} <= memory_methods + assert {"GET"} <= memory_content_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 7471490..a15909a 100644 --- a/tests/test_memory_system_service.py +++ b/tests/test_memory_system_service.py @@ -1,6 +1,12 @@ import asyncio -from memory_system_api.schemas import MessageIngestRequest, ResourceUploadRequest, SearchRequest, SessionContextRequest +from memory_system_api.schemas import ( + MemoryWriteRequest, + MessageIngestRequest, + ResourceUploadRequest, + SearchRequest, + SessionContextRequest, +) from memory_system_api.service import MemorySystemService @@ -120,6 +126,22 @@ class FakeOpenViking: self.calls.append(("delete_resource", user_key, uri, recursive)) return {"status": "ok", "result": {"uri": uri, "estimated_deleted_count": 4}} + async def list_memories(self, user_key: str, uri: str, recursive: bool = True) -> dict: + self.calls.append(("list_memories", user_key, uri, recursive)) + return {"status": "ok", "result": {"children": [{"uri": "viking://user/memories/profile.md"}]}} + + async def read_memory(self, user_key: str, uri: str) -> dict: + self.calls.append(("read_memory", user_key, uri)) + return {"status": "ok", "result": {"uri": uri, "content": "# Profile"}} + + async def write_memory(self, user_key: str, uri: str, content: str, mode: str, wait: bool = True) -> dict: + self.calls.append(("write_memory", user_key, uri, content, mode, wait)) + return {"status": "ok", "result": {"uri": uri, "mode": mode}} + + async def delete_memory(self, user_key: str, uri: str, recursive: bool = False) -> dict: + self.calls.append(("delete_memory", user_key, uri, recursive)) + return {"status": "ok", "result": {"uri": uri, "estimated_deleted_count": 1}} + class FakeEverOS: def __init__(self, fail_on_append: bool = False): @@ -334,6 +356,93 @@ def test_delete_resource_delegates_to_openviking_only(): assert everos.calls == [] +def test_list_memories_delegates_to_openviking_only(): + openviking = FakeOpenViking() + everos = FakeEverOS() + service = MemorySystemService(openviking=openviking, everos=everos) + + response = asyncio.run(service.list_memories( + user_id="tom", + user_key="tom-key", + uri="viking://user/memories", + recursive=True, + )) + + assert response.status == "success" + assert response.memory == {"status": "ok", "result": {"children": [{"uri": "viking://user/memories/profile.md"}]}} + assert openviking.calls == [ + ("credential_for_user", "tom", "tom-key", None), + ("list_memories", "key-tom", "viking://user/memories", True), + ] + assert everos.calls == [] + + +def test_read_memory_delegates_to_openviking_only(): + openviking = FakeOpenViking() + everos = FakeEverOS() + service = MemorySystemService(openviking=openviking, everos=everos) + + response = asyncio.run(service.read_memory( + user_id="tom", + user_key="tom-key", + uri="viking://user/memories/profile.md", + )) + + assert response.status == "success" + assert response.memory == {"status": "ok", "result": {"uri": "viking://user/memories/profile.md", "content": "# Profile"}} + assert openviking.calls == [ + ("credential_for_user", "tom", "tom-key", None), + ("read_memory", "key-tom", "viking://user/memories/profile.md"), + ] + assert everos.calls == [] + + +def test_write_memory_delegates_to_openviking_content_write_only(): + openviking = FakeOpenViking() + everos = FakeEverOS() + service = MemorySystemService(openviking=openviking, everos=everos) + + response = asyncio.run(service.write_memory(MemoryWriteRequest( + user_id="tom", + user_key="tom-key", + uri="viking://user/memories/profile.md", + content="# Profile\n\nLikes Python.", + mode="replace", + wait=True, + ))) + + assert response.status == "success" + assert response.memory == {"status": "ok", "result": {"uri": "viking://user/memories/profile.md", "mode": "replace"}} + assert openviking.calls == [ + ("credential_for_user", "tom", "tom-key", None), + ("write_memory", "key-tom", "viking://user/memories/profile.md", "# Profile\n\nLikes Python.", "replace", True), + ] + assert everos.calls == [] + + +def test_delete_memory_delegates_to_openviking_only_and_defaults_non_recursive(): + openviking = FakeOpenViking() + everos = FakeEverOS() + service = MemorySystemService(openviking=openviking, everos=everos) + + response = asyncio.run(service.delete_memory( + user_id="tom", + user_key="tom-key", + uri="viking://user/memories/preferences/python.md", + )) + + assert response.status == "success" + assert response.memory == { + "status": "ok", + "result": {"uri": "viking://user/memories/preferences/python.md", "estimated_deleted_count": 1}, + } + assert openviking.calls == [ + ("credential_for_user", "tom", "tom-key", None), + ("delete_memory", "key-tom", "viking://user/memories/preferences/python.md", False), + ] + assert everos.calls == [] + + def test_search_removes_vectors_from_items_and_backend_results(): service = MemorySystemService(openviking=FakeOpenViking(), everos=FakeEverOSWithVector())