use super::{ByteBufB64, Json};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct ToolMedia {
pub mime_type: String,
pub data: ByteBufB64,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(tag = "type")]
pub struct ToolPresentation {
pub text: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub media: Vec<ToolMedia>,
}
impl ToolPresentation {
pub fn from_output(output: &Json) -> Option<Self> {
if output.get("type").and_then(Json::as_str) != Some("ToolPresentation") {
return None;
}
serde_json::from_value(output.clone()).ok()
}
pub fn into_output(self) -> Json {
serde_json::to_value(self).expect("tool presentation is JSON serializable")
}
pub fn text_fallback(&self) -> String {
let mut text = self.text.clone();
for media in &self.media {
text.push_str(&format!(
"\n[{} tool media omitted: unsupported by this model API]",
media.mime_type
));
}
text
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn presentation_round_trips_and_old_results_stay_ordinary_json() {
let view = ToolPresentation {
text: "hello".into(),
media: vec![ToolMedia {
mime_type: "audio/wav".into(),
data: ByteBufB64::from(vec![1, 2, 3]),
}],
};
assert_eq!(
ToolPresentation::from_output(&view.clone().into_output()),
Some(view.clone())
);
assert!(view.text_fallback().contains("audio/wav"));
assert!(!view.text_fallback().contains("AQID"));
assert!(ToolPresentation::from_output(&serde_json::json!({"text":"ordinary"})).is_none());
let mut bytes = Vec::new();
cbor2::to_writer(&view, &mut bytes).unwrap();
let decoded: ToolPresentation = cbor2::from_slice(&bytes).unwrap();
assert_eq!(decoded, view);
}
}