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

- 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:
iven
2026-04-07 09:36:12 +08:00
parent 80cadd1158
commit af20487b8d
3 changed files with 371 additions and 0 deletions

View File

@@ -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;

View 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());
}

View File

@@ -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;