use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum Source {
Uri(String),
Data(String),
}
impl Source {
pub fn uri(&self) -> Option<&str> {
match self {
Source::Uri(u) => Some(u),
Source::Data(_) => None,
}
}
pub fn data(&self) -> Option<&str> {
match self {
Source::Data(d) => Some(d),
Source::Uri(_) => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Part {
Text { text: String },
Image { media_type: String, source: Source },
Audio { media_type: String, source: Source },
File {
#[serde(default, skip_serializing_if = "String::is_empty")]
name: String,
media_type: String,
source: Source,
},
Json { json: serde_json::Value },
}
impl Part {
pub fn text(text: impl Into<String>) -> Self {
Part::Text { text: text.into() }
}
pub fn image_uri(media_type: impl Into<String>, uri: impl Into<String>) -> Self {
Part::Image {
media_type: media_type.into(),
source: Source::Uri(uri.into()),
}
}
pub fn image_data(media_type: impl Into<String>, data: impl Into<String>) -> Self {
Part::Image {
media_type: media_type.into(),
source: Source::Data(data.into()),
}
}
pub fn audio_uri(media_type: impl Into<String>, uri: impl Into<String>) -> Self {
Part::Audio {
media_type: media_type.into(),
source: Source::Uri(uri.into()),
}
}
pub fn audio_data(media_type: impl Into<String>, data: impl Into<String>) -> Self {
Part::Audio {
media_type: media_type.into(),
source: Source::Data(data.into()),
}
}
pub fn file_uri(
name: impl Into<String>,
media_type: impl Into<String>,
uri: impl Into<String>,
) -> Self {
Part::File {
name: name.into(),
media_type: media_type.into(),
source: Source::Uri(uri.into()),
}
}
pub fn file_data(
name: impl Into<String>,
media_type: impl Into<String>,
data: impl Into<String>,
) -> Self {
Part::File {
name: name.into(),
media_type: media_type.into(),
source: Source::Data(data.into()),
}
}
pub fn json(value: impl Into<serde_json::Value>) -> Self {
Part::Json { json: value.into() }
}
pub fn kind(&self) -> &'static str {
match self {
Part::Text { .. } => "text",
Part::Image { .. } => "image",
Part::Audio { .. } => "audio",
Part::File { .. } => "file",
Part::Json { .. } => "json",
}
}
pub fn is_text(&self) -> bool {
matches!(self, Part::Text { .. })
}
pub fn as_text(&self) -> Option<&str> {
match self {
Part::Text { text } => Some(text),
_ => None,
}
}
pub fn media_type(&self) -> Option<&str> {
match self {
Part::Image { media_type, .. }
| Part::Audio { media_type, .. }
| Part::File { media_type, .. } => Some(media_type),
_ => None,
}
}
pub fn source(&self) -> Option<&Source> {
match self {
Part::Image { source, .. } | Part::Audio { source, .. } | Part::File { source, .. } => {
Some(source)
}
_ => None,
}
}
pub fn uri(&self) -> Option<&str> {
self.source().and_then(Source::uri)
}
pub fn data(&self) -> Option<&str> {
self.source().and_then(Source::data)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum Role {
User,
Assistant,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Message {
pub role: Role,
pub content: Vec<Part>,
}
impl Message {
pub fn new(role: Role, content: impl IntoIterator<Item = Part>) -> Self {
Self {
role,
content: content.into_iter().collect(),
}
}
pub fn user(text: impl Into<String>) -> Self {
Self::new(Role::User, [Part::text(text)])
}
pub fn assistant(text: impl Into<String>) -> Self {
Self::new(Role::Assistant, [Part::text(text)])
}
pub fn text(&self) -> String {
text_of(&self.content)
}
}
pub fn text_of(parts: &[Part]) -> String {
parts
.iter()
.filter_map(Part::as_text)
.collect::<Vec<_>>()
.join("\n")
}
pub fn modalities(parts: &[Part]) -> Vec<&'static str> {
let mut seen: Vec<&'static str> = Vec::new();
for p in parts {
let k = p.kind();
if !seen.contains(&k) {
seen.push(k);
}
}
seen
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn text_and_modalities() {
let parts = vec![
Part::text("describe this"),
Part::image_uri("image/png", "https://x/y.png"),
Part::text("and this"),
Part::image_data("image/jpeg", "QUJD"),
];
assert_eq!(text_of(&parts), "describe this\nand this");
assert_eq!(modalities(&parts), vec!["text", "image"]);
}
#[test]
fn accessors() {
let img = Part::image_uri("image/png", "u");
assert_eq!(img.kind(), "image");
assert!(!img.is_text());
assert_eq!(img.as_text(), None);
assert_eq!(img.media_type(), Some("image/png"));
assert_eq!(img.uri(), Some("u"));
assert_eq!(img.data(), None);
let blob = Part::image_data("image/png", "QUJD");
assert_eq!(blob.uri(), None);
assert_eq!(blob.data(), Some("QUJD"));
let txt = Part::text("hi");
assert_eq!(txt.kind(), "text");
assert!(txt.is_text());
assert_eq!(txt.as_text(), Some("hi"));
assert_eq!(txt.media_type(), None);
assert_eq!(txt.source(), None);
}
#[test]
fn serializes_with_kind_tag_and_round_trips() {
let parts = vec![
Part::text("hello"),
Part::image_data("image/png", "QkFTRTY0"),
Part::file_uri("notes.pdf", "application/pdf", "https://x/n.pdf"),
Part::file_data("blob.bin", "application/octet-stream", "QUJD"),
Part::json(serde_json::json!({"label": "cat", "p": 0.9})),
];
let line = serde_json::to_string(&parts).unwrap();
assert!(line.contains(r#""kind":"text""#));
assert!(line.contains(r#""kind":"image""#));
assert!(line.contains(r#""source":{"data":"QkFTRTY0"}"#));
let back: Vec<Part> = serde_json::from_str(&line).unwrap();
assert_eq!(back, parts);
}
#[test]
fn media_part_requires_a_source() {
let err = serde_json::from_str::<Part>(r#"{"kind":"image","media_type":"image/png"}"#);
assert!(err.is_err());
let ok = serde_json::from_str::<Part>(
r#"{"kind":"image","media_type":"image/png","source":{"uri":"u"}}"#,
);
assert_eq!(ok.unwrap(), Part::image_uri("image/png", "u"));
}
}