use std::future::Future; use std::time::Duration; use crate::agent::request::AgentRequest; use crate::error::{AiError, AiResult}; pub fn build_input_string(request: &AgentRequest) -> String { let mut input = String::new(); if !request.context.is_empty() { input.push_str("\n"); for chunk in &request.context { let source = chunk.source.as_deref().unwrap_or("unknown"); let score = chunk .score .map(|s| format!("{s:.4}")) .unwrap_or_else(|| "n/a".to_string()); input.push_str(&format!( "\n\n{}\n\n", chunk.id, source, score, chunk.content )); } input.push_str("\n\n"); } for message in &request.messages { match message { super::request::AgentMessage::User(content) => { input.push_str(&format!("User: {content}\n")); } super::request::AgentMessage::Assistant(content) => { input.push_str(&format!("Assistant: {content}\n")); } } } input.push_str(&format!("User: {}", request.input)); input } pub fn estimate_tokens(text: &str) -> u64 { if text.is_empty() { return 0; } (text.chars().count() as f64 / 2.5).ceil() as u64 } pub fn check_token_budget( estimated_input_tokens: u64, accumulated_output_chars: usize, limit: i64, ) -> bool { let output_estimate = (accumulated_output_chars as f64 / 2.5).ceil() as u64; estimated_input_tokens + output_estimate > limit as u64 } pub async fn with_retry( max_attempts: usize, base_delay_ms: u64, f: F, ) -> AiResult where F: Fn() -> Fut, Fut: Future>, { let mut last_error: Option = None; for attempt in 0..max_attempts { match f().await { Ok(result) => return Ok(result), Err(e) if is_retryable(&e) && attempt + 1 < max_attempts => { let delay = Duration::from_millis(base_delay_ms * 2u64.pow(attempt as u32)); tracing::warn!( error = %e, attempt = attempt + 1, max_attempts, delay_ms = delay.as_millis(), "retrying after transient error" ); tokio::time::sleep(delay).await; last_error = Some(e); } Err(e) => return Err(e), } } Err(AiError::ModelRetriesExhausted { attempts: max_attempts, last_error: last_error .map(|e| e.to_string()) .unwrap_or_else(|| "unknown".to_string()), }) } fn is_retryable(error: &AiError) -> bool { matches!( error, AiError::Api(_) | AiError::Response(_) | AiError::ModelRetriesExhausted { .. } ) } pub fn tool_result_content_to_string( content: &rig::one_or_many::OneOrMany, ) -> String { use rig::completion::message::ToolResultContent; content .iter() .filter_map(|item| match item { ToolResultContent::Text(t) => Some(t.text.clone()), _ => None, }) .collect::>() .join("\n") }