pub mod build;
pub mod eval;
pub mod query;
pub mod summary;
pub mod tune;
pub mod validate;
use anyhow::Context;
use clap::{Args, ValueEnum};
use lineprior::BuildConfig;
use std::path::Path;
use std::process::ExitCode;
pub fn exit_code_for_lineprior_error(err: &lineprior::Error) -> ExitCode {
match err {
lineprior::Error::NoObservations => ExitCode::from(2),
lineprior::Error::Io(_) => ExitCode::from(4),
_ => ExitCode::from(3),
}
}
#[derive(Clone, Copy, ValueEnum)]
pub enum ConfidenceModeArg {
Heuristic,
WilsonLowerBound,
Hybrid,
}
impl From<ConfidenceModeArg> for lineprior::ConfidenceMode {
fn from(arg: ConfidenceModeArg) -> Self {
match arg {
ConfidenceModeArg::Heuristic => lineprior::ConfidenceMode::Heuristic,
ConfidenceModeArg::WilsonLowerBound => lineprior::ConfidenceMode::WilsonLowerBound,
ConfidenceModeArg::Hybrid => lineprior::ConfidenceMode::Hybrid,
}
}
}
#[derive(Clone, Copy, ValueEnum)]
pub enum MissingTimestampPolicyArg {
KeepBaseWeight,
Drop,
}
impl From<MissingTimestampPolicyArg> for lineprior::MissingTimestampPolicy {
fn from(arg: MissingTimestampPolicyArg) -> Self {
match arg {
MissingTimestampPolicyArg::KeepBaseWeight => {
lineprior::MissingTimestampPolicy::KeepBaseWeight
}
MissingTimestampPolicyArg::Drop => lineprior::MissingTimestampPolicy::Drop,
}
}
}
#[derive(Clone, Copy, ValueEnum)]
pub enum ObjectiveArg {
Mrr,
Top1,
CoveredMrr,
Top1AtMinCoverage,
SuccessWeightedMrr,
SuccessWeightedTop1,
}
impl From<ObjectiveArg> for lineprior::TuneObjective {
fn from(arg: ObjectiveArg) -> Self {
match arg {
ObjectiveArg::Mrr => lineprior::TuneObjective::Mrr,
ObjectiveArg::Top1 => lineprior::TuneObjective::Top1,
ObjectiveArg::CoveredMrr => lineprior::TuneObjective::CoveredMrr,
ObjectiveArg::Top1AtMinCoverage => lineprior::TuneObjective::Top1AtMinCoverage,
ObjectiveArg::SuccessWeightedMrr => lineprior::TuneObjective::SuccessWeightedMrr,
ObjectiveArg::SuccessWeightedTop1 => lineprior::TuneObjective::SuccessWeightedTop1,
}
}
}
#[derive(Clone, ValueEnum)]
pub(crate) enum SplitBy {
Sequence,
}
fn parse_source_weight_entry(s: &str) -> Result<(String, f64), String> {
let (name, weight) = s
.split_once('=')
.ok_or_else(|| format!("invalid source weight {s:?}, expected `name=weight`"))?;
let weight: f64 = weight
.parse()
.map_err(|_| format!("invalid weight in {s:?}: {weight:?} is not a number"))?;
Ok((name.to_string(), weight))
}
#[derive(Args)]
pub struct BuildConfigArgs {
#[arg(long, default_value_t = 1)]
pub min_count: u64,
#[arg(long, default_value_t = 0.0)]
pub min_weighted_count: f64,
#[arg(long, default_value_t = 0.0)]
pub min_confidence: f64,
#[arg(long)]
pub max_step: Option<u32>,
#[arg(long, default_value_t = 5.0)]
pub smoothing_alpha: f64,
#[arg(long)]
pub max_actions_per_state: Option<usize>,
#[arg(long, default_value_t = lineprior::DEFAULT_CONFIDENCE_K)]
pub confidence_k: f64,
#[arg(long, value_enum, default_value_t = ConfidenceModeArg::Heuristic)]
pub confidence_mode: ConfidenceModeArg,
#[arg(long, default_value_t = lineprior::DEFAULT_CONFIDENCE_Z)]
pub confidence_z: f64,
#[arg(long, value_delimiter = ',')]
pub tags: Vec<String>,
#[arg(long, default_value_t = lineprior::DEFAULT_DRAW_VALUE)]
pub draw_value: f64,
#[arg(long)]
pub time_decay_half_life_days: Option<f64>,
#[arg(long)]
pub time_decay_reference_unix_seconds: Option<i64>,
#[arg(long, value_enum, default_value_t = MissingTimestampPolicyArg::KeepBaseWeight)]
pub missing_timestamp_policy: MissingTimestampPolicyArg,
#[arg(long, value_delimiter = ',', value_parser = parse_source_weight_entry)]
pub source_weights: Vec<(String, f64)>,
#[arg(long, default_value_t = lineprior::DEFAULT_SOURCE_WEIGHT)]
pub default_source_weight: f64,
#[arg(long, default_value_t = 0)]
pub context_order: usize,
}
pub fn resolve_build_config(
config_file: Option<&Path>,
config_args: BuildConfigArgs,
) -> anyhow::Result<BuildConfig> {
match config_file {
Some(path) => {
if !config_args.is_all_default() {
anyhow::bail!(
"--config cannot be combined with individual build-config flags \
(e.g. --min-count, --confidence-mode) -- pass one or the other"
);
}
let file =
std::fs::File::open(path).with_context(|| format!("opening {}", path.display()))?;
let config: BuildConfig = serde_json::from_reader(file)
.with_context(|| format!("parsing {} as BuildConfig JSON", path.display()))?;
Ok(config)
}
None => Ok(config_args.into_build_config()),
}
}
impl BuildConfigArgs {
pub fn is_all_default(&self) -> bool {
self.min_count == 1
&& self.min_weighted_count == 0.0
&& self.min_confidence == 0.0
&& self.max_step.is_none()
&& self.smoothing_alpha == 5.0
&& self.max_actions_per_state.is_none()
&& self.confidence_k == lineprior::DEFAULT_CONFIDENCE_K
&& matches!(self.confidence_mode, ConfidenceModeArg::Heuristic)
&& self.confidence_z == lineprior::DEFAULT_CONFIDENCE_Z
&& self.tags.is_empty()
&& self.draw_value == lineprior::DEFAULT_DRAW_VALUE
&& self.time_decay_half_life_days.is_none()
&& self.time_decay_reference_unix_seconds.is_none()
&& matches!(
self.missing_timestamp_policy,
MissingTimestampPolicyArg::KeepBaseWeight
)
&& self.source_weights.is_empty()
&& self.default_source_weight == lineprior::DEFAULT_SOURCE_WEIGHT
&& self.context_order == 0
}
pub fn into_build_config(self) -> BuildConfig {
BuildConfig {
min_count: self.min_count,
min_weighted_count: self.min_weighted_count,
min_confidence: self.min_confidence,
max_step: self.max_step,
smoothing_alpha: self.smoothing_alpha,
max_actions_per_state: self.max_actions_per_state,
confidence_k: self.confidence_k,
confidence_mode: self.confidence_mode.into(),
confidence_z: self.confidence_z,
draw_value: self.draw_value,
tag_filter: if self.tags.is_empty() {
None
} else {
Some(self.tags)
},
time_decay_half_life_days: self.time_decay_half_life_days,
time_decay_reference_unix_seconds: self.time_decay_reference_unix_seconds,
missing_timestamp_policy: self.missing_timestamp_policy.into(),
source_weights: self.source_weights.into_iter().collect(),
default_source_weight: self.default_source_weight,
context_order: self.context_order,
..BuildConfig::default()
}
}
}