use serde::{Deserialize, Serialize};
use crate::archetype::{IdentityContent, compose_identity_prompt};
use crate::mood::{EmotionalState, compose_mood_prompt};
use crate::sentiment;
use crate::traits::PersonalityProfile;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AiConfig {
pub hoosh_endpoint: String,
pub inject_mood: bool,
pub sentiment_feedback: bool,
}
impl Default for AiConfig {
fn default() -> Self {
Self {
hoosh_endpoint: "http://localhost:8088".into(),
inject_mood: true,
sentiment_feedback: true,
}
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn compose_system_prompt(
profile: &PersonalityProfile,
identity: &IdentityContent,
mood: Option<&EmotionalState>,
spirit: Option<&str>,
) -> String {
let mut prompt = compose_identity_prompt(identity);
let disposition = profile.compose_prompt();
if !disposition.is_empty() {
prompt.push('\n');
prompt.push_str(&disposition);
}
if let Some(state) = mood {
prompt.push('\n');
prompt.push_str(&compose_mood_prompt(state));
}
if let Some(spirit_text) = spirit.filter(|s| !s.is_empty()) {
prompt.push_str("\n## Spirit\n\n");
prompt.push_str(spirit_text);
prompt.push('\n');
}
prompt
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
pub fn apply_sentiment_feedback(
response_text: &str,
state: &mut EmotionalState,
scale: f32,
) -> sentiment::SentimentResult {
let result = sentiment::analyze(response_text);
let scale = scale.clamp(0.0, 1.0);
for &(emotion, intensity) in &result.emotions {
state.stimulate(emotion, intensity * scale);
}
result
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PersonalityMetadata {
pub name: String,
pub description: Option<String>,
pub active_traits: Vec<(String, String)>,
pub mood_state: Option<String>,
pub group_averages: Vec<(String, f32)>,
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn build_personality_metadata(
profile: &PersonalityProfile,
mood: Option<&EmotionalState>,
) -> PersonalityMetadata {
use crate::traits::TraitGroup;
let active_traits = profile
.active_traits()
.into_iter()
.map(|tv| (tv.trait_name.to_string(), tv.level.to_string()))
.collect();
let group_averages = TraitGroup::ALL
.iter()
.map(|&g| (g.to_string(), profile.group_average(g)))
.collect();
let mood_state = mood.map(|s: &EmotionalState| s.classify().to_string());
PersonalityMetadata {
name: profile.name.clone(),
description: profile.description.clone(),
active_traits,
mood_state,
group_averages,
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
pub fn feedback_from_outcome(state: &mut EmotionalState, outcome: InteractionOutcome) {
match outcome {
InteractionOutcome::Praised => state.apply_trigger(&crate::mood::trigger_praised()),
InteractionOutcome::Criticized => state.apply_trigger(&crate::mood::trigger_criticized()),
InteractionOutcome::Surprised => state.apply_trigger(&crate::mood::trigger_surprised()),
InteractionOutcome::Threatened => state.apply_trigger(&crate::mood::trigger_threatened()),
InteractionOutcome::Neutral => {}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum InteractionOutcome {
Praised,
Criticized,
Surprised,
Threatened,
Neutral,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::archetype::IdentityLayer;
use crate::mood::Emotion;
use crate::traits::{TraitKind, TraitLevel};
fn test_profile() -> PersonalityProfile {
let mut p = PersonalityProfile::new("TestBot");
p.set_trait(TraitKind::Humor, TraitLevel::High);
p.set_trait(TraitKind::Warmth, TraitLevel::Highest);
p
}
fn test_identity() -> IdentityContent {
let mut id = IdentityContent::default();
id.set(IdentityLayer::Soul, "You are a helpful test bot.");
id
}
#[test]
fn test_ai_config_default() {
let c = AiConfig::default();
assert_eq!(c.hoosh_endpoint, "http://localhost:8088");
assert!(c.inject_mood);
assert!(c.sentiment_feedback);
}
#[test]
fn test_ai_config_serde() {
let c = AiConfig::default();
let json = serde_json::to_string(&c).unwrap();
let c2: AiConfig = serde_json::from_str(&json).unwrap();
assert_eq!(c2.hoosh_endpoint, c.hoosh_endpoint);
}
#[test]
fn test_compose_system_prompt_basic() {
let profile = test_profile();
let identity = test_identity();
let prompt = compose_system_prompt(&profile, &identity, None, None);
assert!(prompt.contains("In Our Image"));
assert!(prompt.contains("### Soul"));
assert!(prompt.contains("## Personality"));
}
#[test]
fn test_compose_system_prompt_with_mood() {
let profile = test_profile();
let identity = test_identity();
let mut mood = EmotionalState::new();
mood.stimulate(Emotion::Joy, 0.8);
let prompt = compose_system_prompt(&profile, &identity, Some(&mood), None);
assert!(prompt.contains("Current Mood"));
}
#[test]
fn test_compose_system_prompt_with_spirit() {
let profile = test_profile();
let identity = test_identity();
let spirit = "I am driven by curiosity and a love of learning.";
let prompt = compose_system_prompt(&profile, &identity, None, Some(spirit));
assert!(prompt.contains("## Spirit"));
assert!(prompt.contains("curiosity"));
}
#[test]
fn test_compose_system_prompt_full() {
let profile = test_profile();
let identity = test_identity();
let mut mood = EmotionalState::new();
mood.stimulate(Emotion::Joy, 0.5);
let spirit = "Passionate about helping.";
let prompt = compose_system_prompt(&profile, &identity, Some(&mood), Some(spirit));
assert!(prompt.contains("In Our Image"));
assert!(prompt.contains("## Personality"));
assert!(prompt.contains("Current Mood"));
assert!(prompt.contains("## Spirit"));
}
#[test]
fn test_apply_sentiment_feedback_positive() {
let mut state = EmotionalState::new();
let result =
apply_sentiment_feedback("This is wonderful and amazing work!", &mut state, 1.0);
assert!(result.is_positive());
assert!(state.mood.joy > 0.0);
}
#[test]
fn test_apply_sentiment_feedback_negative() {
let mut state = EmotionalState::new();
let result = apply_sentiment_feedback("This is terrible and broken.", &mut state, 1.0);
assert!(result.is_negative());
assert!(state.mood.joy < 0.0);
}
#[test]
fn test_apply_sentiment_feedback_scaled() {
let mut full = EmotionalState::new();
let mut half = EmotionalState::new();
apply_sentiment_feedback("This is great!", &mut full, 1.0);
apply_sentiment_feedback("This is great!", &mut half, 0.5);
assert!(full.mood.joy > half.mood.joy);
}
#[test]
fn test_apply_sentiment_feedback_zero_scale() {
let mut state = EmotionalState::new();
apply_sentiment_feedback("Amazing wonderful fantastic!", &mut state, 0.0);
assert!(state.deviation() < f32::EPSILON);
}
#[test]
fn test_build_personality_metadata() {
let profile = test_profile();
let meta = build_personality_metadata(&profile, None);
assert_eq!(meta.name, "TestBot");
assert!(!meta.active_traits.is_empty());
assert!(meta.mood_state.is_none());
assert_eq!(meta.group_averages.len(), 4);
}
#[test]
fn test_build_personality_metadata_with_mood() {
let profile = test_profile();
let mut mood = EmotionalState::new();
mood.stimulate(Emotion::Joy, 0.8);
let meta = build_personality_metadata(&profile, Some(&mood));
assert!(meta.mood_state.is_some());
}
#[test]
fn test_personality_metadata_serde() {
let profile = test_profile();
let meta = build_personality_metadata(&profile, None);
let json = serde_json::to_string(&meta).unwrap();
let meta2: PersonalityMetadata = serde_json::from_str(&json).unwrap();
assert_eq!(meta2.name, "TestBot");
}
#[test]
fn test_feedback_from_outcome() {
let mut state = EmotionalState::new();
feedback_from_outcome(&mut state, InteractionOutcome::Praised);
assert!(state.mood.joy > 0.0);
let mut state2 = EmotionalState::new();
feedback_from_outcome(&mut state2, InteractionOutcome::Criticized);
assert!(state2.mood.joy < 0.0);
}
#[test]
fn test_feedback_neutral_noop() {
let mut state = EmotionalState::new();
feedback_from_outcome(&mut state, InteractionOutcome::Neutral);
assert!(state.deviation() < f32::EPSILON);
}
#[test]
fn test_interaction_outcome_serde() {
for outcome in [
InteractionOutcome::Praised,
InteractionOutcome::Criticized,
InteractionOutcome::Surprised,
InteractionOutcome::Threatened,
InteractionOutcome::Neutral,
] {
let json = serde_json::to_string(&outcome).unwrap();
let restored: InteractionOutcome = serde_json::from_str(&json).unwrap();
assert_eq!(restored, outcome);
}
}
}