[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
@@ -296,7 +296,7 @@ class AgentConversationManager:
@dataclass(frozen=True, slots=True)
class MemoryManagementPlan:
supported_commands: tuple[str, ...] = ("list", "delete", "disable", "export")
default_enabled: bool = False
default_enabled: bool = True
def is_sensitive_memory_text(text: str) -> bool:
+4 -4
View File
@@ -82,11 +82,11 @@ class AppConfig:
streaming_stt_provider: str = "faster_whisper"
streaming_stt_product_candidate: str = "sensevoice"
streaming_tts_provider: str = "cosyvoice"
memory_enabled: bool = False
memory_enabled: bool = True
memory_provider: str = "faiss_sqlite"
memory_top_k: int = 5
memory_auto_save_sensitive: bool = False
tool_router_enabled: bool = False
tool_router_enabled: bool = True
tool_max_calls_per_turn: int = 5
tool_timeout_ms: int = 30000
openinterpreter_enabled: bool = False
@@ -212,11 +212,11 @@ class AppConfig:
streaming_tts_provider=(
get("STREAMING_TTS_PROVIDER", "cosyvoice") or "cosyvoice"
).lower(),
memory_enabled=get_bool("MEMORY_ENABLED", "0"),
memory_enabled=get_bool("MEMORY_ENABLED", "1"),
memory_provider=(get("MEMORY_PROVIDER", "faiss_sqlite") or "faiss_sqlite").lower(),
memory_top_k=int(get("MEMORY_TOP_K", "5") or "5"),
memory_auto_save_sensitive=get_bool("MEMORY_AUTO_SAVE_SENSITIVE", "0"),
tool_router_enabled=get_bool("TOOL_ROUTER_ENABLED", "0"),
tool_router_enabled=get_bool("TOOL_ROUTER_ENABLED", "1"),
tool_max_calls_per_turn=int(get("TOOL_MAX_CALLS_PER_TURN", "5") or "5"),
tool_timeout_ms=int(get("TOOL_TIMEOUT_MS", "30000") or "30000"),
openinterpreter_enabled=get_bool("OPENINTERPRETER_ENABLED", "0"),
+100
View File
@@ -1,8 +1,18 @@
from __future__ import annotations
import time
from dataclasses import dataclass
from .agent_memory import (
AgentConversationManager,
DisabledMemoryManager,
FaissIndexManifest,
MemoryHealth,
MemoryManager,
SQLiteMemoryManager,
)
from .config import AppConfig
from .conversation import ConversationContext
from .full_duplex_audio import AudioHub, AudioProcessingProvider, build_audio_processing_provider
from .full_duplex_control import CancellationGraph, FullDuplexStateMachine
from .full_duplex_response import (
@@ -17,6 +27,15 @@ from .full_duplex_response import (
from .full_duplex_speech import FakeVadProvider, InterruptController, InterruptionDetector
from .models import AudioFrame, Message, PipelineState
from .runtime import RuntimeSummary
from .tool_router import (
MemorySaveTool,
MemorySearchTool,
ShellReadonlyTool,
ToolCallRequest,
ToolContext,
ToolResult,
ToolRouter,
)
@dataclass(slots=True)
@@ -43,17 +62,36 @@ class FullDuplexAgentRuntime:
audio_hub: AudioHub | None = None,
llm_provider: StreamingLlmProvider | None = None,
tts_provider: StreamingTtsProvider | None = None,
context: ConversationContext | None = None,
memory_manager: MemoryManager | None = None,
memory_manifest: FaissIndexManifest | None = None,
tool_router: ToolRouter | None = None,
) -> None:
self.config = config
self.processor = processor
self.audio_hub = audio_hub
self.llm_provider = llm_provider
self.tts_provider = tts_provider
self.context = context or ConversationContext(
max_messages=config.context_max_messages,
max_chars=config.context_max_chars,
)
self.memory_manager = memory_manager or self._build_memory_manager()
self.memory_manifest = memory_manifest
self.conversation_manager = AgentConversationManager(
context=self.context,
memory=self.memory_manager,
memory_enabled=config.memory_enabled,
memory_top_k=config.memory_top_k,
)
self.tool_router = tool_router or self._build_tool_router()
self.health: FullDuplexRuntimeHealth | None = None
self.state_machine = FullDuplexStateMachine()
self.cancellation_graph = CancellationGraph("turn")
self.interrupt_controller: InterruptController | None = None
self.playback_queue = InterruptiblePlaybackQueue()
self.tool_results: list[ToolResult] = []
self.tool_result_messages: list[Message] = []
def load_audio(self) -> FullDuplexRuntimeHealth:
if self.audio_hub is None:
@@ -152,6 +190,8 @@ class FullDuplexAgentRuntime:
if event.kind == "delta" and event.text_delta:
for sentence in segmenter.accept_delta(event.text_delta):
self._synthesize_and_play_sentence(sentence, tts_session)
elif event.kind == "tool_call" and event.tool_call:
self._handle_tool_call(event.tool_call)
elif event.kind == "finish":
break
tail = segmenter.flush()
@@ -162,6 +202,19 @@ class FullDuplexAgentRuntime:
self.playback_queue.play_next(audio_hub=self.audio_hub, cancellation=self.cancellation_graph.root)
return self.playback_queue.spoken.text
def run_conversation_response_fixture(
self,
user_text: str,
*,
llm_events: list[LlmStreamEvent] | None = None,
) -> str:
messages = self.conversation_manager.build_messages_for_user(user_text)
spoken = self.run_streaming_response_fixture(messages, llm_events=llm_events)
self.conversation_manager.commit_user(user_text)
if spoken:
self.conversation_manager.commit_assistant(spoken)
return spoken
def _synthesize_and_play_sentence(self, sentence: str, tts_session) -> None:
if self.audio_hub is None:
raise RuntimeError("audio hub is not loaded")
@@ -169,6 +222,53 @@ class FullDuplexAgentRuntime:
self.playback_queue.enqueue(sentence, frames)
self.playback_queue.play_next(audio_hub=self.audio_hub, cancellation=self.cancellation_graph.root)
def check_memory_health(self) -> MemoryHealth:
health = self.memory_manager.health_check()
errors = list(health.errors)
if self.memory_manifest is not None and hasattr(self.memory_manager, "all_records"):
records = self.memory_manager.all_records() # type: ignore[attr-defined]
errors.extend(self.memory_manifest.consistency_errors(records))
return MemoryHealth(ok=not errors, errors=tuple(errors))
def _build_memory_manager(self) -> MemoryManager:
if not self.config.memory_enabled:
return DisabledMemoryManager()
return SQLiteMemoryManager(self.config.log_dir / "memory.sqlite3")
def _build_tool_router(self) -> ToolRouter:
if not self.config.tool_router_enabled:
return ToolRouter({})
return ToolRouter(
{
"memory.search": MemorySearchTool(),
"memory.save": MemorySaveTool(),
"shell.readonly": ShellReadonlyTool(),
},
max_calls_per_turn=self.config.tool_max_calls_per_turn,
)
def _handle_tool_call(self, tool_call: dict[str, object]) -> ToolResult:
request = ToolCallRequest(
id=str(tool_call.get("id") or f"tool-{len(self.tool_results) + 1}"),
name=str(tool_call.get("name") or ""),
arguments=dict(tool_call.get("arguments") or {}),
requested_by_turn_id=str(tool_call.get("turn_id") or "turn"),
natural_language_intent=str(tool_call.get("natural_language_intent") or ""),
timeout_ms=self.config.tool_timeout_ms,
)
decision = self.tool_router.route(request, ToolContext(memory=self.memory_manager))
result = self.tool_router.execute(request, decision, ToolContext(memory=self.memory_manager))
self.tool_results.append(result)
if result.output_text:
self.tool_result_messages.append(
Message(
"system",
f"工具结果 {request.name}:\n{result.output_text}",
time.time(),
)
)
return result
def _run_audio_smoke_once(self) -> None:
if self.audio_hub is None:
raise RuntimeError("audio hub is not loaded")