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::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}