Refine memory system user-key flow and search output
This commit is contained in:
@ -9,8 +9,19 @@ class FakeOpenViking:
|
||||
self.fail_on_append = fail_on_append
|
||||
self.calls = []
|
||||
|
||||
async def ensure_user(self, user_id: str) -> str:
|
||||
self.calls.append(("ensure_user", user_id))
|
||||
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"}
|
||||
|
||||
def credential_for_user(
|
||||
self,
|
||||
user_id: str,
|
||||
user_key: str,
|
||||
agent_id: str | None = None,
|
||||
) -> str:
|
||||
self.calls.append(("credential_for_user", user_id, user_key, agent_id))
|
||||
if user_key != f"{user_id}-key":
|
||||
raise PermissionError("Invalid user key")
|
||||
return f"key-{user_id}"
|
||||
|
||||
async def ensure_session(self, user_key: str, session_id: str) -> dict:
|
||||
@ -23,8 +34,8 @@ class FakeOpenViking:
|
||||
raise RuntimeError("openviking append failed")
|
||||
return {"message_count": len([call for call in self.calls if call[0] == "append_message"])}
|
||||
|
||||
async def find(self, user_key: str, user_id: str, query: str, limit: int) -> dict:
|
||||
self.calls.append(("find", user_key, user_id, query, limit))
|
||||
async def find(self, user_key: str, query: str, limit: int) -> dict:
|
||||
self.calls.append(("find", user_key, query, limit))
|
||||
await asyncio.sleep(0.01)
|
||||
return {"items": [{"source": "openviking-find"}]}
|
||||
|
||||
@ -33,6 +44,10 @@ class FakeOpenViking:
|
||||
await asyncio.sleep(0.01)
|
||||
return {"items": [{"source": "openviking-search"}]}
|
||||
|
||||
async def commit_session(self, user_key: str, session_id: str) -> dict:
|
||||
self.calls.append(("commit_session", user_key, session_id))
|
||||
return {"status": "ok", "result": {"task_id": "task-1", "archive_uri": "archive-1"}}
|
||||
|
||||
|
||||
class FakeEverOS:
|
||||
def __init__(self, fail_on_append: bool = False):
|
||||
@ -50,6 +65,10 @@ class FakeEverOS:
|
||||
await asyncio.sleep(0.01)
|
||||
return {"items": [{"source": f"everos-{method}"}]}
|
||||
|
||||
async def flush(self, user_id: str, session_id: str) -> dict:
|
||||
self.calls.append(("flush", user_id, session_id))
|
||||
return {"status": "flushed"}
|
||||
|
||||
|
||||
class FakeEverOSWithVector(FakeEverOS):
|
||||
async def search(self, user_id: str, session_id: str | None, query: str, method: str, limit: int) -> dict:
|
||||
@ -70,6 +89,47 @@ class FakeEverOSWithVector(FakeEverOS):
|
||||
}
|
||||
|
||||
|
||||
class FakeEverOSVerbose(FakeEverOS):
|
||||
async def search(self, user_id: str, session_id: str | None, query: str, method: str, limit: int) -> dict:
|
||||
self.calls.append(("search", user_id, session_id, query, method, limit))
|
||||
return {
|
||||
"data": {
|
||||
"episodes": [
|
||||
{
|
||||
"id": "episode-1",
|
||||
"user_id": user_id,
|
||||
"session_id": session_id,
|
||||
"timestamp": "2026-05-22T07:50:51.750000Z",
|
||||
"summary": "userB 在对话中表示自己喜欢拿铁。",
|
||||
"subject": "UserB 表达对拿铁的喜好",
|
||||
"episode": "userB 在对话中表示自己喜欢拿铁。",
|
||||
"type": "Conversation",
|
||||
"parent_id": "parent-1",
|
||||
"score": 0.72,
|
||||
"atomic_facts": [],
|
||||
}
|
||||
],
|
||||
"profiles": [],
|
||||
"raw_messages": [],
|
||||
"query": {
|
||||
"text": query,
|
||||
"method": method,
|
||||
"filters_applied": {"user_id": user_id, "session_id": session_id},
|
||||
},
|
||||
"original_data": {
|
||||
"episodes": {
|
||||
"episode-1": {
|
||||
"id": "episode-1",
|
||||
"summary": "userB 在对话中表示自己喜欢拿铁。",
|
||||
"episode": "userB 在对话中表示自己喜欢拿铁。",
|
||||
"vector_model": "Qwen3-VL-Embedding-2B",
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_capture_includes_exception_type_when_message_is_empty():
|
||||
service = MemorySystemService(openviking=FakeOpenViking(), everos=FakeEverOS())
|
||||
|
||||
@ -85,20 +145,64 @@ def test_capture_includes_exception_type_when_message_is_empty():
|
||||
assert response.error == "EmptyError"
|
||||
|
||||
|
||||
def test_create_user_delegates_to_openviking_only():
|
||||
openviking = FakeOpenViking()
|
||||
everos = FakeEverOS()
|
||||
service = MemorySystemService(openviking=openviking, everos=everos)
|
||||
|
||||
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 openviking.calls == [("create_user", "alice")]
|
||||
assert everos.calls == []
|
||||
|
||||
|
||||
def test_search_removes_vectors_from_items_and_backend_results():
|
||||
service = MemorySystemService(openviking=FakeOpenViking(), everos=FakeEverOSWithVector())
|
||||
|
||||
response = asyncio.run(service.search(
|
||||
SearchRequest(user_id="tom", 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),
|
||||
))
|
||||
|
||||
assert response.items == [
|
||||
{"source_backend": "openviking", "source": "openviking-find"},
|
||||
{"source_backend": "everos", "id": "episode-1"},
|
||||
{"source_backend": "everos", "memory_type": "episode", "id": "episode-1"},
|
||||
]
|
||||
assert not _has_key(response.backends["everos"].result, "vector")
|
||||
|
||||
|
||||
def test_search_returns_compact_items_and_backend_diagnostics_without_duplicate_raw_payloads():
|
||||
service = MemorySystemService(openviking=FakeOpenViking(), everos=FakeEverOSVerbose())
|
||||
|
||||
response = asyncio.run(service.search(
|
||||
SearchRequest(user_id="tom", user_key="tom-key", session_id="sess-1", query="我喜欢喝什么?", use_llm=True),
|
||||
))
|
||||
|
||||
assert response.items == [
|
||||
{"source_backend": "openviking", "source": "openviking-search"},
|
||||
{
|
||||
"source_backend": "everos",
|
||||
"memory_type": "episode",
|
||||
"id": "episode-1",
|
||||
"user_id": "tom",
|
||||
"session_id": "sess-1",
|
||||
"timestamp": "2026-05-22T07:50:51.750000Z",
|
||||
"summary": "userB 在对话中表示自己喜欢拿铁。",
|
||||
"score": 0.72,
|
||||
},
|
||||
]
|
||||
assert response.backends["everos"].result == {
|
||||
"counts": {"episodes": 1, "profiles": 0, "raw_messages": 0},
|
||||
"query": {
|
||||
"text": "我喜欢喝什么?",
|
||||
"method": "agentic",
|
||||
"filters_applied": {"user_id": "tom", "session_id": "sess-1"},
|
||||
},
|
||||
}
|
||||
assert not _has_key(response.backends["everos"].result, "original_data")
|
||||
|
||||
|
||||
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())
|
||||
@ -115,6 +219,7 @@ def test_ingest_splits_user_and_assistant_messages():
|
||||
response = asyncio.run(service.ingest_messages(
|
||||
MessageIngestRequest(
|
||||
user_id="tom",
|
||||
user_key="tom-key",
|
||||
session_id="sess-1",
|
||||
user_message="我喜欢拿铁",
|
||||
assistant_message="我记住了",
|
||||
@ -124,7 +229,7 @@ def test_ingest_splits_user_and_assistant_messages():
|
||||
assert response.status == "success"
|
||||
assert response.message_count == 2
|
||||
assert openviking.calls == [
|
||||
("ensure_user", "tom"),
|
||||
("credential_for_user", "tom", "tom-key", "sess-1"),
|
||||
("ensure_session", "key-tom", "sess-1"),
|
||||
("append_message", "key-tom", "sess-1", "user", "我喜欢拿铁"),
|
||||
("append_message", "key-tom", "sess-1", "assistant", "我记住了"),
|
||||
@ -139,7 +244,11 @@ def test_ingest_requires_at_least_one_message():
|
||||
service = MemorySystemService(openviking=FakeOpenViking(), everos=FakeEverOS())
|
||||
|
||||
try:
|
||||
asyncio.run(service.ingest_messages(MessageIngestRequest(user_id="tom", session_id="sess-1")))
|
||||
asyncio.run(
|
||||
service.ingest_messages(
|
||||
MessageIngestRequest(user_id="tom", user_key="tom-key", session_id="sess-1"),
|
||||
)
|
||||
)
|
||||
except ValueError as exc:
|
||||
assert "at least one message" in str(exc)
|
||||
else:
|
||||
@ -150,7 +259,7 @@ def test_ingest_returns_partial_success_when_one_backend_fails():
|
||||
service = MemorySystemService(openviking=FakeOpenViking(fail_on_append=True), everos=FakeEverOS())
|
||||
|
||||
response = asyncio.run(service.ingest_messages(
|
||||
MessageIngestRequest(user_id="tom", session_id="sess-1", user_message="hello")
|
||||
MessageIngestRequest(user_id="tom", user_key="tom-key", session_id="sess-1", user_message="hello"),
|
||||
))
|
||||
|
||||
assert response.status == "partial_success"
|
||||
@ -158,13 +267,28 @@ def test_ingest_returns_partial_success_when_one_backend_fails():
|
||||
assert response.backends["everos"].status == "success"
|
||||
|
||||
|
||||
def test_commit_uses_user_key_without_account_id():
|
||||
openviking = FakeOpenViking()
|
||||
everos = FakeEverOS()
|
||||
service = MemorySystemService(openviking=openviking, everos=everos)
|
||||
|
||||
response = asyncio.run(service.commit_session("tom", "tom-key", "sess-1"))
|
||||
|
||||
assert response.status == "success"
|
||||
assert openviking.calls == [
|
||||
("credential_for_user", "tom", "tom-key", "sess-1"),
|
||||
("commit_session", "key-tom", "sess-1"),
|
||||
]
|
||||
assert everos.calls == [("flush", "tom", "sess-1")]
|
||||
|
||||
|
||||
def test_search_uses_find_and_hybrid_without_llm():
|
||||
openviking = FakeOpenViking()
|
||||
everos = FakeEverOS()
|
||||
service = MemorySystemService(openviking=openviking, everos=everos)
|
||||
|
||||
response = asyncio.run(service.search(
|
||||
SearchRequest(user_id="tom", 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),
|
||||
))
|
||||
|
||||
assert response.status == "success"
|
||||
@ -172,7 +296,8 @@ def test_search_uses_find_and_hybrid_without_llm():
|
||||
{"source_backend": "openviking", "source": "openviking-find"},
|
||||
{"source_backend": "everos", "source": "everos-hybrid"},
|
||||
]
|
||||
assert ("find", "key-tom", "tom", "咖啡偏好", 5) in openviking.calls
|
||||
assert ("credential_for_user", "tom", "tom-key", "sess-1") in openviking.calls
|
||||
assert ("find", "key-tom", "咖啡偏好", 5) in openviking.calls
|
||||
assert ("search", "tom", "sess-1", "咖啡偏好", "hybrid", 5) in everos.calls
|
||||
|
||||
|
||||
@ -182,7 +307,7 @@ def test_search_uses_search_and_agentic_with_llm():
|
||||
service = MemorySystemService(openviking=openviking, everos=everos)
|
||||
|
||||
response = asyncio.run(service.search(
|
||||
SearchRequest(user_id="tom", session_id="sess-1", query="咖啡偏好", use_llm=True, limit=5)
|
||||
SearchRequest(user_id="tom", user_key="tom-key", session_id="sess-1", query="咖啡偏好", use_llm=True, limit=5),
|
||||
))
|
||||
|
||||
assert response.status == "success"
|
||||
@ -190,5 +315,6 @@ def test_search_uses_search_and_agentic_with_llm():
|
||||
{"source_backend": "openviking", "source": "openviking-search"},
|
||||
{"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", "tom", "sess-1", "咖啡偏好", "agentic", 5) in everos.calls
|
||||
|
||||
Reference in New Issue
Block a user