use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::id::ToolCallId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum Role {
System,
#[default]
User,
Assistant,
Tool,
Developer,
}
impl Role {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::System => "system",
Self::User => "user",
Self::Assistant => "assistant",
Self::Tool => "tool",
Self::Developer => "developer",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum ImageMime {
#[default]
Jpeg,
Png,
Gif,
WebP,
}
impl ImageMime {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Jpeg => "image/jpeg",
Self::Png => "image/png",
Self::Gif => "image/gif",
Self::WebP => "image/webp",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum ContentPart {
Text {
text: String,
},
Image {
mime: ImageMime,
url: String,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[allow(
clippy::derive_partial_eq_without_eq,
reason = "serde_json::Value is not Eq"
)]
pub struct ToolCall {
pub id: ToolCallId,
pub name: String,
pub arguments: Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[allow(
clippy::derive_partial_eq_without_eq,
reason = "contains ToolCall with JSON Value"
)]
pub struct Message {
pub role: Role,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub parts: Vec<ContentPart>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tool_calls: Vec<ToolCall>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<ToolCallId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
impl Message {
#[must_use]
pub fn system(content: impl Into<String>) -> Self {
Self {
role: Role::System,
content: Some(content.into()),
parts: Vec::new(),
tool_calls: Vec::new(),
tool_call_id: None,
name: None,
}
}
#[must_use]
pub fn user(content: impl Into<String>) -> Self {
Self {
role: Role::User,
content: Some(content.into()),
parts: Vec::new(),
tool_calls: Vec::new(),
tool_call_id: None,
name: None,
}
}
#[must_use]
pub fn assistant(content: impl Into<String>) -> Self {
Self {
role: Role::Assistant,
content: Some(content.into()),
parts: Vec::new(),
tool_calls: Vec::new(),
tool_call_id: None,
name: None,
}
}
#[must_use]
pub const fn assistant_tools(tool_calls: Vec<ToolCall>) -> Self {
Self {
role: Role::Assistant,
content: None,
parts: Vec::new(),
tool_calls,
tool_call_id: None,
name: None,
}
}
#[must_use]
pub fn tool_result(
tool_call_id: ToolCallId,
name: impl Into<String>,
content: impl Into<String>,
) -> Self {
Self {
role: Role::Tool,
content: Some(content.into()),
parts: Vec::new(),
tool_calls: Vec::new(),
tool_call_id: Some(tool_call_id),
name: Some(name.into()),
}
}
#[must_use]
pub fn text(&self) -> String {
if let Some(content) = &self.content {
return content.clone();
}
self.parts
.iter()
.filter_map(|p| match p {
ContentPart::Text { text } => Some(text.as_str()),
ContentPart::Image { .. } => None,
})
.collect::<Vec<_>>()
.join("")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_user() {
let m = Message::user("hello");
let json = serde_json::to_string(&m).expect("ser");
let back: Message = serde_json::from_str(&json).expect("de");
assert_eq!(back.role, Role::User);
assert_eq!(back.text(), "hello");
}
#[test]
fn tool_call_message() {
let id = ToolCallId::new("call_1").expect("id");
let m = Message::assistant_tools(vec![ToolCall {
id: id.clone(),
name: "add".into(),
arguments: serde_json::json!({"a":1,"b":2}),
}]);
assert_eq!(m.tool_calls.len(), 1);
assert_eq!(m.tool_calls.first().map(|c| &c.id), Some(&id));
}
}