[Agent记忆工具]:完成长期记忆和工具路由闭环,包含安全确认和审计脱敏

This commit is contained in:
mkbk
2026-06-19 12:37:05 +08:00
parent 79b3e89b79
commit 81c949a7ec
8 changed files with 223 additions and 16 deletions
+108 -1
View File
@@ -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"