为 OpenViking 内存命中添加最新内容读取功能,并更新相关文档和测试用例

This commit is contained in:
2026-06-03 16:29:59 +08:00
parent c76c8d47d1
commit 8c479e4ecd
4 changed files with 106 additions and 1 deletions

View File

@ -654,6 +654,8 @@ curl -sS -X DELETE -G "$API/resources" \
同时查询 OpenViking 和 EverOS并合并结果。
OpenViking memory 命中会额外用 `content/read` 补读最新正文,并在 item 中返回 `content`。这可以避免 OpenViking 搜索索引里的 `abstract` 暂时落后于刚刚 `replace` 的 memory 内容时误导调用方。
请求体:
| 参数 | 类型 | 必需 | 说明 |

View File

@ -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():

View File

@ -262,7 +262,7 @@ curl -s -X POST <MEMORY_SYSTEM_BASE_URL>/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:

View File

@ -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()