1use std::collections::HashMap;
15
16use serde::{Deserialize, Serialize};
17
18use super::enums::ResourceType;
19use super::message::Message;
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ToolCall {
28 pub tool_call_id: String,
30 pub name: String,
32 #[serde(default)]
34 pub arguments: HashMap<String, serde_json::Value>,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub namespace: Option<String>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct ToolResult {
43 pub tool_call_id: String,
45 pub tool_name: String,
47 #[serde(default)]
49 pub content: serde_json::Value,
50 #[serde(default)]
52 pub is_error: bool,
53}
54
55#[derive(Debug, Clone, Default, Serialize, Deserialize)]
57pub struct Resource {
58 pub resource_request_id: String,
60 pub uri: String,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub name: Option<String>,
65 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub description: Option<String>,
68 pub resource_type: ResourceType,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub content: Option<String>,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub blob: Option<Vec<u8>>,
76 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub mime_type: Option<String>,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub size_bytes: Option<u64>,
82 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
84 pub annotations: HashMap<String, serde_json::Value>,
85 #[serde(default, skip_serializing_if = "Option::is_none")]
87 pub version: Option<String>,
88}
89
90impl Resource {
91 pub fn is_embedded(&self) -> bool {
93 self.content.is_some() || self.blob.is_some()
94 }
95
96 pub fn get_text_content(&self) -> Option<&str> {
98 self.content.as_deref()
99 }
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct ResourceReference {
105 pub resource_request_id: String,
107 pub uri: String,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
111 pub name: Option<String>,
112 pub resource_type: ResourceType,
114 #[serde(default, skip_serializing_if = "Option::is_none")]
116 pub range_start: Option<u64>,
117 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub range_end: Option<u64>,
120 #[serde(default, skip_serializing_if = "Option::is_none")]
122 pub selector: Option<String>,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct PromptRequest {
128 pub prompt_request_id: String,
130 pub name: String,
132 #[serde(default)]
134 pub arguments: HashMap<String, serde_json::Value>,
135 #[serde(default, skip_serializing_if = "Option::is_none")]
137 pub server_id: Option<String>,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct PromptResult {
143 pub prompt_request_id: String,
145 pub prompt_name: String,
147 #[serde(default)]
149 pub messages: Vec<Message>,
150 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub content: Option<String>,
153 #[serde(default)]
155 pub is_error: bool,
156 #[serde(default, skip_serializing_if = "Option::is_none")]
158 pub error_message: Option<String>,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct ImageSource {
168 #[serde(rename = "type")]
170 pub source_type: String,
171 pub data: String,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub media_type: Option<String>,
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct VideoSource {
181 #[serde(rename = "type")]
183 pub source_type: String,
184 pub data: String,
186 #[serde(default, skip_serializing_if = "Option::is_none")]
188 pub media_type: Option<String>,
189 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub duration_ms: Option<u64>,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct AudioSource {
197 #[serde(rename = "type")]
199 pub source_type: String,
200 pub data: String,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub media_type: Option<String>,
205 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub duration_ms: Option<u64>,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct DocumentSource {
213 #[serde(rename = "type")]
215 pub source_type: String,
216 pub data: String,
218 #[serde(default, skip_serializing_if = "Option::is_none")]
220 pub media_type: Option<String>,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
223 pub title: Option<String>,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
238#[serde(tag = "content_type")]
239pub enum ContentPart {
240 #[serde(rename = "text")]
242 Text { text: String },
243
244 #[serde(rename = "thinking")]
246 Thinking { text: String },
247
248 #[serde(rename = "tool_call")]
250 ToolCall { content: ToolCall },
251
252 #[serde(rename = "tool_result")]
254 ToolResult { content: ToolResult },
255
256 #[serde(rename = "resource")]
258 Resource { content: Resource },
259
260 #[serde(rename = "resource_ref")]
262 ResourceRef { content: ResourceReference },
263
264 #[serde(rename = "prompt_request")]
266 PromptRequest { content: PromptRequest },
267
268 #[serde(rename = "prompt_result")]
270 PromptResult { content: PromptResult },
271
272 #[serde(rename = "image")]
274 Image { content: ImageSource },
275
276 #[serde(rename = "video")]
278 Video { content: VideoSource },
279
280 #[serde(rename = "audio")]
282 Audio { content: AudioSource },
283
284 #[serde(rename = "document")]
286 Document { content: DocumentSource },
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 #[test]
294 fn test_text_content_part_serde() {
295 let json = r#"{"content_type":"text","text":"Hello, world!"}"#;
296 let part: ContentPart = serde_json::from_str(json).unwrap();
297 match &part {
298 ContentPart::Text { text } => assert_eq!(text, "Hello, world!"),
299 _ => panic!("expected Text variant"),
300 }
301 let roundtrip = serde_json::to_string(&part).unwrap();
302 let part2: ContentPart = serde_json::from_str(&roundtrip).unwrap();
303 match part2 {
304 ContentPart::Text { text } => assert_eq!(text, "Hello, world!"),
305 _ => panic!("expected Text variant"),
306 }
307 }
308
309 #[test]
310 fn test_tool_call_content_part_serde() {
311 let json = r#"{
312 "content_type": "tool_call",
313 "content": {
314 "tool_call_id": "tc_001",
315 "name": "get_weather",
316 "arguments": {"city": "London"}
317 }
318 }"#;
319 let part: ContentPart = serde_json::from_str(json).unwrap();
320 match &part {
321 ContentPart::ToolCall { content } => {
322 assert_eq!(content.name, "get_weather");
323 assert_eq!(content.tool_call_id, "tc_001");
324 assert_eq!(content.arguments["city"], "London");
325 },
326 _ => panic!("expected ToolCall variant"),
327 }
328 }
329
330 #[test]
331 fn test_tool_result_content_part_serde() {
332 let json = r#"{
333 "content_type": "tool_result",
334 "content": {
335 "tool_call_id": "tc_001",
336 "tool_name": "get_weather",
337 "content": {"temp": 20, "unit": "C"},
338 "is_error": false
339 }
340 }"#;
341 let part: ContentPart = serde_json::from_str(json).unwrap();
342 match &part {
343 ContentPart::ToolResult { content } => {
344 assert_eq!(content.tool_name, "get_weather");
345 assert!(!content.is_error);
346 },
347 _ => panic!("expected ToolResult variant"),
348 }
349 }
350
351 #[test]
352 fn test_resource_content_part_serde() {
353 let json = r#"{
354 "content_type": "resource",
355 "content": {
356 "resource_request_id": "rr_001",
357 "uri": "file:///data.txt",
358 "resource_type": "file",
359 "content": "Hello from file"
360 }
361 }"#;
362 let part: ContentPart = serde_json::from_str(json).unwrap();
363 match &part {
364 ContentPart::Resource { content } => {
365 assert_eq!(content.uri, "file:///data.txt");
366 assert!(content.is_embedded());
367 assert_eq!(content.get_text_content(), Some("Hello from file"));
368 },
369 _ => panic!("expected Resource variant"),
370 }
371 }
372
373 #[test]
374 fn test_resource_ref_content_part_serde() {
375 let json = r#"{
376 "content_type": "resource_ref",
377 "content": {
378 "resource_request_id": "rr_002",
379 "uri": "db://users/42",
380 "resource_type": "database"
381 }
382 }"#;
383 let part: ContentPart = serde_json::from_str(json).unwrap();
384 match &part {
385 ContentPart::ResourceRef { content } => {
386 assert_eq!(content.uri, "db://users/42");
387 assert_eq!(content.resource_type, ResourceType::Database);
388 },
389 _ => panic!("expected ResourceRef variant"),
390 }
391 }
392
393 #[test]
394 fn test_image_content_part_serde() {
395 let json = r#"{
396 "content_type": "image",
397 "content": {
398 "type": "url",
399 "data": "https://example.com/photo.jpg",
400 "media_type": "image/jpeg"
401 }
402 }"#;
403 let part: ContentPart = serde_json::from_str(json).unwrap();
404 match &part {
405 ContentPart::Image { content } => {
406 assert_eq!(content.source_type, "url");
407 assert_eq!(content.data, "https://example.com/photo.jpg");
408 },
409 _ => panic!("expected Image variant"),
410 }
411 }
412
413 #[test]
414 fn test_prompt_request_content_part_serde() {
415 let json = r#"{
416 "content_type": "prompt_request",
417 "content": {
418 "prompt_request_id": "pr_001",
419 "name": "summarize",
420 "arguments": {"text": "Long document..."}
421 }
422 }"#;
423 let part: ContentPart = serde_json::from_str(json).unwrap();
424 match &part {
425 ContentPart::PromptRequest { content } => {
426 assert_eq!(content.name, "summarize");
427 },
428 _ => panic!("expected PromptRequest variant"),
429 }
430 }
431
432 #[test]
433 fn test_thinking_content_part_serde() {
434 let json = r#"{"content_type":"thinking","text":"Let me analyze..."}"#;
435 let part: ContentPart = serde_json::from_str(json).unwrap();
436 match &part {
437 ContentPart::Thinking { text } => assert_eq!(text, "Let me analyze..."),
438 _ => panic!("expected Thinking variant"),
439 }
440 }
441
442 #[test]
443 fn test_tool_call_construction() {
444 let tc = ToolCall {
445 tool_call_id: "tc_001".into(),
446 name: "search".into(),
447 arguments: [("query".to_string(), serde_json::json!("rust"))].into(),
448 namespace: None,
449 };
450 assert_eq!(tc.name, "search");
451 assert_eq!(tc.arguments["query"], "rust");
452 }
453
454 #[test]
455 fn test_resource_is_embedded() {
456 let embedded = Resource {
457 resource_request_id: "rr_001".into(),
458 uri: "file:///data.txt".into(),
459 name: None,
460 description: None,
461 resource_type: ResourceType::File,
462 content: Some("data".into()),
463 blob: None,
464 mime_type: None,
465 size_bytes: None,
466 annotations: HashMap::new(),
467 version: None,
468 };
469 assert!(embedded.is_embedded());
470
471 let not_embedded = Resource {
472 resource_request_id: "rr_002".into(),
473 uri: "file:///other.txt".into(),
474 name: None,
475 description: None,
476 resource_type: ResourceType::File,
477 content: None,
478 blob: None,
479 mime_type: None,
480 size_bytes: None,
481 annotations: HashMap::new(),
482 version: None,
483 };
484 assert!(!not_embedded.is_embedded());
485 }
486}