use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentBlock {
Text(TextContent),
Image(ImageContent),
Audio(AudioContent),
ResourceLink(ResourceLink),
Resource(EmbeddedResource),
#[serde(untagged)]
Other(serde_json::Value),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TextContent {
pub text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
#[serde(default, rename = "_meta", skip_serializing_if = "BTreeMap::is_empty")]
pub meta: BTreeMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ImageContent {
pub data: String,
pub mime_type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
#[serde(default, rename = "_meta", skip_serializing_if = "BTreeMap::is_empty")]
pub meta: BTreeMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AudioContent {
pub data: String,
pub mime_type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
#[serde(default, rename = "_meta", skip_serializing_if = "BTreeMap::is_empty")]
pub meta: BTreeMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourceLink {
pub uri: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub size: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
#[serde(default, rename = "_meta", skip_serializing_if = "BTreeMap::is_empty")]
pub meta: BTreeMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EmbeddedResource {
pub resource: EmbeddedResourceBody,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
#[serde(default, rename = "_meta", skip_serializing_if = "BTreeMap::is_empty")]
pub meta: BTreeMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "mimeType", rename_all = "camelCase")]
pub enum EmbeddedResourceBody {
Text {
text: String,
},
Blob {
blob: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Annotations {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub audience: Option<Vec<ContentAudience>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub priority: Option<ContentPriority>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ContentAudience {
User,
Assistant,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ContentPriority {
Background,
Normal,
High,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn a_text_content_block_round_trips() -> Result<(), serde_json::Error> {
let block = ContentBlock::Text(TextContent {
text: "hello".to_owned(),
annotations: None,
meta: BTreeMap::new(),
});
let value = serde_json::to_value(&block)?;
assert_eq!(value.get("type"), Some(&json!("text")));
assert_eq!(value.get("text"), Some(&json!("hello")));
let round: ContentBlock = serde_json::from_value(value)?;
assert_eq!(round, block);
Ok(())
}
#[test]
fn an_image_content_block_round_trips() -> Result<(), serde_json::Error> {
let block = ContentBlock::Image(ImageContent {
data: "aGVsbG8=".to_owned(),
mime_type: "image/png".to_owned(),
annotations: None,
meta: BTreeMap::new(),
});
let value = serde_json::to_value(&block)?;
assert_eq!(value.get("type"), Some(&json!("image")));
assert_eq!(value.get("data"), Some(&json!("aGVsbG8=")));
assert_eq!(value.get("mimeType"), Some(&json!("image/png")));
Ok(())
}
#[test]
fn an_audio_content_block_round_trips() -> Result<(), serde_json::Error> {
let block = ContentBlock::Audio(AudioContent {
data: "ZGF0YQ==".to_owned(),
mime_type: "audio/mp3".to_owned(),
annotations: None,
meta: BTreeMap::new(),
});
let value = serde_json::to_value(&block)?;
assert_eq!(value.get("type"), Some(&json!("audio")));
assert_eq!(value.get("mimeType"), Some(&json!("audio/mp3")));
Ok(())
}
#[test]
fn a_resource_link_block_round_trips() -> Result<(), serde_json::Error> {
let block = ContentBlock::ResourceLink(ResourceLink {
uri: "file:///tmp/example.txt".to_owned(),
name: Some("example.txt".to_owned()),
description: None,
mime_type: Some("text/plain".to_owned()),
size: Some(12),
annotations: None,
meta: BTreeMap::new(),
});
let value = serde_json::to_value(&block)?;
assert_eq!(value.get("type"), Some(&json!("resource_link")));
assert_eq!(value.get("uri"), Some(&json!("file:///tmp/example.txt")));
assert_eq!(value.get("size"), Some(&json!(12)));
Ok(())
}
#[test]
fn an_embedded_resource_text_block_round_trips() -> Result<(), serde_json::Error> {
let block = ContentBlock::Resource(EmbeddedResource {
resource: EmbeddedResourceBody::Text {
text: "inline content".to_owned(),
},
annotations: None,
meta: BTreeMap::new(),
});
let value = serde_json::to_value(&block)?;
assert_eq!(value.get("type"), Some(&json!("resource")));
let resource = value.get("resource").ok_or_else(|| {
serde_json::Error::io(std::io::Error::other("missing `resource` field"))
})?;
assert_eq!(resource.get("text"), Some(&json!("inline content")));
Ok(())
}
#[test]
fn an_unknown_content_type_lands_in_the_other_variant() -> Result<(), serde_json::Error> {
let raw = json!({"type": "future_block", "payload": "anything"});
let block: ContentBlock = serde_json::from_value(raw)?;
match block {
ContentBlock::Other(value) => {
assert_eq!(value.get("type"), Some(&json!("future_block")));
}
ContentBlock::Text(_)
| ContentBlock::Image(_)
| ContentBlock::Audio(_)
| ContentBlock::ResourceLink(_)
| ContentBlock::Resource(_) => {
return Err(serde_json::Error::io(std::io::Error::other(
"an unknown type landed in the wrong variant",
)));
}
}
Ok(())
}
}