use serde::{Deserialize, Serialize};
use super::tool::ToolCall;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Role {
System,
User,
Assistant,
Tool,
}
impl Role {
pub fn as_str(&self) -> &'static str {
match self {
Role::System => "system",
Role::User => "user",
Role::Assistant => "assistant",
Role::Tool => "tool",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum Content {
Text(String),
Parts(Vec<ContentPart>),
}
impl Content {
pub fn text(text: impl Into<String>) -> Self {
Content::Text(text.into())
}
pub fn parts(parts: Vec<ContentPart>) -> Self {
Content::Parts(parts)
}
pub fn as_text(&self) -> Option<&str> {
match self {
Content::Text(text) => Some(text),
Content::Parts(_) => None,
}
}
pub fn extract_text(&self) -> String {
match self {
Content::Text(text) => text.clone(),
Content::Parts(parts) => parts
.iter()
.filter_map(|p| match p {
ContentPart::Text { text } => Some(text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join(""),
}
}
}
impl From<String> for Content {
fn from(text: String) -> Self {
Content::Text(text)
}
}
impl From<&str> for Content {
fn from(text: &str) -> Self {
Content::Text(text.to_string())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type")]
pub enum ContentPart {
#[serde(rename = "text")]
Text { text: String },
#[serde(rename = "image_url")]
ImageUrl { image_url: ImageUrl },
}
impl ContentPart {
pub fn text(text: impl Into<String>) -> Self {
ContentPart::Text { text: text.into() }
}
pub fn image_url(url: impl Into<String>) -> Self {
ContentPart::ImageUrl {
image_url: ImageUrl::new(url),
}
}
pub fn image_base64(data: impl Into<String>, media_type: impl Into<String>) -> Self {
ContentPart::ImageUrl {
image_url: ImageUrl::base64(data, media_type),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ImageUrl {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
impl ImageUrl {
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
detail: None,
}
}
pub fn base64(data: impl Into<String>, media_type: impl Into<String>) -> Self {
Self {
url: format!("data:{};base64,{}", media_type.into(), data.into()),
detail: None,
}
}
pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
self.detail = Some(detail.into());
self
}
pub fn is_base64(&self) -> bool {
self.url.starts_with("data:")
}
pub fn parse_base64(&self) -> Option<(&str, &str)> {
if !self.is_base64() {
return None;
}
let after_data = self.url.strip_prefix("data:")?;
let semi_idx = after_data.find(';')?;
let media_type = &after_data[..semi_idx];
let data = after_data.strip_prefix(&format!("{};base64,", media_type))?;
Some((media_type, data))
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Message {
pub role: Role,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<Content>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCall>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
}
impl Message {
pub fn new(role: Role, content: impl Into<Content>) -> Self {
Self {
role,
content: Some(content.into()),
name: None,
tool_calls: None,
tool_call_id: None,
}
}
pub fn system(content: impl Into<String>) -> Self {
Self::new(Role::System, Content::text(content))
}
pub fn user(content: impl Into<String>) -> Self {
Self::new(Role::User, Content::text(content))
}
pub fn assistant(content: impl Into<String>) -> Self {
Self::new(Role::Assistant, Content::text(content))
}
pub fn assistant_with_tool_calls(tool_calls: Vec<ToolCall>) -> Self {
Self {
role: Role::Assistant,
content: None,
name: None,
tool_calls: Some(tool_calls),
tool_call_id: None,
}
}
pub fn tool(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
Self {
role: Role::Tool,
content: Some(Content::text(content)),
name: None,
tool_calls: None,
tool_call_id: Some(tool_call_id.into()),
}
}
pub fn user_with_parts(parts: Vec<ContentPart>) -> Self {
Self::new(Role::User, Content::parts(parts))
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn text_content(&self) -> Option<String> {
self.content.as_ref().map(|c| c.extract_text())
}
}
impl Default for Message {
fn default() -> Self {
Self {
role: Role::User,
content: None,
name: None,
tool_calls: None,
tool_call_id: None,
}
}
}