use core::fmt::Debug;
use alloc::{string::String, vec::Vec};
use url::Url;
#[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, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Message {
attachments: Vec<Url>,
annotation: Vec<Annotation>,
content: String,
role: Role,
}
impl Message {
#[must_use]
pub const fn role(&self) -> Role {
self.role
}
#[must_use]
pub const fn content(&self) -> &str {
self.content.as_str()
}
#[must_use]
pub const fn attachments(&self) -> &[Url] {
self.attachments.as_slice()
}
#[must_use]
pub const fn annotations(&self) -> &[Annotation] {
self.annotation.as_slice()
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct UrlAnnotation {
pub url: Url,
pub title: String,
pub content: String,
pub start: usize,
pub end: usize,
}
impl UrlAnnotation {
#[must_use]
pub fn new(
url: impl TryInto<Url, Error: Debug>,
title: impl Into<String>,
content: impl Into<String>,
start: usize,
end: usize,
) -> Self {
Self {
url: url.try_into().unwrap(),
title: title.into(),
content: content.into(),
start,
end,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Annotation {
Url(UrlAnnotation),
}
impl Message {
#[must_use]
pub const fn new(role: Role, content: String) -> Self {
Self {
role,
content,
attachments: Vec::new(),
annotation: Vec::new(),
}
}
pub fn user(content: impl Into<String>) -> Self {
Self::new(Role::User, content.into())
}
pub fn assistant(content: impl Into<String>) -> Self {
Self::new(Role::Assistant, content.into())
}
pub fn system(content: impl Into<String>) -> Self {
Self::new(Role::System, content.into())
}
pub fn tool(content: impl Into<String>) -> Self {
Self::new(Role::Tool, content.into())
}
#[must_use]
pub fn with_attachment<U: TryInto<Url, Error: Debug>>(mut self, url: U) -> Self {
self.attachments.push(url.try_into().unwrap());
self
}
#[must_use]
pub fn with_attachments<U: TryInto<Url, Error: Debug>>(
mut self,
urls: impl IntoIterator<Item = U>,
) -> Self {
self.attachments
.extend(urls.into_iter().map(|url| url.try_into().unwrap()));
self
}
#[must_use]
pub fn with_annotation(mut self, annotation: Annotation) -> Self {
self.annotation.push(annotation);
self
}
#[must_use]
pub fn with_annotations(mut self, annotations: impl IntoIterator<Item = Annotation>) -> Self {
self.annotation.extend(annotations);
self
}
}
impl Annotation {
#[must_use]
pub fn url(
url: impl TryInto<Url, Error: Debug>,
title: impl Into<String>,
content: impl Into<String>,
start: usize,
end: usize,
) -> Self {
Self::Url(UrlAnnotation::new(url, title, content, start, end))
}
}
#[cfg(test)]
mod tests {
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);
assert_ne!(Role::System, Role::Tool);
}
#[test]
fn message_creation() {
let message = Message::new(Role::User, "Hello".into());
assert_eq!(message.role, Role::User);
assert_eq!(message.content, "Hello");
assert!(message.attachments.is_empty());
assert!(message.annotation.is_empty());
}
#[test]
fn message_convenience_constructors() {
let user_msg = Message::user("User message");
assert_eq!(user_msg.role, Role::User);
assert_eq!(user_msg.content, "User message");
let assistant_msg = Message::assistant("Assistant message");
assert_eq!(assistant_msg.role, Role::Assistant);
assert_eq!(assistant_msg.content, "Assistant message");
let system_msg = Message::system("System message");
assert_eq!(system_msg.role, Role::System);
assert_eq!(system_msg.content, "System message");
let tool_msg = Message::tool("Tool message");
assert_eq!(tool_msg.role, Role::Tool);
assert_eq!(tool_msg.content, "Tool message");
}
#[test]
fn message_with_attachment() {
let url = "https://example.com".parse::<Url>().unwrap();
let message = Message::user("Hello").with_attachment(url.clone());
assert_eq!(message.attachments.len(), 1);
assert_eq!(message.attachments[0], url);
}
#[test]
fn message_with_multiple_attachments() {
let urls = [
"https://example.com".parse::<Url>().unwrap(),
"https://example.org".parse::<Url>().unwrap(),
];
let message = Message::user("Hello").with_attachments(urls.clone());
assert_eq!(message.attachments.len(), 2);
assert_eq!(message.attachments, urls);
}
#[test]
fn url_annotation() {
let url = "https://example.com".parse::<Url>().unwrap();
let annotation = UrlAnnotation {
url: url.clone(),
title: "Example".into(),
content: "Example content".into(),
start: 0,
end: 10,
};
assert_eq!(annotation.url, url);
assert_eq!(annotation.title, "Example");
assert_eq!(annotation.content, "Example content");
assert_eq!(annotation.start, 0);
assert_eq!(annotation.end, 10);
}
#[test]
fn annotation_enum() {
let url = "https://example.com".parse::<Url>().unwrap();
let url_annotation = UrlAnnotation {
url,
title: "Example".into(),
content: "Example content".into(),
start: 0,
end: 10,
};
let annotation = Annotation::Url(url_annotation.clone());
match annotation {
Annotation::Url(url_anno) => {
assert_eq!(url_anno.title, url_annotation.title);
assert_eq!(url_anno.content, url_annotation.content);
}
}
}
#[test]
fn message_debug() {
let message = Message::user("Test message");
let debug_str = alloc::format!("{message:?}");
assert!(debug_str.contains("User"));
assert!(debug_str.contains("Test message"));
}
#[test]
fn message_clone() {
let original = Message::user("Original message");
let cloned = original.clone();
assert_eq!(original.role, cloned.role);
assert_eq!(original.content, cloned.content);
assert_eq!(original.attachments.len(), cloned.attachments.len());
assert_eq!(original.annotation.len(), cloned.annotation.len());
}
#[test]
fn message_with_annotation() {
let url = "https://example.com".parse::<Url>().unwrap();
let url_annotation = Annotation::url(url.clone(), "Example", "Example content", 0, 10);
let message = Message::user("Visit https://example.com").with_annotation(url_annotation);
assert_eq!(message.annotation.len(), 1);
match &message.annotation[0] {
Annotation::Url(annotation) => {
assert_eq!(annotation.url, url);
assert_eq!(annotation.title, "Example");
assert_eq!(annotation.content, "Example content");
assert_eq!(annotation.start, 0);
assert_eq!(annotation.end, 10);
}
}
}
#[test]
fn message_with_multiple_annotations() {
let url1 = "https://example.com";
let url2 = "https://example.org";
let annotations = [
Annotation::url(url1, "Example 1", "First example", 0, 10),
Annotation::url(url2, "Example 2", "Second example", 20, 30),
];
let message = Message::user("Visit these sites").with_annotations(annotations);
assert_eq!(message.annotation.len(), 2);
}
#[test]
fn url_annotation_constructor() {
let url = "https://example.com".parse::<Url>().unwrap();
let annotation = UrlAnnotation::new(url.clone(), "Test Title", "Test Content", 5, 15);
assert_eq!(annotation.url, url);
assert_eq!(annotation.title, "Test Title");
assert_eq!(annotation.content, "Test Content");
assert_eq!(annotation.start, 5);
assert_eq!(annotation.end, 15);
}
}