[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
+1 -1
View File
@@ -134,7 +134,7 @@ class AgentMemoryTests(unittest.TestCase):
self.assertIn("delete", plan.supported_commands)
self.assertIn("disable", plan.supported_commands)
self.assertIn("export", plan.supported_commands)
self.assertFalse(plan.default_enabled)
self.assertTrue(plan.default_enabled)
if __name__ == "__main__":
+2 -2
View File
@@ -61,9 +61,9 @@ class CliAcceptanceTests(unittest.TestCase):
self.assertEqual(data["streaming_stt_provider"], "faster_whisper")
self.assertEqual(data["streaming_stt_product_candidate"], "sensevoice")
self.assertEqual(data["streaming_tts_provider"], "cosyvoice")
self.assertFalse(data["memory_enabled"])
self.assertTrue(data["memory_enabled"])
self.assertEqual(data["memory_provider"], "faiss_sqlite")
self.assertFalse(data["tool_router_enabled"])
self.assertTrue(data["tool_router_enabled"])
self.assertFalse(data["openinterpreter_enabled"])
self.assertFalse(data["browser_playwright_enabled"])
self.assertFalse(data["computer_control_enabled"])
+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"
+2 -2
View File
@@ -135,11 +135,11 @@ class ModelsConfigTests(unittest.TestCase):
self.assertEqual(config.streaming_stt_provider, "faster_whisper")
self.assertEqual(config.streaming_stt_product_candidate, "sensevoice")
self.assertEqual(config.streaming_tts_provider, "cosyvoice")
self.assertFalse(config.memory_enabled)
self.assertTrue(config.memory_enabled)
self.assertEqual(config.memory_provider, "faiss_sqlite")
self.assertEqual(config.memory_top_k, 5)
self.assertFalse(config.memory_auto_save_sensitive)
self.assertFalse(config.tool_router_enabled)
self.assertTrue(config.tool_router_enabled)
self.assertEqual(config.tool_max_calls_per_turn, 5)
self.assertEqual(config.tool_timeout_ms, 30000)
self.assertFalse(config.openinterpreter_enabled)