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