Refactor OpenViking Memory API and User Management

- Updated API authentication headers to use `X-API-Key` for both admin and user APIs.
- Modified the account creation process to directly create user-specific accounts without requiring an admin workspace.
- Enhanced user creation to return account-specific details, including `admin_user_id`.
- Introduced new endpoints for retrieving task status and user profiles, allowing for more flexible user data management.
- Updated search functionality to include additional parameters such as `level` and `score_threshold`.
- Improved the handling of user keys in the storage layer to associate them with specific accounts.
- Added tests to validate the new user account creation process and search functionalities, ensuring proper integration with the OpenViking service.
- Included new documentation to reflect changes in API usage and expected request/response formats.
This commit is contained in:
2026-05-27 16:09:28 +08:00
parent a89807b174
commit 70cda923b2
13 changed files with 543 additions and 165 deletions

View File

@ -11,7 +11,7 @@ class FakeOpenViking:
async def create_user(self, user_id: str) -> dict:
self.calls.append(("create_user", user_id))
return {"account_id": "admin", "user_id": user_id, "user_key": f"{user_id}-key"}
return {"account_id": f"{user_id}_account", "admin_user_id": user_id, "user_key": f"{user_id}-key"}
def credential_for_user(
self,
@ -39,11 +39,39 @@ class FakeOpenViking:
await asyncio.sleep(0.01)
return {"items": [{"source": "openviking-find"}]}
async def search(self, user_key: str, session_id: str | None, query: str, limit: int) -> dict:
self.calls.append(("search", user_key, session_id, query, limit))
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))
await asyncio.sleep(0.01)
return {"items": [{"source": "openviking-search"}]}
async def search_profile_memories(self, user_key: str, query: str, limit: int, level: int) -> dict:
self.calls.append(("search_profile_memories", user_key, query, limit, level))
return {
"status": "ok",
"result": {
"memories": [
{
"context_type": "memory",
"uri": "viking://user/tom/memories/preferences/coffee.md",
"level": 2,
"score": 0.91,
"abstract": "用户喜欢喝咖啡。",
}
],
"resources": [],
"skills": [],
"total": 1,
},
}
async def get_session_context(self, user_key: str, session_id: str) -> dict:
self.calls.append(("get_session_context", user_key, session_id))
return {
@ -82,6 +110,25 @@ class FakeEverOS:
self.calls.append(("flush", user_id, session_id))
return {"status": "flushed"}
async def get_profile(self, user_id: str) -> dict:
self.calls.append(("get_profile", user_id))
return {
"data": {
"episodes": [],
"profiles": [
{
"id": "profile-1",
"user_id": user_id,
"profile_data": {"summary": "喜欢咖啡"},
}
],
"agent_cases": [],
"agent_skills": [],
"total_count": 1,
"count": 1,
}
}
class FakeEverOSWithVector(FakeEverOS):
async def search(self, user_id: str, session_id: str | None, query: str, method: str, limit: int) -> dict:
@ -166,7 +213,7 @@ def test_create_user_delegates_to_openviking_only():
response = asyncio.run(service.create_user("alice"))
assert response.status == "success"
assert response.account == {"account_id": "admin", "user_id": "alice", "user_key": "alice-key"}
assert response.account == {"account_id": "alice_account", "admin_user_id": "alice", "user_key": "alice-key"}
assert openviking.calls == [("create_user", "alice")]
assert everos.calls == []
@ -179,7 +226,7 @@ def test_search_removes_vectors_from_items_and_backend_results():
))
assert response.items == [
{"source_backend": "openviking", "source": "openviking-find"},
{"source_backend": "openviking", "source": "openviking-search"},
{"source_backend": "everos", "memory_type": "episode", "id": "episode-1"},
]
assert not _has_key(response.backends["everos"].result, "vector")
@ -253,6 +300,56 @@ def test_session_context_combines_openviking_context_and_everos_search_items():
assert ("search", "tom", "sess-1", "我喜欢喝什么?", "hybrid", 5) in everos.calls
def test_profile_combines_everos_profile_and_openviking_memory_search():
openviking = FakeOpenViking()
everos = FakeEverOS()
service = MemorySystemService(openviking=openviking, everos=everos)
response = asyncio.run(service.get_profile("tom", "tom-key", query="我想喝东西", limit=10, level=2))
assert response.status == "success"
assert response.profile == {
"data": {
"episodes": [],
"profiles": [
{
"id": "profile-1",
"user_id": "tom",
"profile_data": {"summary": "喜欢咖啡"},
}
],
"agent_cases": [],
"agent_skills": [],
"total_count": 1,
"count": 1,
}
}
assert response.items == [
{
"source_backend": "openviking",
"context_type": "memory",
"uri": "viking://user/tom/memories/preferences/coffee.md",
"level": 2,
"score": 0.91,
"abstract": "用户喜欢喝咖啡。",
}
]
assert response.backends["everos"].result == {
"total_count": 1,
"count": 1,
"counts": {
"episodes": 0,
"profiles": 1,
"agent_cases": 0,
"agent_skills": 0,
},
}
assert "profiles" not in response.backends["everos"].result
assert ("credential_for_user", "tom", "tom-key", None) in openviking.calls
assert ("search_profile_memories", "key-tom", "我想喝东西", 10, 2) in openviking.calls
assert everos.calls == [("get_profile", "tom")]
def _has_key(value, key: str) -> bool:
if isinstance(value, dict):
return key in value or any(_has_key(item, key) for item in value.values())
@ -332,22 +429,32 @@ def test_commit_uses_user_key_without_account_id():
assert everos.calls == [("flush", "tom", "sess-1")]
def test_search_uses_find_and_hybrid_without_llm():
def test_search_uses_openviking_search_and_hybrid_without_llm():
openviking = FakeOpenViking()
everos = FakeEverOS()
service = MemorySystemService(openviking=openviking, everos=everos)
response = asyncio.run(service.search(
SearchRequest(user_id="tom", user_key="tom-key", session_id="sess-1", query="咖啡偏好", use_llm=False, limit=5),
SearchRequest(
user_id="tom",
user_key="tom-key",
session_id="sess-1",
query="咖啡偏好",
use_llm=False,
limit=5,
level=3,
score_threshold=0.7,
target_uri="viking://user/custom/memories",
),
))
assert response.status == "success"
assert response.items == [
{"source_backend": "openviking", "source": "openviking-find"},
{"source_backend": "openviking", "source": "openviking-search"},
{"source_backend": "everos", "source": "everos-hybrid"},
]
assert ("credential_for_user", "tom", "tom-key", "sess-1") in openviking.calls
assert ("find", "key-tom", "咖啡偏好", 5) in openviking.calls
assert ("search", "key-tom", "咖啡偏好", 5, 3, 0.7, "viking://user/custom/memories") in openviking.calls
assert ("search", "tom", "sess-1", "咖啡偏好", "hybrid", 5) in everos.calls
@ -366,5 +473,5 @@ def test_search_uses_search_and_agentic_with_llm():
{"source_backend": "everos", "source": "everos-agentic"},
]
assert ("credential_for_user", "tom", "tom-key", "sess-1") in openviking.calls
assert ("search", "key-tom", "sess-1", "咖啡偏好", 5) in openviking.calls
assert ("search", "key-tom", "咖啡偏好", 5, 2, 0.8, "viking://user/memories") in openviking.calls
assert ("search", "tom", "sess-1", "咖啡偏好", "agentic", 5) in everos.calls