[Agent记忆工具]:完成长期记忆和工具路由闭环,包含安全确认和审计脱敏
This commit is contained in:
@@ -4,7 +4,7 @@ import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from owner_voice_pet.agent_memory import FakeMemoryManager, MemoryRecordInput, SQLiteMemoryManager
|
||||
from owner_voice_pet.agent_memory import FaissIndexManifest, FakeMemoryManager, MemoryRecordInput, SQLiteMemoryManager
|
||||
from owner_voice_pet.config import AppConfig
|
||||
from owner_voice_pet.full_duplex_audio import FakeWebRtcAudioProcessingProvider, RenderReferenceRingBuffer
|
||||
from owner_voice_pet.full_duplex_control import CancellationGraph, FullDuplexStateMachine
|
||||
@@ -150,6 +150,113 @@ class FullDuplexIntegrationTests(unittest.TestCase):
|
||||
self.assertEqual(runtime.audio_hub.render_reference.frame_count, 2)
|
||||
self.assertEqual(runtime.playback_queue.pending_items, 0)
|
||||
|
||||
def test_full_duplex_runtime_injects_memory_context_before_llm(self) -> None:
|
||||
memory = FakeMemoryManager()
|
||||
memory.save(MemoryRecordInput("preference", "用户喜欢 Python"))
|
||||
llm = FakeStreamingLlmProvider([LlmStreamEvent("delta", "记住了。"), LlmStreamEvent("finish", finish_reason="stop")])
|
||||
runtime = FullDuplexAgentRuntime(
|
||||
config=AppConfig(audio_apm_provider="fake", audio_apm_required=False, memory_enabled=True),
|
||||
memory_manager=memory,
|
||||
llm_provider=llm,
|
||||
tts_provider=FakeStreamingTtsProvider(),
|
||||
)
|
||||
|
||||
spoken = runtime.run_conversation_response_fixture("Python 项目怎么做?")
|
||||
|
||||
self.assertEqual(spoken, "记住了。")
|
||||
self.assertIn("长期记忆", llm.requests[0][1].content)
|
||||
self.assertIn("用户喜欢 Python", llm.requests[0][1].content)
|
||||
self.assertEqual([message.role for message in runtime.context.messages()], ["user", "assistant"])
|
||||
|
||||
def test_full_duplex_runtime_memory_health_detects_manifest_mismatch(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
memory = SQLiteMemoryManager(Path(tmp) / "memory.sqlite3")
|
||||
saved = memory.save(MemoryRecordInput("fact", "Owner 正在做全双工语音助手"))
|
||||
broken = FaissIndexManifest(
|
||||
embedding_model="fake",
|
||||
record_ids=(saved.id, "missing-id"),
|
||||
checksums={saved.id: "wrong"},
|
||||
)
|
||||
runtime = FullDuplexAgentRuntime(
|
||||
config=AppConfig(audio_apm_provider="fake", audio_apm_required=False, memory_enabled=True),
|
||||
memory_manager=memory,
|
||||
memory_manifest=broken,
|
||||
)
|
||||
|
||||
health = runtime.check_memory_health()
|
||||
|
||||
self.assertFalse(health.ok)
|
||||
self.assertTrue(any("missing-id" in error for error in health.errors))
|
||||
self.assertTrue(any("checksum mismatch" in error for error in health.errors))
|
||||
|
||||
def test_full_duplex_runtime_routes_memory_search_tool_call(self) -> None:
|
||||
memory = FakeMemoryManager()
|
||||
memory.save(MemoryRecordInput("project", "Owner 项目正在做全双工 Agent"))
|
||||
runtime = FullDuplexAgentRuntime(
|
||||
config=AppConfig(
|
||||
audio_apm_provider="fake",
|
||||
audio_apm_required=False,
|
||||
memory_enabled=True,
|
||||
tool_router_enabled=True,
|
||||
),
|
||||
memory_manager=memory,
|
||||
llm_provider=FakeStreamingLlmProvider(
|
||||
[
|
||||
LlmStreamEvent(
|
||||
"tool_call",
|
||||
tool_call={
|
||||
"id": "tool-1",
|
||||
"name": "memory.search",
|
||||
"arguments": {"query": "Owner Agent", "top_k": 1},
|
||||
"turn_id": "turn-1",
|
||||
},
|
||||
),
|
||||
LlmStreamEvent("delta", "查到了。"),
|
||||
LlmStreamEvent("finish", finish_reason="stop"),
|
||||
]
|
||||
),
|
||||
tts_provider=FakeStreamingTtsProvider(),
|
||||
)
|
||||
|
||||
spoken = runtime.run_conversation_response_fixture("查一下当前项目")
|
||||
|
||||
self.assertEqual(spoken, "查到了。")
|
||||
self.assertEqual(runtime.tool_results[0].status, "success")
|
||||
self.assertIn("全双工 Agent", runtime.tool_results[0].output_text)
|
||||
self.assertIn("工具结果 memory.search", runtime.tool_result_messages[0].content)
|
||||
|
||||
def test_full_duplex_runtime_high_risk_tool_call_requires_confirmation(self) -> None:
|
||||
runtime = FullDuplexAgentRuntime(
|
||||
config=AppConfig(
|
||||
audio_apm_provider="fake",
|
||||
audio_apm_required=False,
|
||||
memory_enabled=True,
|
||||
tool_router_enabled=True,
|
||||
),
|
||||
memory_manager=FakeMemoryManager(),
|
||||
llm_provider=FakeStreamingLlmProvider(
|
||||
[
|
||||
LlmStreamEvent(
|
||||
"tool_call",
|
||||
tool_call={
|
||||
"id": "tool-1",
|
||||
"name": "memory.search",
|
||||
"arguments": {"query": "账号"},
|
||||
"natural_language_intent": "上传账号资料",
|
||||
"turn_id": "turn-1",
|
||||
},
|
||||
),
|
||||
LlmStreamEvent("finish", finish_reason="stop"),
|
||||
]
|
||||
),
|
||||
tts_provider=FakeStreamingTtsProvider(),
|
||||
)
|
||||
|
||||
runtime.run_conversation_response_fixture("上传账号资料")
|
||||
|
||||
self.assertEqual(runtime.tool_results[0].status, "confirmation_required")
|
||||
self.assertEqual(runtime.tool_router.audit_log[0].action, "require_confirmation")
|
||||
|
||||
def test_memory_restart_and_tool_search_integration(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
db_path = Path(tmp) / "memory.sqlite3"
|
||||
|
||||
Reference in New Issue
Block a user