systemprompt_agent/services/a2a_server/processing/
ai_executor.rs1use futures::StreamExt;
13use std::sync::Arc;
14use tokio::sync::mpsc;
15
16use crate::models::a2a::Artifact;
17use crate::services::SkillService;
18use systemprompt_models::{
19 AiMessage, AiProvider, AiRequest, CallToolResult, MessageRole, RequestContext, StreamChunk,
20 ToolCall, ToolResultFormatter,
21};
22
23use super::message::StreamEvent;
24use crate::models::AgentRuntimeInfo;
25
26pub fn resolve_provider_config(
27 request_context: &RequestContext,
28 agent_runtime: &AgentRuntimeInfo,
29 ai_service: &dyn AiProvider,
30) -> (String, String, u32) {
31 let tool_config = request_context.tool_model_config();
32
33 let provider = tool_config
34 .and_then(|c| c.provider.as_deref())
35 .or(agent_runtime.provider.as_deref())
36 .unwrap_or_else(|| ai_service.default_provider())
37 .to_owned();
38 let model = tool_config
39 .and_then(|c| c.model.as_deref())
40 .or(agent_runtime.model.as_deref())
41 .unwrap_or_else(|| ai_service.default_model())
42 .to_owned();
43 let max_tokens = tool_config
44 .and_then(|c| c.max_output_tokens)
45 .or(agent_runtime.max_output_tokens)
46 .unwrap_or_else(|| ai_service.default_max_output_tokens());
47
48 if tool_config.is_some() {
49 tracing::debug!(
50 provider,
51 model,
52 max_output_tokens = max_tokens,
53 "Using tool_model_config"
54 );
55 }
56
57 (provider, model, max_tokens)
58}
59
60#[expect(
61 missing_debug_implementations,
62 reason = "params struct holds non-Debug references"
63)]
64pub struct SynthesizeToolResultsParams<'a> {
65 pub ai_service: Arc<dyn AiProvider>,
66 pub agent_runtime: &'a AgentRuntimeInfo,
67 pub original_messages: Vec<AiMessage>,
68 pub initial_response: &'a str,
69 pub tool_calls: &'a [ToolCall],
70 pub tool_results: &'a [CallToolResult],
71 pub artifacts: &'a [Artifact],
72 pub tx: mpsc::Sender<StreamEvent>,
73 pub request_context: RequestContext,
74 pub skill_service: Arc<SkillService>,
75}
76
77pub async fn synthesize_tool_results_with_artifacts(
78 params: SynthesizeToolResultsParams<'_>,
79) -> Result<String, ()> {
80 let SynthesizeToolResultsParams {
81 ai_service,
82 agent_runtime,
83 original_messages,
84 initial_response,
85 tool_calls,
86 tool_results,
87 artifacts,
88 tx,
89 request_context,
90 skill_service: _skill_service,
91 } = params;
92 let tool_results_context = ToolResultFormatter::format_for_synthesis(tool_calls, tool_results);
93 let artifact_references = build_artifact_references(artifacts);
94
95 let synthesis_prompt = build_synthesis_prompt(
96 tool_calls.len(),
97 &tool_results_context,
98 &artifact_references,
99 );
100
101 let mut synthesis_messages = original_messages;
102 synthesis_messages.push(AiMessage {
103 role: MessageRole::Assistant,
104 content: initial_response.to_owned(),
105 parts: Vec::new(),
106 });
107 synthesis_messages.push(AiMessage {
108 role: MessageRole::User,
109 content: synthesis_prompt,
110 parts: Vec::new(),
111 });
112
113 tracing::info!(
114 tool_result_count = tool_results.len(),
115 "Calling AI to synthesize tool results"
116 );
117
118 let (provider, model, max_output_tokens) =
119 resolve_provider_config(&request_context, agent_runtime, ai_service.as_ref());
120
121 let synthesis_request = AiRequest::builder(
122 synthesis_messages,
123 &provider,
124 &model,
125 max_output_tokens,
126 request_context,
127 )
128 .build();
129
130 match ai_service.generate(&synthesis_request).await {
131 Ok(response) => {
132 let synthesized_text = response.content;
133
134 tracing::info!(text_len = synthesized_text.len(), "Synthesis complete");
135
136 if tx
137 .try_send(StreamEvent::Text(synthesized_text.clone()))
138 .is_err()
139 {
140 tracing::debug!("Stream receiver dropped during synthesis");
141 }
142
143 Ok(synthesized_text)
144 },
145 Err(e) => {
146 tracing::error!(error = %e, "Synthesis failed");
147 Err(())
148 },
149 }
150}
151
152pub async fn process_without_tools(
153 ai_service: Arc<dyn AiProvider>,
154 agent_runtime: &AgentRuntimeInfo,
155 ai_messages: Vec<AiMessage>,
156 tx: mpsc::Sender<StreamEvent>,
157 request_context: RequestContext,
158) -> Result<(String, Vec<ToolCall>, Vec<CallToolResult>), ()> {
159 let (provider, model, max_output_tokens) =
160 resolve_provider_config(&request_context, agent_runtime, ai_service.as_ref());
161
162 let generate_request = AiRequest::builder(
163 ai_messages,
164 &provider,
165 &model,
166 max_output_tokens,
167 request_context,
168 )
169 .build();
170
171 match ai_service.generate_stream(&generate_request).await {
172 Ok(mut stream) => {
173 let mut accumulated_text = String::new();
174 while let Some(chunk) = stream.next().await {
175 match chunk {
176 Ok(StreamChunk::Text(text)) => {
177 accumulated_text.push_str(&text);
178 if tx.try_send(StreamEvent::Text(text)).is_err() {
179 tracing::debug!("Stream receiver dropped during generation");
180 }
181 },
182 Ok(StreamChunk::Usage { .. }) => {},
183 Err(e) => {
184 if tx.try_send(StreamEvent::Error(e.to_string())).is_err() {
185 tracing::debug!("Stream receiver dropped while sending error");
186 }
187 return Err(());
188 },
189 }
190 }
191 Ok((accumulated_text, Vec::new(), Vec::new()))
192 },
193 Err(e) => {
194 if tx.try_send(StreamEvent::Error(e.to_string())).is_err() {
195 tracing::debug!("Stream receiver dropped while sending error");
196 }
197 Err(())
198 },
199 }
200}
201
202fn build_synthesis_prompt(
203 tool_count: usize,
204 tool_results_context: &str,
205 artifact_references: &str,
206) -> String {
207 format!(
208 r#"# Tool Execution Complete
209
210You executed {} tool(s). Now provide a BRIEF conversational response.
211
212## Tool Results Summary
213
214{}
215
216## Artifacts Created
217
218{}
219
220## CRITICAL RULES - READ CAREFULLY
221
2221. **NEVER repeat artifact content** - The user sees artifacts separately. Your message should REFERENCE them, never duplicate their content.
2232. **Maximum 100 words** - Be extremely concise. 2-3 sentences is ideal.
2243. **Describe what was done, not what it contains** - Say "I've created a blog post about X" NOT "Here is the blog post: [full content]"
2254. **Be conversational** - Natural, friendly summary. Not a report or transcript.
2265. **Reference artifacts naturally** - Use format like "(see the artifact for the full content)"
227
228## BAD EXAMPLE (DO NOT DO THIS)
229"I've created your blog post. Here's the content:
230
231[2000 words of article text]
232
233Let me know if you'd like any changes."
234
235## GOOD EXAMPLE
236"Done! I've created a blog post exploring the Human-AI collaboration workflow. The article covers the key differences between automation and augmentation approaches, with practical steps for maintaining your authentic voice. Take a look at the artifact and let me know if you'd like any adjustments."
237
238---
239
240Provide your brief, conversational response now. Remember: the artifact has the content - your message is just the friendly summary."#,
241 tool_count, tool_results_context, artifact_references
242 )
243}
244
245fn build_artifact_references(artifacts: &[Artifact]) -> String {
246 if artifacts.is_empty() {
247 return "No artifacts were created.".to_owned();
248 }
249
250 artifacts
251 .iter()
252 .map(|artifact| {
253 let artifact_type = &artifact.metadata.artifact_type;
254 let artifact_name = artifact
255 .title
256 .clone()
257 .unwrap_or_else(|| artifact.id.to_string());
258
259 format!(
260 "- **{}** ({}): Reference as '(see {} for details)'",
261 artifact_name, artifact_type, artifact_name
262 )
263 })
264 .collect::<Vec<_>>()
265 .join("\n")
266}