butterfly-bot 0.3.1

Butterfly Bot is an opinionated personal-ops AI assistant built for people who want results, not setup overhead.
Documentation
use std::collections::HashMap;

use async_trait::async_trait;
use tokio::sync::Mutex;

use crate::error::Result;
use crate::interfaces::brain::{BrainContext, BrainEvent, BrainPlugin};

#[derive(Debug, Clone)]
pub struct EngagementProfile {
    pub total_messages: u32,
    pub engagement_level: String,
    pub churn_risk: f32,
}

pub struct BusinessIntelligenceBrain {
    profiles: Mutex<HashMap<String, EngagementProfile>>,
    last_profile: Mutex<Option<EngagementProfile>>,
}

impl BusinessIntelligenceBrain {
    pub fn new() -> Self {
        Self {
            profiles: Mutex::new(HashMap::new()),
            last_profile: Mutex::new(None),
        }
    }

    pub async fn last_profile(&self) -> Option<EngagementProfile> {
        let guard = self.last_profile.lock().await;
        guard.clone()
    }

    fn classify_engagement(total_messages: u32) -> (String, f32) {
        if total_messages >= 25 {
            ("power_user".to_string(), 0.1)
        } else if total_messages >= 10 {
            ("active".to_string(), 0.25)
        } else if total_messages >= 3 {
            ("casual".to_string(), 0.4)
        } else {
            ("at_risk".to_string(), 0.7)
        }
    }
}

#[async_trait]
impl BrainPlugin for BusinessIntelligenceBrain {
    fn name(&self) -> &str {
        "business_intelligence"
    }

    fn description(&self) -> &str {
        "Tracks lightweight engagement signals"
    }

    async fn on_event(&self, event: BrainEvent, ctx: &BrainContext) -> Result<()> {
        if let BrainEvent::UserMessage { .. } = event {
            let user_id = ctx.user_id.clone().unwrap_or_default();
            let mut profiles = self.profiles.lock().await;
            let entry = profiles
                .entry(user_id.clone())
                .or_insert_with(|| EngagementProfile {
                    total_messages: 0,
                    engagement_level: "new".to_string(),
                    churn_risk: 0.5,
                });
            entry.total_messages += 1;
            let (level, churn) = Self::classify_engagement(entry.total_messages);
            entry.engagement_level = level;
            entry.churn_risk = churn;
            let mut last = self.last_profile.lock().await;
            *last = Some(entry.clone());
        }
        Ok(())
    }
}