From af20487b8dd022d7fc1e8cb958b550cf52d52a6a Mon Sep 17 00:00:00 2001 From: iven Date: Tue, 7 Apr 2026 09:36:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(intelligence):=20add=20personality=20detec?= =?UTF-8?q?tor=20=E2=80=94=20auto-adjust=20from=20conversation=20signals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- desktop/src-tauri/src/intelligence/mod.rs | 1 + .../src/intelligence/personality_detector.rs | 350 ++++++++++++++++++ desktop/src-tauri/src/intelligence_hooks.rs | 20 + 3 files changed, 371 insertions(+) create mode 100644 desktop/src-tauri/src/intelligence/personality_detector.rs diff --git a/desktop/src-tauri/src/intelligence/mod.rs b/desktop/src-tauri/src/intelligence/mod.rs index 55a6e8d..81497e1 100644 --- a/desktop/src-tauri/src/intelligence/mod.rs +++ b/desktop/src-tauri/src/intelligence/mod.rs @@ -34,6 +34,7 @@ pub mod validation; pub mod extraction_adapter; pub mod pain_aggregator; pub mod solution_generator; +pub mod personality_detector; // Re-export main types for convenience pub use heartbeat::HeartbeatEngineState; diff --git a/desktop/src-tauri/src/intelligence/personality_detector.rs b/desktop/src-tauri/src/intelligence/personality_detector.rs new file mode 100644 index 0000000..7553267 --- /dev/null +++ b/desktop/src-tauri/src/intelligence/personality_detector.rs @@ -0,0 +1,350 @@ +//! Personality detector — detects personality adjustment signals from conversation. +//! +//! Analyzes user messages for implicit personality preferences and stores +//! adjustments to `viking://user/memories/preferences/personality`. + +use serde::{Deserialize, Serialize}; + +// --------------------------------------------------------------------------- +// Personality configuration +// --------------------------------------------------------------------------- + +/// Adjustable personality dimensions. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PersonalityConfig { + pub tone: PersonalityTone, + pub proactiveness: PersonalityProactiveness, + pub formality: PersonalityFormality, + pub humor: PersonalityHumor, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "snake_case")] +pub enum PersonalityTone { + #[default] + Professional, + Casual, + Simple, + Technical, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "snake_case")] +pub enum PersonalityProactiveness { + #[default] + Standard, + High, + Low, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "snake_case")] +pub enum PersonalityFormality { + #[default] + SemiFormal, + Formal, + Informal, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "snake_case")] +pub enum PersonalityHumor { + #[default] + Light, + None, + Moderate, +} + +impl Default for PersonalityConfig { + fn default() -> Self { + Self { + tone: PersonalityTone::Professional, + proactiveness: PersonalityProactiveness::Standard, + formality: PersonalityFormality::SemiFormal, + humor: PersonalityHumor::Light, + } + } +} + +// --------------------------------------------------------------------------- +// Signal patterns for personality adjustment +// --------------------------------------------------------------------------- + +/// A detected personality adjustment signal. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PersonalityAdjustment { + pub dimension: String, + pub from_value: String, + pub to_value: String, + pub trigger: String, +} + +/// Analyze a user message for personality adjustment signals. +/// +/// Returns adjustments that should be applied based on what the user said. +pub fn detect_personality_signals( + user_message: &str, + current: &PersonalityConfig, +) -> Vec { + let mut adjustments = Vec::new(); + let lower = user_message.to_lowercase(); + + // Tone signals + if ["说简单点", "简单说", "说人话", "大白话", "别用术语"].iter().any(|k| lower.contains(k)) { + if current.tone != PersonalityTone::Simple { + adjustments.push(PersonalityAdjustment { + dimension: "tone".into(), + from_value: format!("{:?}", current.tone), + to_value: "Simple".into(), + trigger: "用户要求简化表达".into(), + }); + } + } else if ["详细点", "技术细节", "深入分析", "专业角度"].iter().any(|k| lower.contains(k)) { + if current.tone != PersonalityTone::Technical { + adjustments.push(PersonalityAdjustment { + dimension: "tone".into(), + from_value: format!("{:?}", current.tone), + to_value: "Technical".into(), + trigger: "用户要求深入技术分析".into(), + }); + } + } else if ["轻松点", "随便聊", "不用那么正式"].iter().any(|k| lower.contains(k)) { + if current.tone != PersonalityTone::Casual { + adjustments.push(PersonalityAdjustment { + dimension: "tone".into(), + from_value: format!("{:?}", current.tone), + to_value: "Casual".into(), + trigger: "用户偏好轻松对话风格".into(), + }); + } + } + + // Formality signals + if ["别叫我", "叫我名字", "不用叫x总", "别叫老板"].iter().any(|k| lower.contains(k)) { + if current.formality != PersonalityFormality::Informal { + adjustments.push(PersonalityAdjustment { + dimension: "formality".into(), + from_value: format!("{:?}", current.formality), + to_value: "Informal".into(), + trigger: "用户希望降低正式程度".into(), + }); + } + } + + // Proactiveness signals + if ["别老是催我", "别主动提醒", "安静点", "别打扰"].iter().any(|k| lower.contains(k)) { + if current.proactiveness != PersonalityProactiveness::Low { + adjustments.push(PersonalityAdjustment { + dimension: "proactiveness".into(), + from_value: format!("{:?}", current.proactiveness), + to_value: "Low".into(), + trigger: "用户希望减少主动行为".into(), + }); + } + } else if ["提醒我", "及时通知", "主动告诉我", "别忘了告诉我"].iter().any(|k| lower.contains(k)) { + if current.proactiveness != PersonalityProactiveness::High { + adjustments.push(PersonalityAdjustment { + dimension: "proactiveness".into(), + from_value: format!("{:?}", current.proactiveness), + to_value: "High".into(), + trigger: "用户希望更多主动提醒".into(), + }); + } + } + + // Humor signals + if ["别开玩笑", "正经点", "严肃"].iter().any(|k| lower.contains(k)) { + if current.humor != PersonalityHumor::None { + adjustments.push(PersonalityAdjustment { + dimension: "humor".into(), + from_value: format!("{:?}", current.humor), + to_value: "None".into(), + trigger: "用户偏好严肃风格".into(), + }); + } + } + + adjustments +} + +/// Apply detected adjustments to a personality config, returning a new config. +pub fn apply_personality_adjustments( + current: &PersonalityConfig, + adjustments: &[PersonalityAdjustment], +) -> PersonalityConfig { + let mut config = current.clone(); + for adj in adjustments { + match adj.dimension.as_str() { + "tone" => { + config.tone = match adj.to_value.as_str() { + "Simple" => PersonalityTone::Simple, + "Technical" => PersonalityTone::Technical, + "Casual" => PersonalityTone::Casual, + _ => config.tone, + }; + } + "formality" => { + config.formality = match adj.to_value.as_str() { + "Informal" => PersonalityFormality::Informal, + "Formal" => PersonalityFormality::Formal, + _ => config.formality, + }; + } + "proactiveness" => { + config.proactiveness = match adj.to_value.as_str() { + "Low" => PersonalityProactiveness::Low, + "High" => PersonalityProactiveness::High, + _ => config.proactiveness, + }; + } + "humor" => { + config.humor = match adj.to_value.as_str() { + "None" => PersonalityHumor::None, + "Moderate" => PersonalityHumor::Moderate, + _ => config.humor, + }; + } + _ => {} + } + } + config +} + +/// Build a personality injection string for system prompts. +pub fn build_personality_prompt(config: &PersonalityConfig) -> String { + let tone_desc = match config.tone { + PersonalityTone::Professional => "专业、清晰", + PersonalityTone::Casual => "轻松、亲切", + PersonalityTone::Simple => "简洁、易懂", + PersonalityTone::Technical => "深入、专业", + }; + let formality_desc = match config.formality { + PersonalityFormality::Formal => "使用敬语和正式称呼", + PersonalityFormality::SemiFormal => "适度正式,可使用昵称", + PersonalityFormality::Informal => "平等对话,使用名字", + }; + let proactiveness_desc = match config.proactiveness { + PersonalityProactiveness::High => "主动发现问题并提前提醒", + PersonalityProactiveness::Standard => "在用户提问时主动提供相关建议", + PersonalityProactiveness::Low => "仅在用户明确要求时提供信息", + }; + let humor_desc = match config.humor { + PersonalityHumor::Light => "偶尔轻松幽默", + PersonalityHumor::None => "保持严肃", + PersonalityHumor::Moderate => "适度使用幽默", + }; + + format!( + "## 对话风格\n\n- 语气:{}\n- 称呼:{}\n- 主动性:{}\n- 幽默:{}", + tone_desc, formality_desc, proactiveness_desc, humor_desc + ) +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_default_personality() { + let config = PersonalityConfig::default(); + assert_eq!(config.tone, PersonalityTone::Professional); + assert_eq!(config.formality, PersonalityFormality::SemiFormal); + assert_eq!(config.proactiveness, PersonalityProactiveness::Standard); + assert_eq!(config.humor, PersonalityHumor::Light); + } + + #[test] + fn test_detect_tone_simplification() { + let config = PersonalityConfig::default(); + let adjustments = detect_personality_signals("说简单点吧", &config); + assert_eq!(adjustments.len(), 1); + assert_eq!(adjustments[0].dimension, "tone"); + assert_eq!(adjustments[0].to_value, "Simple"); + } + + #[test] + fn test_detect_formality_change() { + let config = PersonalityConfig::default(); + let adjustments = detect_personality_signals("别叫我张总,叫我名字就行", &config); + assert!(!adjustments.is_empty()); + assert!(adjustments.iter().any(|a| a.dimension == "formality")); + } + + #[test] + fn test_detect_proactiveness_low() { + let config = PersonalityConfig::default(); + let adjustments = detect_personality_signals("别老是催我做这做那", &config); + assert!(!adjustments.is_empty()); + assert!(adjustments.iter().any(|a| a.dimension == "proactiveness" && a.to_value == "Low")); + } + + #[test] + fn test_no_adjustment_for_neutral() { + let config = PersonalityConfig::default(); + let adjustments = detect_personality_signals("帮我查一下物流状态", &config); + assert!(adjustments.is_empty()); + } + + #[test] + fn test_apply_adjustments() { + let config = PersonalityConfig::default(); + let adjustments = vec![PersonalityAdjustment { + dimension: "tone".into(), + from_value: "Professional".into(), + to_value: "Simple".into(), + trigger: "test".into(), + }]; + let new_config = apply_personality_adjustments(&config, &adjustments); + assert_eq!(new_config.tone, PersonalityTone::Simple); + // Other dimensions unchanged + assert_eq!(new_config.formality, config.formality); + } + + #[test] + fn test_build_personality_prompt() { + let config = PersonalityConfig::default(); + let prompt = build_personality_prompt(&config); + assert!(prompt.contains("专业")); + assert!(prompt.contains("适度正式")); + assert!(prompt.contains("偶尔轻松")); + } + + #[test] + fn test_serialization_roundtrip() { + let config = PersonalityConfig::default(); + let json = serde_json::to_string(&config).unwrap(); + let decoded: PersonalityConfig = serde_json::from_str(&json).unwrap(); + assert_eq!(decoded.tone, config.tone); + assert_eq!(decoded.formality, config.formality); + } +} + +// --------------------------------------------------------------------------- +// Persistence helpers (stub — TODO: integrate with VikingStorage) +// --------------------------------------------------------------------------- + +use std::sync::OnceLock; +use std::sync::Mutex; + +static PERSONALITY_STORE: OnceLock>> = OnceLock::new(); + +fn personality_store() -> &'static Mutex> { + PERSONALITY_STORE.get_or_init(|| Mutex::new(std::collections::HashMap::new())) +} + +/// Load personality config for a given agent. +/// Returns default config if none is stored. +pub fn load_personality_config(agent_id: &str) -> PersonalityConfig { + let store = personality_store().lock().unwrap(); + store.get(agent_id).cloned().unwrap_or_default() +} + +/// Save personality config for a given agent. +pub fn save_personality_config(agent_id: &str, config: &PersonalityConfig) { + let mut store = personality_store().lock().unwrap(); + store.insert(agent_id.to_string(), config.clone()); +} diff --git a/desktop/src-tauri/src/intelligence_hooks.rs b/desktop/src-tauri/src/intelligence_hooks.rs index 8d9b20b..856e0b0 100644 --- a/desktop/src-tauri/src/intelligence_hooks.rs +++ b/desktop/src-tauri/src/intelligence_hooks.rs @@ -58,6 +58,26 @@ pub async fn post_conversation_hook( 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;