1use std::collections::BTreeMap;
38
39use serde::{Deserialize, Serialize};
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(tag = "type", rename_all = "snake_case")]
49pub enum ContentBlock {
50 Text(TextContent),
53 Image(ImageContent),
56 Audio(AudioContent),
59 ResourceLink(ResourceLink),
63 Resource(EmbeddedResource),
66 #[serde(untagged)]
71 Other(serde_json::Value),
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(rename_all = "camelCase")]
77pub struct TextContent {
78 pub text: String,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub annotations: Option<Annotations>,
86 #[serde(default, rename = "_meta", skip_serializing_if = "BTreeMap::is_empty")]
88 pub meta: BTreeMap<String, serde_json::Value>,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93#[serde(rename_all = "camelCase")]
94pub struct ImageContent {
95 pub data: String,
99 pub mime_type: String,
101 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub annotations: Option<Annotations>,
104 #[serde(default, rename = "_meta", skip_serializing_if = "BTreeMap::is_empty")]
106 pub meta: BTreeMap<String, serde_json::Value>,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
111#[serde(rename_all = "camelCase")]
112pub struct AudioContent {
113 pub data: String,
115 pub mime_type: String,
117 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub annotations: Option<Annotations>,
120 #[serde(default, rename = "_meta", skip_serializing_if = "BTreeMap::is_empty")]
122 pub meta: BTreeMap<String, serde_json::Value>,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
130#[serde(rename_all = "camelCase")]
131pub struct ResourceLink {
132 pub uri: String,
136 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub name: Option<String>,
139 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub description: Option<String>,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub mime_type: Option<String>,
145 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub size: Option<u64>,
148 #[serde(default, skip_serializing_if = "Option::is_none")]
150 pub annotations: Option<Annotations>,
151 #[serde(default, rename = "_meta", skip_serializing_if = "BTreeMap::is_empty")]
153 pub meta: BTreeMap<String, serde_json::Value>,
154}
155
156#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
160#[serde(rename_all = "camelCase")]
161pub struct EmbeddedResource {
162 pub resource: EmbeddedResourceBody,
164 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub annotations: Option<Annotations>,
167 #[serde(default, rename = "_meta", skip_serializing_if = "BTreeMap::is_empty")]
169 pub meta: BTreeMap<String, serde_json::Value>,
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
176#[serde(tag = "mimeType", rename_all = "camelCase")]
177pub enum EmbeddedResourceBody {
178 Text {
182 text: String,
184 },
185 Blob {
188 blob: String,
190 },
191}
192
193#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
197#[serde(rename_all = "camelCase")]
198pub struct Annotations {
199 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub audience: Option<Vec<ContentAudience>>,
203 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub priority: Option<ContentPriority>,
206}
207
208#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
210#[serde(rename_all = "lowercase")]
211pub enum ContentAudience {
212 User,
214 Assistant,
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
223#[serde(rename_all = "lowercase")]
224pub enum ContentPriority {
225 Background,
228 Normal,
230 High,
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use serde_json::json;
239
240 #[test]
244 fn a_text_content_block_round_trips() -> Result<(), serde_json::Error> {
245 let block = ContentBlock::Text(TextContent {
246 text: "hello".to_owned(),
247 annotations: None,
248 meta: BTreeMap::new(),
249 });
250 let value = serde_json::to_value(&block)?;
251 assert_eq!(value.get("type"), Some(&json!("text")));
252 assert_eq!(value.get("text"), Some(&json!("hello")));
253 let round: ContentBlock = serde_json::from_value(value)?;
254 assert_eq!(round, block);
255 Ok(())
256 }
257
258 #[test]
261 fn an_image_content_block_round_trips() -> Result<(), serde_json::Error> {
262 let block = ContentBlock::Image(ImageContent {
263 data: "aGVsbG8=".to_owned(),
264 mime_type: "image/png".to_owned(),
265 annotations: None,
266 meta: BTreeMap::new(),
267 });
268 let value = serde_json::to_value(&block)?;
269 assert_eq!(value.get("type"), Some(&json!("image")));
270 assert_eq!(value.get("data"), Some(&json!("aGVsbG8=")));
271 assert_eq!(value.get("mimeType"), Some(&json!("image/png")));
272 Ok(())
273 }
274
275 #[test]
278 fn an_audio_content_block_round_trips() -> Result<(), serde_json::Error> {
279 let block = ContentBlock::Audio(AudioContent {
280 data: "ZGF0YQ==".to_owned(),
281 mime_type: "audio/mp3".to_owned(),
282 annotations: None,
283 meta: BTreeMap::new(),
284 });
285 let value = serde_json::to_value(&block)?;
286 assert_eq!(value.get("type"), Some(&json!("audio")));
287 assert_eq!(value.get("mimeType"), Some(&json!("audio/mp3")));
288 Ok(())
289 }
290
291 #[test]
294 fn a_resource_link_block_round_trips() -> Result<(), serde_json::Error> {
295 let block = ContentBlock::ResourceLink(ResourceLink {
296 uri: "file:///tmp/example.txt".to_owned(),
297 name: Some("example.txt".to_owned()),
298 description: None,
299 mime_type: Some("text/plain".to_owned()),
300 size: Some(12),
301 annotations: None,
302 meta: BTreeMap::new(),
303 });
304 let value = serde_json::to_value(&block)?;
305 assert_eq!(value.get("type"), Some(&json!("resource_link")));
306 assert_eq!(value.get("uri"), Some(&json!("file:///tmp/example.txt")));
307 assert_eq!(value.get("size"), Some(&json!(12)));
308 Ok(())
309 }
310
311 #[test]
315 fn an_embedded_resource_text_block_round_trips() -> Result<(), serde_json::Error> {
316 let block = ContentBlock::Resource(EmbeddedResource {
317 resource: EmbeddedResourceBody::Text {
318 text: "inline content".to_owned(),
319 },
320 annotations: None,
321 meta: BTreeMap::new(),
322 });
323 let value = serde_json::to_value(&block)?;
324 assert_eq!(value.get("type"), Some(&json!("resource")));
325 let resource = value.get("resource").ok_or_else(|| {
326 serde_json::Error::io(std::io::Error::other("missing `resource` field"))
327 })?;
328 assert_eq!(resource.get("text"), Some(&json!("inline content")));
329 Ok(())
330 }
331
332 #[test]
336 fn an_unknown_content_type_lands_in_the_other_variant() -> Result<(), serde_json::Error> {
337 let raw = json!({"type": "future_block", "payload": "anything"});
338 let block: ContentBlock = serde_json::from_value(raw)?;
339 match block {
340 ContentBlock::Other(value) => {
341 assert_eq!(value.get("type"), Some(&json!("future_block")));
342 }
343 ContentBlock::Text(_)
344 | ContentBlock::Image(_)
345 | ContentBlock::Audio(_)
346 | ContentBlock::ResourceLink(_)
347 | ContentBlock::Resource(_) => {
348 return Err(serde_json::Error::io(std::io::Error::other(
349 "an unknown type landed in the wrong variant",
350 )));
351 }
352 }
353 Ok(())
354 }
355}