unai 0.0.1

Common AI abstractions
Documentation
use thiserror::Error;

/// Controls the randomness of the output. A higher temperature results in more random outputs, while a lower temperature results in more deterministic outputs.
///
/// It is recommended to set either temperature or topP, but not both.
#[derive(Debug, Clone, PartialEq)]
pub struct Temperature(f32);

impl Temperature {
    pub fn as_f32(&self) -> f32 {
        self.0
    }
}

#[derive(Debug, Error)]
#[error("Expected a value between 0.0 and 2.0, received {0}")]
pub struct InvalidTemperatureValueError(f32);

impl TryFrom<f32> for Temperature {
    type Error = InvalidTemperatureValueError;

    fn try_from(value: f32) -> Result<Self, Self::Error> {
        if !(0.0..=2.0).contains(&value) {
            return Err(InvalidTemperatureValueError(value));
        }

        Ok(Temperature(value))
    }
}

/// Top P (nucleus sampling)
///
/// This parameter controls the diversity of the output by sampling from the smallest possible set of tokens whose cumulative probability exceeds the value of topP. A higher topP value results in more diverse outputs.
#[derive(Debug, Clone, PartialEq)]
pub struct TopP(f32);

impl TopP {
    pub fn as_f32(&self) -> f32 {
        self.0
    }
}

#[derive(Debug, Error)]
#[error("Expected a value between 0.0 and 1.0, received {0}")]
pub struct InvalidTopPValueError(f32);

impl TryFrom<f32> for TopP {
    type Error = InvalidTopPValueError;

    fn try_from(value: f32) -> Result<Self, Self::Error> {
        if !(0.0..=1.0).contains(&value) {
            return Err(InvalidTopPValueError(value));
        }

        Ok(TopP(value))
    }
}

/// Settings for completion generation
#[derive(Debug, Clone, Default)]
pub struct Settings {
    /// Controls the randomness of the output. A higher temperature results in more random outputs, while a lower temperature results in more deterministic outputs.
    ///
    /// It is recommended to set either temperature or topP, but not both.
    pub temperature: Option<Temperature>,

    /// Top P (nucleus sampling)
    ///
    /// This parameter controls the diversity of the output by sampling from the smallest possible set of tokens whose cumulative probability exceeds the value of topP. A higher topP value results in more diverse outputs.
    pub top_p: Option<TopP>,

    /// Controls the diversity of the output by limiting the number of tokens to sample from. A higher topK value results in more diverse outputs.
    pub top_k: Option<u32>,

    /// This controls the maximum length of the generated output.
    pub max_output_tokens: Option<u32>,

    /// This penalizes new tokens based on whether they appear in the text so far, encouraging the model to produce more diverse outputs.
    pub presence_penalty: Option<f32>,

    /// These are sequences of tokens that will cause the model to stop generating further tokens.
    pub stop_sequences: Option<Vec<String>>,

    /// This is used to ensure reproducibility of results.
    pub seed: Option<i32>,
}

impl Settings {
    pub fn new() -> Self {
        Settings::default()
    }

    /// Merges two Settings instances, prioritizing the values from `other` when they are `Some`.
    pub fn merge(self, other: Settings) -> Self {
        Settings {
            temperature: other.temperature.or(self.temperature),
            top_p: other.top_p.or(self.top_p),
            top_k: other.top_k.or(self.top_k),
            seed: other.seed.or(self.seed),
            stop_sequences: other.stop_sequences.or(self.stop_sequences),
            presence_penalty: other.presence_penalty.or(self.presence_penalty),
            max_output_tokens: other.max_output_tokens.or(self.max_output_tokens),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_merge_should_prioritize_other() {
        let settings_1 = Settings::default();

        let settings_2 = Settings {
            temperature: Some(Temperature::try_from(0.7).unwrap()),
            ..Default::default()
        };

        let merged_settings = settings_1.merge(settings_2);

        assert_eq!(
            merged_settings.temperature,
            Some(Temperature::try_from(0.7).unwrap())
        );
    }

    #[test]
    fn test_merge_should_keep_original_when_other_is_none() {
        let settings_1 = Settings {
            temperature: Some(Temperature::try_from(0.7).unwrap()),
            ..Default::default()
        };

        let settings_2 = Settings::default();

        let merged_settings = settings_1.merge(settings_2);

        assert_eq!(
            merged_settings.temperature,
            Some(Temperature::try_from(0.7).unwrap())
        );
    }
}