Skip to main content

distri_types/
a2a_converters.rs

1use distri_a2a::{
2    DataPart, EventKind, FileObject, FilePart, Message, Part, Role, Task, TaskState, TaskStatus,
3    TextPart,
4};
5
6use serde::{Deserialize, Serialize};
7use serde_json::json;
8
9use crate::{AgentError, core::FileType};
10
11#[derive(Debug, Serialize, Deserialize, Clone)]
12#[serde(rename_all = "snake_case")]
13pub enum MessageMetadata {
14    Text,
15    Plan,
16    ToolCall,
17    ToolResult,
18}
19
20/// A2A Extension for agent metadata
21/// This allows tracking which agent generated each message
22#[derive(Debug, Serialize, Deserialize, Clone)]
23pub struct AgentMetadata {
24    /// The ID of the agent that generated this message
25    pub agent_id: String,
26    /// Optional agent name for display purposes
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub agent_name: Option<String>,
29}
30
31impl From<crate::Message> for MessageMetadata {
32    fn from(message: crate::Message) -> Self {
33        for part in message.parts.iter() {
34            match part {
35                crate::Part::ToolCall(_) => return MessageMetadata::ToolCall,
36                crate::Part::ToolResult(_) => return MessageMetadata::ToolResult,
37                _ => continue,
38            }
39        }
40        MessageMetadata::Text
41    }
42}
43
44impl TryFrom<Message> for crate::Message {
45    type Error = AgentError;
46
47    fn try_from(message: Message) -> Result<Self, Self::Error> {
48        let mut parts = Vec::new();
49        for part in message.parts {
50            match part {
51                Part::Text(t) => parts.push(crate::Part::Text(t.text.clone())),
52                Part::Data(d) => {
53                    if let Some(part_type) = d.data.get("part_type").and_then(|v| v.as_str()) {
54                        if let Some(data_content) = d.data.get("data") {
55                            // Create the properly structured object for
56
57                            let structured = json!({
58                                "part_type": part_type,
59                                "data": data_content
60                            });
61
62                            let part: crate::Part = serde_json::from_value(structured)?;
63                            parts.push(part);
64                        } else {
65                            return Err(AgentError::Validation(
66                                "Missing data
67                field for typed part"
68                                    .to_string(),
69                            ));
70                        }
71                    } else {
72                        return Err(AgentError::Validation(
73                            "Invalid part
74                type"
75                                .to_string(),
76                        ));
77                    }
78                }
79                Part::File(f) => {
80                    let mime_type = f.mime_type();
81                    if let Some(mime_type) = mime_type {
82                        if mime_type.starts_with("image/") {
83                            let ft = file_object_to_filetype(f.file.clone());
84                            parts.push(crate::Part::Image(ft));
85                        } else {
86                            return Err(AgentError::UnsupportedFileType(mime_type.to_string()));
87                        }
88                    } else {
89                        return Err(AgentError::UnsupportedFileType("unknown".to_string()));
90                    }
91                }
92            }
93        }
94
95        let is_tool = parts.iter().any(|part| {
96            if let crate::Part::ToolResult(_) = part {
97                return true;
98            }
99            false
100        });
101
102        // Extract parts_metadata from message metadata if present
103        let parts_metadata: Option<crate::PartsMetadata> = message
104            .metadata
105            .as_ref()
106            .and_then(|m| m.get("parts"))
107            .and_then(|p| serde_json::from_value(p.clone()).ok());
108
109        Ok(crate::Message {
110            id: message.message_id.clone(),
111            role: if is_tool {
112                crate::MessageRole::Tool
113            } else {
114                match message.role {
115                    Role::User => crate::MessageRole::User,
116                    Role::Agent => crate::MessageRole::Assistant,
117                }
118            },
119            name: None,
120            parts,
121            parts_metadata,
122            ..Default::default()
123        })
124    }
125}
126
127impl From<crate::TaskStatus> for TaskState {
128    fn from(status: crate::TaskStatus) -> Self {
129        match status {
130            crate::TaskStatus::Pending => TaskState::Submitted,
131            crate::TaskStatus::Running => TaskState::Working,
132            crate::TaskStatus::InputRequired => TaskState::InputRequired,
133            crate::TaskStatus::Completed => TaskState::Completed,
134            crate::TaskStatus::Failed => TaskState::Failed,
135            crate::TaskStatus::Canceled => TaskState::Canceled,
136        }
137    }
138}
139
140impl From<crate::Part> for Part {
141    fn from(part: crate::Part) -> Self {
142        match part {
143            crate::Part::Text(text) => Part::Text(TextPart { text: text }),
144            crate::Part::Image(image) => Part::File(FilePart {
145                file: filetype_to_fileobject(image),
146                metadata: None,
147            }),
148
149            // handle all  the additional parts with a part_type
150            x => Part::Data(DataPart {
151                data: serde_json::to_value(x).unwrap(),
152            }),
153        }
154    }
155}
156
157fn file_object_to_filetype(file: FileObject) -> FileType {
158    match file {
159        FileObject::WithBytes {
160            bytes,
161            mime_type,
162            name,
163        } => FileType::Bytes {
164            bytes,
165            mime_type: mime_type.unwrap_or_default(),
166            name,
167        },
168        FileObject::WithUri {
169            uri,
170            mime_type,
171            name,
172        } => FileType::Url {
173            url: uri,
174            mime_type: mime_type.unwrap_or_default(),
175            name,
176        },
177    }
178}
179
180fn filetype_to_fileobject(file: FileType) -> FileObject {
181    match file {
182        FileType::Bytes {
183            bytes,
184            mime_type,
185            name,
186        } => FileObject::WithBytes {
187            bytes,
188            mime_type: Some(mime_type),
189            name: name.clone(),
190        },
191        FileType::Url {
192            url,
193            mime_type,
194            name,
195        } => FileObject::WithUri {
196            uri: url.clone(),
197            mime_type: Some(mime_type),
198            name: name.clone(),
199        },
200    }
201}
202
203impl From<crate::Task> for Task {
204    fn from(task: crate::Task) -> Self {
205        let history = vec![];
206        Task {
207            id: task.id.clone(),
208            status: TaskStatus {
209                state: match task.status {
210                    crate::TaskStatus::Pending => TaskState::Submitted,
211                    crate::TaskStatus::Running => TaskState::Working,
212                    crate::TaskStatus::InputRequired => TaskState::InputRequired,
213                    crate::TaskStatus::Completed => TaskState::Completed,
214                    crate::TaskStatus::Failed => TaskState::Failed,
215                    crate::TaskStatus::Canceled => TaskState::Canceled,
216                },
217                message: None,
218                timestamp: None,
219            },
220            kind: EventKind::Task,
221            context_id: task.thread_id.clone(),
222            artifacts: vec![],
223            history,
224            metadata: None,
225        }
226    }
227}
228
229impl From<crate::MessageRole> for Role {
230    fn from(role: crate::MessageRole) -> Self {
231        match role {
232            crate::MessageRole::User => Role::User,
233            crate::MessageRole::Assistant => Role::Agent,
234            // Developer messages are mapped to User for A2A protocol
235            // since they contain context that should be treated like user input
236            crate::MessageRole::Developer => Role::User,
237            _ => Role::Agent,
238        }
239    }
240}