use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentBlock {
#[serde(rename = "text")]
Text {
text: String,
},
#[serde(rename = "image")]
Image {
#[serde(skip_serializing_if = "Option::is_none")]
source: Option<ImageSource>,
},
#[serde(rename = "tool_use")]
ToolUse {
id: String,
name: String,
#[serde(default)]
input: serde_json::Value,
},
#[serde(rename = "tool_result")]
ToolResult {
tool_use_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
is_error: Option<bool>,
},
#[serde(rename = "thinking")]
Thinking {
thinking: String,
},
#[serde(rename = "document")]
Document {
source: DocumentSource,
#[serde(skip_serializing_if = "Option::is_none")]
title: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ImageSource {
#[serde(rename = "base64")]
Base64 {
media_type: String,
data: String,
},
#[serde(rename = "url")]
Url {
url: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum DocumentSource {
#[serde(rename = "pdf")]
Pdf {
data: String,
},
#[serde(rename = "text")]
Text {
text: String,
},
#[serde(rename = "url")]
Url {
url: String,
},
}
impl ContentBlock {
pub fn text(text: impl Into<String>) -> Self {
Self::Text { text: text.into() }
}
pub fn tool_use(
id: impl Into<String>,
name: impl Into<String>,
input: serde_json::Value,
) -> Self {
Self::ToolUse {
id: id.into(),
name: name.into(),
input,
}
}
pub fn tool_result(tool_use_id: impl Into<String>, content: impl Into<String>) -> Self {
Self::ToolResult {
tool_use_id: tool_use_id.into(),
content: Some(content.into()),
is_error: None,
}
}
pub fn tool_error(tool_use_id: impl Into<String>, error: impl Into<String>) -> Self {
Self::ToolResult {
tool_use_id: tool_use_id.into(),
content: Some(error.into()),
is_error: Some(true),
}
}
pub fn thinking(thinking: impl Into<String>) -> Self {
Self::Thinking {
thinking: thinking.into(),
}
}
pub fn type_name(&self) -> &'static str {
match self {
Self::Text { .. } => "text",
Self::Image { .. } => "image",
Self::ToolUse { .. } => "tool_use",
Self::ToolResult { .. } => "tool_result",
Self::Thinking { .. } => "thinking",
Self::Document { .. } => "document",
}
}
pub fn is_text(&self) -> bool {
matches!(self, Self::Text { .. })
}
pub fn is_tool_use(&self) -> bool {
matches!(self, Self::ToolUse { .. })
}
pub fn is_tool_result(&self) -> bool {
matches!(self, Self::ToolResult { .. })
}
pub fn as_text(&self) -> Option<&str> {
match self {
Self::Text { text } => Some(text),
_ => None,
}
}
pub fn as_tool_use(&self) -> Option<(&str, &str, &serde_json::Value)> {
match self {
Self::ToolUse { id, name, input } => Some((id, name, input)),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_text_content_serialization() {
let content = ContentBlock::text("Hello, world!");
let json = serde_json::to_string(&content).unwrap();
let deserialized: ContentBlock = serde_json::from_str(&json).unwrap();
assert_eq!(content, deserialized);
}
#[test]
fn test_tool_use_content() {
let content =
ContentBlock::tool_use("id_123", "bash", serde_json::json!({ "command": "ls" }));
assert!(content.is_tool_use());
assert_eq!(content.type_name(), "tool_use");
}
#[test]
fn test_content_type_checks() {
let text = ContentBlock::text("test");
assert!(text.is_text());
assert!(!text.is_tool_use());
}
}