use serde::{Deserialize, Serialize};
use crate::types::{Normalized01, ThresholdClassifier};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EqProfile {
pub perception: Normalized01,
pub facilitation: Normalized01,
pub understanding: Normalized01,
pub management: Normalized01,
}
impl Default for EqProfile {
fn default() -> Self {
Self {
perception: Normalized01::HALF,
facilitation: Normalized01::HALF,
understanding: Normalized01::HALF,
management: Normalized01::HALF,
}
}
}
impl EqProfile {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_scores(
perception: f32,
facilitation: f32,
understanding: f32,
management: f32,
) -> Self {
Self {
perception: Normalized01::new(perception),
facilitation: Normalized01::new(facilitation),
understanding: Normalized01::new(understanding),
management: Normalized01::new(management),
}
}
#[must_use]
#[inline]
pub fn overall(&self) -> f32 {
self.perception.get() * 0.15
+ self.facilitation.get() * 0.20
+ self.understanding.get() * 0.30
+ self.management.get() * 0.35
}
#[must_use]
#[inline]
pub fn get(&self, branch: EqBranch) -> f32 {
match branch {
EqBranch::Perception => self.perception.get(),
EqBranch::Facilitation => self.facilitation.get(),
EqBranch::Understanding => self.understanding.get(),
EqBranch::Management => self.management.get(),
}
}
#[inline]
pub fn set(&mut self, branch: EqBranch, value: f32) {
let v = Normalized01::new(value);
match branch {
EqBranch::Perception => self.perception = v,
EqBranch::Facilitation => self.facilitation = v,
EqBranch::Understanding => self.understanding = v,
EqBranch::Management => self.management = v,
}
}
#[must_use]
pub fn level(&self) -> EqLevel {
const CLASSIFIER: ThresholdClassifier<EqLevel> = ThresholdClassifier::new(
&[
(0.8, EqLevel::Exceptional),
(0.6, EqLevel::High),
(0.4, EqLevel::Average),
(0.2, EqLevel::Low),
],
EqLevel::Minimal,
);
CLASSIFIER.classify(self.overall())
}
#[must_use]
pub fn perception_bonus(&self) -> f32 {
0.5 + self.perception.get()
}
#[must_use]
pub fn facilitation_bonus(&self) -> f32 {
0.5 + self.facilitation.get()
}
#[must_use]
pub fn management_bonus(&self) -> f32 {
0.5 + self.management.get()
}
#[must_use]
pub fn stress_recovery_bonus(&self) -> f32 {
0.8 + self.management.get() * 0.7
}
#[must_use]
pub fn contagion_resistance(&self) -> f32 {
self.management.get() * 0.5
}
#[must_use]
pub fn appraisal_bonus(&self) -> f32 {
0.5 + self.understanding.get()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum EqBranch {
Perception,
Facilitation,
Understanding,
Management,
}
impl EqBranch {
pub const ALL: &'static [EqBranch] = &[
Self::Perception,
Self::Facilitation,
Self::Understanding,
Self::Management,
];
}
impl_display!(EqBranch {
Perception => "perception",
Facilitation => "facilitation",
Understanding => "understanding",
Management => "management",
});
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum EqLevel {
Minimal,
Low,
Average,
High,
Exceptional,
}
impl_display!(EqLevel {
Minimal => "minimal",
Low => "low",
Average => "average",
High => "high",
Exceptional => "exceptional",
});
#[cfg(feature = "traits")]
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn eq_from_personality(profile: &crate::traits::PersonalityProfile) -> EqProfile {
use crate::traits::TraitKind;
let empathy = profile.get_trait(TraitKind::Empathy).normalized();
let curiosity = profile.get_trait(TraitKind::Curiosity).normalized();
let creativity = profile.get_trait(TraitKind::Creativity).normalized();
let confidence = profile.get_trait(TraitKind::Confidence).normalized();
let patience = profile.get_trait(TraitKind::Patience).normalized();
let pedagogy = profile.get_trait(TraitKind::Pedagogy).normalized();
let formality = profile.get_trait(TraitKind::Formality).normalized();
let to_score = |v: f32| ((v + 1.0) / 2.0).clamp(0.0, 1.0);
EqProfile {
perception: Normalized01::new(to_score((empathy + curiosity) / 2.0)),
facilitation: Normalized01::new(to_score((creativity + confidence) / 2.0)),
understanding: Normalized01::new(to_score((empathy + patience + pedagogy) / 3.0)),
management: Normalized01::new(to_score((patience + confidence + formality) / 3.0)),
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn compose_eq_prompt(eq: &EqProfile) -> String {
use std::fmt::Write;
let mut prompt = String::with_capacity(200);
prompt.push_str("## Emotional Intelligence\n\n");
let _ = writeln!(
prompt,
"- Overall EQ: {} ({:.0}%)",
eq.level(),
eq.overall() * 100.0
);
for &branch in EqBranch::ALL {
let score = eq.get(branch);
let label = match score {
s if s >= 0.8 => "exceptional",
s if s >= 0.6 => "strong",
s if s >= 0.4 => "moderate",
s if s >= 0.2 => "developing",
_ => "limited",
};
let _ = writeln!(prompt, "- {branch}: {label}");
}
prompt
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default() {
let eq = EqProfile::new();
assert!((eq.perception.get() - 0.5).abs() < f32::EPSILON);
assert!((eq.facilitation.get() - 0.5).abs() < f32::EPSILON);
assert!((eq.understanding.get() - 0.5).abs() < f32::EPSILON);
assert!((eq.management.get() - 0.5).abs() < f32::EPSILON);
}
#[test]
fn test_with_scores_clamps() {
let eq = EqProfile::with_scores(1.5, -0.5, 0.7, 0.3);
assert!((eq.perception.get() - 1.0).abs() < f32::EPSILON);
assert!(eq.facilitation.get().abs() < f32::EPSILON);
assert!((eq.understanding.get() - 0.7).abs() < f32::EPSILON);
}
#[test]
fn test_overall_weighted() {
let eq = EqProfile::with_scores(1.0, 1.0, 1.0, 1.0);
assert!((eq.overall() - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_overall_zero() {
let eq = EqProfile::with_scores(0.0, 0.0, 0.0, 0.0);
assert!(eq.overall().abs() < f32::EPSILON);
}
#[test]
fn test_overall_management_heavy() {
let high_mgmt = EqProfile::with_scores(0.0, 0.0, 0.0, 1.0);
let high_perc = EqProfile::with_scores(1.0, 0.0, 0.0, 0.0);
assert!(high_mgmt.overall() > high_perc.overall());
}
#[test]
fn test_get_set() {
let mut eq = EqProfile::new();
eq.set(EqBranch::Perception, 0.9);
assert!((eq.get(EqBranch::Perception) - 0.9).abs() < f32::EPSILON);
eq.set(EqBranch::Management, 1.5); assert!((eq.get(EqBranch::Management) - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_level_exceptional() {
let eq = EqProfile::with_scores(1.0, 1.0, 1.0, 1.0);
assert_eq!(eq.level(), EqLevel::Exceptional);
}
#[test]
fn test_level_minimal() {
let eq = EqProfile::with_scores(0.0, 0.0, 0.0, 0.0);
assert_eq!(eq.level(), EqLevel::Minimal);
}
#[test]
fn test_level_average() {
let eq = EqProfile::new(); assert_eq!(eq.level(), EqLevel::Average);
}
#[test]
fn test_perception_bonus_range() {
let low = EqProfile::with_scores(0.0, 0.5, 0.5, 0.5);
let high = EqProfile::with_scores(1.0, 0.5, 0.5, 0.5);
assert!((low.perception_bonus() - 0.5).abs() < f32::EPSILON);
assert!((high.perception_bonus() - 1.5).abs() < f32::EPSILON);
}
#[test]
fn test_management_bonus_range() {
let low = EqProfile::with_scores(0.5, 0.5, 0.5, 0.0);
let high = EqProfile::with_scores(0.5, 0.5, 0.5, 1.0);
assert!((low.management_bonus() - 0.5).abs() < f32::EPSILON);
assert!((high.management_bonus() - 1.5).abs() < f32::EPSILON);
}
#[test]
fn test_stress_recovery_bonus() {
let eq = EqProfile::with_scores(0.5, 0.5, 0.5, 1.0);
assert!((eq.stress_recovery_bonus() - 1.5).abs() < f32::EPSILON);
let low = EqProfile::with_scores(0.5, 0.5, 0.5, 0.0);
assert!((low.stress_recovery_bonus() - 0.8).abs() < f32::EPSILON);
}
#[test]
fn test_contagion_resistance() {
let eq = EqProfile::with_scores(0.5, 0.5, 0.5, 1.0);
assert!((eq.contagion_resistance() - 0.5).abs() < f32::EPSILON);
let low = EqProfile::with_scores(0.5, 0.5, 0.5, 0.0);
assert!(low.contagion_resistance().abs() < f32::EPSILON);
}
#[test]
fn test_branch_display() {
assert_eq!(EqBranch::Perception.to_string(), "perception");
assert_eq!(EqBranch::Management.to_string(), "management");
}
#[test]
fn test_level_display() {
assert_eq!(EqLevel::Exceptional.to_string(), "exceptional");
assert_eq!(EqLevel::Minimal.to_string(), "minimal");
}
#[test]
fn test_branch_all() {
assert_eq!(EqBranch::ALL.len(), 4);
}
#[test]
fn test_compose_prompt() {
let eq = EqProfile::with_scores(0.9, 0.7, 0.8, 0.6);
let prompt = compose_eq_prompt(&eq);
assert!(prompt.contains("## Emotional Intelligence"));
assert!(prompt.contains("perception"));
assert!(prompt.contains("management"));
}
#[test]
fn test_serde_profile() {
let eq = EqProfile::with_scores(0.8, 0.6, 0.7, 0.9);
let json = serde_json::to_string(&eq).unwrap();
let eq2: EqProfile = serde_json::from_str(&json).unwrap();
assert!((eq2.perception.get() - eq.perception.get()).abs() < f32::EPSILON);
assert!((eq2.management.get() - eq.management.get()).abs() < f32::EPSILON);
}
#[test]
fn test_serde_branch() {
let b = EqBranch::Understanding;
let json = serde_json::to_string(&b).unwrap();
let b2: EqBranch = serde_json::from_str(&json).unwrap();
assert_eq!(b2, b);
}
#[test]
fn test_serde_level() {
let l = EqLevel::High;
let json = serde_json::to_string(&l).unwrap();
let l2: EqLevel = serde_json::from_str(&json).unwrap();
assert_eq!(l2, l);
}
#[cfg(feature = "traits")]
#[test]
fn test_eq_from_personality_empathetic() {
let mut p = crate::traits::PersonalityProfile::new("empath");
p.set_trait(
crate::traits::TraitKind::Empathy,
crate::traits::TraitLevel::Highest,
);
p.set_trait(
crate::traits::TraitKind::Patience,
crate::traits::TraitLevel::Highest,
);
p.set_trait(
crate::traits::TraitKind::Curiosity,
crate::traits::TraitLevel::High,
);
let eq = eq_from_personality(&p);
assert!(
eq.perception.get() > 0.6,
"perception: {}",
eq.perception.get()
);
assert!(
eq.understanding.get() > 0.6,
"understanding: {}",
eq.understanding.get()
);
}
#[cfg(feature = "traits")]
#[test]
fn test_eq_from_personality_stoic() {
let mut p = crate::traits::PersonalityProfile::new("stoic");
p.set_trait(
crate::traits::TraitKind::Formality,
crate::traits::TraitLevel::Highest,
);
p.set_trait(
crate::traits::TraitKind::Confidence,
crate::traits::TraitLevel::Highest,
);
p.set_trait(
crate::traits::TraitKind::Empathy,
crate::traits::TraitLevel::Lowest,
);
let eq = eq_from_personality(&p);
assert!(
eq.management.get() > eq.perception.get(),
"mgmt={} perc={}",
eq.management.get(),
eq.perception.get()
);
}
#[cfg(feature = "traits")]
#[test]
fn test_eq_from_personality_balanced() {
let p = crate::traits::PersonalityProfile::new("balanced");
let eq = eq_from_personality(&p);
for &branch in EqBranch::ALL {
let s = eq.get(branch);
assert!((s - 0.5).abs() < 0.1, "{branch}: {s} (expected ~0.5)");
}
}
}