systemprompt_agent/services/a2a_server/processing/message/stream_processor/
mod.rs1mod helpers;
12mod processing;
13
14use base64::Engine;
15use std::sync::Arc;
16
17use crate::models::a2a::{FilePart, Message, Part};
18use crate::repository::execution::ExecutionStepRepository;
19use crate::services::{ContextService, SkillService};
20use systemprompt_models::{
21 AiContentPart, AiProvider, is_supported_audio, is_supported_image, is_supported_text,
22 is_supported_video,
23};
24
25#[expect(
26 missing_debug_implementations,
27 reason = "params struct holds non-Debug references"
28)]
29pub struct StreamProcessor {
30 pub ai_service: Arc<dyn AiProvider>,
31 pub context_service: ContextService,
32 pub skill_service: Arc<SkillService>,
33 pub execution_step_repo: Arc<ExecutionStepRepository>,
34}
35
36impl StreamProcessor {
37 pub fn extract_message_content(message: &Message) -> (String, Vec<AiContentPart>) {
38 let mut text_content = String::new();
39 let mut content_parts = Vec::new();
40
41 for part in &message.parts {
42 match part {
43 Part::Text(text_part) => {
44 if text_content.is_empty() {
45 text_content.clone_from(&text_part.text);
46 }
47 content_parts.push(AiContentPart::text(&text_part.text));
48 },
49 Part::File(file_part) => {
50 if let Some(content_part) = Self::file_to_content_part(file_part) {
51 content_parts.push(content_part);
52 }
53 },
54 Part::Data(_) => {},
55 }
56 }
57
58 (text_content, content_parts)
59 }
60
61 fn file_to_content_part(file_part: &FilePart) -> Option<AiContentPart> {
62 let mime_type = file_part.file.mime_type.as_deref()?;
63 let file_name = file_part.file.name.as_deref().unwrap_or("unnamed");
64
65 let bytes = file_part.file.bytes.as_deref()?;
66
67 if is_supported_image(mime_type) {
68 return Some(AiContentPart::image(mime_type, bytes));
69 }
70
71 if is_supported_audio(mime_type) {
72 return Some(AiContentPart::audio(mime_type, bytes));
73 }
74
75 if is_supported_video(mime_type) {
76 return Some(AiContentPart::video(mime_type, bytes));
77 }
78
79 if is_supported_text(mime_type) {
80 return Self::decode_text_file(bytes, file_name, mime_type);
81 }
82
83 tracing::warn!(
84 file_name = %file_name,
85 mime_type = %mime_type,
86 "Unsupported file type - file will not be sent to AI"
87 );
88 None
89 }
90
91 fn decode_text_file(bytes: &str, file_name: &str, mime_type: &str) -> Option<AiContentPart> {
92 let decoded = base64::engine::general_purpose::STANDARD
93 .decode(bytes)
94 .map_err(|e| {
95 tracing::warn!(
96 file_name = %file_name,
97 mime_type = %mime_type,
98 error = %e,
99 "Failed to decode base64 text file"
100 );
101 e
102 })
103 .ok()?;
104
105 let text_content = String::from_utf8(decoded)
106 .map_err(|e| {
107 tracing::warn!(
108 file_name = %file_name,
109 mime_type = %mime_type,
110 error = %e,
111 "Failed to decode text file as UTF-8"
112 );
113 e
114 })
115 .ok()?;
116
117 let formatted = format!("[File: {file_name} ({mime_type})]\n{text_content}");
118 Some(AiContentPart::text(formatted))
119 }
120}