use crate::error::{SwarmError, SwarmResult};
use crate::types::{AgentFunction, Message, RetryStrategy, Steps};
use quick_xml::de::from_str as xml_from_str;
use regex::Regex;
use serde_json::{json, Value};
use std::future::Future;
use std::pin::Pin;
use std::sync::OnceLock;
use std::time::Duration;
pub fn debug_print(debug: bool, message: &str) {
if debug {
tracing::debug!("{}", message);
}
}
pub fn merge_chunk_message(message: &mut Message, delta: &serde_json::Map<String, Value>) {
for (key, value) in delta {
match key.as_str() {
"content" => {
if let Some(content) = value.as_str() {
message.append_content_fragment(content);
}
}
"function_call" => {
message.merge_function_call_delta(value);
}
_ => {}
}
}
}
pub fn function_to_json(func: &AgentFunction) -> SwarmResult<Value> {
Ok(json!({
"name": func.name(),
"description": func.description(),
"parameters": func.parameters_schema(),
}))
}
pub fn parse_steps_from_xml(xml_content: &str) -> SwarmResult<Steps> {
let steps: Steps = xml_from_str(xml_content)
.map_err(|e| SwarmError::XmlError(format!("Failed to parse XML steps: {}", e)))?;
for step in &steps.steps {
if step.prompt.trim().is_empty() {
return Err(SwarmError::ValidationError(format!(
"Step {} has an empty prompt",
step.number
)));
}
}
Ok(steps)
}
pub fn extract_xml_steps(instructions: &str) -> SwarmResult<(String, Option<String>)> {
static STEPS_RE: OnceLock<Regex> = OnceLock::new();
let re = STEPS_RE.get_or_init(|| {
Regex::new(r"(?s)<steps\b[^>]*>.*?</steps>").expect("static steps regex must compile")
});
let mut instructions_without_xml = instructions.to_string();
let mut xml_steps = None;
if let Some(mat) = re.find(instructions) {
let xml_content = mat.as_str();
instructions_without_xml.replace_range(mat.range(), "");
xml_steps = Some(xml_content.to_string());
}
Ok((instructions_without_xml.trim().to_string(), xml_steps))
}
pub fn safe_truncate(s: &str, max_len: usize) -> String {
if s.len() <= max_len {
s.to_string()
} else {
let truncate_at = (0..=max_len)
.rev()
.find(|&i| s.is_char_boundary(i))
.unwrap_or(0);
format!("{}…", &s[..truncate_at])
}
}
pub async fn with_retry<F, T>(strategy: &RetryStrategy, mut f: F) -> SwarmResult<T>
where
F: FnMut() -> Pin<Box<dyn Future<Output = SwarmResult<T>> + 'static>>,
{
let mut delay = strategy.initial_delay();
for attempt in 0..=strategy.max_retries() {
match f().await {
Ok(value) => return Ok(value),
Err(err) if attempt < strategy.max_retries() && err.is_retriable() => {
tracing::warn!(
"Retryable error on attempt {}/{}, retrying in {}ms: {}",
attempt + 1,
strategy.max_retries(),
delay.as_millis(),
err
);
tokio::time::sleep(delay).await;
let next_ms = (delay.as_millis() as f64 * strategy.backoff_factor() as f64) as u64;
delay = Duration::from_millis(next_ms.min(strategy.max_delay().as_millis() as u64));
}
Err(err) => return Err(err),
}
}
Err(SwarmError::Other("Retry attempts exhausted".to_string()))
}