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