butterfly-bot 0.3.0

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

use serde_json::Value;

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

pub struct BrainManager {
    config: Value,
    plugins: HashMap<String, Arc<dyn BrainPlugin>>,
    plugin_factories: HashMap<String, BrainFactory>,
}

type BrainFactory = Arc<dyn Fn(Value) -> Arc<dyn BrainPlugin> + Send + Sync>;

impl BrainManager {
    pub fn new(config: Value) -> Self {
        Self {
            config,
            plugins: HashMap::new(),
            plugin_factories: HashMap::new(),
        }
    }

    pub fn register_factory<F>(&mut self, name: &str, factory: F)
    where
        F: Fn(Value) -> Arc<dyn BrainPlugin> + Send + Sync + 'static,
    {
        self.plugin_factories
            .insert(name.to_string(), Arc::new(factory));
    }

    pub fn register_plugin(&mut self, plugin: Arc<dyn BrainPlugin>) -> bool {
        let name = plugin.name().to_string();
        if self.plugins.contains_key(&name) {
            return false;
        }
        self.plugins.insert(name, plugin);
        true
    }

    pub fn get_plugin(&self, name: &str) -> Option<Arc<dyn BrainPlugin>> {
        self.plugins.get(name).cloned()
    }

    pub fn list_plugins(&self) -> Vec<Value> {
        self.plugins
            .values()
            .map(|p| {
                serde_json::json!({
                    "name": p.name(),
                    "description": p.description(),
                })
            })
            .collect()
    }

    pub fn load_plugins(&mut self) -> Vec<String> {
        let mut loaded = Vec::new();

        let plugin_entries = self
            .config
            .get("brains")
            .and_then(|value| value.as_array())
            .cloned();

        let mut to_load: Vec<(String, Value)> = Vec::new();

        if let Some(entries) = plugin_entries {
            for entry in entries {
                match entry {
                    Value::String(name) => {
                        to_load.push((name, Value::Null));
                    }
                    Value::Object(map) => {
                        let name = map
                            .get("name")
                            .or_else(|| map.get("class"))
                            .and_then(|value| value.as_str())
                            .map(|name| name.to_string());
                        if let Some(name) = name {
                            let config = map.get("config").cloned().unwrap_or(Value::Null);
                            to_load.push((name, config));
                        }
                    }
                    _ => {}
                }
            }
        } else {
            let mut names: Vec<String> = self.plugin_factories.keys().cloned().collect();
            names.sort();
            for name in names {
                to_load.push((name, Value::Null));
            }
        }

        for (name, config) in to_load {
            if self.plugins.contains_key(&name) {
                continue;
            }
            if let Some(factory) = self.plugin_factories.get(&name) {
                let plugin = factory(config);
                if self.register_plugin(plugin) {
                    loaded.push(name);
                }
            }
        }

        loaded
    }

    pub fn configure(&mut self, config: Value) {
        self.config = config;
    }

    pub async fn dispatch(&self, event: BrainEvent, ctx: &BrainContext) {
        for plugin in self.plugins.values() {
            let _ = plugin.on_event(event.clone(), ctx).await;
        }
    }
}