use serde::{Deserialize, Serialize};
use super::tools::ToolCall;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Role {
System,
User,
Assistant,
Tool,
}
impl std::fmt::Display for Role {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Role::System => write!(f, "system"),
Role::User => write!(f, "user"),
Role::Assistant => write!(f, "assistant"),
Role::Tool => write!(f, "tool"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MessageContent {
Text(String),
Parts(Vec<ContentPart>),
}
impl Default for MessageContent {
fn default() -> Self {
MessageContent::Text(String::new())
}
}
impl MessageContent {
pub fn as_str(&self) -> Option<&str> {
match self {
Self::Text(s) => Some(s.as_str()),
Self::Parts(parts) => parts.iter().find_map(|p| match p {
ContentPart::Text { text } => Some(text.as_str()),
_ => None,
}),
}
}
pub fn to_text_lossy(&self) -> String {
match self {
Self::Text(s) => s.clone(),
Self::Parts(parts) => parts
.iter()
.filter_map(|p| match p {
ContentPart::Text { text } => Some(text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join(""),
}
}
}
impl From<String> for MessageContent {
fn from(s: String) -> Self { Self::Text(s) }
}
impl From<&str> for MessageContent {
fn from(s: &str) -> Self { Self::Text(s.to_string()) }
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentPart {
Text { text: String },
ImageUrl { image_url: ImageUrl },
InputAudio { input_audio: AudioInput },
Document { document: DocumentInput },
}
impl ContentPart {
pub fn text(text: impl Into<String>) -> Self {
Self::Text { text: text.into() }
}
pub fn image_url(url: impl Into<String>) -> Self {
Self::ImageUrl { image_url: ImageUrl { url: url.into(), detail: None } }
}
pub fn audio(data: impl Into<String>, format: impl Into<String>) -> Self {
Self::InputAudio {
input_audio: AudioInput { data: data.into(), format: format.into() },
}
}
pub fn document(data: impl Into<String>, media_type: impl Into<String>) -> Self {
Self::Document {
document: DocumentInput { data: data.into(), media_type: media_type.into() },
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageUrl {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AudioInput {
pub data: String,
pub format: String,
}
impl AudioInput {
pub fn mime_type(&self) -> String {
match self.format.as_str() {
"mp3" => "audio/mpeg",
"flac" => "audio/flac",
"opus" => "audio/ogg; codecs=opus",
"aac" => "audio/aac",
"pcm16" => "audio/pcm",
_ => "audio/wav", }.to_string()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DocumentInput {
pub data: String,
pub media_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AudioOutput {
pub id: Option<String>,
pub data: String,
pub expires_at: Option<u64>,
pub transcript: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatMessage {
pub role: Role,
#[serde(default, deserialize_with = "deser_nullable_content")]
pub content: MessageContent,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub audio: Option<AudioOutput>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCall>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
}
fn deser_nullable_content<'de, D>(d: D) -> Result<MessageContent, D::Error>
where
D: serde::Deserializer<'de>,
{
let opt = Option::<MessageContent>::deserialize(d)?;
Ok(opt.unwrap_or_default())
}
impl ChatMessage {
pub fn system(content: impl Into<String>) -> Self {
Self {
role: Role::System,
content: MessageContent::Text(content.into()),
audio: None, tool_calls: None, tool_call_id: None,
}
}
pub fn user(content: impl Into<String>) -> Self {
Self {
role: Role::User,
content: MessageContent::Text(content.into()),
audio: None, tool_calls: None, tool_call_id: None,
}
}
pub fn assistant(content: impl Into<String>) -> Self {
Self {
role: Role::Assistant,
content: MessageContent::Text(content.into()),
audio: None, tool_calls: None, tool_call_id: None,
}
}
pub fn tool_result(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
Self {
role: Role::Tool,
content: MessageContent::Text(content.into()),
tool_call_id: Some(tool_call_id.into()),
tool_calls: None,
audio: None,
}
}
pub fn user_with_image(text: impl Into<String>, image_url: impl Into<String>) -> Self {
Self {
role: Role::User,
content: MessageContent::Parts(vec![
ContentPart::text(text),
ContentPart::image_url(image_url),
]),
audio: None, tool_calls: None, tool_call_id: None,
}
}
pub fn with_parts(role: Role, parts: Vec<ContentPart>) -> Self {
Self {
role,
content: MessageContent::Parts(parts),
audio: None, tool_calls: None, tool_call_id: None,
}
}
}