rstructor 0.2.10

Rust equivalent of Python's Instructor + Pydantic: Extract structured, validated data from LLMs (OpenAI, Anthropic, Grok, Gemini) using type-safe Rust structs and enums
Documentation
use rstructor::{
    AnthropicClient, Instructor, LLMClient,
    logging::{LogLevel, init_logging},
};
use serde::{Deserialize, Serialize};

#[derive(Instructor, Serialize, Deserialize, Debug)]
#[llm(
    description = "Weather forecast for a location",
    validate = "validate_weather_forecast"
)]
struct WeatherForecast {
    #[llm(description = "Location/city name")]
    location: String,

    #[llm(description = "Current temperature in Celsius", example = 25.5)]
    current_temperature: f32,

    #[llm(description = "Forecast for upcoming days")]
    forecast: Vec<DayForecast>,
}

#[derive(Instructor, Serialize, Deserialize, Debug)]
#[llm(
    description = "Weather forecast for a specific day",
    validate = "validate_day_forecast"
)]
struct DayForecast {
    #[llm(description = "Day of the week", example = "Monday")]
    day: String,

    #[llm(description = "Temperature in Celsius", example = 28.5)]
    temperature: f32,

    #[llm(description = "Weather conditions", example = "Sunny")]
    #[serde(alias = "weather")]
    conditions: String,
}

// Custom validation function referenced by #[llm(validate = "validate_weather_forecast")]
fn validate_weather_forecast(forecast: &WeatherForecast) -> rstructor::Result<()> {
    // Check that location is not empty
    if forecast.location.trim().is_empty() {
        return Err(rstructor::RStructorError::ValidationError(
            "Location cannot be empty".to_string(),
        ));
    }

    // Check temperature is in reasonable range
    if forecast.current_temperature < -100.0 || forecast.current_temperature > 70.0 {
        return Err(rstructor::RStructorError::ValidationError(format!(
            "Current temperature must be between -100 and 70°C, got {}",
            forecast.current_temperature
        )));
    }

    // Check that we have at least one forecast day
    if forecast.forecast.is_empty() {
        return Err(rstructor::RStructorError::ValidationError(
            "Forecast must include at least one day".to_string(),
        ));
    }

    Ok(())
}

// Custom validation function referenced by #[llm(validate = "validate_day_forecast")]
fn validate_day_forecast(day: &DayForecast) -> rstructor::Result<()> {
    // Check that day is not empty
    if day.day.trim().is_empty() {
        return Err(rstructor::RStructorError::ValidationError(
            "Day cannot be empty".to_string(),
        ));
    }

    // Check temperature is in reasonable range
    if day.temperature < -100.0 || day.temperature > 70.0 {
        return Err(rstructor::RStructorError::ValidationError(format!(
            "Forecast temperature must be between -100 and 70°C, got {}",
            day.temperature
        )));
    }

    // Check that conditions is not empty
    if day.conditions.trim().is_empty() {
        return Err(rstructor::RStructorError::ValidationError(
            "Weather conditions cannot be empty".to_string(),
        ));
    }

    Ok(())
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Initialize logging with DEBUG level to show detailed logs including retries
    // You can also set RSTRUCTOR_LOG environment variable to override this
    // E.g.: RSTRUCTOR_LOG=debug,rstructor::backend=trace cargo run --example logging_example
    init_logging(LogLevel::Debug);

    println!("Starting weather forecast example with detailed logging...");
    println!("This example demonstrates retry logic with validation errors.");

    // Get API key from environment variable with better error handling
    let api_key = match std::env::var("ANTHROPIC_API_KEY") {
        Ok(key) => key,
        Err(_) => {
            eprintln!("⚠️  ANTHROPIC_API_KEY environment variable not set!");
            eprintln!("Please set it with: export ANTHROPIC_API_KEY=your_api_key");
            eprintln!(
                "For demonstration purposes, using a fake key (will show API errors in logs)"
            );
            "dummy-key-for-demonstration".to_string()
        }
    };

    // Create a client with higher temperature to increase chances of validation errors
    let client = AnthropicClient::new(api_key)?.temperature(0.7); // Higher temperature = more creativity = more validation errors

    println!("\nSending request to Anthropic API with increased randomness...");
    println!("Will retry up to 3 times on validation errors with detailed logging.\n");

    // Simple prompt - schema handles structure enforcement
    let prompt = "What's the weather forecast for Tokyo for the next 3 days?";

    // This line will be logged with spans and info - using 3 retries for more chances to see retry logs
    let forecast_result = client.materialize::<WeatherForecast>(prompt).await;

    match forecast_result {
        Ok(forecast) => {
            println!("\n✅ Successfully generated forecast after potential retries!");
            println!("\nGenerated forecast for {}", forecast.location);
            println!("Current temperature: {}°C", forecast.current_temperature);
            println!("\nUpcoming forecast:");

            for day in forecast.forecast {
                println!("- {}: {}°C, {}", day.day, day.temperature, day.conditions);
            }
        }
        Err(e) => {
            println!("\n❌ Failed to generate forecast after retries");
            println!("Error: {}", e);
            println!("\nThe logs above show the detailed retry attempts and validation errors.");
        }
    }

    Ok(())
}