Skip to main content

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