756 lines
27 KiB
Python
756 lines
27 KiB
Python
import asyncio
|
|
|
|
from memory_system_api.schemas import (
|
|
MemoryWriteRequest,
|
|
MessageIngestRequest,
|
|
ResourceUploadRequest,
|
|
SearchRequest,
|
|
SessionContextRequest,
|
|
)
|
|
from memory_system_api.service import MemorySystemService
|
|
|
|
|
|
class FakeOpenViking:
|
|
def __init__(self, fail_on_append: bool = False):
|
|
self.fail_on_append = fail_on_append
|
|
self.calls = []
|
|
|
|
async def create_user(self, user_id: str) -> dict:
|
|
self.calls.append(("create_user", user_id))
|
|
return {"account_id": f"{user_id}_account", "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:
|
|
self.calls.append(("ensure_session", user_key, session_id))
|
|
return {"session_id": session_id}
|
|
|
|
async def append_message(self, user_key: str, session_id: str, role: str, content: str) -> dict:
|
|
self.calls.append(("append_message", user_key, session_id, role, content))
|
|
if self.fail_on_append:
|
|
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, query: str, limit: int) -> dict:
|
|
self.calls.append(("find", user_key, query, limit))
|
|
await asyncio.sleep(0.01)
|
|
return {"items": [{"source": "openviking-find"}]}
|
|
|
|
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 {
|
|
"status": "ok",
|
|
"result": {
|
|
"latest_archive_overview": "# Working Memory\nUser likes coffee.",
|
|
"pre_archive_abstracts": [],
|
|
"messages": [],
|
|
"estimatedTokens": 42,
|
|
"stats": {"totalArchives": 1},
|
|
},
|
|
}
|
|
|
|
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"}}
|
|
|
|
async def upload_temp_file(self, user_key: str, path) -> dict:
|
|
self.calls.append(("upload_temp_file", user_key, str(path)))
|
|
return {"status": "ok", "result": {"temp_file_id": "upload_report.pdf"}}
|
|
|
|
async def add_resource(
|
|
self,
|
|
user_key: str,
|
|
*,
|
|
to: str,
|
|
reason: str | None,
|
|
wait: bool,
|
|
directly_upload_media: bool,
|
|
path: str | None = None,
|
|
temp_file_id: str | None = None,
|
|
) -> dict:
|
|
self.calls.append((
|
|
"add_resource",
|
|
user_key,
|
|
path,
|
|
temp_file_id,
|
|
to,
|
|
reason,
|
|
wait,
|
|
directly_upload_media,
|
|
))
|
|
return {"status": "ok", "result": {"uri": to}}
|
|
|
|
async def delete_resource(self, user_key: str, uri: str, recursive: bool = True) -> dict:
|
|
self.calls.append(("delete_resource", user_key, uri, recursive))
|
|
return {"status": "ok", "result": {"uri": uri, "estimated_deleted_count": 4}}
|
|
|
|
async def list_memories(self, user_key: str, uri: str, recursive: bool = True) -> dict:
|
|
self.calls.append(("list_memories", user_key, uri, recursive))
|
|
return {"status": "ok", "result": {"children": [{"uri": "viking://user/memories/profile.md"}]}}
|
|
|
|
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": "# Profile"}}
|
|
|
|
async def write_memory(self, user_key: str, uri: str, content: str, mode: str, wait: bool = True) -> dict:
|
|
self.calls.append(("write_memory", user_key, uri, content, mode, wait))
|
|
return {"status": "ok", "result": {"uri": uri, "mode": mode}}
|
|
|
|
async def delete_memory(self, user_key: str, uri: str, recursive: bool = False) -> dict:
|
|
self.calls.append(("delete_memory", user_key, uri, recursive))
|
|
return {"status": "ok", "result": {"uri": uri, "estimated_deleted_count": 1}}
|
|
|
|
|
|
class FakeEverOS:
|
|
def __init__(self, fail_on_append: bool = False):
|
|
self.fail_on_append = fail_on_append
|
|
self.calls = []
|
|
|
|
async def append_message(self, user_id: str, session_id: str, role: str, content: str) -> dict:
|
|
self.calls.append(("append_message", user_id, session_id, role, content))
|
|
if self.fail_on_append:
|
|
raise RuntimeError("everos append failed")
|
|
return {"status": "accumulated"}
|
|
|
|
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))
|
|
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"}
|
|
|
|
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:
|
|
self.calls.append(("search", user_id, session_id, query, method, limit))
|
|
return {
|
|
"data": {
|
|
"episodes": [{"id": "episode-1", "vector": [0.1, 0.2]}],
|
|
"original_data": {
|
|
"episodes": {
|
|
"episode-1": {
|
|
"summary": "喜欢拿铁",
|
|
"vector": [0.1, 0.2],
|
|
"nested": {"vector": [0.3]},
|
|
}
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
|
|
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())
|
|
|
|
class EmptyError(Exception):
|
|
pass
|
|
|
|
async def fail():
|
|
raise EmptyError()
|
|
|
|
response = asyncio.run(service._capture(fail))
|
|
|
|
assert response.status == "failed"
|
|
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": "alice_account", "admin_user_id": "alice", "user_key": "alice-key"}
|
|
assert openviking.calls == [("create_user", "alice")]
|
|
assert everos.calls == []
|
|
|
|
|
|
def test_upload_resource_with_url_delegates_directly_to_openviking_add_resource():
|
|
openviking = FakeOpenViking()
|
|
service = MemorySystemService(openviking=openviking, everos=FakeEverOS())
|
|
|
|
response = asyncio.run(service.upload_resource(ResourceUploadRequest(
|
|
user_id="tom",
|
|
user_key="tom-key",
|
|
path="https://example.com/images/photo.png",
|
|
to="viking://resources/tom/images/photo.png",
|
|
reason="上传远程图片",
|
|
)))
|
|
|
|
assert response.status == "success"
|
|
assert response.resource == {"status": "ok", "result": {"uri": "viking://resources/tom/images/photo.png"}}
|
|
assert openviking.calls == [
|
|
("credential_for_user", "tom", "tom-key", None),
|
|
(
|
|
"add_resource",
|
|
"key-tom",
|
|
"https://example.com/images/photo.png",
|
|
None,
|
|
"viking://resources/tom/images/photo.png",
|
|
"上传远程图片",
|
|
True,
|
|
True,
|
|
),
|
|
]
|
|
|
|
|
|
def test_upload_resource_with_local_path_uploads_temp_file_first(tmp_path):
|
|
path = tmp_path / "report.pdf"
|
|
path.write_bytes(b"pdf-bytes")
|
|
openviking = FakeOpenViking()
|
|
service = MemorySystemService(openviking=openviking, everos=FakeEverOS())
|
|
|
|
response = asyncio.run(service.upload_resource(ResourceUploadRequest(
|
|
user_id="tom",
|
|
user_key="tom-key",
|
|
path=str(path),
|
|
to="viking://resources/tom/files/report.pdf",
|
|
reason="上传本地文件",
|
|
)))
|
|
|
|
assert response.status == "success"
|
|
assert response.resource == {"status": "ok", "result": {"uri": "viking://resources/tom/files/report.pdf"}}
|
|
assert openviking.calls == [
|
|
("credential_for_user", "tom", "tom-key", None),
|
|
("upload_temp_file", "key-tom", str(path)),
|
|
(
|
|
"add_resource",
|
|
"key-tom",
|
|
None,
|
|
"upload_report.pdf",
|
|
"viking://resources/tom/files/report.pdf",
|
|
"上传本地文件",
|
|
True,
|
|
True,
|
|
),
|
|
]
|
|
|
|
|
|
def test_delete_resource_delegates_to_openviking_only():
|
|
openviking = FakeOpenViking()
|
|
everos = FakeEverOS()
|
|
service = MemorySystemService(openviking=openviking, everos=everos)
|
|
|
|
response = asyncio.run(service.delete_resource(
|
|
user_id="tom",
|
|
user_key="tom-key",
|
|
uri="viking://resources/tom/files/report.pdf",
|
|
recursive=True,
|
|
))
|
|
|
|
assert response.status == "success"
|
|
assert response.resource == {
|
|
"status": "ok",
|
|
"result": {"uri": "viking://resources/tom/files/report.pdf", "estimated_deleted_count": 4},
|
|
}
|
|
assert openviking.calls == [
|
|
("credential_for_user", "tom", "tom-key", None),
|
|
("delete_resource", "key-tom", "viking://resources/tom/files/report.pdf", True),
|
|
]
|
|
assert everos.calls == []
|
|
|
|
|
|
def test_list_memories_delegates_to_openviking_only():
|
|
openviking = FakeOpenViking()
|
|
everos = FakeEverOS()
|
|
service = MemorySystemService(openviking=openviking, everos=everos)
|
|
|
|
response = asyncio.run(service.list_memories(
|
|
user_id="tom",
|
|
user_key="tom-key",
|
|
uri="viking://user/memories",
|
|
recursive=True,
|
|
))
|
|
|
|
assert response.status == "success"
|
|
assert response.memory == {"status": "ok", "result": {"children": [{"uri": "viking://user/memories/profile.md"}]}}
|
|
assert openviking.calls == [
|
|
("credential_for_user", "tom", "tom-key", None),
|
|
("list_memories", "key-tom", "viking://user/memories", True),
|
|
]
|
|
assert everos.calls == []
|
|
|
|
|
|
def test_read_memory_delegates_to_openviking_only():
|
|
openviking = FakeOpenViking()
|
|
everos = FakeEverOS()
|
|
service = MemorySystemService(openviking=openviking, everos=everos)
|
|
|
|
response = asyncio.run(service.read_memory(
|
|
user_id="tom",
|
|
user_key="tom-key",
|
|
uri="viking://user/memories/profile.md",
|
|
))
|
|
|
|
assert response.status == "success"
|
|
assert response.memory == {"status": "ok", "result": {"uri": "viking://user/memories/profile.md", "content": "# Profile"}}
|
|
assert openviking.calls == [
|
|
("credential_for_user", "tom", "tom-key", None),
|
|
("read_memory", "key-tom", "viking://user/memories/profile.md"),
|
|
]
|
|
assert everos.calls == []
|
|
|
|
|
|
def test_write_memory_delegates_to_openviking_content_write_only():
|
|
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/memories/profile.md",
|
|
content="# Profile\n\nLikes Python.",
|
|
mode="replace",
|
|
wait=True,
|
|
)))
|
|
|
|
assert response.status == "success"
|
|
assert response.memory == {"status": "ok", "result": {"uri": "viking://user/memories/profile.md", "mode": "replace"}}
|
|
assert openviking.calls == [
|
|
("credential_for_user", "tom", "tom-key", None),
|
|
("write_memory", "key-tom", "viking://user/memories/profile.md", "# Profile\n\nLikes Python.", "replace", True),
|
|
]
|
|
assert everos.calls == []
|
|
|
|
|
|
def test_delete_memory_delegates_to_openviking_only_and_defaults_non_recursive():
|
|
openviking = FakeOpenViking()
|
|
everos = FakeEverOS()
|
|
service = MemorySystemService(openviking=openviking, everos=everos)
|
|
|
|
response = asyncio.run(service.delete_memory(
|
|
user_id="tom",
|
|
user_key="tom-key",
|
|
uri="viking://user/memories/preferences/python.md",
|
|
))
|
|
|
|
assert response.status == "success"
|
|
assert response.memory == {
|
|
"status": "ok",
|
|
"result": {"uri": "viking://user/memories/preferences/python.md", "estimated_deleted_count": 1},
|
|
}
|
|
assert openviking.calls == [
|
|
("credential_for_user", "tom", "tom-key", None),
|
|
("delete_memory", "key-tom", "viking://user/memories/preferences/python.md", False),
|
|
]
|
|
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", user_key="tom-key", session_id="sess-1", query="咖啡偏好", use_llm=False, limit=5),
|
|
))
|
|
|
|
assert response.items == [
|
|
{"source_backend": "openviking", "source": "openviking-search"},
|
|
{"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 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()
|
|
service = MemorySystemService(openviking=openviking, everos=everos)
|
|
|
|
response = asyncio.run(
|
|
service.get_session_context(
|
|
"sess-1",
|
|
SessionContextRequest(user_id="tom", user_key="tom-key", query="我喜欢喝什么?", limit=5),
|
|
)
|
|
)
|
|
|
|
assert response.status == "success"
|
|
assert response.context == {
|
|
"latest_archive_overview": "# Working Memory\nUser likes coffee.",
|
|
"pre_archive_abstracts": [],
|
|
"messages": [],
|
|
"estimatedTokens": 42,
|
|
"stats": {"totalArchives": 1},
|
|
}
|
|
assert response.items == [
|
|
{
|
|
"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 ("credential_for_user", "tom", "tom-key", "sess-1") in openviking.calls
|
|
assert ("get_session_context", "key-tom", "sess-1") in openviking.calls
|
|
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())
|
|
if isinstance(value, list):
|
|
return any(_has_key(item, key) for item in value)
|
|
return False
|
|
|
|
|
|
def test_ingest_splits_user_and_assistant_messages():
|
|
openviking = FakeOpenViking()
|
|
everos = FakeEverOS()
|
|
service = MemorySystemService(openviking=openviking, everos=everos)
|
|
|
|
response = asyncio.run(service.ingest_messages(
|
|
MessageIngestRequest(
|
|
user_id="tom",
|
|
user_key="tom-key",
|
|
session_id="sess-1",
|
|
user_message="我喜欢拿铁",
|
|
assistant_message="我记住了",
|
|
)
|
|
))
|
|
|
|
assert response.status == "success"
|
|
assert response.message_count == 2
|
|
assert openviking.calls == [
|
|
("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", "我记住了"),
|
|
]
|
|
assert everos.calls == [
|
|
("append_message", "tom", "sess-1", "user", "我喜欢拿铁"),
|
|
("append_message", "tom", "sess-1", "assistant", "我记住了"),
|
|
]
|
|
|
|
|
|
def test_ingest_requires_at_least_one_message():
|
|
service = MemorySystemService(openviking=FakeOpenViking(), everos=FakeEverOS())
|
|
|
|
try:
|
|
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:
|
|
raise AssertionError("expected ValueError")
|
|
|
|
|
|
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", user_key="tom-key", session_id="sess-1", user_message="hello"),
|
|
))
|
|
|
|
assert response.status == "partial_success"
|
|
assert response.backends["openviking"].status == "failed"
|
|
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_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,
|
|
level=3,
|
|
score_threshold=0.7,
|
|
target_uri="viking://user/custom/memories",
|
|
),
|
|
))
|
|
|
|
assert response.status == "success"
|
|
assert response.items == [
|
|
{"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 ("search", "key-tom", "咖啡偏好", 5, 3, 0.7, "viking://user/custom/memories") in openviking.calls
|
|
assert ("search", "tom", "sess-1", "咖啡偏好", "hybrid", 5) in everos.calls
|
|
|
|
|
|
def test_search_uses_search_and_agentic_with_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=True, limit=5),
|
|
))
|
|
|
|
assert response.status == "success"
|
|
assert response.items == [
|
|
{"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", "咖啡偏好", 5, 2, 0.8, "viking://user/memories") in openviking.calls
|
|
assert ("search", "tom", "sess-1", "咖啡偏好", "agentic", 5) in everos.calls
|