claude_agent/types/content/
mod.rs

1//! Content block types for messages.
2
3mod image;
4mod server_tools;
5mod tool_blocks;
6
7use std::path::Path;
8
9use serde::{Deserialize, Serialize};
10
11pub use image::ImageSource;
12pub use server_tools::{
13    ServerToolUseBlock, WebFetchResultItem, WebFetchToolResultBlock, WebFetchToolResultContent,
14    WebFetchToolResultError, WebSearchResultItem, WebSearchToolResultBlock,
15    WebSearchToolResultContent, WebSearchToolResultError,
16};
17pub use tool_blocks::{ToolResultBlock, ToolResultContent, ToolResultContentBlock, ToolUseBlock};
18
19use super::citations::Citation;
20use super::document::DocumentBlock;
21use super::search::SearchResultBlock;
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24#[serde(tag = "type", rename_all = "snake_case")]
25pub enum ContentBlock {
26    Text {
27        text: String,
28        #[serde(skip_serializing_if = "Option::is_none")]
29        citations: Option<Vec<Citation>>,
30    },
31    Image {
32        source: ImageSource,
33    },
34    Document(DocumentBlock),
35    #[serde(rename = "search_result")]
36    SearchResult(SearchResultBlock),
37    #[serde(rename = "tool_use")]
38    ToolUse(ToolUseBlock),
39    #[serde(rename = "tool_result")]
40    ToolResult(ToolResultBlock),
41    Thinking(ThinkingBlock),
42    #[serde(rename = "redacted_thinking")]
43    RedactedThinking {
44        data: String,
45    },
46    #[serde(rename = "server_tool_use")]
47    ServerToolUse(ServerToolUseBlock),
48    #[serde(rename = "web_search_tool_result")]
49    WebSearchToolResult(WebSearchToolResultBlock),
50    #[serde(rename = "web_fetch_tool_result")]
51    WebFetchToolResult(WebFetchToolResultBlock),
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct ThinkingBlock {
56    pub thinking: String,
57    pub signature: String,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct TextBlock {
62    pub text: String,
63}
64
65impl From<&str> for ContentBlock {
66    fn from(text: &str) -> Self {
67        ContentBlock::Text {
68            text: text.to_string(),
69            citations: None,
70        }
71    }
72}
73
74impl From<String> for ContentBlock {
75    fn from(text: String) -> Self {
76        ContentBlock::Text {
77            text,
78            citations: None,
79        }
80    }
81}
82
83impl From<DocumentBlock> for ContentBlock {
84    fn from(doc: DocumentBlock) -> Self {
85        ContentBlock::Document(doc)
86    }
87}
88
89impl From<SearchResultBlock> for ContentBlock {
90    fn from(result: SearchResultBlock) -> Self {
91        ContentBlock::SearchResult(result)
92    }
93}
94
95impl ContentBlock {
96    pub fn text(text: impl Into<String>) -> Self {
97        ContentBlock::Text {
98            text: text.into(),
99            citations: None,
100        }
101    }
102
103    pub fn text_with_citations(text: impl Into<String>, citations: Vec<Citation>) -> Self {
104        ContentBlock::Text {
105            text: text.into(),
106            citations: if citations.is_empty() {
107                None
108            } else {
109                Some(citations)
110            },
111        }
112    }
113
114    pub fn document(doc: DocumentBlock) -> Self {
115        ContentBlock::Document(doc)
116    }
117
118    pub fn search_result(result: SearchResultBlock) -> Self {
119        ContentBlock::SearchResult(result)
120    }
121
122    pub fn image(source: ImageSource) -> Self {
123        ContentBlock::Image { source }
124    }
125
126    pub fn image_base64(media_type: impl Into<String>, data: impl Into<String>) -> Self {
127        ContentBlock::Image {
128            source: ImageSource::base64(media_type, data),
129        }
130    }
131
132    pub fn image_url(url: impl Into<String>) -> Self {
133        ContentBlock::Image {
134            source: ImageSource::from_url(url),
135        }
136    }
137
138    pub fn image_file(file_id: impl Into<String>) -> Self {
139        ContentBlock::Image {
140            source: ImageSource::from_file(file_id),
141        }
142    }
143
144    pub async fn image_from_path(path: impl AsRef<Path>) -> crate::Result<Self> {
145        Ok(ContentBlock::Image {
146            source: ImageSource::from_path(path).await?,
147        })
148    }
149
150    pub fn as_text(&self) -> Option<&str> {
151        match self {
152            ContentBlock::Text { text, .. } => Some(text),
153            _ => None,
154        }
155    }
156
157    pub fn citations(&self) -> Option<&[Citation]> {
158        match self {
159            ContentBlock::Text { citations, .. } => citations.as_deref(),
160            _ => None,
161        }
162    }
163
164    pub fn has_citations(&self) -> bool {
165        matches!(self, ContentBlock::Text { citations: Some(c), .. } if !c.is_empty())
166    }
167
168    pub fn as_document(&self) -> Option<&DocumentBlock> {
169        match self {
170            ContentBlock::Document(doc) => Some(doc),
171            _ => None,
172        }
173    }
174
175    pub fn as_search_result(&self) -> Option<&SearchResultBlock> {
176        match self {
177            ContentBlock::SearchResult(sr) => Some(sr),
178            _ => None,
179        }
180    }
181
182    pub fn is_document(&self) -> bool {
183        matches!(self, ContentBlock::Document(_))
184    }
185
186    pub fn is_search_result(&self) -> bool {
187        matches!(self, ContentBlock::SearchResult(_))
188    }
189
190    pub fn is_image(&self) -> bool {
191        matches!(self, ContentBlock::Image { .. })
192    }
193
194    pub fn as_image(&self) -> Option<&ImageSource> {
195        match self {
196            ContentBlock::Image { source } => Some(source),
197            _ => None,
198        }
199    }
200
201    pub fn as_thinking(&self) -> Option<&ThinkingBlock> {
202        match self {
203            ContentBlock::Thinking(block) => Some(block),
204            _ => None,
205        }
206    }
207
208    pub fn is_thinking(&self) -> bool {
209        matches!(
210            self,
211            ContentBlock::Thinking(_) | ContentBlock::RedactedThinking { .. }
212        )
213    }
214
215    pub fn is_server_tool_use(&self) -> bool {
216        matches!(self, ContentBlock::ServerToolUse(_))
217    }
218
219    pub fn as_server_tool_use(&self) -> Option<&ServerToolUseBlock> {
220        match self {
221            ContentBlock::ServerToolUse(block) => Some(block),
222            _ => None,
223        }
224    }
225
226    pub fn is_web_search_result(&self) -> bool {
227        matches!(self, ContentBlock::WebSearchToolResult(_))
228    }
229
230    pub fn as_web_search_result(&self) -> Option<&WebSearchToolResultBlock> {
231        match self {
232            ContentBlock::WebSearchToolResult(block) => Some(block),
233            _ => None,
234        }
235    }
236
237    pub fn is_web_fetch_result(&self) -> bool {
238        matches!(self, ContentBlock::WebFetchToolResult(_))
239    }
240
241    pub fn as_web_fetch_result(&self) -> Option<&WebFetchToolResultBlock> {
242        match self {
243            ContentBlock::WebFetchToolResult(block) => Some(block),
244            _ => None,
245        }
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    #[test]
254    fn test_content_block_text() {
255        let block = ContentBlock::text("Hello");
256        assert_eq!(block.as_text(), Some("Hello"));
257        assert!(!block.has_citations());
258    }
259
260    #[test]
261    fn test_content_block_from_document() {
262        let doc = DocumentBlock::text("content");
263        let block: ContentBlock = doc.into();
264        assert!(block.is_document());
265    }
266
267    #[test]
268    fn test_content_block_image() {
269        let block = ContentBlock::image_file("file_123");
270        assert!(block.is_image());
271        assert!(block.as_image().is_some());
272        assert_eq!(block.as_image().unwrap().file_id(), Some("file_123"));
273
274        let block = ContentBlock::image_url("https://example.com/img.png");
275        assert!(block.is_image());
276    }
277
278    #[tokio::test]
279    async fn test_content_block_image_from_path() {
280        let dir = tempfile::tempdir().unwrap();
281        let jpeg_path = dir.path().join("test.jpg");
282
283        let jpeg_data: [u8; 4] = [0xFF, 0xD8, 0xFF, 0xE0];
284        tokio::fs::write(&jpeg_path, &jpeg_data).await.unwrap();
285
286        let block = ContentBlock::image_from_path(&jpeg_path).await.unwrap();
287        assert!(block.is_image());
288
289        let source = block.as_image().unwrap();
290        assert!(source.is_base64());
291        assert_eq!(source.media_type(), Some("image/jpeg"));
292    }
293
294    #[test]
295    fn test_thinking_block_serialization() {
296        let block = ThinkingBlock {
297            thinking: "Let me analyze this...".to_string(),
298            signature: "sig_abc123".to_string(),
299        };
300        let json = serde_json::to_string(&block).unwrap();
301        assert!(json.contains("\"thinking\":\"Let me analyze this...\""));
302        assert!(json.contains("\"signature\":\"sig_abc123\""));
303    }
304
305    #[test]
306    fn test_thinking_block_deserialization() {
307        let json = r#"{"thinking":"Step by step reasoning","signature":"sig_xyz"}"#;
308        let block: ThinkingBlock = serde_json::from_str(json).unwrap();
309        assert_eq!(block.thinking, "Step by step reasoning");
310        assert_eq!(block.signature, "sig_xyz");
311    }
312
313    #[test]
314    fn test_content_block_thinking_variant() {
315        let thinking = ThinkingBlock {
316            thinking: "Analysis".to_string(),
317            signature: "sig".to_string(),
318        };
319        let block = ContentBlock::Thinking(thinking);
320        assert!(block.is_thinking());
321        assert!(block.as_thinking().is_some());
322        assert_eq!(block.as_thinking().unwrap().thinking, "Analysis");
323    }
324
325    #[test]
326    fn test_content_block_redacted_thinking() {
327        let block = ContentBlock::RedactedThinking {
328            data: "encrypted_data".to_string(),
329        };
330        assert!(block.is_thinking());
331        assert!(block.as_thinking().is_none());
332    }
333
334    #[test]
335    fn test_thinking_content_block_serialization() {
336        let block = ContentBlock::Thinking(ThinkingBlock {
337            thinking: "Reasoning here".to_string(),
338            signature: "sig123".to_string(),
339        });
340        let json = serde_json::to_string(&block).unwrap();
341        assert!(json.contains("\"type\":\"thinking\""));
342        assert!(json.contains("\"thinking\":\"Reasoning here\""));
343    }
344
345    #[test]
346    fn test_redacted_thinking_serialization() {
347        let block = ContentBlock::RedactedThinking {
348            data: "redacted_content".to_string(),
349        };
350        let json = serde_json::to_string(&block).unwrap();
351        assert!(json.contains("\"type\":\"redacted_thinking\""));
352        assert!(json.contains("\"data\":\"redacted_content\""));
353    }
354
355    #[test]
356    fn test_content_block_server_tool_helpers() {
357        let text_block = ContentBlock::text("Hello");
358        assert!(!text_block.is_server_tool_use());
359        assert!(!text_block.is_web_search_result());
360        assert!(!text_block.is_web_fetch_result());
361        assert!(text_block.as_server_tool_use().is_none());
362        assert!(text_block.as_web_search_result().is_none());
363        assert!(text_block.as_web_fetch_result().is_none());
364    }
365}