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::{Instructor, RStructorError, SchemaType};
use serde::{Deserialize, Serialize};

#[derive(Instructor, Serialize, Deserialize)]
#[llm(validate = "validate_weather")]
struct WeatherInfo {
    #[llm(description = "City name to get weather for")]
    city: String,

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

    #[llm(description = "Weather description")]
    description: Option<String>,
}

// Custom validation function referenced by #[llm(validate = "validate_weather")]
fn validate_weather(info: &WeatherInfo) -> rstructor::Result<()> {
    // Validate city name isn't empty
    if info.city.trim().is_empty() {
        return Err(RStructorError::ValidationError(
            "City name cannot be empty".to_string(),
        ));
    }

    // Validate temperature is within a reasonable range
    // Reasonable range for Earth temperatures in Celsius
    if info.temperature < -100.0 || info.temperature > 70.0 {
        return Err(RStructorError::ValidationError(format!(
            "Temperature must be between -100°C and 70°C, got {}°C",
            info.temperature
        )));
    }

    Ok(())
}

#[tokio::main]
async fn main() -> rstructor::Result<()> {
    // Get the schema generated by the derive macro
    let schema = WeatherInfo::schema();

    println!("Weather Info Schema (auto-generated by Instructor derive):");
    println!("{}", schema);

    // Create a sample instance and test validation
    let valid_weather = WeatherInfo {
        city: "Paris".to_string(),
        temperature: 25.5,
        description: Some("Sunny".to_string()),
    };

    println!("\nValidating valid weather info...");
    match valid_weather.validate() {
        Ok(_) => println!("Validation passed!"),
        Err(e) => println!("Validation failed: {}", e),
    }

    println!("\nTesting validation with invalid data...");
    let invalid_weather = WeatherInfo {
        city: "Madrid".to_string(),
        temperature: 120.0, // Too high for Earth
        description: Some("Extremely hot".to_string()),
    };

    match invalid_weather.validate() {
        Ok(_) => println!("Validation passed (should not happen)"),
        Err(e) => println!("Validation failed as expected: {}", e),
    }

    // Try with environment variable for API key
    if let Ok(api_key) = std::env::var("OPENAI_API_KEY") {
        println!("\nAPI key found! Trying an actual weather query with OpenAI...");

        use rstructor::{LLMClient, OpenAIClient};

        // Create an OpenAI client
        let client = OpenAIClient::new(api_key)?.temperature(0.0);

        // Define a prompt
        let prompt = "What's the weather like in Paris right now?";
        println!("Prompt: {}", prompt);

        // Call the LLM to get a structured output with retry
        match client.materialize::<WeatherInfo>(prompt).await {
            Ok(weather) => {
                println!("\nReceived weather info from OpenAI:");
                println!("Weather for {}: {} °C", weather.city, weather.temperature);
                if let Some(desc) = weather.description {
                    println!("Description: {}", desc);
                }

                // Weather comes back validated!
                println!("\nNote: The data has already been validated by the materialize method!");
            }
            Err(e) => println!("Error getting weather from OpenAI: {}", e),
        }
    } else {
        println!("\nNo OPENAI_API_KEY found in environment variables.");
        println!("To test with a real API call, set this variable and run again.");
    }

    // Print the sample instance
    println!("\nSample Weather Info:");
    println!("{}", serde_json::to_string_pretty(&valid_weather).unwrap());

    Ok(())
}