1use bon::Builder;
2use serde::{Deserialize, Deserializer, Serialize};
3use serde_json::{Map, Value};
4
5#[cfg(feature = "tracing")]
6use tracing::instrument;
7
8use crate::domain::error::A2AError;
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
15#[serde(rename_all = "lowercase")]
16pub enum Role {
17 User,
18 Agent,
19}
20
21#[derive(Debug, Clone, Serialize)]
48pub struct FileContent {
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub name: Option<String>,
51 #[serde(skip_serializing_if = "Option::is_none", rename = "mimeType")]
52 pub mime_type: Option<String>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub bytes: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
56 pub uri: Option<String>,
57}
58
59impl<'de> Deserialize<'de> for FileContent {
62 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
63 where
64 D: Deserializer<'de>,
65 {
66 #[derive(Deserialize)]
68 struct FileContentHelper {
69 name: Option<String>,
70 #[serde(rename = "mimeType")]
71 mime_type: Option<String>,
72 bytes: Option<String>,
73 uri: Option<String>,
74 }
75
76 let helper = FileContentHelper::deserialize(deserializer)?;
77
78 let file_content = FileContent {
80 name: helper.name,
81 mime_type: helper.mime_type,
82 bytes: helper.bytes,
83 uri: helper.uri,
84 };
85
86 match file_content.validate() {
88 Ok(_) => Ok(file_content),
89 Err(err) => {
90 Err(serde::de::Error::custom(format!(
92 "FileContent validation error: {}",
93 err
94 )))
95 }
96 }
97 }
98}
99
100impl FileContent {
101 #[cfg_attr(feature = "tracing", instrument(skip(self), fields(
103 file.name = ?self.name,
104 file.has_bytes = self.bytes.is_some(),
105 file.has_uri = self.uri.is_some()
106 )))]
107 pub fn validate(&self) -> Result<(), A2AError> {
108 match (&self.bytes, &self.uri) {
109 (Some(_), None) | (None, Some(_)) => {
110 #[cfg(feature = "tracing")]
111 tracing::debug!("File content validation successful");
112 Ok(())
113 }
114 (Some(_), Some(_)) => {
115 #[cfg(feature = "tracing")]
116 tracing::error!("File content has both bytes and uri");
117 Err(A2AError::InvalidParams(
118 "Cannot provide both bytes and uri".to_string(),
119 ))
120 }
121 (None, None) => {
122 #[cfg(feature = "tracing")]
123 tracing::error!("File content has neither bytes nor uri");
124 Err(A2AError::InvalidParams(
125 "Must provide either bytes or uri".to_string(),
126 ))
127 }
128 }
129 }
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
134#[serde(tag = "kind")]
135pub enum Part {
136 #[serde(rename = "text")]
137 Text {
138 text: String,
139 #[serde(skip_serializing_if = "Option::is_none")]
140 metadata: Option<Map<String, Value>>,
141 },
142 #[serde(rename = "file")]
143 File {
144 file: FileContent,
145 #[serde(skip_serializing_if = "Option::is_none")]
146 metadata: Option<Map<String, Value>>,
147 },
148 #[serde(rename = "data")]
149 Data {
150 data: Map<String, Value>,
151 #[serde(skip_serializing_if = "Option::is_none")]
152 metadata: Option<Map<String, Value>>,
153 },
154}
155
156impl Part {
157 #[cfg(test)]
159 pub fn get_text(&self) -> Option<&str> {
160 match self {
161 Part::Text { text, .. } => Some(text),
162 _ => None,
163 }
164 }
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
187pub struct Message {
188 pub role: Role,
189 #[builder(default = Vec::new())]
190 pub parts: Vec<Part>,
191 #[serde(skip_serializing_if = "Option::is_none")]
192 pub metadata: Option<Map<String, Value>>,
193 #[serde(skip_serializing_if = "Option::is_none", rename = "referenceTaskIds")]
194 pub reference_task_ids: Option<Vec<String>>,
195 #[serde(rename = "messageId")]
196 pub message_id: String,
197 #[serde(skip_serializing_if = "Option::is_none", rename = "taskId")]
198 pub task_id: Option<String>,
199 #[serde(skip_serializing_if = "Option::is_none", rename = "contextId")]
200 pub context_id: Option<String>,
201 #[serde(skip_serializing_if = "Option::is_none")]
203 pub extensions: Option<Vec<String>>,
204 #[builder(default = "message".to_string())]
205 pub kind: String, }
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct Artifact {
232 #[serde(rename = "artifactId")]
233 pub artifact_id: String,
234 #[serde(skip_serializing_if = "Option::is_none")]
235 pub name: Option<String>,
236 #[serde(skip_serializing_if = "Option::is_none")]
237 pub description: Option<String>,
238 pub parts: Vec<Part>,
239 #[serde(skip_serializing_if = "Option::is_none")]
240 pub metadata: Option<Map<String, Value>>,
241 #[serde(skip_serializing_if = "Option::is_none")]
243 pub extensions: Option<Vec<String>>,
244}
245
246impl Part {
248 #[inline]
250 pub fn text(content: String) -> Self {
251 Part::Text {
252 text: content,
253 metadata: None,
254 }
255 }
256
257 #[inline]
259 pub fn text_with_metadata(content: String, metadata: Map<String, Value>) -> Self {
260 Part::Text {
261 text: content,
262 metadata: Some(metadata),
263 }
264 }
265
266 #[inline]
268 pub fn data(data: Map<String, Value>) -> Self {
269 Part::Data {
270 data,
271 metadata: None,
272 }
273 }
274
275 pub fn file_from_bytes(bytes: String, name: Option<String>, mime_type: Option<String>) -> Self {
277 let file_content = FileContent {
278 name,
279 mime_type,
280 bytes: Some(bytes),
281 uri: None,
282 };
283
284 file_content
286 .validate()
287 .expect("FileContent must have either bytes or uri, not both or neither");
288
289 Part::File {
290 file: file_content,
291 metadata: None,
292 }
293 }
294
295 pub fn file_from_uri(uri: String, name: Option<String>, mime_type: Option<String>) -> Self {
297 let file_content = FileContent {
298 name,
299 mime_type,
300 bytes: None,
301 uri: Some(uri),
302 };
303
304 debug_assert!(
306 file_content.validate().is_ok(),
307 "FileContent validation failed"
308 );
309
310 Part::File {
311 file: file_content,
312 metadata: None,
313 }
314 }
315
316 pub fn text_builder(content: String) -> PartBuilder {
318 PartBuilder::new_text(content)
319 }
320
321 pub fn data_builder(data: Map<String, Value>) -> PartBuilder {
323 PartBuilder::new_data(data)
324 }
325
326 pub fn file_builder() -> FilePartBuilder {
328 FilePartBuilder::new()
329 }
330}
331
332pub struct PartBuilder {
334 part: Part,
335}
336
337impl PartBuilder {
338 fn new_text(content: String) -> Self {
339 Self {
340 part: Part::Text {
341 text: content,
342 metadata: None,
343 },
344 }
345 }
346
347 fn new_data(data: Map<String, Value>) -> Self {
348 Self {
349 part: Part::Data {
350 data,
351 metadata: None,
352 },
353 }
354 }
355
356 pub fn with_metadata(mut self, metadata: Map<String, Value>) -> Self {
358 match &mut self.part {
359 Part::Text { metadata: meta, .. } => *meta = Some(metadata),
360 Part::Data { metadata: meta, .. } => *meta = Some(metadata),
361 Part::File { metadata: meta, .. } => *meta = Some(metadata),
362 }
363 self
364 }
365
366 pub fn build(self) -> Part {
368 self.part
369 }
370}
371
372pub struct FilePartBuilder {
374 name: Option<String>,
375 mime_type: Option<String>,
376 bytes: Option<String>,
377 uri: Option<String>,
378 metadata: Option<Map<String, Value>>,
379}
380
381impl FilePartBuilder {
382 fn new() -> Self {
383 Self {
384 name: None,
385 mime_type: None,
386 bytes: None,
387 uri: None,
388 metadata: None,
389 }
390 }
391
392 pub fn name(mut self, name: String) -> Self {
394 self.name = Some(name);
395 self
396 }
397
398 pub fn mime_type(mut self, mime_type: String) -> Self {
400 self.mime_type = Some(mime_type);
401 self
402 }
403
404 pub fn bytes(mut self, bytes: String) -> Self {
406 self.bytes = Some(bytes);
407 self.uri = None; self
409 }
410
411 pub fn uri(mut self, uri: String) -> Self {
413 self.uri = Some(uri);
414 self.bytes = None; self
416 }
417
418 pub fn with_metadata(mut self, metadata: Map<String, Value>) -> Self {
420 self.metadata = Some(metadata);
421 self
422 }
423
424 pub fn build(self) -> Result<Part, A2AError> {
426 let file_content = FileContent {
427 name: self.name,
428 mime_type: self.mime_type,
429 bytes: self.bytes,
430 uri: self.uri,
431 };
432
433 file_content.validate()?;
435
436 Ok(Part::File {
437 file: file_content,
438 metadata: self.metadata,
439 })
440 }
441}
442
443impl Message {
445 pub fn user_text(text: String, message_id: String) -> Self {
447 Self {
448 role: Role::User,
449 parts: vec![Part::text(text)],
450 metadata: None,
451 reference_task_ids: None,
452 message_id,
453 task_id: None,
454 context_id: None,
455 extensions: None,
456 kind: "message".to_string(),
457 }
458 }
459
460 pub fn agent_text(text: String, message_id: String) -> Self {
462 Self {
463 role: Role::Agent,
464 parts: vec![Part::text(text)],
465 metadata: None,
466 reference_task_ids: None,
467 message_id,
468 task_id: None,
469 context_id: None,
470 extensions: None,
471 kind: "message".to_string(),
472 }
473 }
474
475 pub fn add_part(&mut self, part: Part) {
477 if let Part::File { file, .. } = &part {
479 debug_assert!(
481 file.validate().is_ok(),
482 "Invalid file content in Part::File"
483 );
484 }
485
486 self.parts.push(part);
487 }
488
489 #[cfg_attr(feature = "tracing", instrument(skip(self, part), fields(
491 message.id = %self.message_id
492 )))]
493 pub fn add_part_validated(&mut self, part: Part) -> Result<(), A2AError> {
494 if let Part::File { file, .. } = &part {
496 file.validate()?;
497 }
498
499 #[cfg(feature = "tracing")]
500 {
501 let part_type = match &part {
502 Part::Text { .. } => "text",
503 Part::File { .. } => "file",
504 Part::Data { .. } => "data",
505 };
506 tracing::debug!(part_type = part_type, "Part added successfully to message");
507 }
508
509 self.parts.push(part);
510 Ok(())
511 }
512
513 #[cfg_attr(feature = "tracing", instrument(skip(self), fields(
515 message.id = %self.message_id,
516 message.role = ?self.role,
517 message.parts_count = self.parts.len(),
518 message.kind = %self.kind
519 )))]
520 pub fn validate(&self) -> Result<(), A2AError> {
521 #[cfg(feature = "tracing")]
522 tracing::debug!("Validating message");
523
524 for (index, part) in self.parts.iter().enumerate() {
526 if let Part::File { file, .. } = part {
527 #[cfg(feature = "tracing")]
528 tracing::trace!("Validating file part at index {}", index);
529 file.validate()?;
530 }
531 }
532
533 if self.kind != "message" {
535 #[cfg(feature = "tracing")]
536 tracing::error!("Invalid message kind: {}", self.kind);
537 return Err(A2AError::InvalidParams(
538 "Message kind must be 'message'".to_string(),
539 ));
540 }
541
542 #[cfg(feature = "tracing")]
543 tracing::debug!("Message validation successful");
544 Ok(())
545 }
546}