use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MessageRole {
System,
User,
Assistant,
Tool,
}
impl MessageRole {
pub fn can_initiate(&self) -> bool {
matches!(self, MessageRole::System | MessageRole::User)
}
pub fn can_respond(&self) -> bool {
matches!(self, MessageRole::Assistant | MessageRole::Tool)
}
}
impl std::fmt::Display for MessageRole {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MessageRole::System => write!(f, "system"),
MessageRole::User => write!(f, "user"),
MessageRole::Assistant => write!(f, "assistant"),
MessageRole::Tool => write!(f, "tool"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MessageContent {
Text(String),
MultiModal {
text: Option<String>,
attachments: Vec<ContentAttachment>,
},
}
impl MessageContent {
pub fn text(content: impl Into<String>) -> Self {
Self::Text(content.into())
}
pub fn multi_modal(text: impl Into<String>) -> Self {
Self::MultiModal {
text: Some(text.into()),
attachments: Vec::new(),
}
}
pub fn with_attachment(mut self, attachment: ContentAttachment) -> Self {
match &mut self {
Self::MultiModal { attachments, .. } => {
attachments.push(attachment);
}
Self::Text(text) => {
let text = text.clone();
self = Self::MultiModal {
text: Some(text),
attachments: vec![attachment],
};
}
}
self
}
pub fn text_content(&self) -> Option<&str> {
match self {
Self::Text(text) => Some(text),
Self::MultiModal { text, .. } => text.as_deref(),
}
}
pub fn attachments(&self) -> &[ContentAttachment] {
match self {
Self::Text(_) => &[],
Self::MultiModal { attachments, .. } => attachments,
}
}
pub fn is_empty(&self) -> bool {
match self {
Self::Text(text) => text.is_empty(),
Self::MultiModal { text, attachments } => {
text.as_ref().map_or(true, |t| t.is_empty()) && attachments.is_empty()
}
}
}
}
impl From<String> for MessageContent {
fn from(text: String) -> Self {
Self::Text(text)
}
}
impl From<&str> for MessageContent {
fn from(text: &str) -> Self {
Self::Text(text.to_string())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContentAttachment {
pub attachment_type: AttachmentType,
pub content: AttachmentContent,
pub metadata: Option<HashMap<String, serde_json::Value>>,
}
impl ContentAttachment {
pub fn image_base64(
mime_type: impl Into<String>,
data: impl Into<String>,
) -> Self {
Self {
attachment_type: AttachmentType::Image,
content: AttachmentContent::Base64 {
mime_type: mime_type.into(),
data: data.into(),
},
metadata: None,
}
}
pub fn image_url(url: impl Into<String>) -> Self {
Self {
attachment_type: AttachmentType::Image,
content: AttachmentContent::Url {
url: url.into(),
},
metadata: None,
}
}
pub fn with_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.metadata.get_or_insert_with(HashMap::new).insert(key.into(), value);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AttachmentType {
Image,
Audio,
Video,
Document,
Other,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum AttachmentContent {
Base64 {
mime_type: String,
data: String,
},
Url {
url: String,
},
#[serde(skip)]
Bytes {
mime_type: String,
data: Vec<u8>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatMessage {
pub role: MessageRole,
pub content: MessageContent,
pub name: Option<String>,
pub tool_calls: Option<Vec<ToolCall>>,
pub tool_call_id: Option<String>,
pub metadata: HashMap<String, serde_json::Value>,
#[serde(with = "chrono::serde::ts_seconds_option")]
pub timestamp: Option<chrono::DateTime<chrono::Utc>>,
}
impl ChatMessage {
pub fn new(role: MessageRole, content: impl Into<MessageContent>) -> Self {
Self {
role,
content: content.into(),
name: None,
tool_calls: None,
tool_call_id: None,
metadata: HashMap::new(),
timestamp: Some(chrono::Utc::now()),
}
}
pub fn system(content: impl Into<MessageContent>) -> Self {
Self::new(MessageRole::System, content)
}
pub fn user(content: impl Into<MessageContent>) -> Self {
Self::new(MessageRole::User, content)
}
pub fn assistant(content: impl Into<MessageContent>) -> Self {
Self::new(MessageRole::Assistant, content)
}
pub fn tool(tool_call_id: impl Into<String>, content: impl Into<MessageContent>) -> Self {
Self {
role: MessageRole::Tool,
content: content.into(),
name: None,
tool_calls: None,
tool_call_id: Some(tool_call_id.into()),
metadata: HashMap::new(),
timestamp: Some(chrono::Utc::now()),
}
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn with_tool_calls(mut self, tool_calls: Vec<ToolCall>) -> Self {
self.tool_calls = Some(tool_calls);
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.metadata.insert(key.into(), value);
self
}
pub fn text(&self) -> Option<&str> {
self.content.text_content()
}
pub fn is_empty(&self) -> bool {
self.content.is_empty()
}
pub fn len(&self) -> usize {
self.text().map_or(0, |t| t.len())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
#[serde(rename = "type")]
pub call_type: ToolCallType,
pub function: ToolFunction,
}
impl ToolCall {
pub fn function(
id: impl Into<String>,
name: impl Into<String>,
arguments: serde_json::Value,
) -> Self {
Self {
id: id.into(),
call_type: ToolCallType::Function,
function: ToolFunction {
name: name.into(),
arguments,
},
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ToolCallType {
Function,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolFunction {
pub name: String,
pub arguments: serde_json::Value,
}
pub struct MessageBuilder {
message: ChatMessage,
}
impl MessageBuilder {
pub fn new(role: MessageRole) -> Self {
Self {
message: ChatMessage {
role,
content: MessageContent::Text(String::new()),
name: None,
tool_calls: None,
tool_call_id: None,
metadata: HashMap::new(),
timestamp: Some(chrono::Utc::now()),
},
}
}
pub fn content(mut self, content: impl Into<MessageContent>) -> Self {
self.message.content = content.into();
self
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.message.name = Some(name.into());
self
}
pub fn tool_calls(mut self, tool_calls: Vec<ToolCall>) -> Self {
self.message.tool_calls = Some(tool_calls);
self
}
pub fn tool_call_id(mut self, tool_call_id: impl Into<String>) -> Self {
self.message.tool_call_id = Some(tool_call_id.into());
self
}
pub fn metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.message.metadata.insert(key.into(), value);
self
}
pub fn build(self) -> ChatMessage {
self.message
}
}
impl Default for MessageBuilder {
fn default() -> Self {
Self::new(MessageRole::User)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_message_creation() {
let msg = ChatMessage::user("Hello, world!");
assert_eq!(msg.role, MessageRole::User);
assert_eq!(msg.text(), Some("Hello, world!"));
assert!(!msg.is_empty());
}
#[test]
fn test_message_builder() {
let msg = MessageBuilder::new(MessageRole::Assistant)
.content("Hello there!")
.name("Assistant")
.metadata("source", serde_json::Value::String("test".to_string()))
.build();
assert_eq!(msg.role, MessageRole::Assistant);
assert_eq!(msg.text(), Some("Hello there!"));
assert_eq!(msg.name, Some("Assistant".to_string()));
assert!(msg.metadata.contains_key("source"));
}
#[test]
fn test_multi_modal_content() {
let content = MessageContent::multi_modal("Check this image")
.with_attachment(ContentAttachment::image_url("https://example.com/image.jpg"));
assert_eq!(content.text_content(), Some("Check this image"));
assert_eq!(content.attachments().len(), 1);
}
}