use std::time::Duration;
pub const DEFAULT_RETRY_DELAYS_SECS: [u64; 5] = [1, 3, 10, 20, 60];
pub const DEFAULT_MAX_RETRIES: usize = 10;
#[derive(Debug, Clone)]
pub struct RetryConfig {
pub max_retries: usize,
pub delays_secs: Vec<u64>,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_retries: DEFAULT_MAX_RETRIES,
delays_secs: DEFAULT_RETRY_DELAYS_SECS.to_vec(),
}
}
}
impl RetryConfig {
pub fn with_max_retries(max_retries: usize) -> Self {
Self {
max_retries,
..Default::default()
}
}
pub fn with_delays(max_retries: usize, delays_secs: Vec<u64>) -> Self {
Self {
max_retries,
delays_secs,
}
}
pub fn no_retries() -> Self {
Self {
max_retries: 0,
delays_secs: vec![],
}
}
pub fn delay_for_attempt(&self, attempt: usize) -> Duration {
let secs = self
.delays_secs
.get(attempt)
.copied()
.unwrap_or_else(|| *self.delays_secs.last().unwrap_or(&60));
Duration::from_secs(secs)
}
pub fn retries_enabled(&self) -> bool {
self.max_retries > 0
}
}
#[derive(Debug, Clone, Default)]
pub struct ResponseValidation {
pub min_length: Option<usize>,
pub max_tool_errors: Option<usize>,
pub max_validation_retries: usize,
}
impl ResponseValidation {
pub fn min_length(min_length: usize) -> Self {
Self {
min_length: Some(min_length),
max_tool_errors: None,
max_validation_retries: 3,
}
}
pub fn max_tool_errors(max_errors: usize) -> Self {
Self {
min_length: None,
max_tool_errors: Some(max_errors),
max_validation_retries: 3,
}
}
pub fn new(min_length: usize, max_tool_errors: usize) -> Self {
Self {
min_length: Some(min_length),
max_tool_errors: Some(max_tool_errors),
max_validation_retries: 3,
}
}
pub fn with_max_retries(mut self, max_retries: usize) -> Self {
self.max_validation_retries = max_retries;
self
}
pub fn is_enabled(&self) -> bool {
self.min_length.is_some() || self.max_tool_errors.is_some()
}
pub fn check_length(&self, content: &str) -> bool {
match self.min_length {
Some(min) => content.len() >= min,
None => true,
}
}
pub fn check_tool_errors(&self, error_count: usize) -> bool {
match self.max_tool_errors {
Some(max) => error_count <= max,
None => true,
}
}
pub fn validate(&self, content: &str, tool_error_count: usize) -> ValidationResult {
if !self.check_length(content) {
return ValidationResult::TooShort {
actual: content.len(),
minimum: self.min_length.unwrap_or(0),
};
}
if !self.check_tool_errors(tool_error_count) {
return ValidationResult::TooManyToolErrors {
actual: tool_error_count,
maximum: self.max_tool_errors.unwrap_or(0),
};
}
ValidationResult::Valid
}
pub fn validate_skip_length(&self, tool_error_count: usize) -> ValidationResult {
if !self.check_tool_errors(tool_error_count) {
return ValidationResult::TooManyToolErrors {
actual: tool_error_count,
maximum: self.max_tool_errors.unwrap_or(0),
};
}
ValidationResult::Valid
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ValidationResult {
Valid,
TooShort {
actual: usize,
minimum: usize,
},
TooManyToolErrors {
actual: usize,
maximum: usize,
},
}
impl ValidationResult {
pub fn is_valid(&self) -> bool {
matches!(self, ValidationResult::Valid)
}
pub fn should_retry(&self) -> bool {
!self.is_valid()
}
}
impl std::fmt::Display for ValidationResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ValidationResult::Valid => write!(f, "valid"),
ValidationResult::TooShort { actual, minimum } => {
write!(
f,
"response too short ({} chars, minimum {})",
actual, minimum
)
}
ValidationResult::TooManyToolErrors { actual, maximum } => {
write!(
f,
"too many tool errors ({} errors, maximum {})",
actual, maximum
)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_retry_config_default() {
let config = RetryConfig::default();
assert_eq!(config.max_retries, 10);
assert_eq!(config.delays_secs.len(), 5);
assert!(config.retries_enabled());
}
#[test]
fn test_retry_config_delay_for_attempt() {
let config = RetryConfig::default();
assert_eq!(config.delay_for_attempt(0), Duration::from_secs(1));
assert_eq!(config.delay_for_attempt(1), Duration::from_secs(3));
assert_eq!(config.delay_for_attempt(2), Duration::from_secs(10));
assert_eq!(config.delay_for_attempt(10), Duration::from_secs(60));
}
#[test]
fn test_retry_config_no_retries() {
let config = RetryConfig::no_retries();
assert_eq!(config.max_retries, 0);
assert!(!config.retries_enabled());
}
#[test]
fn test_retry_config_custom() {
let config = RetryConfig::with_max_retries(3);
assert_eq!(config.max_retries, 3);
assert!(config.retries_enabled());
}
#[test]
fn test_retry_config_with_delays() {
let config = RetryConfig::with_delays(2, vec![5, 10]);
assert_eq!(config.max_retries, 2);
assert_eq!(config.delay_for_attempt(0), Duration::from_secs(5));
assert_eq!(config.delay_for_attempt(1), Duration::from_secs(10));
assert_eq!(config.delay_for_attempt(5), Duration::from_secs(10));
}
#[test]
fn test_response_validation_default() {
let validation = ResponseValidation::default();
assert!(!validation.is_enabled());
assert!(validation.check_length(""));
assert!(validation.check_tool_errors(100));
}
#[test]
fn test_response_validation_min_length() {
let validation = ResponseValidation::min_length(100);
assert!(validation.is_enabled());
assert!(!validation.check_length("short"));
assert_eq!(
validation.validate("short", 0),
ValidationResult::TooShort {
actual: 5,
minimum: 100
}
);
let long_content = "x".repeat(100);
assert!(validation.check_length(&long_content));
assert_eq!(
validation.validate(&long_content, 0),
ValidationResult::Valid
);
}
#[test]
fn test_response_validation_max_tool_errors() {
let validation = ResponseValidation::max_tool_errors(3);
assert!(validation.is_enabled());
assert!(validation.check_tool_errors(2));
assert_eq!(validation.validate("content", 2), ValidationResult::Valid);
assert!(!validation.check_tool_errors(5));
assert_eq!(
validation.validate("content", 5),
ValidationResult::TooManyToolErrors {
actual: 5,
maximum: 3
}
);
}
#[test]
fn test_response_validation_combined() {
let validation = ResponseValidation::new(50, 3);
assert!(validation.is_enabled());
let content = "x".repeat(50);
assert_eq!(validation.validate(&content, 2), ValidationResult::Valid);
assert_eq!(
validation.validate("short", 2),
ValidationResult::TooShort {
actual: 5,
minimum: 50
}
);
assert_eq!(
validation.validate(&content, 5),
ValidationResult::TooManyToolErrors {
actual: 5,
maximum: 3
}
);
}
#[test]
fn test_validation_result_display() {
assert_eq!(format!("{}", ValidationResult::Valid), "valid");
assert_eq!(
format!(
"{}",
ValidationResult::TooShort {
actual: 10,
minimum: 100
}
),
"response too short (10 chars, minimum 100)"
);
assert_eq!(
format!(
"{}",
ValidationResult::TooManyToolErrors {
actual: 5,
maximum: 3
}
),
"too many tool errors (5 errors, maximum 3)"
);
}
}