zoey-core 0.1.1

ZoeyAI core runtime and types — privacy-first AI agent framework optimized for local models
Documentation
use crate::types::*;
use crate::Result;

pub fn mp1_language(text: &str) -> String {
    let s = text.to_lowercase();
    if s.contains("¿") || s.contains("¡") || s.contains(" hola ") || s.contains(" gracias ") {
        return "es".to_string();
    }
    if s.contains(" bonjour ") || s.contains(" merci ") || s.contains(" français ") {
        return "fr".to_string();
    }
    if s.contains(" hallo ") || s.contains(" danke ") {
        return "de".to_string();
    }
    "en".to_string()
}

pub fn mp2_classify(text: &str) -> (String, String, String) {
    let t = text.to_lowercase();
    let intent =
        if t.contains('?') || t.starts_with("what") || t.starts_with("how") || t.starts_with("why")
        {
            "Question"
        } else if t.contains("please") || t.starts_with("can you") {
            "Request"
        } else if t.contains("hello") || t.contains("hi") {
            "Greeting"
        } else {
            "Statement"
        };
    let positive = ["great", "thanks", "awesome", "good", "love"];
    let negative = ["bad", "hate", "terrible", "awful", "worse"];
    let mut pos = 0;
    let mut neg = 0;
    for w in positive.iter() {
        if t.contains(w) {
            pos += 1;
        }
    }
    for w in negative.iter() {
        if t.contains(w) {
            neg += 1;
        }
    }
    let sentiment = if pos > neg {
        "Positive"
    } else if neg > pos {
        "Negative"
    } else {
        "Neutral"
    };
    let tone = if t.contains("please") || t.contains("thank") {
        "Formal"
    } else if t.contains("lol") || t.contains(":)") {
        "Casual"
    } else {
        "Professional"
    };
    (intent.to_string(), sentiment.to_string(), tone.to_string())
}

pub fn mp3_topics_keywords(text: &str) -> (Vec<String>, Vec<String>) {
    let stop = [
        "the", "a", "an", "and", "or", "to", "of", "in", "on", "for", "with", "is", "it", "this",
        "that", "i", "you", "we", "they", "be", "are", "was", "were", "as", "at",
    ];
    let mut freq: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
    for token in text
        .split(|c: char| !c.is_alphanumeric())
        .filter(|s| !s.is_empty())
    {
        let lc = token.to_lowercase();
        if stop.contains(&lc.as_str()) {
            continue;
        }
        *freq.entry(lc).or_insert(0) += 1;
    }
    let mut items: Vec<(String, usize)> = freq.into_iter().collect();
    items.sort_by(|a, b| b.1.cmp(&a.1));
    let keywords: Vec<String> = items.iter().take(8).map(|(k, _)| k.clone()).collect();
    let topics = keywords.iter().take(4).cloned().collect();
    (topics, keywords)
}

pub fn mp4_entities_simple(text: &str) -> Vec<String> {
    let mut entities = Vec::new();
    let mut current = Vec::new();
    for w in text.split_whitespace() {
        let first = w.chars().next();
        if let Some(c) = first {
            if c.is_uppercase() {
                current.push(w.trim_matches(|ch: char| !ch.is_alphanumeric()).to_string());
            } else {
                if current.len() >= 1 {
                    entities.push(current.join(" "));
                    current.clear();
                }
            }
        } else {
            if current.len() >= 1 {
                entities.push(current.join(" "));
                current.clear();
            }
        }
    }
    if !current.is_empty() {
        entities.push(current.join(" "));
    }
    let mut uniq = std::collections::HashSet::new();
    let mut out = Vec::new();
    for e in entities {
        if !e.is_empty() && uniq.insert(e.to_lowercase()) {
            out.push(e);
        }
    }
    out
}

pub async fn mp5_complexity(
    runtime: std::sync::Arc<std::sync::RwLock<crate::AgentRuntime>>,
    message: &Memory,
) -> Option<crate::planner::complexity::ComplexityAssessment> {
    let analyzer = crate::planner::complexity::ComplexityAnalyzer::new();
    let state = State::new();
    analyzer.assess(message, &state).await.ok()
}

pub async fn mp6_embedding_queue(
    runtime: std::sync::Arc<std::sync::RwLock<crate::AgentRuntime>>,
    message: &Memory,
    text: String,
) -> bool {
    let rt_arc = runtime;
    let enabled = {
        let rt = rt_arc.read().unwrap();
        rt.get_setting("ui:phase0_embeddings")
            .and_then(|v| v.as_bool())
            .unwrap_or(false)
    };
    let adapter_opt = rt_arc.read().unwrap().adapter.read().unwrap().clone();
    let handler_opt = rt_arc
        .read()
        .unwrap()
        .models
        .read()
        .unwrap()
        .get("TEXT_EMBEDDING")
        .and_then(|v| v.first().map(|p| p.handler.clone()));
    let prompt_text = {
        let rt = rt_arc.read().unwrap();
        rt.get_setting("ui:lastPrompt")
            .and_then(|v| v.as_str().map(|s| s.to_string()))
            .unwrap_or_else(|| text.clone())
    };
    let mut updated = false;
    if enabled {
        if let (Some(adapter), Some(handler)) = (adapter_opt, handler_opt) {
            let params = crate::types::GenerateTextParams {
                prompt: prompt_text,
                max_tokens: None,
                temperature: None,
                top_p: None,
                stop: None,
                model: None,
                frequency_penalty: None,
                presence_penalty: None,
            };
            let mh_params = crate::types::ModelHandlerParams {
                runtime: std::sync::Arc::new(()),
                params,
            };
            if let Ok(json) = (handler)(mh_params).await {
                if let Ok(vec) = serde_json::from_str::<Vec<f32>>(&json) {
                    let mut mem = message.clone();
                    mem.embedding = Some(vec);
                    let _ = adapter.update_memory(&mem).await;
                    updated = true;
                }
            }
        }
    }
    if let Ok(mut rt) = rt_arc.write() {
        let key = if enabled && updated {
            "phase0:embedding:updated"
        } else {
            "phase0:embedding:queued"
        };
        rt.set_setting(key, serde_json::json!(true), false);
    }
    enabled && updated
}

pub fn mp7_available_actions(
    runtime: std::sync::Arc<std::sync::RwLock<crate::AgentRuntime>>,
) -> Vec<String> {
    let r = runtime.read().unwrap();
    let names: Vec<String> = {
        let ag = r.actions.read().unwrap();
        ag.iter().map(|a| a.name().to_string()).collect()
    };
    names
}

pub fn mp8_agent_candidates(
    runtime: std::sync::Arc<std::sync::RwLock<crate::AgentRuntime>>,
) -> Vec<String> {
    let r = runtime.read().unwrap();
    let mut out: Vec<String> = Vec::new();
    if let Some(svc) = r.get_service_by_name("multi-agent-coordination") {
        let capability = if let Some(intent) = r
            .get_setting("ui:intent")
            .and_then(|v| v.as_str().map(|s| s.to_string()))
        {
            match intent.as_str() {
                "Question" => "qa",
                "Request" => "task_execution",
                _ => "conversation",
            }
        } else {
            "conversation"
        };
        if let Some(cands) = svc.query_agents(capability) {
            for (id, _score) in cands {
                out.push(id.to_string());
            }
        }
    }
    out
}

pub fn mp9_keywords_phonetic_similarity(keywords: &[String]) -> (Vec<(String, String)>, f32) {
    use crate::nlp::{double_metaphone, normalized_similarity};
    let metaphones: Vec<(String, String)> = keywords.iter().map(|k| double_metaphone(k)).collect();
    let sim_hint: f32 = if keywords.len() > 1 {
        normalized_similarity(&keywords[0], &keywords[1])
    } else {
        0.0
    };
    (metaphones, sim_hint)
}