[Agent记忆工具]:完成长期记忆和工具路由闭环,包含安全确认和审计脱敏
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user