ambi 0.3.6

A flexible, multi-backend, customizable AI agent framework, entirely based on Rust.
Documentation
// src/types/message.rs

use serde::{Deserialize, Serialize};
use std::fmt;

/// Defines a single modality block within a user's message.
/// Used to construct rich, multi-modal prompts (e.g., mixing text and images) for Vision-Language Models (VLMs).
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ContentPart {
    /// A standard plain text segment.
    Text {
        /// The raw text content.
        text: String,
    },
    /// An image segment.
    Image {
        /// The image data, provided as either a base64-encoded string (e.g., `data:image/jpeg;base64,...`)
        /// or a standard HTTP URL, depending on the underlying engine's support.
        base64: String,
    },
}

/// Represents a single conversational turn or event within the LLM dialogue history.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub enum Message {
    /// Represents a system-level directive.
    ///
    /// **⚠️ Architectural Note:**
    /// Pushing `System` messages directly into `ChatHistory` is considered an anti-pattern in the Ambi framework.
    /// To ensure absolute safety against context eviction and to maximize KV Cache prefix matching,
    /// system instructions should be defined via:
    /// 1. `Agent::preamble` for static, immutable personas (Blueprint level).
    /// 2. `AgentState::dynamic_context` for volatile background knowledge like RAG results (Session level).
    System {
        /// The textual instruction content.
        content: String,
    },

    /// Represents a prompt or query provided by the human user.
    /// Supports multi-modal compositions (e.g., text paired with images).
    User {
        /// The sequential collection of text and media segments.
        content: Vec<ContentPart>,
    },

    /// Represents the observation or result returned after executing a tool (function call).
    Tool {
        /// The stringified output of the tool execution (often JSON, but can be plain text).
        content: String,
        /// The unique identifier linking this observation back to the specific tool call requested by the Assistant.
        tool_id: Option<String>,
    },

    /// Represents the response generated by the AI assistant.
    Assistant {
        /// The natural language text generated by the model.
        content: String,
        /// A list of actionable tool invocations requested by the model.
        /// Tuple signature: `(tool_name, arguments_json, tool_call_id)`
        tool_calls: Vec<(String, serde_json::Value, String)>,
    },
}

impl Message {
    /// Calculates the raw character length of all textual content within this message.
    /// Useful for lightweight heuristic checks or logging.
    pub fn text_len(&self) -> usize {
        match self {
            Message::System { content } => content.len(),
            Message::User { content } => content
                .iter()
                .map(|p| match p {
                    ContentPart::Text { text } => text.len(),
                    _ => 0,
                })
                .sum(),
            Message::Tool { content, .. } => content.len(),
            Message::Assistant { content, .. } => content.len(),
        }
    }

    /// A convenience constructor to quickly instantiate a multi-modal user message
    /// containing both text and a base64-encoded image.
    pub fn user_multimodal(text: &str, image_base64: &str) -> Self {
        Self::User {
            content: vec![
                ContentPart::Text {
                    text: text.to_string(),
                },
                ContentPart::Image {
                    base64: image_base64.to_string(),
                },
            ],
        }
    }
}

impl fmt::Display for Message {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Message::System { content } => write!(f, "{}", content),
            Message::User { content } => {
                let text: String = content
                    .iter()
                    .filter_map(|p| match p {
                        ContentPart::Text { text } => Some(text.as_str()),
                        _ => None,
                    })
                    .collect();
                write!(f, "{}", text)
            }
            Message::Tool { content, .. } => write!(f, "{}", content),
            Message::Assistant { content, .. } => write!(f, "{}", content),
        }
    }
}