use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct Message {
pub id: String,
pub role: MessageRole,
pub parts: Vec<MessagePart>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
impl Message {
pub fn user(parts: Vec<MessagePart>) -> Self {
Self {
id: Uuid::new_v4().to_string(),
role: MessageRole::User,
parts,
metadata: None,
}
}
pub fn agent(parts: Vec<MessagePart>) -> Self {
Self {
id: Uuid::new_v4().to_string(),
role: MessageRole::Agent,
parts,
metadata: None,
}
}
pub fn user_text(text: impl Into<String>) -> Self {
Self::user(vec![MessagePart::text(text)])
}
pub fn agent_text(text: impl Into<String>) -> Self {
Self::agent(vec![MessagePart::text(text)])
}
pub fn text_content(&self) -> String {
self.parts
.iter()
.filter_map(|p| match p {
MessagePart::Text { text, .. } => Some(text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum MessageRole {
User,
Agent,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum MessagePart {
#[serde(rename = "text")]
Text {
text: String,
#[serde(skip_serializing_if = "Option::is_none")]
media_type: Option<String>,
},
#[serde(rename = "file")]
File { file: FilePart },
#[serde(rename = "data")]
Data { data: DataPart },
}
impl MessagePart {
pub fn text(text: impl Into<String>) -> Self {
Self::Text {
text: text.into(),
media_type: None,
}
}
pub fn text_with_type(text: impl Into<String>, media_type: impl Into<String>) -> Self {
Self::Text {
text: text.into(),
media_type: Some(media_type.into()),
}
}
pub fn file_inline(
name: impl Into<String>,
media_type: impl Into<String>,
data: Vec<u8>,
) -> Self {
use base64::Engine;
Self::File {
file: FilePart {
name: Some(name.into()),
media_type: Some(media_type.into()),
data: Some(base64::engine::general_purpose::STANDARD.encode(data)),
url: None,
},
}
}
pub fn file_url(url: impl Into<String>, name: Option<String>) -> Self {
Self::File {
file: FilePart {
name,
media_type: None,
data: None,
url: Some(url.into()),
},
}
}
pub fn data(value: serde_json::Value, media_type: Option<String>) -> Self {
Self::Data {
data: DataPart { value, media_type },
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct FilePart {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub media_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct DataPart {
pub value: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub media_type: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_message_creation() {
let msg = Message::user_text("Hello, summarize this document");
assert_eq!(msg.role, MessageRole::User);
assert_eq!(msg.parts.len(), 1);
assert_eq!(msg.text_content(), "Hello, summarize this document");
}
#[test]
fn test_message_serialization() {
let msg = Message::user(vec![
MessagePart::text("Check this file"),
MessagePart::file_url("https://example.com/doc.pdf", Some("doc.pdf".into())),
MessagePart::data(
serde_json::json!({"priority": "high"}),
Some("application/json".into()),
),
]);
let json = serde_json::to_string_pretty(&msg).unwrap();
let parsed: Message = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.parts.len(), 3);
}
}