claude_agent/types/content/
mod.rs1mod 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 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}