feat(intelligence): add personality detector — auto-adjust from conversation signals
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
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>
This commit is contained in:
@@ -34,6 +34,7 @@ pub mod validation;
|
|||||||
pub mod extraction_adapter;
|
pub mod extraction_adapter;
|
||||||
pub mod pain_aggregator;
|
pub mod pain_aggregator;
|
||||||
pub mod solution_generator;
|
pub mod solution_generator;
|
||||||
|
pub mod personality_detector;
|
||||||
|
|
||||||
// Re-export main types for convenience
|
// Re-export main types for convenience
|
||||||
pub use heartbeat::HeartbeatEngineState;
|
pub use heartbeat::HeartbeatEngineState;
|
||||||
|
|||||||
350
desktop/src-tauri/src/intelligence/personality_detector.rs
Normal file
350
desktop/src-tauri/src/intelligence/personality_detector.rs
Normal file
@@ -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<PersonalityAdjustment> {
|
||||||
|
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<Mutex<std::collections::HashMap<String, PersonalityConfig>>> = OnceLock::new();
|
||||||
|
|
||||||
|
fn personality_store() -> &'static Mutex<std::collections::HashMap<String, PersonalityConfig>> {
|
||||||
|
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());
|
||||||
|
}
|
||||||
@@ -58,6 +58,26 @@ pub async fn post_conversation_hook(
|
|||||||
crate::intelligence::heartbeat::record_interaction(agent_id);
|
crate::intelligence::heartbeat::record_interaction(agent_id);
|
||||||
debug!("[intelligence_hooks] Recorded interaction for agent: {}", 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
|
// Step 2: Record conversation for reflection
|
||||||
let mut engine = reflection_state.lock().await;
|
let mut engine = reflection_state.lock().await;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user