diff --git a/README.md b/README.md index 3284a88..4ce9a76 100644 --- a/README.md +++ b/README.md @@ -530,12 +530,27 @@ curl -sS -G "$API/memories/content" \ |---|---|---:|---| | `user_id` | string | 是 | 用户 ID | | `user_key` | string | 是 | `/users` 返回的 user key | -| `uri` | string | 是 | 目标 memory URI,例如 `viking://user/memories/profile.md` | +| `uri` | string | 是 | 目标 memory URI,例如 `viking://user/userA/memories/preferences/饮食偏好.md` | | `content` | string | 是 | 要写入的 Markdown/text 内容 | | `mode` | `create`/`replace`/`append` | 否 | 写入模式,默认 `create` | | `wait` | bool | 否 | 是否等待索引刷新,默认 `true` | -覆盖修改: +新增 memory 时传 `mode: "create"`。URI 应直接写到目标分类目录下,不要额外插入 `/user` 目录;例如偏好类 memory 使用 `viking://user/userA/memories/preferences/饮食偏好.md`。 + +```bash +curl -sS -X POST "$API/memories" \ + -H "Content-Type: application/json" \ + -d '{ + "user_id": "userA", + "user_key": "'"$USER_KEY"'", + "uri": "viking://user/userA/memories/preferences/饮食偏好.md", + "content": "用户喜欢喝茶", + "mode": "create", + "wait": true + }' +``` + +覆盖修改时把 `mode` 改为 `replace`: ```bash curl -sS -X POST "$API/memories" \ @@ -550,7 +565,7 @@ curl -sS -X POST "$API/memories" \ }' ``` -追加补充时把 `mode` 改为 `append`;新增 memory 时可用默认的 `create`。 +追加补充时把 `mode` 改为 `append`。 ### `DELETE /memories` @@ -654,8 +669,6 @@ curl -sS -X DELETE -G "$API/resources" \ 同时查询 OpenViking 和 EverOS,并合并结果。 -OpenViking memory 命中会额外用 `content/read` 补读最新正文,并在 item 中返回 `content`。这可以避免 OpenViking 搜索索引里的 `abstract` 暂时落后于刚刚 `replace` 的 memory 内容时误导调用方。 - 请求体: | 参数 | 类型 | 必需 | 说明 | diff --git a/memory_system_api/service.py b/memory_system_api/service.py index 327c728..20f4823 100644 --- a/memory_system_api/service.py +++ b/memory_system_api/service.py @@ -228,10 +228,6 @@ class MemorySystemService: backends = await self._run_backends(openviking=search_openviking, everos=search_everos) backends = self._remove_vectors_from_backends(backends) - backends["openviking"] = await self._enrich_openviking_search_memory_content( - backends["openviking"], - credential, - ) items = self._merge_search_items(backends) compact_backends = self._compact_search_backends(backends) return SearchResponse( @@ -344,52 +340,6 @@ class MemorySystemService: return "partial_success" return "failed" - async def _enrich_openviking_search_memory_content( - self, - backend: BackendStatus, - credential: Any, - ) -> BackendStatus: - if backend.status != "success": - return backend - memory_items = self._openviking_search_memory_items(backend.result) - if not memory_items: - return backend - await asyncio.gather( - *(self._add_latest_memory_content(credential, item) for item in memory_items), - ) - return backend - - async def _add_latest_memory_content(self, credential: Any, item: dict[str, Any]) -> None: - uri = item.get("uri") - if not isinstance(uri, str) or not uri.startswith("viking://"): - return - try: - result = await self.openviking.read_memory(credential, uri) - except Exception: # noqa: BLE001 - return - data = result.get("result") if isinstance(result, dict) and isinstance(result.get("result"), dict) else result - content = data.get("content") if isinstance(data, dict) else None - if isinstance(content, str): - item["content"] = content - - def _openviking_search_memory_items(self, result: Any) -> list[dict[str, Any]]: - if not isinstance(result, dict): - return [] - if isinstance(result.get("items"), list): - return [ - item for item in result["items"] - if isinstance(item, dict) and self._is_openviking_memory_item(item) - ] - data = result.get("data") if isinstance(result.get("data"), dict) else result - if isinstance(data.get("result"), dict): - data = data["result"] - if not isinstance(data, dict) or not isinstance(data.get("memories"), list): - return [] - return [item for item in data["memories"] if isinstance(item, dict)] - - def _is_openviking_memory_item(self, item: dict[str, Any]) -> bool: - return item.get("context_type") == "memory" or item.get("memory_type") == "memory" - def _merge_search_items(self, backends: dict[str, BackendStatus]) -> list[dict[str, Any]]: items: list[dict[str, Any]] = [] for backend_name, backend in backends.items(): diff --git a/skills/memory-system-api/references/api.md b/skills/memory-system-api/references/api.md index 5ef25a2..aa78535 100644 --- a/skills/memory-system-api/references/api.md +++ b/skills/memory-system-api/references/api.md @@ -197,13 +197,15 @@ curl -sS -X POST "/memory-system/memories" \ -d '{ "user_id": "", "user_key": "", - "uri": "viking://user/memories/preferences/python.md", - "content": "# Python 偏好\n\n用户偏好使用 Python 做数据分析,常用 pandas。", - "mode": "replace", + "uri": "viking://user//memories/preferences/饮食偏好.md", + "content": "用户喜欢喝茶", + "mode": "create", "wait": true }' ``` +For preference memories, write directly under `memories/preferences/`; do not add an extra `/user` path segment. + `mode` supports: - `create`: create a new memory URI. @@ -262,7 +264,7 @@ curl -s -X POST /memory-system/search \ }' ``` -Raw API search responses include merged `items` plus compact backend diagnostics. Fields named `vector` are stripped recursively before the API returns JSON. The API does not return EverOS `original_data` or full episode payloads inside `backends` anymore. OpenViking search uses `/api/v1/search/search`; `target_uri`, `level`, and `score_threshold` are optional request fields. For OpenViking memory hits, the API also reads the matched URI with `content/read` and returns the latest body in `content` when available. +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: diff --git a/tests/test_memory_system_service.py b/tests/test_memory_system_service.py index 2c151ed..6b8bcbd 100644 --- a/tests/test_memory_system_service.py +++ b/tests/test_memory_system_service.py @@ -420,6 +420,29 @@ def test_write_memory_delegates_to_openviking_content_write_only(): assert everos.calls == [] +def test_write_memory_supports_openviking_create_mode(): + 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/tom/memories/preferences/饮食偏好.md", + content="用户喜欢喝茶", + mode="create", + wait=True, + ))) + + assert response.status == "success" + assert response.memory == {"status": "ok", "result": {"uri": "viking://user/tom/memories/preferences/饮食偏好.md", "mode": "create"}} + assert openviking.calls == [ + ("credential_for_user", "tom", "tom-key", None), + ("write_memory", "key-tom", "viking://user/tom/memories/preferences/饮食偏好.md", "用户喜欢喝茶", "create", True), + ] + assert everos.calls == [] + + def test_delete_memory_delegates_to_openviking_only_and_defaults_non_recursive(): openviking = FakeOpenViking() everos = FakeEverOS() @@ -488,59 +511,6 @@ def test_search_returns_compact_items_and_backend_diagnostics_without_duplicate_ assert not _has_key(response.backends["everos"].result, "original_data") -def test_search_includes_latest_openviking_memory_content_for_memory_hits(): - class FakeOpenVikingWithStaleSearch(FakeOpenViking): - 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)) - return { - "status": "ok", - "result": { - "memories": [ - { - "context_type": "memory", - "uri": "viking://user/tom/memories/preferences/drink.md", - "level": 2, - "score": 0.91, - "abstract": "用户喜欢拿铁咖啡。", - } - ], - "resources": [], - "skills": [], - "total": 1, - }, - } - - 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": "用户不喜欢咖啡,只喜欢下午喝奶茶"}} - - openviking = FakeOpenVikingWithStaleSearch() - service = MemorySystemService(openviking=openviking, everos=FakeEverOS()) - - response = asyncio.run(service.search( - SearchRequest(user_id="tom", user_key="tom-key", session_id="sess-1", query="我喜欢喝什么?", limit=5), - )) - - assert response.items[0] == { - "source_backend": "openviking", - "context_type": "memory", - "uri": "viking://user/tom/memories/preferences/drink.md", - "level": 2, - "score": 0.91, - "abstract": "用户喜欢拿铁咖啡。", - "content": "用户不喜欢咖啡,只喜欢下午喝奶茶", - } - assert ("read_memory", "key-tom", "viking://user/tom/memories/preferences/drink.md") in openviking.calls - - def test_session_context_combines_openviking_context_and_everos_search_items(): openviking = FakeOpenViking() everos = FakeEverOSVerbose()