use alloc::{string::String, vec::Vec};
use mime::Mime;
use url::Url;
use super::event::ToolCall;
use super::reasoning::ReasoningState;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Attachment {
url: Url,
#[cfg_attr(feature = "serde", serde(with = "mime_serde"))]
media_type: Mime,
}
impl Attachment {
#[must_use]
pub const fn new(url: Url, media_type: Mime) -> Self {
Self { url, media_type }
}
#[must_use]
pub const fn url(&self) -> &Url {
&self.url
}
#[must_use]
pub const fn media_type(&self) -> &Mime {
&self.media_type
}
#[must_use]
pub fn with_url(self, url: Url) -> Self {
Self {
url,
media_type: self.media_type,
}
}
#[must_use]
pub fn into_parts(self) -> (Url, Mime) {
(self.url, self.media_type)
}
}
#[cfg(feature = "serde")]
mod mime_serde {
use alloc::string::String;
use core::str::FromStr;
use mime::Mime;
use serde::{Deserialize, Deserializer, Serializer, de::Error as _};
pub fn serialize<S>(media_type: &Mime, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(media_type.as_ref())
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Mime, D::Error>
where
D: Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
Mime::from_str(&raw).map_err(D::Error::custom)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Role {
User,
Assistant,
System,
Tool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "role", rename_all = "snake_case"))]
pub enum Message {
User {
content: String,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Vec::is_empty")
)]
attachments: Vec<Attachment>,
},
Assistant {
content: String,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Vec::is_empty")
)]
tool_calls: Vec<ToolCall>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Vec::is_empty")
)]
reasoning: Vec<ReasoningState>,
},
System {
content: String,
},
Tool {
content: String,
tool_call_id: String,
},
}
impl Message {
#[must_use]
pub const fn role(&self) -> Role {
match self {
Self::User { .. } => Role::User,
Self::Assistant { .. } => Role::Assistant,
Self::System { .. } => Role::System,
Self::Tool { .. } => Role::Tool,
}
}
#[must_use]
pub fn content(&self) -> &str {
match self {
Self::User { content, .. }
| Self::Assistant { content, .. }
| Self::System { content }
| Self::Tool { content, .. } => content,
}
}
#[must_use]
pub fn attachments(&self) -> &[Attachment] {
match self {
Self::User { attachments, .. } => attachments,
_ => &[],
}
}
#[must_use]
pub fn tool_calls(&self) -> &[ToolCall] {
match self {
Self::Assistant { tool_calls, .. } => tool_calls,
_ => &[],
}
}
#[must_use]
pub fn tool_call_id(&self) -> Option<&str> {
match self {
Self::Tool { tool_call_id, .. } => Some(tool_call_id),
_ => None,
}
}
pub fn user(content: impl Into<String>) -> Self {
Self::User {
content: content.into(),
attachments: Vec::new(),
}
}
pub fn assistant(content: impl Into<String>) -> Self {
Self::Assistant {
content: content.into(),
tool_calls: Vec::new(),
reasoning: Vec::new(),
}
}
pub fn assistant_with_tool_calls(
content: impl Into<String>,
tool_calls: Vec<ToolCall>,
) -> Self {
Self::Assistant {
content: content.into(),
tool_calls,
reasoning: Vec::new(),
}
}
pub fn assistant_with_reasoning(
content: impl Into<String>,
tool_calls: Vec<ToolCall>,
reasoning: Vec<ReasoningState>,
) -> Self {
Self::Assistant {
content: content.into(),
tool_calls,
reasoning,
}
}
#[must_use]
pub fn reasoning(&self) -> &[ReasoningState] {
match self {
Self::Assistant { reasoning, .. } => reasoning,
_ => &[],
}
}
pub fn system(content: impl Into<String>) -> Self {
Self::System {
content: content.into(),
}
}
pub fn tool(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
Self::Tool {
content: content.into(),
tool_call_id: tool_call_id.into(),
}
}
#[must_use]
pub fn with_attachment(mut self, attachment: Attachment) -> Self {
if let Self::User { attachments, .. } = &mut self {
attachments.push(attachment);
}
self
}
#[must_use]
pub fn with_attachments(mut self, values: impl IntoIterator<Item = Attachment>) -> Self {
if let Self::User { attachments, .. } = &mut self {
attachments.extend(values);
}
self
}
#[must_use]
pub fn with_tool_calls(mut self, calls: Vec<ToolCall>) -> Self {
if let Self::Assistant { tool_calls, .. } = &mut self {
*tool_calls = calls;
}
self
}
}
#[cfg(test)]
mod tests {
use alloc::vec;
use super::*;
#[test]
fn role_equality() {
assert_eq!(Role::User, Role::User);
assert_eq!(Role::Assistant, Role::Assistant);
assert_eq!(Role::System, Role::System);
assert_eq!(Role::Tool, Role::Tool);
assert_ne!(Role::User, Role::Assistant);
}
#[test]
fn message_creation() {
let user = Message::user("Hello");
assert_eq!(user.role(), Role::User);
assert_eq!(user.content(), "Hello");
let assistant = Message::assistant("Hi there!");
assert_eq!(assistant.role(), Role::Assistant);
assert_eq!(assistant.content(), "Hi there!");
let system = Message::system("Be helpful");
assert_eq!(system.role(), Role::System);
assert_eq!(system.content(), "Be helpful");
let tool = Message::tool("call_123", "Success");
assert_eq!(tool.role(), Role::Tool);
assert_eq!(tool.content(), "Success");
assert_eq!(tool.tool_call_id(), Some("call_123"));
}
#[test]
fn assistant_with_tool_calls() {
let tool_calls = vec![ToolCall::new(
"call_1",
"get_weather",
serde_json::json!({"city": "NYC"}),
)];
let msg = Message::assistant_with_tool_calls("", tool_calls);
assert_eq!(msg.tool_calls().len(), 1);
assert_eq!(msg.tool_calls()[0].name, "get_weather");
}
#[test]
fn message_with_attachment() {
let attachment = Attachment::new(
"https://example.com/image.png".parse::<Url>().unwrap(),
mime::IMAGE_PNG,
);
let message = Message::user("Hello").with_attachment(attachment.clone());
assert_eq!(message.attachments(), &[attachment]);
}
#[test]
fn message_with_attachments() {
let attachments = vec![
Attachment::new(
"https://example.com/a.png".parse::<Url>().unwrap(),
mime::IMAGE_PNG,
),
Attachment::new(
"https://example.com/b.pdf".parse::<Url>().unwrap(),
mime::APPLICATION_PDF,
),
];
let message = Message::user("Hello").with_attachments(attachments.clone());
assert_eq!(message.attachments(), attachments.as_slice());
}
#[test]
fn attachments_are_ignored_for_non_user_messages() {
let attachment = Attachment::new(
"https://example.com/a.png".parse::<Url>().unwrap(),
mime::IMAGE_PNG,
);
let message = Message::assistant("Hello").with_attachment(attachment);
assert!(
message.attachments().is_empty(),
"expected no attachments, got {:?}",
message.attachments()
);
}
#[test]
fn message_clone() {
let original = Message::user("Original");
let cloned = original.clone();
assert_eq!(original.content(), cloned.content());
}
}