use serde::{Deserialize, Serialize};
use serde_json::Value;
#[cfg(not(feature = "std"))]
use alloc::{collections::BTreeMap as HashMap, string::String, vec, vec::Vec};
#[cfg(feature = "std")]
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Role {
#[default]
User,
Assistant,
}
impl core::fmt::Display for Role {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::User => f.write_str("user"),
Self::Assistant => f.write_str("assistant"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type")]
pub enum Content {
#[serde(rename = "text")]
Text(TextContent),
#[serde(rename = "image")]
Image(ImageContent),
#[serde(rename = "audio")]
Audio(AudioContent),
#[serde(rename = "resource_link")]
ResourceLink(ResourceLink),
#[serde(rename = "resource")]
Resource(EmbeddedResource),
}
impl Default for Content {
fn default() -> Self {
Self::text("")
}
}
impl Content {
#[must_use]
pub fn text(text: impl Into<String>) -> Self {
Self::Text(TextContent {
text: text.into(),
annotations: None,
meta: None,
})
}
#[must_use]
pub fn image(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
Self::Image(ImageContent {
data: data.into(),
mime_type: mime_type.into(),
annotations: None,
meta: None,
})
}
#[must_use]
pub fn audio(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
Self::Audio(AudioContent {
data: data.into(),
mime_type: mime_type.into(),
annotations: None,
meta: None,
})
}
#[must_use]
pub fn resource_link(resource: crate::definitions::Resource) -> Self {
Self::ResourceLink(ResourceLink {
uri: resource.uri,
name: resource.name,
description: resource.description,
title: resource.title,
icons: resource.icons,
mime_type: resource.mime_type,
annotations: resource.annotations,
size: resource.size,
meta: resource.meta,
})
}
#[must_use]
pub fn resource(uri: impl Into<String>, text: impl Into<String>) -> Self {
Self::Resource(EmbeddedResource {
resource: ResourceContents::Text(TextResourceContents {
uri: uri.into(),
mime_type: Some("text/plain".into()),
text: text.into(),
meta: None,
}),
annotations: None,
meta: None,
})
}
#[must_use]
pub fn is_text(&self) -> bool {
matches!(self, Self::Text(_))
}
#[must_use]
pub fn as_text(&self) -> Option<&str> {
match self {
Self::Text(t) => Some(&t.text),
_ => None,
}
}
#[must_use]
pub fn is_image(&self) -> bool {
matches!(self, Self::Image(_))
}
#[must_use]
pub fn is_audio(&self) -> bool {
matches!(self, Self::Audio(_))
}
#[must_use]
pub fn is_resource_link(&self) -> bool {
matches!(self, Self::ResourceLink(_))
}
#[must_use]
pub fn is_resource(&self) -> bool {
matches!(self, Self::Resource(_))
}
#[must_use]
pub fn with_annotations(mut self, annotations: Annotations) -> Self {
match &mut self {
Self::Text(t) => t.annotations = Some(annotations),
Self::Image(i) => i.annotations = Some(annotations),
Self::Audio(a) => a.annotations = Some(annotations),
Self::ResourceLink(r) => {
r.annotations = Some(crate::definitions::ResourceAnnotations {
audience: annotations.audience,
priority: annotations.priority,
last_modified: annotations.last_modified,
})
}
Self::Resource(r) => r.annotations = Some(annotations),
}
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type")]
pub enum SamplingContent {
#[serde(rename = "text")]
Text(TextContent),
#[serde(rename = "image")]
Image(ImageContent),
#[serde(rename = "audio")]
Audio(AudioContent),
#[serde(rename = "tool_use")]
ToolUse(ToolUseContent),
#[serde(rename = "tool_result")]
ToolResult(ToolResultContent),
}
impl Default for SamplingContent {
fn default() -> Self {
Self::text("")
}
}
impl SamplingContent {
#[must_use]
pub fn text(text: impl Into<String>) -> Self {
Self::Text(TextContent {
text: text.into(),
annotations: None,
meta: None,
})
}
#[must_use]
pub fn as_text(&self) -> Option<&str> {
match self {
Self::Text(t) => Some(&t.text),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum SamplingContentBlock {
Single(SamplingContent),
Multiple(Vec<SamplingContent>),
}
impl Default for SamplingContentBlock {
fn default() -> Self {
Self::Single(SamplingContent::default())
}
}
impl SamplingContentBlock {
#[must_use]
pub fn as_text(&self) -> Option<&str> {
match self {
Self::Single(c) => c.as_text(),
Self::Multiple(v) => v.iter().find_map(|c| c.as_text()),
}
}
#[must_use]
pub fn to_vec(&self) -> Vec<&SamplingContent> {
match self {
Self::Single(c) => vec![c],
Self::Multiple(v) => v.iter().collect(),
}
}
}
impl Serialize for SamplingContentBlock {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Self::Single(c) => c.serialize(serializer),
Self::Multiple(v) => v.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for SamplingContentBlock {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = Value::deserialize(deserializer)?;
if value.is_array() {
let v: Vec<SamplingContent> =
serde_json::from_value(value).map_err(serde::de::Error::custom)?;
Ok(Self::Multiple(v))
} else {
let c: SamplingContent =
serde_json::from_value(value).map_err(serde::de::Error::custom)?;
Ok(Self::Single(c))
}
}
}
impl From<SamplingContent> for SamplingContentBlock {
fn from(c: SamplingContent) -> Self {
Self::Single(c)
}
}
impl From<Vec<SamplingContent>> for SamplingContentBlock {
fn from(v: Vec<SamplingContent>) -> Self {
Self::Multiple(v)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TextContent {
pub text: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, Value>>,
}
impl TextContent {
#[must_use]
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
annotations: None,
meta: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ImageContent {
pub data: String,
#[serde(rename = "mimeType")]
pub mime_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AudioContent {
pub data: String,
#[serde(rename = "mimeType")]
pub mime_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ToolUseContent {
pub id: String,
pub name: String,
pub input: HashMap<String, Value>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ToolResultContent {
#[serde(rename = "toolUseId")]
pub tool_use_id: String,
pub content: Vec<Content>,
#[serde(rename = "structuredContent", skip_serializing_if = "Option::is_none")]
pub structured_content: Option<Value>,
#[serde(rename = "isError", skip_serializing_if = "Option::is_none")]
pub is_error: Option<bool>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ResourceLink {
pub uri: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icons: Option<Vec<crate::definitions::Icon>>,
#[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<crate::definitions::ResourceAnnotations>,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<u64>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct EmbeddedResource {
pub resource: ResourceContents,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum ResourceContents {
Text(TextResourceContents),
Blob(BlobResourceContents),
}
pub type ResourceContent = ResourceContents;
impl ResourceContents {
#[must_use]
pub fn uri(&self) -> &str {
match self {
Self::Text(t) => &t.uri,
Self::Blob(b) => &b.uri,
}
}
#[must_use]
pub fn text(&self) -> Option<&str> {
match self {
Self::Text(t) => Some(&t.text),
Self::Blob(_) => None,
}
}
#[must_use]
pub fn blob(&self) -> Option<&str> {
match self {
Self::Text(_) => None,
Self::Blob(b) => Some(&b.blob),
}
}
#[must_use]
pub fn mime_type(&self) -> Option<&str> {
match self {
Self::Text(t) => t.mime_type.as_deref(),
Self::Blob(b) => b.mime_type.as_deref(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TextResourceContents {
pub uri: String,
#[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
pub text: String,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct BlobResourceContents {
pub uri: String,
#[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
pub blob: String,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, Value>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct Annotations {
#[serde(skip_serializing_if = "Option::is_none")]
pub audience: Option<Vec<Role>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<f64>,
#[serde(rename = "lastModified", skip_serializing_if = "Option::is_none")]
pub last_modified: Option<String>,
}
impl Annotations {
#[must_use]
pub fn for_user() -> Self {
Self {
audience: Some(vec![Role::User]),
..Default::default()
}
}
#[must_use]
pub fn for_assistant() -> Self {
Self {
audience: Some(vec![Role::Assistant]),
..Default::default()
}
}
#[must_use]
pub fn with_priority(mut self, priority: f64) -> Self {
self.priority = Some(priority);
self
}
#[must_use]
pub fn with_last_modified(mut self, timestamp: impl Into<String>) -> Self {
self.last_modified = Some(timestamp.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Message {
pub role: Role,
pub content: Content,
}
impl Message {
#[must_use]
pub fn new(role: Role, content: Content) -> Self {
Self { role, content }
}
#[must_use]
pub fn user(text: impl Into<String>) -> Self {
Self {
role: Role::User,
content: Content::text(text),
}
}
#[must_use]
pub fn assistant(text: impl Into<String>) -> Self {
Self {
role: Role::Assistant,
content: Content::text(text),
}
}
#[must_use]
pub fn is_user(&self) -> bool {
self.role == Role::User
}
#[must_use]
pub fn is_assistant(&self) -> bool {
self.role == Role::Assistant
}
}
pub type PromptMessage = Message;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_content_text() {
let content = Content::text("Hello");
assert!(content.is_text());
assert_eq!(content.as_text(), Some("Hello"));
}
#[test]
fn test_content_image() {
let content = Content::image("base64data", "image/png");
assert!(content.is_image());
assert!(!content.is_text());
}
#[test]
fn test_content_serde() {
let content = Content::text("Hello");
let json = serde_json::to_string(&content).unwrap();
assert!(json.contains("\"type\":\"text\""));
assert!(json.contains("\"text\":\"Hello\""));
}
#[test]
fn test_resource_link_serde() {
let link = Content::ResourceLink(ResourceLink {
uri: "file:///test.txt".into(),
name: "test".into(),
description: None,
title: None,
icons: None,
mime_type: Some("text/plain".into()),
annotations: None,
size: None,
meta: None,
});
let json = serde_json::to_string(&link).unwrap();
assert!(json.contains("\"type\":\"resource_link\""));
assert!(json.contains("\"uri\":\"file:///test.txt\""));
let parsed: Content = serde_json::from_str(&json).unwrap();
assert!(parsed.is_resource_link());
}
#[test]
fn test_sampling_content_tool_use_serde() {
let content = SamplingContent::ToolUse(ToolUseContent {
id: "tu_1".into(),
name: "search".into(),
input: [("query".to_string(), Value::String("test".into()))].into(),
meta: None,
});
let json = serde_json::to_string(&content).unwrap();
assert!(json.contains("\"type\":\"tool_use\""));
assert!(json.contains("\"id\":\"tu_1\""));
let parsed: SamplingContent = serde_json::from_str(&json).unwrap();
assert!(matches!(parsed, SamplingContent::ToolUse(_)));
}
#[test]
fn test_sampling_content_block_single() {
let block = SamplingContentBlock::Single(SamplingContent::text("hello"));
let json = serde_json::to_string(&block).unwrap();
assert!(json.starts_with('{'));
let parsed: SamplingContentBlock = serde_json::from_str(&json).unwrap();
assert!(matches!(parsed, SamplingContentBlock::Single(_)));
}
#[test]
fn test_sampling_content_block_multiple() {
let block = SamplingContentBlock::Multiple(vec![
SamplingContent::text("hello"),
SamplingContent::text("world"),
]);
let json = serde_json::to_string(&block).unwrap();
assert!(json.starts_with('['));
let parsed: SamplingContentBlock = serde_json::from_str(&json).unwrap();
assert!(matches!(parsed, SamplingContentBlock::Multiple(v) if v.len() == 2));
}
#[test]
fn test_message_user() {
let msg = Message::user("Hello");
assert!(msg.is_user());
assert!(!msg.is_assistant());
}
#[test]
fn test_message_assistant() {
let msg = Message::assistant("Hi there");
assert!(msg.is_assistant());
assert!(!msg.is_user());
}
#[test]
fn test_annotations_for_user() {
let ann = Annotations::for_user().with_priority(1.0);
assert_eq!(ann.audience, Some(vec![Role::User]));
assert_eq!(ann.priority, Some(1.0));
}
#[test]
fn test_content_with_annotations() {
let content = Content::text("Hello").with_annotations(Annotations::for_user());
if let Content::Text(t) = content {
assert!(t.annotations.is_some());
} else {
panic!("Expected text content");
}
}
#[test]
fn test_resource_contents_text_deser() {
let json = r#"{"uri":"file:///test.txt","mimeType":"text/plain","text":"hello"}"#;
let rc: ResourceContents = serde_json::from_str(json).unwrap();
assert!(matches!(rc, ResourceContents::Text(_)));
assert_eq!(rc.uri(), "file:///test.txt");
assert_eq!(rc.text(), Some("hello"));
assert!(rc.blob().is_none());
}
#[test]
fn test_resource_contents_blob_deser() {
let json = r#"{"uri":"file:///img.png","mimeType":"image/png","blob":"aGVsbG8="}"#;
let rc: ResourceContents = serde_json::from_str(json).unwrap();
assert!(matches!(rc, ResourceContents::Blob(_)));
assert_eq!(rc.uri(), "file:///img.png");
assert_eq!(rc.blob(), Some("aGVsbG8="));
assert!(rc.text().is_none());
}
#[test]
fn test_resource_contents_round_trip() {
let text = ResourceContents::Text(TextResourceContents {
uri: "file:///a.txt".into(),
mime_type: Some("text/plain".into()),
text: "content".into(),
meta: None,
});
let json = serde_json::to_string(&text).unwrap();
let parsed: ResourceContents = serde_json::from_str(&json).unwrap();
assert_eq!(text, parsed);
let blob = ResourceContents::Blob(BlobResourceContents {
uri: "file:///b.bin".into(),
mime_type: Some("application/octet-stream".into()),
blob: "AQID".into(),
meta: None,
});
let json = serde_json::to_string(&blob).unwrap();
let parsed: ResourceContents = serde_json::from_str(&json).unwrap();
assert_eq!(blob, parsed);
}
#[test]
fn test_sampling_content_tool_result_serde() {
let content = SamplingContent::ToolResult(ToolResultContent {
tool_use_id: "tu_1".into(),
content: vec![Content::text("result data")],
structured_content: Some(serde_json::json!({"key": "value"})),
is_error: Some(false),
meta: None,
});
let json = serde_json::to_string(&content).unwrap();
assert!(json.contains("\"type\":\"tool_result\""));
assert!(json.contains("\"toolUseId\":\"tu_1\""));
assert!(json.contains("\"structuredContent\""));
let parsed: SamplingContent = serde_json::from_str(&json).unwrap();
assert!(matches!(parsed, SamplingContent::ToolResult(_)));
}
#[test]
fn test_sampling_content_block_empty_array() {
let parsed: SamplingContentBlock = serde_json::from_str("[]").unwrap();
assert!(matches!(parsed, SamplingContentBlock::Multiple(v) if v.is_empty()));
}
#[test]
fn test_sampling_content_block_single_element_array() {
let single_obj = r#"{"type":"text","text":"x"}"#;
let single_arr = r#"[{"type":"text","text":"x"}]"#;
let parsed_obj: SamplingContentBlock = serde_json::from_str(single_obj).unwrap();
assert!(matches!(parsed_obj, SamplingContentBlock::Single(_)));
let parsed_arr: SamplingContentBlock = serde_json::from_str(single_arr).unwrap();
assert!(matches!(parsed_arr, SamplingContentBlock::Multiple(v) if v.len() == 1));
}
#[test]
fn test_content_all_type_discriminants() {
let variants: Vec<(&str, Content)> = vec![
("text", Content::text("hi")),
("image", Content::image("data", "image/png")),
("audio", Content::audio("data", "audio/wav")),
(
"resource_link",
Content::ResourceLink(ResourceLink {
uri: "file:///x".into(),
name: "x".into(),
description: None,
title: None,
icons: None,
mime_type: None,
annotations: None,
size: None,
meta: None,
}),
),
("resource", Content::resource("file:///x", "text")),
];
for (expected_type, content) in variants {
let json = serde_json::to_string(&content).unwrap();
assert!(
json.contains(&format!("\"type\":\"{}\"", expected_type)),
"Missing type discriminant for {expected_type}: {json}"
);
let parsed: Content = serde_json::from_str(&json).unwrap();
assert_eq!(content, parsed, "Round-trip failed for {expected_type}");
}
}
#[test]
fn test_meta_field_skip_serializing_if_none() {
let content = TextContent::new("hello");
let json = serde_json::to_string(&content).unwrap();
assert!(!json.contains("_meta"), "None meta should be omitted");
let mut meta = HashMap::new();
meta.insert("key".into(), Value::String("val".into()));
let content = TextContent {
text: "hello".into(),
annotations: None,
meta: Some(meta),
};
let json = serde_json::to_string(&content).unwrap();
assert!(json.contains("\"_meta\""), "Some meta should be present");
}
#[test]
fn test_resource_contents_meta_field() {
let mut meta = HashMap::new();
meta.insert("k".into(), Value::Bool(true));
let rc = ResourceContents::Text(TextResourceContents {
uri: "x".into(),
mime_type: None,
text: "y".into(),
meta: Some(meta),
});
let json = serde_json::to_string(&rc).unwrap();
assert!(json.contains("\"_meta\""));
let parsed: ResourceContents = serde_json::from_str(&json).unwrap();
assert_eq!(rc, parsed);
}
#[test]
fn test_sampling_content_block_to_vec() {
let single = SamplingContentBlock::Single(SamplingContent::text("a"));
assert_eq!(single.to_vec().len(), 1);
let multi = SamplingContentBlock::Multiple(vec![
SamplingContent::text("a"),
SamplingContent::text("b"),
]);
assert_eq!(multi.to_vec().len(), 2);
assert_eq!(multi.as_text(), Some("a"));
}
}