473 lines
14 KiB
Python
473 lines
14 KiB
Python
"""
|
||
Agent Memory - 模型测试
|
||
|
||
测试数据模型的正确性
|
||
运行方式:
|
||
pytest tests/test_models.py -v
|
||
"""
|
||
|
||
import pytest
|
||
from datetime import datetime
|
||
from typing import Dict, Any
|
||
|
||
from hy_memory.models import (
|
||
# 枚举
|
||
MemoryLayer,
|
||
MemoryInputType,
|
||
AgentProcessMode,
|
||
TaskStatus,
|
||
DeleteScope,
|
||
# 数据类
|
||
QAPair,
|
||
AddRequest,
|
||
AddResponse,
|
||
AsyncAddResponse,
|
||
RecallRequest,
|
||
RecallResponse,
|
||
UpdateRequest,
|
||
DeleteRequest,
|
||
BatchDeleteRequest,
|
||
MemoryEntry,
|
||
MemoryMetadata,
|
||
)
|
||
|
||
|
||
# ==================== 枚举测试 ====================
|
||
|
||
class TestEnums:
|
||
"""枚举测试"""
|
||
|
||
def test_memory_layer_values(self):
|
||
"""测试记忆层枚举值 (V3 七层模型 + V1/V2 兼容别名)"""
|
||
# V3 原始层
|
||
assert MemoryLayer.L4_IDENTITY.value == "l4_identity"
|
||
assert MemoryLayer.L2_FACT.value == "l2_fact"
|
||
assert MemoryLayer.L3_SUMMARY.value == "l3_summary"
|
||
assert MemoryLayer.L5_KNOWLEDGE.value == "l5_knowledge"
|
||
assert MemoryLayer.L6_SCHEMA.value == "l6_schema"
|
||
assert MemoryLayer.L1_RAW.value == "l1_raw"
|
||
# V2 兼容别名(旧枚举名指向新值)
|
||
assert MemoryLayer.L6_IDENTITY.value == "l4_identity"
|
||
assert MemoryLayer.L4_KNOWLEDGE.value == "l5_knowledge"
|
||
assert MemoryLayer.L5_SCHEMA.value == "l6_schema"
|
||
# V1 兼容别名(值与 V3 对应层相同)
|
||
assert MemoryLayer.PROFILE.value == "l4_identity"
|
||
assert MemoryLayer.DIALOGUE.value == "l2_fact"
|
||
assert MemoryLayer.SUMMARY.value == "l3_summary"
|
||
assert MemoryLayer.KNOWLEDGE.value == "l5_knowledge"
|
||
assert MemoryLayer.RAW.value == "l1_raw"
|
||
|
||
def test_memory_layer_from_string(self):
|
||
"""测试从字符串创建记忆层"""
|
||
assert MemoryLayer.from_string("profile") == MemoryLayer.L4_IDENTITY
|
||
assert MemoryLayer.from_string("PROFILE") == MemoryLayer.L4_IDENTITY
|
||
assert MemoryLayer.from_string("Profile") == MemoryLayer.L4_IDENTITY
|
||
# V2 旧值兼容
|
||
assert MemoryLayer.from_string("l6_identity") == MemoryLayer.L4_IDENTITY
|
||
assert MemoryLayer.from_string("l5_schema") == MemoryLayer.L6_SCHEMA
|
||
assert MemoryLayer.from_string("l4_knowledge") == MemoryLayer.L5_KNOWLEDGE
|
||
assert MemoryLayer.from_string("l1_raw") == MemoryLayer.L1_RAW
|
||
|
||
def test_memory_layer_invalid_string(self):
|
||
"""测试无效字符串创建记忆层"""
|
||
with pytest.raises(ValueError):
|
||
MemoryLayer.from_string("invalid_layer")
|
||
|
||
def test_memory_input_type_values(self):
|
||
"""测试输入类型枚举值"""
|
||
assert MemoryInputType.TEXT.value == "text"
|
||
assert MemoryInputType.QA.value == "qa"
|
||
assert MemoryInputType.DIALOGUE.value == "dialogue"
|
||
|
||
def test_agent_process_mode_values(self):
|
||
"""测试 Agent 处理模式枚举值"""
|
||
assert AgentProcessMode.DISABLED.value == "disabled"
|
||
assert AgentProcessMode.FULL.value == "full"
|
||
|
||
def test_task_status_values(self):
|
||
"""测试任务状态枚举值"""
|
||
assert TaskStatus.PENDING.value == "pending"
|
||
assert TaskStatus.PROCESSING.value == "processing"
|
||
assert TaskStatus.COMPLETED.value == "completed"
|
||
assert TaskStatus.FAILED.value == "failed"
|
||
|
||
def test_delete_scope_values(self):
|
||
"""测试删除范围枚举值"""
|
||
assert DeleteScope.MEMORY.value == "memory"
|
||
assert DeleteScope.SCENE.value == "scene"
|
||
assert DeleteScope.USER.value == "user"
|
||
assert DeleteScope.APP.value == "app"
|
||
|
||
|
||
# ==================== QAPair 测试 ====================
|
||
|
||
class TestQAPair:
|
||
"""QAPair 测试"""
|
||
|
||
def test_qa_pair_creation(self):
|
||
"""测试创建 QAPair"""
|
||
qa = QAPair(
|
||
question="你叫什么名字?",
|
||
answer="我叫小明"
|
||
)
|
||
|
||
assert qa.question == "你叫什么名字?"
|
||
assert qa.answer == "我叫小明"
|
||
|
||
def test_qa_pair_to_dict(self):
|
||
"""测试 QAPair 转字典"""
|
||
qa = QAPair(question="Q1", answer="A1")
|
||
d = qa.to_dict()
|
||
|
||
assert d == {"question": "Q1", "answer": "A1"}
|
||
|
||
def test_qa_pair_from_dict(self):
|
||
"""测试从字典创建 QAPair"""
|
||
d = {"question": "Q1", "answer": "A1"}
|
||
qa = QAPair.from_dict(d)
|
||
|
||
assert qa.question == "Q1"
|
||
assert qa.answer == "A1"
|
||
|
||
def test_qa_pair_to_content(self):
|
||
"""测试 QAPair 转内容字符串"""
|
||
qa = QAPair(question="你好吗?", answer="我很好")
|
||
content = qa.to_content()
|
||
|
||
assert "Q:" in content
|
||
assert "A:" in content
|
||
assert "你好吗?" in content
|
||
assert "我很好" in content
|
||
|
||
|
||
# ==================== AddRequest 测试 ====================
|
||
|
||
class TestAddRequest:
|
||
"""AddRequest 测试"""
|
||
|
||
def test_add_request_text_basic(self):
|
||
"""测试基本文本请求"""
|
||
request = AddRequest(
|
||
uid="user1",
|
||
agent_id="scene1",
|
||
content="测试内容"
|
||
)
|
||
|
||
assert request.uid == "user1"
|
||
assert request.agent_id == "scene1"
|
||
assert request.content == "测试内容"
|
||
assert request.input_type == MemoryInputType.TEXT
|
||
|
||
def test_add_request_qa(self):
|
||
"""测试 QA 请求"""
|
||
request = AddRequest(
|
||
uid="user1",
|
||
agent_id="scene1",
|
||
input_type=MemoryInputType.QA,
|
||
qa_pair=QAPair(question="Q", answer="A")
|
||
)
|
||
|
||
assert request.input_type == MemoryInputType.QA
|
||
assert request.qa_pair is not None
|
||
|
||
def test_add_request_get_content_text(self):
|
||
"""测试获取文本内容"""
|
||
request = AddRequest(
|
||
uid="user1",
|
||
agent_id="scene1",
|
||
content="文本内容"
|
||
)
|
||
|
||
assert request.get_content() == "文本内容"
|
||
|
||
def test_add_request_get_content_qa(self):
|
||
"""测试获取 QA 内容"""
|
||
request = AddRequest(
|
||
uid="user1",
|
||
agent_id="scene1",
|
||
input_type=MemoryInputType.QA,
|
||
qa_pair=QAPair(question="问题", answer="答案")
|
||
)
|
||
|
||
content = request.get_content()
|
||
assert "问题" in content
|
||
assert "答案" in content
|
||
|
||
def test_add_request_validates_text_content(self):
|
||
"""测试验证文本内容"""
|
||
with pytest.raises(ValueError):
|
||
AddRequest(
|
||
uid="user1",
|
||
agent_id="scene1",
|
||
input_type=MemoryInputType.TEXT,
|
||
content=None # 文本类型必须有 content
|
||
)
|
||
|
||
def test_add_request_validates_qa_pair(self):
|
||
"""测试验证 QA 对"""
|
||
with pytest.raises(ValueError):
|
||
AddRequest(
|
||
uid="user1",
|
||
agent_id="scene1",
|
||
input_type=MemoryInputType.QA,
|
||
qa_pair=None # QA 类型必须有 qa_pair
|
||
)
|
||
|
||
def test_add_request_layer_normalization(self):
|
||
"""测试层名称规范化"""
|
||
request = AddRequest(
|
||
uid="user1",
|
||
agent_id="scene1",
|
||
content="内容",
|
||
layer="profile" # 字符串形式
|
||
)
|
||
|
||
assert request.layer == MemoryLayer.PROFILE
|
||
|
||
def test_add_request_agent_mode_normalization(self):
|
||
"""测试 agent 模式规范化"""
|
||
request = AddRequest(
|
||
uid="user1",
|
||
agent_id="scene1",
|
||
content="内容",
|
||
agent_mode="full" # 字符串形式
|
||
)
|
||
|
||
assert request.agent_mode == AgentProcessMode.FULL
|
||
|
||
def test_add_request_auto_extract_compatibility(self):
|
||
"""测试 auto_extract 兼容性"""
|
||
request = AddRequest(
|
||
uid="user1",
|
||
agent_id="scene1",
|
||
content="内容",
|
||
auto_extract=True # 旧参数
|
||
)
|
||
|
||
# auto_extract=True 应该等同于 agent_mode=full
|
||
assert request.agent_mode == AgentProcessMode.FULL
|
||
|
||
def test_add_request_to_dict(self):
|
||
"""测试转字典"""
|
||
request = AddRequest(
|
||
uid="user1",
|
||
agent_id="scene1",
|
||
content="内容",
|
||
importance=0.8,
|
||
tags=["tag1", "tag2"]
|
||
)
|
||
|
||
d = request.to_dict()
|
||
|
||
assert d["uid"] == "user1"
|
||
assert d["importance"] == 0.8
|
||
assert d["tags"] == ["tag1", "tag2"]
|
||
|
||
|
||
# ==================== AddResponse 测试 ====================
|
||
|
||
class TestAddResponse:
|
||
"""AddResponse 测试"""
|
||
|
||
def test_add_response_success(self):
|
||
"""测试成功响应"""
|
||
response = AddResponse(
|
||
success=True,
|
||
memory_id="mem_123",
|
||
layer=MemoryLayer.PROFILE,
|
||
message="Added successfully"
|
||
)
|
||
|
||
assert response.success is True
|
||
assert response.memory_id == "mem_123"
|
||
assert response.layer == MemoryLayer.PROFILE
|
||
|
||
def test_add_response_with_extracted_ids(self):
|
||
"""测试带提取 ID 的响应"""
|
||
response = AddResponse(
|
||
success=True,
|
||
memory_id="mem_123",
|
||
extracted_ids=["ext_1", "ext_2"],
|
||
abstract_key_ids=["key_1", "key_2"]
|
||
)
|
||
|
||
assert len(response.extracted_ids) == 2
|
||
assert len(response.abstract_key_ids) == 2
|
||
|
||
def test_add_response_to_dict(self):
|
||
"""测试转字典"""
|
||
response = AddResponse(
|
||
success=True,
|
||
memory_id="mem_123",
|
||
layer=MemoryLayer.KNOWLEDGE
|
||
)
|
||
|
||
d = response.to_dict()
|
||
|
||
assert d["success"] is True
|
||
assert d["memory_id"] == "mem_123"
|
||
assert d["layer"] == "l5_knowledge"
|
||
|
||
|
||
# ==================== AsyncAddResponse 测试 ====================
|
||
|
||
class TestAsyncAddResponse:
|
||
"""AsyncAddResponse 测试"""
|
||
|
||
def test_async_response(self):
|
||
"""测试异步响应"""
|
||
response = AsyncAddResponse(
|
||
success=True,
|
||
task_id="task_123",
|
||
status=TaskStatus.PENDING,
|
||
estimated_time=30
|
||
)
|
||
|
||
assert response.success is True
|
||
assert response.task_id == "task_123"
|
||
assert response.status == TaskStatus.PENDING
|
||
assert response.estimated_time == 30
|
||
|
||
def test_async_response_to_dict(self):
|
||
"""测试转字典"""
|
||
response = AsyncAddResponse(
|
||
success=True,
|
||
task_id="task_123",
|
||
status=TaskStatus.PROCESSING
|
||
)
|
||
|
||
d = response.to_dict()
|
||
|
||
assert d["task_id"] == "task_123"
|
||
assert d["status"] == "processing"
|
||
|
||
|
||
# ==================== RecallRequest 测试 ====================
|
||
|
||
class TestRecallRequest:
|
||
"""RecallRequest 测试"""
|
||
|
||
def test_recall_request_basic(self):
|
||
"""测试基本召回请求"""
|
||
request = RecallRequest(
|
||
query="用户偏好",
|
||
uid="user1"
|
||
)
|
||
|
||
assert request.query == "用户偏好"
|
||
assert request.limit == 10 # 默认值
|
||
|
||
def test_recall_request_with_filters(self):
|
||
"""测试带过滤条件的召回请求"""
|
||
request = RecallRequest(
|
||
query="电影",
|
||
uid="user1",
|
||
layers=[MemoryLayer.PROFILE, MemoryLayer.KNOWLEDGE],
|
||
tags=["兴趣"],
|
||
min_importance=0.5
|
||
)
|
||
|
||
assert len(request.layers) == 2
|
||
assert "兴趣" in request.tags
|
||
assert request.min_importance == 0.5
|
||
|
||
def test_recall_request_weights(self):
|
||
"""测试权重参数"""
|
||
request = RecallRequest(
|
||
query="test",
|
||
uid="user1",
|
||
semantic_weight=0.6,
|
||
recency_weight=0.2,
|
||
importance_weight=0.15,
|
||
access_weight=0.05
|
||
)
|
||
|
||
# 权重之和应该接近 1
|
||
total = (request.semantic_weight + request.recency_weight +
|
||
request.importance_weight + request.access_weight)
|
||
assert abs(total - 1.0) < 0.01
|
||
|
||
|
||
# ==================== MemoryEntry 测试 ====================
|
||
|
||
class TestMemoryEntry:
|
||
"""MemoryEntry 测试 (V2: MemoryEntry 是 MemoryNode 的别名,字段扁平化)"""
|
||
|
||
def test_memory_entry_creation(self):
|
||
"""测试创建 MemoryEntry"""
|
||
entry = MemoryEntry(
|
||
content="测试内容",
|
||
user_id="user1",
|
||
agent_id="scene1",
|
||
layer=MemoryLayer.PROFILE,
|
||
)
|
||
|
||
assert entry.content == "测试内容"
|
||
assert entry.layer == MemoryLayer.L4_IDENTITY # PROFILE 是 L4_IDENTITY 的别名
|
||
|
||
def test_memory_entry_build_isolation_key(self):
|
||
"""测试构建隔离键(两级: user_id + agent_id)"""
|
||
key = MemoryEntry.build_isolation_key("user1", "scene1")
|
||
|
||
assert "user1" in key
|
||
assert "scene1" in key
|
||
|
||
def test_memory_entry_to_dict(self):
|
||
"""测试转字典"""
|
||
entry = MemoryEntry(
|
||
content="内容",
|
||
user_id="user1",
|
||
agent_id="scene1",
|
||
layer=MemoryLayer.DIALOGUE,
|
||
)
|
||
|
||
d = entry.to_dict()
|
||
|
||
assert d["content"] == "内容"
|
||
assert d["layer"] == "l2_fact" # DIALOGUE -> l2_fact
|
||
assert d["user_id"] == "user1"
|
||
|
||
|
||
# ==================== BatchDeleteRequest 测试 ====================
|
||
|
||
class TestBatchDeleteRequest:
|
||
"""BatchDeleteRequest 测试"""
|
||
|
||
def test_batch_delete_by_ids(self):
|
||
"""测试按 ID 批量删除"""
|
||
request = BatchDeleteRequest(
|
||
scope=DeleteScope.MEMORY,
|
||
memory_ids=["id1", "id2", "id3"]
|
||
)
|
||
|
||
assert request.scope == DeleteScope.MEMORY
|
||
assert len(request.memory_ids) == 3
|
||
|
||
def test_batch_delete_by_scene(self):
|
||
"""测试按场景批量删除"""
|
||
request = BatchDeleteRequest(
|
||
scope=DeleteScope.SCENE,
|
||
uid="user1",
|
||
agent_id="scene1"
|
||
)
|
||
|
||
assert request.scope == DeleteScope.SCENE
|
||
assert request.uid == "user1"
|
||
assert request.agent_id == "scene1"
|
||
|
||
def test_batch_delete_dangerous_requires_confirm(self):
|
||
"""测试危险操作需要确认"""
|
||
request = BatchDeleteRequest(
|
||
scope=DeleteScope.USER,
|
||
uid="user1",
|
||
confirm=True
|
||
)
|
||
|
||
assert request.confirm is True
|
||
|
||
|
||
# ==================== 运行测试 ====================
|
||
|
||
if __name__ == "__main__":
|
||
pytest.main([__file__, "-v"])
|