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:
BIN
tests/Prompt Engineer.png
Normal file
BIN
tests/Prompt Engineer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 548 KiB |
@ -13,7 +13,7 @@ class FakeStore:
|
||||
def get_user_key(self, user_id: str) -> str | None:
|
||||
return self.users.get(user_id)
|
||||
|
||||
def save_user_key(self, user_id: str, user_key: str) -> None:
|
||||
def save_user_key(self, user_id: str, user_key: str, account_id: str = "admin") -> None:
|
||||
self.users[user_id] = user_key
|
||||
|
||||
def get_account_key(self, account_id: str) -> str | None:
|
||||
@ -91,12 +91,24 @@ def test_openviking_accepts_matching_user_credentials():
|
||||
credential = client.credential_for_user("tom", "tom-key", agent_id="sess-1")
|
||||
|
||||
assert credential.api_key == "tom-key"
|
||||
assert credential.account_id == "admin"
|
||||
assert credential.account_id == "tom_account"
|
||||
assert credential.user_id == "tom"
|
||||
assert credential.agent_id == "sess-1"
|
||||
|
||||
|
||||
def test_openviking_create_user_initializes_admin_workspace_first():
|
||||
def test_openviking_client_uses_x_api_key_for_user_keys():
|
||||
client = OpenVikingMemorySystemClient(store=FakeStore())
|
||||
client.root_key = "root-key"
|
||||
|
||||
http_client = client._client("tom-key")
|
||||
try:
|
||||
assert http_client.headers["X-API-Key"] == "tom-key"
|
||||
assert "Authorization" not in http_client.headers
|
||||
finally:
|
||||
asyncio.run(http_client.aclose())
|
||||
|
||||
|
||||
def test_openviking_create_user_creates_isolated_admin_account():
|
||||
store = FakeStore()
|
||||
client = OpenVikingMemorySystemClient(store=store)
|
||||
client.root_key = "root-key"
|
||||
@ -104,11 +116,14 @@ def test_openviking_create_user_initializes_admin_workspace_first():
|
||||
responses = [
|
||||
FakeResponse(
|
||||
200,
|
||||
{"status": "ok", "result": {"account_id": "admin", "admin_user_id": "admin", "user_key": "admin-key"}},
|
||||
),
|
||||
FakeResponse(
|
||||
200,
|
||||
{"status": "ok", "result": {"account_id": "admin", "user_id": "userA", "user_key": "userA-key"}},
|
||||
{
|
||||
"status": "ok",
|
||||
"result": {
|
||||
"account_id": "userA_account",
|
||||
"admin_user_id": "userA",
|
||||
"user_key": "userA-key",
|
||||
},
|
||||
},
|
||||
),
|
||||
]
|
||||
client._client = lambda api_key, extra_headers=None: FakeAsyncClient( # type: ignore[method-assign]
|
||||
@ -120,28 +135,28 @@ def test_openviking_create_user_initializes_admin_workspace_first():
|
||||
|
||||
result = asyncio.run(client.create_user("userA"))
|
||||
|
||||
assert result == {"status": "ok", "result": {"account_id": "admin", "user_id": "userA", "user_key": "userA-key"}}
|
||||
assert store.accounts == {"admin": "admin-key"}
|
||||
assert store.users == {"admin": "admin-key", "userA": "userA-key"}
|
||||
assert result == {
|
||||
"status": "ok",
|
||||
"result": {
|
||||
"account_id": "userA_account",
|
||||
"admin_user_id": "userA",
|
||||
"user_key": "userA-key",
|
||||
},
|
||||
}
|
||||
assert store.accounts == {"userA_account": "userA-key"}
|
||||
assert store.users == {"userA": "userA-key"}
|
||||
assert calls == [
|
||||
(
|
||||
"post",
|
||||
"root-key",
|
||||
{},
|
||||
"/api/v1/admin/accounts",
|
||||
{"account_id": "admin", "admin_user_id": "admin"},
|
||||
),
|
||||
(
|
||||
"post",
|
||||
"root-key",
|
||||
{},
|
||||
"/api/v1/admin/accounts/admin/users",
|
||||
{"user_id": "userA", "role": "user"},
|
||||
{"account_id": "userA_account", "admin_user_id": "userA"},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def test_openviking_create_user_reuses_existing_admin_workspace():
|
||||
def test_openviking_create_user_creates_account_even_when_admin_workspace_exists():
|
||||
store = FakeStore()
|
||||
store.save_account_key("admin", "admin", "admin-key")
|
||||
client = OpenVikingMemorySystemClient(store=store)
|
||||
@ -150,7 +165,14 @@ def test_openviking_create_user_reuses_existing_admin_workspace():
|
||||
responses = [
|
||||
FakeResponse(
|
||||
200,
|
||||
{"status": "ok", "result": {"account_id": "admin", "user_id": "userB", "user_key": "userB-key"}},
|
||||
{
|
||||
"status": "ok",
|
||||
"result": {
|
||||
"account_id": "userB_account",
|
||||
"admin_user_id": "userB",
|
||||
"user_key": "userB-key",
|
||||
},
|
||||
},
|
||||
)
|
||||
]
|
||||
client._client = lambda api_key, extra_headers=None: FakeAsyncClient( # type: ignore[method-assign]
|
||||
@ -162,15 +184,23 @@ def test_openviking_create_user_reuses_existing_admin_workspace():
|
||||
|
||||
result = asyncio.run(client.create_user("userB"))
|
||||
|
||||
assert result == {"status": "ok", "result": {"account_id": "admin", "user_id": "userB", "user_key": "userB-key"}}
|
||||
assert result == {
|
||||
"status": "ok",
|
||||
"result": {
|
||||
"account_id": "userB_account",
|
||||
"admin_user_id": "userB",
|
||||
"user_key": "userB-key",
|
||||
},
|
||||
}
|
||||
assert store.accounts == {"admin": "admin-key", "userB_account": "userB-key"}
|
||||
assert store.users == {"userB": "userB-key"}
|
||||
assert calls == [
|
||||
(
|
||||
"post",
|
||||
"root-key",
|
||||
{},
|
||||
"/api/v1/admin/accounts/admin/users",
|
||||
{"user_id": "userB", "role": "user"},
|
||||
"/api/v1/admin/accounts",
|
||||
{"account_id": "userB_account", "admin_user_id": "userB"},
|
||||
)
|
||||
]
|
||||
|
||||
@ -229,7 +259,7 @@ def test_openviking_find_uses_current_identity_memory_scope():
|
||||
]
|
||||
|
||||
|
||||
def test_openviking_search_uses_session_target_uri():
|
||||
def test_openviking_search_uses_fixed_user_memory_target_with_level_and_score_threshold():
|
||||
client = OpenVikingMemorySystemClient(store=FakeStore())
|
||||
calls = []
|
||||
responses = [FakeResponse(200, {"status": "ok", "result": {"memories": []}})]
|
||||
@ -241,7 +271,7 @@ def test_openviking_search_uses_session_target_uri():
|
||||
)
|
||||
credential = client.user_credential("tom-key", "tom", agent_id="sess-1")
|
||||
|
||||
result = asyncio.run(client.search(credential, "sess-1", "咖啡", 5))
|
||||
result = asyncio.run(client.search(credential, "咖啡", 5, level=3, score_threshold=0.7))
|
||||
|
||||
assert result == {"status": "ok", "result": {"memories": []}}
|
||||
assert calls == [
|
||||
@ -250,7 +280,83 @@ def test_openviking_search_uses_session_target_uri():
|
||||
"tom-key",
|
||||
{},
|
||||
"/api/v1/search/search",
|
||||
{"query": "咖啡", "limit": 5, "session_id": "sess-1", "target_uri": "viking://user/tom/sess-1"},
|
||||
{
|
||||
"query": "咖啡",
|
||||
"target_uri": "viking://user/memories",
|
||||
"limit": 5,
|
||||
"level": 3,
|
||||
"score_threshold": 0.7,
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_openviking_search_accepts_custom_target_uri():
|
||||
client = OpenVikingMemorySystemClient(store=FakeStore())
|
||||
calls = []
|
||||
responses = [FakeResponse(200, {"status": "ok", "result": {"memories": []}})]
|
||||
client._client = lambda api_key, extra_headers=None: FakeAsyncClient( # type: ignore[method-assign]
|
||||
calls,
|
||||
responses,
|
||||
api_key,
|
||||
extra_headers or {},
|
||||
)
|
||||
credential = client.user_credential("tom-key", "tom", agent_id="sess-1")
|
||||
|
||||
result = asyncio.run(client.search(
|
||||
credential,
|
||||
"咖啡",
|
||||
5,
|
||||
level=3,
|
||||
score_threshold=0.7,
|
||||
target_uri="viking://user/custom/memories",
|
||||
))
|
||||
|
||||
assert result == {"status": "ok", "result": {"memories": []}}
|
||||
assert calls == [
|
||||
(
|
||||
"post",
|
||||
"tom-key",
|
||||
{},
|
||||
"/api/v1/search/search",
|
||||
{
|
||||
"query": "咖啡",
|
||||
"target_uri": "viking://user/custom/memories",
|
||||
"limit": 5,
|
||||
"level": 3,
|
||||
"score_threshold": 0.7,
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_openviking_profile_search_uses_user_memory_target_and_level():
|
||||
client = OpenVikingMemorySystemClient(store=FakeStore())
|
||||
calls = []
|
||||
responses = [FakeResponse(200, {"status": "ok", "result": {"memories": []}})]
|
||||
client._client = lambda api_key, extra_headers=None: FakeAsyncClient( # type: ignore[method-assign]
|
||||
calls,
|
||||
responses,
|
||||
api_key,
|
||||
extra_headers or {},
|
||||
)
|
||||
credential = client.user_credential("tom-key", "tom")
|
||||
|
||||
result = asyncio.run(client.search_profile_memories(credential, "我想喝东西", 10, 2))
|
||||
|
||||
assert result == {"status": "ok", "result": {"memories": []}}
|
||||
assert calls == [
|
||||
(
|
||||
"post",
|
||||
"tom-key",
|
||||
{},
|
||||
"/api/v1/search/search",
|
||||
{
|
||||
"query": "我想喝东西",
|
||||
"limit": 10,
|
||||
"level": 2,
|
||||
"target_uri": "viking://user/memories",
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@ -14,6 +14,20 @@ def test_memory_system_server_exposes_routes():
|
||||
assert {"GET", "POST"} <= context_methods
|
||||
assert "/memory-system/search" in paths
|
||||
assert "/memory-system/users/{user_id}/profile" in paths
|
||||
task_methods = {
|
||||
method
|
||||
for route in app.routes
|
||||
if getattr(route, "path", "") == "/memory-system/openviking/tasks/{task_id}"
|
||||
for method in getattr(route, "methods", set())
|
||||
}
|
||||
profile_methods = {
|
||||
method
|
||||
for route in app.routes
|
||||
if getattr(route, "path", "") == "/memory-system/users/{user_id}/profile"
|
||||
for method in getattr(route, "methods", set())
|
||||
}
|
||||
assert {"GET", "POST"} <= task_methods
|
||||
assert {"GET", "POST"} <= profile_methods
|
||||
|
||||
|
||||
def test_memory_system_messages_does_not_require_account_key_header():
|
||||
|
||||
@ -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
|
||||
|
||||
BIN
tests/大语言模型应用.pdf
Normal file
BIN
tests/大语言模型应用.pdf
Normal file
Binary file not shown.
Reference in New Issue
Block a user