Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
- PersonalityConfig with 4 dimensions: tone, proactiveness, formality, humor - Signal detection from Chinese user messages (e.g. "说简单点" → Simple tone) - apply_personality_adjustments() returns new immutable config - build_personality_prompt() injects personality into system prompts - Integrated into post_conversation_hook for automatic detection - In-memory persistence via OnceLock (VikingStorage integration TODO) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
250 lines
8.8 KiB
Rust
250 lines
8.8 KiB
Rust
//! Intelligence Hooks - Pre/Post conversation integration
|
|
//!
|
|
//! Bridges the intelligence layer modules (identity, memory, heartbeat, reflection)
|
|
//! into the kernel's chat flow at the Tauri command boundary.
|
|
//!
|
|
//! Architecture: kernel_commands.rs → intelligence_hooks → intelligence modules → Viking/Kernel
|
|
|
|
use tracing::{debug, warn};
|
|
|
|
use std::sync::Arc;
|
|
|
|
use crate::intelligence::identity::IdentityManagerState;
|
|
use crate::intelligence::heartbeat::HeartbeatEngineState;
|
|
use crate::intelligence::reflection::{MemoryEntryForAnalysis, ReflectionEngineState};
|
|
use zclaw_runtime::driver::LlmDriver;
|
|
|
|
/// Run pre-conversation intelligence hooks
|
|
///
|
|
/// Builds identity-enhanced system prompt (SOUL.md + instructions).
|
|
///
|
|
/// NOTE: Memory context injection is NOT done here — it is handled by
|
|
/// `MemoryMiddleware.before_completion()` in the Kernel's middleware chain.
|
|
/// Previously, both paths injected memories, causing duplicate injection.
|
|
pub async fn pre_conversation_hook(
|
|
agent_id: &str,
|
|
_user_message: &str,
|
|
identity_state: &IdentityManagerState,
|
|
) -> Result<String, String> {
|
|
// Build identity-enhanced system prompt (SOUL.md + instructions)
|
|
// Memory context is injected by MemoryMiddleware in the kernel middleware chain,
|
|
// not here, to avoid duplicate injection.
|
|
let enhanced_prompt = match build_identity_prompt(agent_id, "", identity_state).await {
|
|
Ok(prompt) => prompt,
|
|
Err(e) => {
|
|
warn!(
|
|
"[intelligence_hooks] Failed to build identity prompt for agent {}: {}",
|
|
agent_id, e
|
|
);
|
|
String::new()
|
|
}
|
|
};
|
|
|
|
Ok(enhanced_prompt)
|
|
}
|
|
|
|
/// Run post-conversation intelligence hooks
|
|
///
|
|
/// 1. Record interaction for heartbeat engine
|
|
/// 2. Record conversation for reflection engine, trigger reflection if needed
|
|
pub async fn post_conversation_hook(
|
|
agent_id: &str,
|
|
_user_message: &str,
|
|
_heartbeat_state: &HeartbeatEngineState,
|
|
reflection_state: &ReflectionEngineState,
|
|
llm_driver: Option<Arc<dyn LlmDriver>>,
|
|
) {
|
|
// Step 1: Record interaction for heartbeat
|
|
crate::intelligence::heartbeat::record_interaction(agent_id);
|
|
debug!("[intelligence_hooks] Recorded interaction for agent: {}", agent_id);
|
|
|
|
// Step 1.5: Detect personality adjustment signals
|
|
if !_user_message.is_empty() {
|
|
let config = crate::intelligence::personality_detector::load_personality_config(agent_id);
|
|
let adjustments = crate::intelligence::personality_detector::detect_personality_signals(
|
|
_user_message, &config,
|
|
);
|
|
if !adjustments.is_empty() {
|
|
let new_config = crate::intelligence::personality_detector::apply_personality_adjustments(
|
|
&config, &adjustments,
|
|
);
|
|
crate::intelligence::personality_detector::save_personality_config(agent_id, &new_config);
|
|
for adj in &adjustments {
|
|
debug!(
|
|
"[intelligence_hooks] Personality adjusted: {} {} -> {} (trigger: {})",
|
|
adj.dimension, adj.from_value, adj.to_value, adj.trigger
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Step 2: Record conversation for reflection
|
|
let mut engine = reflection_state.lock().await;
|
|
|
|
// Apply restored state on first call (one-shot after app restart)
|
|
if let Some(restored_state) = crate::intelligence::reflection::pop_restored_state(agent_id) {
|
|
engine.apply_restored_state(restored_state);
|
|
}
|
|
if let Some(restored_result) = crate::intelligence::reflection::pop_restored_result(agent_id) {
|
|
engine.apply_restored_result(restored_result);
|
|
}
|
|
|
|
engine.record_conversation();
|
|
debug!(
|
|
"[intelligence_hooks] Conversation count updated for agent: {}",
|
|
agent_id
|
|
);
|
|
|
|
if engine.should_reflect() {
|
|
debug!(
|
|
"[intelligence_hooks] Reflection threshold reached for agent: {}",
|
|
agent_id
|
|
);
|
|
|
|
// Query actual memories from VikingStorage for reflection analysis
|
|
let memories = match query_memories_for_reflection(agent_id).await {
|
|
Ok(m) => m,
|
|
Err(e) => {
|
|
warn!(
|
|
"[intelligence_hooks] Failed to query memories for reflection (agent {}): {}",
|
|
agent_id, e
|
|
);
|
|
Vec::new()
|
|
}
|
|
};
|
|
|
|
debug!(
|
|
"[intelligence_hooks] Fetched {} memories for reflection",
|
|
memories.len()
|
|
);
|
|
|
|
let reflection_result = engine.reflect(agent_id, &memories, llm_driver.clone()).await;
|
|
debug!(
|
|
"[intelligence_hooks] Reflection completed: {} patterns, {} suggestions",
|
|
reflection_result.patterns.len(),
|
|
reflection_result.improvements.len()
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Build memory context by searching VikingStorage for relevant memories
|
|
///
|
|
/// NOTE: Memory injection is now handled by MemoryMiddleware in the Kernel
|
|
/// middleware chain. This function is kept as a utility for ad-hoc queries.
|
|
#[allow(dead_code)]
|
|
async fn build_memory_context(
|
|
agent_id: &str,
|
|
user_message: &str,
|
|
) -> Result<String, String> {
|
|
// Try Viking storage (has FTS5 + TF-IDF + Embedding)
|
|
let storage = crate::viking_commands::get_storage().await?;
|
|
|
|
// FindOptions from zclaw_growth
|
|
let options = zclaw_growth::FindOptions {
|
|
scope: Some(format!("agent://{}", agent_id)),
|
|
limit: Some(8),
|
|
min_similarity: Some(0.2),
|
|
};
|
|
|
|
// find is on the VikingStorage trait — call via trait to dispatch correctly
|
|
let results: Vec<zclaw_growth::MemoryEntry> =
|
|
zclaw_growth::VikingStorage::find(storage.as_ref(), user_message, options)
|
|
.await
|
|
.map_err(|e| format!("Memory search failed: {}", e))?;
|
|
|
|
if results.is_empty() {
|
|
return Ok(String::new());
|
|
}
|
|
|
|
// Format memories into context string
|
|
let mut context = String::from("## 相关记忆\n\n");
|
|
let mut token_estimate: usize = 0;
|
|
let max_tokens: usize = 500;
|
|
|
|
for entry in &results {
|
|
// Prefer overview (L1 summary) over full content
|
|
// overview is Option<String> — use as_deref to get Option<&str>
|
|
let overview_str = entry.overview.as_deref().unwrap_or("");
|
|
let text = if !overview_str.is_empty() {
|
|
overview_str
|
|
} else {
|
|
&entry.content
|
|
};
|
|
|
|
// Truncate long entries (char-safe for CJK text)
|
|
let truncated = if text.chars().count() > 100 {
|
|
let truncated: String = text.chars().take(100).collect();
|
|
format!("{}...", truncated)
|
|
} else {
|
|
text.to_string()
|
|
};
|
|
|
|
// Simple token estimate (~1.5 tokens per CJK char, ~0.25 per other)
|
|
let tokens: usize = truncated.chars()
|
|
.map(|c: char| if c.is_ascii() { 1 } else { 2 })
|
|
.sum();
|
|
|
|
if token_estimate + tokens > max_tokens {
|
|
break;
|
|
}
|
|
|
|
context.push_str(&format!("- [{}] {}\n", entry.memory_type, truncated));
|
|
token_estimate += tokens;
|
|
}
|
|
|
|
Ok(context)
|
|
}
|
|
|
|
/// Build identity-enhanced system prompt
|
|
async fn build_identity_prompt(
|
|
agent_id: &str,
|
|
memory_context: &str,
|
|
identity_state: &IdentityManagerState,
|
|
) -> Result<String, String> {
|
|
// IdentityManagerState is Arc<tokio::sync::Mutex<AgentIdentityManager>>
|
|
// tokio::sync::Mutex::lock() returns MutexGuard directly
|
|
let mut manager = identity_state.lock().await;
|
|
|
|
let prompt = manager.build_system_prompt(
|
|
agent_id,
|
|
if memory_context.is_empty() { None } else { Some(memory_context) },
|
|
);
|
|
|
|
Ok(prompt)
|
|
}
|
|
|
|
/// Query agent memories from VikingStorage and convert to MemoryEntryForAnalysis
|
|
/// for the reflection engine.
|
|
///
|
|
/// Fetches up to 50 recent memories scoped to the given agent, without token
|
|
/// truncation (unlike build_memory_context which is size-limited for prompts).
|
|
async fn query_memories_for_reflection(
|
|
agent_id: &str,
|
|
) -> Result<Vec<MemoryEntryForAnalysis>, String> {
|
|
let storage = crate::viking_commands::get_storage().await?;
|
|
|
|
let options = zclaw_growth::FindOptions {
|
|
scope: Some(format!("agent://{}", agent_id)),
|
|
limit: Some(50),
|
|
min_similarity: Some(0.0), // Fetch all, no similarity filter
|
|
};
|
|
|
|
let results: Vec<zclaw_growth::MemoryEntry> =
|
|
zclaw_growth::VikingStorage::find(storage.as_ref(), "", options)
|
|
.await
|
|
.map_err(|e| format!("Memory query for reflection failed: {}", e))?;
|
|
|
|
let memories: Vec<MemoryEntryForAnalysis> = results
|
|
.into_iter()
|
|
.map(|entry| MemoryEntryForAnalysis {
|
|
memory_type: entry.memory_type.to_string(),
|
|
content: entry.content,
|
|
importance: entry.importance as usize,
|
|
access_count: entry.access_count as usize,
|
|
tags: entry.keywords,
|
|
})
|
|
.collect();
|
|
|
|
Ok(memories)
|
|
}
|