use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TextBlock {
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ThinkingBlock {
pub thinking: String,
pub redacted: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ImageSource {
pub data: String,
pub media_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub arguments: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentBlock {
Text(TextBlock),
Thinking(ThinkingBlock),
Image { source: ImageSource },
ToolCall(ToolCall),
}
impl ContentBlock {
pub fn text(s: String) -> Self {
ContentBlock::Text(TextBlock { text: s })
}
pub fn as_text(&self) -> Option<&str> {
match self {
ContentBlock::Text(block) => Some(&block.text),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Message {
System {
content: Vec<ContentBlock>,
},
User {
content: Vec<ContentBlock>,
},
Assistant {
content: Vec<ContentBlock>,
},
ToolResult {
tool_call_id: String,
content: Vec<ContentBlock>,
},
}
impl Message {
pub fn role(&self) -> &str {
match self {
Message::System { .. } => "system",
Message::User { .. } => "user",
Message::Assistant { .. } => "assistant",
Message::ToolResult { .. } => "tool_result",
}
}
pub fn content(&self) -> &Vec<ContentBlock> {
match self {
Message::System { content }
| Message::User { content }
| Message::Assistant { content }
| Message::ToolResult { content, .. } => content,
}
}
pub fn extract_text(&self) -> String {
match self {
Message::System { content } => Self::join_text(content),
Message::User { content } => Self::join_text(content),
Message::Assistant { content } => Self::join_text(content),
Message::ToolResult { content, .. } => Self::join_text(content),
}
}
fn join_text(blocks: &[ContentBlock]) -> String {
blocks
.iter()
.filter_map(|b| b.as_text().map(|s| s.to_string()))
.collect::<Vec<_>>()
.join("")
}
pub fn extract_tool_calls(&self) -> Vec<ToolCall> {
match self {
Message::Assistant { content } => content
.iter()
.filter_map(|b| {
if let ContentBlock::ToolCall(tc) = b {
Some(tc.clone())
} else {
None
}
})
.collect(),
_ => Vec::new(),
}
}
}
pub fn text_block(s: String) -> Vec<ContentBlock> {
vec![ContentBlock::text(s)]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_content_block_text() {
let block = ContentBlock::text("hello".to_string());
assert_eq!(block.as_text(), Some("hello"));
}
#[test]
fn test_content_block_tool_call_no_as_text() {
let block = ContentBlock::ToolCall(ToolCall {
id: "1".into(),
name: "test".into(),
arguments: serde_json::json!({}),
});
assert_eq!(block.as_text(), None);
}
#[test]
fn test_message_extract_text() {
let msg = Message::User {
content: text_block("hello world".to_string()),
};
assert_eq!(msg.extract_text(), "hello world");
}
#[test]
fn test_message_extract_tool_calls() {
let tc = ToolCall {
id: "1".into(),
name: "test".into(),
arguments: serde_json::json!({}),
};
let msg = Message::Assistant {
content: vec![ContentBlock::ToolCall(tc.clone())],
};
let calls = msg.extract_tool_calls();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].name, "test");
}
}