butterfly-bot 0.2.6

Butterfly Bot is an opinionated personal-ops AI assistant built for people who want results, not setup overhead.
Documentation
use async_trait::async_trait;
use futures::stream::BoxStream;
use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::error::Result;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
    pub name: String,
    pub arguments: Value,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LlmResponse {
    pub text: String,
    pub tool_calls: Vec<ToolCall>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatEvent {
    pub event_type: String,
    pub delta: Option<String>,
    pub name: Option<String>,
    pub arguments_delta: Option<String>,
    pub finish_reason: Option<String>,
    pub error: Option<String>,
}

#[derive(Debug, Clone)]
pub struct ImageInput {
    pub data: ImageData,
}

#[derive(Debug, Clone)]
pub enum ImageData {
    Url(String),
    Bytes(Vec<u8>),
}

#[async_trait]
pub trait LlmProvider: Send + Sync {
    async fn generate_text(
        &self,
        prompt: &str,
        system_prompt: &str,
        tools: Option<Vec<Value>>,
    ) -> Result<String>;

    async fn generate_with_tools(
        &self,
        prompt: &str,
        system_prompt: &str,
        tools: Vec<Value>,
    ) -> Result<LlmResponse>;

    fn chat_stream(
        &self,
        messages: Vec<Value>,
        tools: Option<Vec<Value>>,
    ) -> BoxStream<'static, Result<ChatEvent>>;

    async fn parse_structured_output(
        &self,
        prompt: &str,
        system_prompt: &str,
        json_schema: Value,
        tools: Option<Vec<Value>>,
    ) -> Result<Value>;

    async fn tts(&self, text: &str, voice: &str, response_format: &str) -> Result<Vec<u8>>;

    async fn transcribe_audio(&self, audio_bytes: Vec<u8>, input_format: &str) -> Result<String>;

    async fn generate_text_with_images(
        &self,
        prompt: &str,
        images: Vec<ImageInput>,
        system_prompt: &str,
        detail: &str,
        tools: Option<Vec<Value>>,
    ) -> Result<String>;

    async fn embed(&self, inputs: Vec<String>, model: Option<&str>) -> Result<Vec<Vec<f32>>>;
}

#[async_trait]
pub trait MemoryProvider: Send + Sync {
    async fn append_message(&self, user_id: &str, role: &str, content: &str) -> Result<()>;
    async fn get_history(&self, user_id: &str, limit: usize) -> Result<Vec<String>>;
    async fn clear_history(&self, user_id: &str) -> Result<()>;

    async fn store(&self, user_id: &str, messages: Vec<Value>) -> Result<()> {
        for msg in messages {
            let role = msg.get("role").and_then(|v| v.as_str()).unwrap_or("user");
            let content = msg.get("content").and_then(|v| v.as_str()).unwrap_or("");
            self.append_message(user_id, role, content).await?;
        }
        Ok(())
    }

    async fn retrieve(&self, user_id: &str) -> Result<String> {
        Ok(self.get_history(user_id, 0).await?.join("\n"))
    }

    async fn delete(&self, user_id: &str) -> Result<()> {
        self.clear_history(user_id).await
    }

    fn find(
        &self,
        _collection: &str,
        _query: Value,
        _sort: Option<Vec<(String, i32)>>,
        _limit: Option<u64>,
        _skip: Option<u64>,
    ) -> Result<Vec<Value>> {
        Ok(Vec::new())
    }

    fn count_documents(&self, _collection: &str, _query: Value) -> Result<u64> {
        Ok(0)
    }

    async fn search(&self, _user_id: &str, _query: &str, _limit: usize) -> Result<Vec<String>> {
        Ok(Vec::new())
    }
}