diff --git a/README.md b/README.md index 4ac39ff..3284a88 100644 --- a/README.md +++ b/README.md @@ -654,6 +654,8 @@ 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 20f4823..327c728 100644 --- a/memory_system_api/service.py +++ b/memory_system_api/service.py @@ -228,6 +228,10 @@ 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( @@ -340,6 +344,52 @@ 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 12dc5aa..5ef25a2 100644 --- a/skills/memory-system-api/references/api.md +++ b/skills/memory-system-api/references/api.md @@ -262,7 +262,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. +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. 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 a15909a..2c151ed 100644 --- a/tests/test_memory_system_service.py +++ b/tests/test_memory_system_service.py @@ -488,6 +488,59 @@ 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()