gemini_adk_rs/a2a/
part_converter.rs

1//! Bidirectional GenAI <-> A2A Part conversion.
2
3use super::types::{A2aFileContent, A2aPart};
4use gemini_genai_rs::prelude::{
5    Blob, CodeExecutionResult, ExecutableCode, FunctionCall, FunctionResponse, Part,
6};
7use std::collections::HashMap;
8
9/// Metadata key identifying the ADK type of a data part.
10pub const ADK_TYPE_KEY: &str = "adk_type";
11/// Metadata key flagging a function call as long-running.
12pub const ADK_IS_LONG_RUNNING_KEY: &str = "adk_is_long_running";
13/// Metadata key for thought/reasoning content.
14pub const ADK_THOUGHT_KEY: &str = "adk_thought";
15
16/// Type tag for function call data parts.
17pub const DATA_TYPE_FUNCTION_CALL: &str = "function_call";
18/// Type tag for function response data parts.
19pub const DATA_TYPE_FUNCTION_RESPONSE: &str = "function_response";
20/// Type tag for code execution result data parts.
21pub const DATA_TYPE_CODE_EXEC_RESULT: &str = "code_execution_result";
22/// Type tag for executable code data parts.
23pub const DATA_TYPE_EXECUTABLE_CODE: &str = "executable_code";
24
25/// Convert GenAI Parts to A2A Parts.
26pub fn to_a2a_parts(parts: &[Part], long_running_tool_ids: &[String]) -> Vec<A2aPart> {
27    parts
28        .iter()
29        .filter_map(|p| to_a2a_part(p, long_running_tool_ids))
30        .collect()
31}
32
33/// Convert a single GenAI Part to an A2A Part.
34pub fn to_a2a_part(part: &Part, long_running_tool_ids: &[String]) -> Option<A2aPart> {
35    match part {
36        Part::Text { text } => Some(A2aPart::Text {
37            text: text.clone(),
38            metadata: None,
39        }),
40        Part::InlineData { inline_data } => Some(A2aPart::File {
41            file: A2aFileContent {
42                name: None,
43                mime_type: Some(inline_data.mime_type.clone()),
44                bytes: Some(inline_data.data.clone()),
45                uri: None,
46            },
47            metadata: None,
48        }),
49        Part::FunctionCall { function_call } => {
50            let mut metadata = HashMap::new();
51            metadata.insert(
52                ADK_TYPE_KEY.to_string(),
53                serde_json::json!(DATA_TYPE_FUNCTION_CALL),
54            );
55            if let Some(id) = &function_call.id {
56                if long_running_tool_ids.contains(id) {
57                    metadata.insert(ADK_IS_LONG_RUNNING_KEY.to_string(), serde_json::json!(true));
58                }
59            }
60            Some(A2aPart::Data {
61                data: serde_json::json!({
62                    "name": function_call.name,
63                    "args": function_call.args,
64                    "id": function_call.id,
65                }),
66                metadata: Some(metadata),
67            })
68        }
69        Part::FunctionResponse { function_response } => {
70            let mut metadata = HashMap::new();
71            metadata.insert(
72                ADK_TYPE_KEY.to_string(),
73                serde_json::json!(DATA_TYPE_FUNCTION_RESPONSE),
74            );
75            Some(A2aPart::Data {
76                data: serde_json::json!({
77                    "name": function_response.name,
78                    "response": function_response.response,
79                    "id": function_response.id,
80                }),
81                metadata: Some(metadata),
82            })
83        }
84        Part::ExecutableCode { executable_code } => {
85            let mut metadata = HashMap::new();
86            metadata.insert(
87                ADK_TYPE_KEY.to_string(),
88                serde_json::json!(DATA_TYPE_EXECUTABLE_CODE),
89            );
90            Some(A2aPart::Data {
91                data: serde_json::json!({
92                    "language": executable_code.language,
93                    "code": executable_code.code,
94                }),
95                metadata: Some(metadata),
96            })
97        }
98        Part::CodeExecutionResult {
99            code_execution_result,
100        } => {
101            let mut metadata = HashMap::new();
102            metadata.insert(
103                ADK_TYPE_KEY.to_string(),
104                serde_json::json!(DATA_TYPE_CODE_EXEC_RESULT),
105            );
106            Some(A2aPart::Data {
107                data: serde_json::json!({
108                    "outcome": code_execution_result.outcome,
109                    "output": code_execution_result.output,
110                }),
111                metadata: Some(metadata),
112            })
113        }
114        Part::Thought { text, .. } => Some(A2aPart::Text {
115            text: text.clone(),
116            metadata: None,
117        }),
118    }
119}
120
121/// Convert A2A Parts to GenAI Parts.
122pub fn to_genai_parts(a2a_parts: &[A2aPart]) -> Vec<Part> {
123    a2a_parts.iter().filter_map(to_genai_part).collect()
124}
125
126/// Convert a single A2A Part to a GenAI Part.
127pub fn to_genai_part(a2a_part: &A2aPart) -> Option<Part> {
128    match a2a_part {
129        A2aPart::Text { text, .. } => Some(Part::Text { text: text.clone() }),
130        A2aPart::File { file, .. } => {
131            // Convert to InlineData if bytes present; URI-based files can't be represented
132            file.bytes.as_ref().map(|bytes| Part::InlineData {
133                inline_data: Blob {
134                    mime_type: file.mime_type.clone().unwrap_or_default(),
135                    data: bytes.clone(),
136                },
137            })
138        }
139        A2aPart::Data { data, metadata } => {
140            let adk_type = metadata
141                .as_ref()
142                .and_then(|m| m.get(ADK_TYPE_KEY))
143                .and_then(|v| v.as_str());
144
145            match adk_type {
146                Some(DATA_TYPE_FUNCTION_CALL) => Some(Part::FunctionCall {
147                    function_call: FunctionCall {
148                        name: data.get("name")?.as_str()?.to_string(),
149                        args: data.get("args").cloned().unwrap_or(serde_json::json!({})),
150                        id: data.get("id").and_then(|v| v.as_str()).map(String::from),
151                    },
152                }),
153                Some(DATA_TYPE_FUNCTION_RESPONSE) => Some(Part::FunctionResponse {
154                    function_response: FunctionResponse {
155                        name: data.get("name")?.as_str()?.to_string(),
156                        response: data
157                            .get("response")
158                            .cloned()
159                            .unwrap_or(serde_json::json!({})),
160                        id: data.get("id").and_then(|v| v.as_str()).map(String::from),
161                        scheduling: None,
162                    },
163                }),
164                Some(DATA_TYPE_EXECUTABLE_CODE) => Some(Part::ExecutableCode {
165                    executable_code: ExecutableCode {
166                        language: data.get("language")?.as_str()?.to_string(),
167                        code: data.get("code")?.as_str()?.to_string(),
168                    },
169                }),
170                Some(DATA_TYPE_CODE_EXEC_RESULT) => Some(Part::CodeExecutionResult {
171                    code_execution_result: CodeExecutionResult {
172                        outcome: data.get("outcome")?.as_str()?.to_string(),
173                        output: data
174                            .get("output")
175                            .and_then(|v| v.as_str())
176                            .map(String::from),
177                    },
178                }),
179                _ => None,
180            }
181        }
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn text_part_to_a2a() {
191        let part = Part::Text {
192            text: "hello world".to_string(),
193        };
194        let a2a = to_a2a_part(&part, &[]).unwrap();
195        match a2a {
196            A2aPart::Text { text, metadata } => {
197                assert_eq!(text, "hello world");
198                assert!(metadata.is_none());
199            }
200            _ => panic!("Expected Text part"),
201        }
202    }
203
204    #[test]
205    fn inline_data_to_a2a_file() {
206        let part = Part::InlineData {
207            inline_data: Blob {
208                mime_type: "image/png".to_string(),
209                data: "base64data".to_string(),
210            },
211        };
212        let a2a = to_a2a_part(&part, &[]).unwrap();
213        match a2a {
214            A2aPart::File { file, metadata } => {
215                assert_eq!(file.mime_type.as_deref(), Some("image/png"));
216                assert_eq!(file.bytes.as_deref(), Some("base64data"));
217                assert!(file.uri.is_none());
218                assert!(file.name.is_none());
219                assert!(metadata.is_none());
220            }
221            _ => panic!("Expected File part"),
222        }
223    }
224
225    #[test]
226    fn function_call_to_a2a_data_with_metadata() {
227        let part = Part::FunctionCall {
228            function_call: FunctionCall {
229                name: "get_weather".to_string(),
230                args: serde_json::json!({"city": "London"}),
231                id: Some("call-1".to_string()),
232            },
233        };
234        let a2a = to_a2a_part(&part, &[]).unwrap();
235        match &a2a {
236            A2aPart::Data { data, metadata } => {
237                assert_eq!(data["name"], "get_weather");
238                assert_eq!(data["args"]["city"], "London");
239                assert_eq!(data["id"], "call-1");
240                let meta = metadata.as_ref().unwrap();
241                assert_eq!(meta[ADK_TYPE_KEY], DATA_TYPE_FUNCTION_CALL);
242                assert!(!meta.contains_key(ADK_IS_LONG_RUNNING_KEY));
243            }
244            _ => panic!("Expected Data part"),
245        }
246    }
247
248    #[test]
249    fn function_call_long_running() {
250        let part = Part::FunctionCall {
251            function_call: FunctionCall {
252                name: "slow_op".to_string(),
253                args: serde_json::json!({}),
254                id: Some("lr-1".to_string()),
255            },
256        };
257        let long_running = vec!["lr-1".to_string()];
258        let a2a = to_a2a_part(&part, &long_running).unwrap();
259        match &a2a {
260            A2aPart::Data { metadata, .. } => {
261                let meta = metadata.as_ref().unwrap();
262                assert_eq!(meta[ADK_IS_LONG_RUNNING_KEY], serde_json::json!(true));
263            }
264            _ => panic!("Expected Data part"),
265        }
266    }
267
268    #[test]
269    fn function_response_to_a2a_data() {
270        let part = Part::FunctionResponse {
271            function_response: FunctionResponse {
272                name: "get_weather".to_string(),
273                response: serde_json::json!({"temp": 20}),
274                id: Some("call-1".to_string()),
275                scheduling: None,
276            },
277        };
278        let a2a = to_a2a_part(&part, &[]).unwrap();
279        match &a2a {
280            A2aPart::Data { data, metadata } => {
281                assert_eq!(data["name"], "get_weather");
282                assert_eq!(data["response"]["temp"], 20);
283                assert_eq!(data["id"], "call-1");
284                let meta = metadata.as_ref().unwrap();
285                assert_eq!(meta[ADK_TYPE_KEY], DATA_TYPE_FUNCTION_RESPONSE);
286            }
287            _ => panic!("Expected Data part"),
288        }
289    }
290
291    #[test]
292    fn executable_code_to_a2a_data() {
293        let part = Part::ExecutableCode {
294            executable_code: ExecutableCode {
295                language: "python".to_string(),
296                code: "print('hi')".to_string(),
297            },
298        };
299        let a2a = to_a2a_part(&part, &[]).unwrap();
300        match &a2a {
301            A2aPart::Data { data, metadata } => {
302                assert_eq!(data["language"], "python");
303                assert_eq!(data["code"], "print('hi')");
304                let meta = metadata.as_ref().unwrap();
305                assert_eq!(meta[ADK_TYPE_KEY], DATA_TYPE_EXECUTABLE_CODE);
306            }
307            _ => panic!("Expected Data part"),
308        }
309    }
310
311    #[test]
312    fn code_execution_result_to_a2a_data() {
313        let part = Part::CodeExecutionResult {
314            code_execution_result: CodeExecutionResult {
315                outcome: "success".to_string(),
316                output: Some("hi".to_string()),
317            },
318        };
319        let a2a = to_a2a_part(&part, &[]).unwrap();
320        match &a2a {
321            A2aPart::Data { data, metadata } => {
322                assert_eq!(data["outcome"], "success");
323                assert_eq!(data["output"], "hi");
324                let meta = metadata.as_ref().unwrap();
325                assert_eq!(meta[ADK_TYPE_KEY], DATA_TYPE_CODE_EXEC_RESULT);
326            }
327            _ => panic!("Expected Data part"),
328        }
329    }
330
331    #[test]
332    fn a2a_text_to_genai_part() {
333        let a2a = A2aPart::Text {
334            text: "hello".to_string(),
335            metadata: None,
336        };
337        let part = to_genai_part(&a2a).unwrap();
338        match part {
339            Part::Text { text } => assert_eq!(text, "hello"),
340            _ => panic!("Expected Text part"),
341        }
342    }
343
344    #[test]
345    fn a2a_file_to_genai_inline_data() {
346        let a2a = A2aPart::File {
347            file: A2aFileContent {
348                name: None,
349                mime_type: Some("audio/pcm".to_string()),
350                bytes: Some("pcmdata".to_string()),
351                uri: None,
352            },
353            metadata: None,
354        };
355        let part = to_genai_part(&a2a).unwrap();
356        match part {
357            Part::InlineData { inline_data } => {
358                assert_eq!(inline_data.mime_type, "audio/pcm");
359                assert_eq!(inline_data.data, "pcmdata");
360            }
361            _ => panic!("Expected InlineData part"),
362        }
363    }
364
365    #[test]
366    fn a2a_file_uri_only_returns_none() {
367        let a2a = A2aPart::File {
368            file: A2aFileContent {
369                name: None,
370                mime_type: Some("image/png".to_string()),
371                bytes: None,
372                uri: Some("gs://bucket/img.png".to_string()),
373            },
374            metadata: None,
375        };
376        assert!(to_genai_part(&a2a).is_none());
377    }
378
379    #[test]
380    fn a2a_data_function_call_to_genai() {
381        let mut metadata = HashMap::new();
382        metadata.insert(
383            ADK_TYPE_KEY.to_string(),
384            serde_json::json!(DATA_TYPE_FUNCTION_CALL),
385        );
386        let a2a = A2aPart::Data {
387            data: serde_json::json!({
388                "name": "search",
389                "args": {"query": "rust"},
390                "id": "fc-1",
391            }),
392            metadata: Some(metadata),
393        };
394        let part = to_genai_part(&a2a).unwrap();
395        match part {
396            Part::FunctionCall { function_call } => {
397                assert_eq!(function_call.name, "search");
398                assert_eq!(function_call.args["query"], "rust");
399                assert_eq!(function_call.id.as_deref(), Some("fc-1"));
400            }
401            _ => panic!("Expected FunctionCall part"),
402        }
403    }
404
405    #[test]
406    fn a2a_data_function_response_to_genai() {
407        let mut metadata = HashMap::new();
408        metadata.insert(
409            ADK_TYPE_KEY.to_string(),
410            serde_json::json!(DATA_TYPE_FUNCTION_RESPONSE),
411        );
412        let a2a = A2aPart::Data {
413            data: serde_json::json!({
414                "name": "search",
415                "response": {"results": [1, 2, 3]},
416                "id": "fc-1",
417            }),
418            metadata: Some(metadata),
419        };
420        let part = to_genai_part(&a2a).unwrap();
421        match part {
422            Part::FunctionResponse { function_response } => {
423                assert_eq!(function_response.name, "search");
424                assert_eq!(
425                    function_response.response["results"],
426                    serde_json::json!([1, 2, 3])
427                );
428                assert_eq!(function_response.id.as_deref(), Some("fc-1"));
429            }
430            _ => panic!("Expected FunctionResponse part"),
431        }
432    }
433
434    #[test]
435    fn a2a_data_executable_code_to_genai() {
436        let mut metadata = HashMap::new();
437        metadata.insert(
438            ADK_TYPE_KEY.to_string(),
439            serde_json::json!(DATA_TYPE_EXECUTABLE_CODE),
440        );
441        let a2a = A2aPart::Data {
442            data: serde_json::json!({
443                "language": "python",
444                "code": "x = 1",
445            }),
446            metadata: Some(metadata),
447        };
448        let part = to_genai_part(&a2a).unwrap();
449        match part {
450            Part::ExecutableCode { executable_code } => {
451                assert_eq!(executable_code.language, "python");
452                assert_eq!(executable_code.code, "x = 1");
453            }
454            _ => panic!("Expected ExecutableCode part"),
455        }
456    }
457
458    #[test]
459    fn a2a_data_code_exec_result_to_genai() {
460        let mut metadata = HashMap::new();
461        metadata.insert(
462            ADK_TYPE_KEY.to_string(),
463            serde_json::json!(DATA_TYPE_CODE_EXEC_RESULT),
464        );
465        let a2a = A2aPart::Data {
466            data: serde_json::json!({
467                "outcome": "success",
468                "output": "42",
469            }),
470            metadata: Some(metadata),
471        };
472        let part = to_genai_part(&a2a).unwrap();
473        match part {
474            Part::CodeExecutionResult {
475                code_execution_result,
476            } => {
477                assert_eq!(code_execution_result.outcome, "success");
478                assert_eq!(code_execution_result.output.as_deref(), Some("42"));
479            }
480            _ => panic!("Expected CodeExecutionResult part"),
481        }
482    }
483
484    #[test]
485    fn a2a_data_unknown_type_returns_none() {
486        let a2a = A2aPart::Data {
487            data: serde_json::json!({"foo": "bar"}),
488            metadata: None,
489        };
490        assert!(to_genai_part(&a2a).is_none());
491    }
492
493    // Round-trip tests
494
495    #[test]
496    fn round_trip_text() {
497        let original = Part::Text {
498            text: "round trip".to_string(),
499        };
500        let a2a = to_a2a_part(&original, &[]).unwrap();
501        let back = to_genai_part(&a2a).unwrap();
502        assert_eq!(back, original);
503    }
504
505    #[test]
506    fn round_trip_function_call() {
507        let original = Part::FunctionCall {
508            function_call: FunctionCall {
509                name: "my_tool".to_string(),
510                args: serde_json::json!({"x": 10}),
511                id: Some("id-1".to_string()),
512            },
513        };
514        let a2a = to_a2a_part(&original, &[]).unwrap();
515        let back = to_genai_part(&a2a).unwrap();
516        assert_eq!(back, original);
517    }
518
519    #[test]
520    fn round_trip_function_response() {
521        let original = Part::FunctionResponse {
522            function_response: FunctionResponse {
523                name: "my_tool".to_string(),
524                response: serde_json::json!({"result": "ok"}),
525                id: Some("id-1".to_string()),
526                scheduling: None,
527            },
528        };
529        let a2a = to_a2a_part(&original, &[]).unwrap();
530        let back = to_genai_part(&a2a).unwrap();
531        assert_eq!(back, original);
532    }
533
534    #[test]
535    fn to_a2a_parts_filters_and_collects() {
536        let parts = vec![
537            Part::Text {
538                text: "a".to_string(),
539            },
540            Part::Text {
541                text: "b".to_string(),
542            },
543        ];
544        let a2a = to_a2a_parts(&parts, &[]);
545        assert_eq!(a2a.len(), 2);
546    }
547
548    #[test]
549    fn to_genai_parts_filters_and_collects() {
550        let a2a_parts = vec![
551            A2aPart::Text {
552                text: "x".to_string(),
553                metadata: None,
554            },
555            // This data part has no adk_type, so it will be filtered out
556            A2aPart::Data {
557                data: serde_json::json!({}),
558                metadata: None,
559            },
560        ];
561        let genai = to_genai_parts(&a2a_parts);
562        assert_eq!(genai.len(), 1);
563    }
564}