更新文档,调整内存 URI 格式和写入模式示例;移除过时的 OpenViking 内容补充逻辑,并添加相应的单元测试
This commit is contained in:
23
README.md
23
README.md
@ -530,12 +530,27 @@ curl -sS -G "$API/memories/content" \
|
|||||||
|---|---|---:|---|
|
|---|---|---:|---|
|
||||||
| `user_id` | string | 是 | 用户 ID |
|
| `user_id` | string | 是 | 用户 ID |
|
||||||
| `user_key` | string | 是 | `/users` 返回的 user key |
|
| `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 内容 |
|
| `content` | string | 是 | 要写入的 Markdown/text 内容 |
|
||||||
| `mode` | `create`/`replace`/`append` | 否 | 写入模式,默认 `create` |
|
| `mode` | `create`/`replace`/`append` | 否 | 写入模式,默认 `create` |
|
||||||
| `wait` | bool | 否 | 是否等待索引刷新,默认 `true` |
|
| `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
|
```bash
|
||||||
curl -sS -X POST "$API/memories" \
|
curl -sS -X POST "$API/memories" \
|
||||||
@ -550,7 +565,7 @@ curl -sS -X POST "$API/memories" \
|
|||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
追加补充时把 `mode` 改为 `append`;新增 memory 时可用默认的 `create`。
|
追加补充时把 `mode` 改为 `append`。
|
||||||
|
|
||||||
### `DELETE /memories`
|
### `DELETE /memories`
|
||||||
|
|
||||||
@ -654,8 +669,6 @@ curl -sS -X DELETE -G "$API/resources" \
|
|||||||
|
|
||||||
同时查询 OpenViking 和 EverOS,并合并结果。
|
同时查询 OpenViking 和 EverOS,并合并结果。
|
||||||
|
|
||||||
OpenViking memory 命中会额外用 `content/read` 补读最新正文,并在 item 中返回 `content`。这可以避免 OpenViking 搜索索引里的 `abstract` 暂时落后于刚刚 `replace` 的 memory 内容时误导调用方。
|
|
||||||
|
|
||||||
请求体:
|
请求体:
|
||||||
|
|
||||||
| 参数 | 类型 | 必需 | 说明 |
|
| 参数 | 类型 | 必需 | 说明 |
|
||||||
|
|||||||
@ -228,10 +228,6 @@ class MemorySystemService:
|
|||||||
|
|
||||||
backends = await self._run_backends(openviking=search_openviking, everos=search_everos)
|
backends = await self._run_backends(openviking=search_openviking, everos=search_everos)
|
||||||
backends = self._remove_vectors_from_backends(backends)
|
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)
|
items = self._merge_search_items(backends)
|
||||||
compact_backends = self._compact_search_backends(backends)
|
compact_backends = self._compact_search_backends(backends)
|
||||||
return SearchResponse(
|
return SearchResponse(
|
||||||
@ -344,52 +340,6 @@ class MemorySystemService:
|
|||||||
return "partial_success"
|
return "partial_success"
|
||||||
return "failed"
|
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]]:
|
def _merge_search_items(self, backends: dict[str, BackendStatus]) -> list[dict[str, Any]]:
|
||||||
items: list[dict[str, Any]] = []
|
items: list[dict[str, Any]] = []
|
||||||
for backend_name, backend in backends.items():
|
for backend_name, backend in backends.items():
|
||||||
|
|||||||
@ -197,13 +197,15 @@ curl -sS -X POST "<MEMORY_SYSTEM_BASE_URL>/memory-system/memories" \
|
|||||||
-d '{
|
-d '{
|
||||||
"user_id": "<USER_ID>",
|
"user_id": "<USER_ID>",
|
||||||
"user_key": "<USER_KEY>",
|
"user_key": "<USER_KEY>",
|
||||||
"uri": "viking://user/memories/preferences/python.md",
|
"uri": "viking://user/<USER_ID>/memories/preferences/饮食偏好.md",
|
||||||
"content": "# Python 偏好\n\n用户偏好使用 Python 做数据分析,常用 pandas。",
|
"content": "用户喜欢喝茶",
|
||||||
"mode": "replace",
|
"mode": "create",
|
||||||
"wait": true
|
"wait": true
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For preference memories, write directly under `memories/preferences/`; do not add an extra `/user` path segment.
|
||||||
|
|
||||||
`mode` supports:
|
`mode` supports:
|
||||||
|
|
||||||
- `create`: create a new memory URI.
|
- `create`: create a new memory URI.
|
||||||
@ -262,7 +264,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. 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:
|
The search response shape should now look more like:
|
||||||
|
|
||||||
|
|||||||
@ -420,6 +420,29 @@ def test_write_memory_delegates_to_openviking_content_write_only():
|
|||||||
assert everos.calls == []
|
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():
|
def test_delete_memory_delegates_to_openviking_only_and_defaults_non_recursive():
|
||||||
openviking = FakeOpenViking()
|
openviking = FakeOpenViking()
|
||||||
everos = FakeEverOS()
|
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")
|
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():
|
def test_session_context_combines_openviking_context_and_everos_search_items():
|
||||||
openviking = FakeOpenViking()
|
openviking = FakeOpenViking()
|
||||||
everos = FakeEverOSVerbose()
|
everos = FakeEverOSVerbose()
|
||||||
|
|||||||
Reference in New Issue
Block a user