use anyhow::{Result, anyhow};
use clap::Parser;
use clap::Subcommand;
use log::{debug, error};
use std::fs;
use std::path::PathBuf;
use crate::config::CompatibilityConfig;
use crate::enums::FileType;
use crate::enums::LlmModel;
use crate::enums::LlmProvider;
use crate::utils;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub action: Action,
}
#[derive(Subcommand, Debug, Clone)]
pub enum Action {
Recommend {
#[arg(short, long, help = "Path to the input file")]
input_csv: PathBuf,
#[arg(short, long, help = "Path to the guidelines file")]
guidelines_csv: PathBuf,
#[arg(short, long, help = "Path to the context file")]
context_csv: Option<PathBuf>,
#[arg(short, long, help = "Number of recommendations")]
num_recommendations: Option<u32>,
#[arg(short, long, value_enum, help = "LLM Provider", default_value_t = LlmProvider::Google)]
llm_provider: LlmProvider,
#[arg(short, long, help = "LLM Provider Model", default_value_t = LlmModel::GeminiFlash2)]
model: LlmModel,
#[arg(long, help = "Max tokens for output")]
max_tokens: Option<u32>,
#[arg(long, help = "Temperature")]
temperature: Option<f32>,
},
Enhance {
#[arg(short, long, help = "Path to the input file")]
input_csv: PathBuf,
},
}
impl Cli {
fn check_file_exists(&self, path: &PathBuf, file_type: &str) -> Result<()> {
fs::metadata(path).map_err(|e| {
error!("Error checking {} existence: {}", file_type, e);
anyhow!("The specified {} file does not exist.", file_type)
})?;
Ok(())
}
pub fn validate_file_paths_exist(&self) -> Result<()> {
debug!("Arguments received {:#?}", &self);
debug!("Validating file paths.");
match &self.action {
Action::Recommend {
input_csv,
guidelines_csv,
context_csv,
..
} => {
self.check_file_exists(input_csv, "Input file")?;
self.check_file_exists(guidelines_csv, "Guidelines file")?;
if let Some(context_path) = context_csv {
self.check_file_exists(context_path, "Context file")?;
}
}
Action::Enhance { .. } => {
unimplemented!(" ==> In progress of implementation <== ")
}
}
Ok(())
}
fn get_path_for_file_type(&self, file_type: FileType) -> Result<&PathBuf> {
match &self.action {
Action::Recommend {
input_csv,
guidelines_csv,
context_csv,
..
} => match file_type {
FileType::Input => Ok(input_csv),
FileType::Guidelines => Ok(guidelines_csv),
FileType::Context => Ok(context_csv.as_ref().unwrap()),
},
Action::Enhance { .. } => {
unimplemented!(" ==> In progress of implementation <== ")
}
}
}
pub fn read_file(&self, file_type: FileType) -> Result<String> {
debug!("Reading {:?} file.", file_type);
let path = self.get_path_for_file_type(file_type)?;
let content = utils::read_csv(path)?;
Ok(content)
}
pub fn validate_llm_provider_model_compatibility(
&self,
compat: &CompatibilityConfig,
) -> Result<()> {
match &self.action {
Action::Recommend {
llm_provider,
model,
..
} => match llm_provider {
LlmProvider::Google => {
compat.google.get(&model.to_string()).ok_or_else(|| {
anyhow!(
"Model '{:#?}' is not compatible with '{:#?}' provider.",
model,
llm_provider
)
})?;
Ok(())
}
LlmProvider::Openai => {
compat.openai.get(&model.to_string()).ok_or_else(|| {
anyhow!(
"Model '{:#?}' is not compatible with '{:#?}' provider.",
model,
llm_provider
)
})?;
Ok(())
}
},
Action::Enhance { .. } => {
unimplemented!(" ==> In progress of implementation <== ")
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::enums::*;
use anyhow::Result;
use std::{collections::HashMap, env::temp_dir, fs::File, io::Write};
#[test]
fn test_check_file_exists_success() -> Result<()> {
let temp_dir = temp_dir();
let file_path = temp_dir.as_path().join("test_file.txt");
File::create(&file_path)?;
let cli = Cli {
action: Action::Recommend {
input_csv: PathBuf::from("dummy_input.csv"),
guidelines_csv: PathBuf::from("dummy_guidelines.csv"),
context_csv: None,
num_recommendations: None,
llm_provider: LlmProvider::Google,
model: LlmModel::GeminiFlash2,
max_tokens: None,
temperature: None,
},
};
cli.check_file_exists(&file_path, "Test file")?;
Ok(())
}
#[test]
fn test_check_file_exists_failure() -> Result<()> {
let cli = Cli {
action: Action::Recommend {
input_csv: PathBuf::from("dummy_input.csv"),
guidelines_csv: PathBuf::from("dummy_guidelines.csv"),
context_csv: None,
num_recommendations: None,
llm_provider: LlmProvider::Google,
model: LlmModel::GeminiFlash2,
max_tokens: None,
temperature: None,
},
};
let non_existent_path = PathBuf::from("non_existent_file.txt");
let result = cli.check_file_exists(&non_existent_path, "Test file");
assert!(result.is_err());
Ok(())
}
#[test]
fn test_get_path_for_file_type() -> Result<()> {
let input_path = PathBuf::from("input.csv");
let guidelines_path = PathBuf::from("guidelines.csv");
let context_path = PathBuf::from("context.csv");
let cli = Cli {
action: Action::Recommend {
input_csv: input_path.clone(),
guidelines_csv: guidelines_path.clone(),
context_csv: Some(context_path.clone()),
num_recommendations: None,
llm_provider: LlmProvider::Google,
model: LlmModel::GeminiFlash2,
max_tokens: None,
temperature: None,
},
};
assert_eq!(cli.get_path_for_file_type(FileType::Input)?, &input_path);
assert_eq!(
cli.get_path_for_file_type(FileType::Guidelines)?,
&guidelines_path
);
assert_eq!(
cli.get_path_for_file_type(FileType::Context)?,
&context_path
);
Ok(())
}
#[test]
fn test_validate_llm_provider_model_compatibility_success() -> Result<()> {
let compat = CompatibilityConfig {
google: {
let mut map = HashMap::new();
map.insert(
"gemini-2.0-flash".to_string(),
"gemini-2.0-flash".to_string(),
);
map
},
openai: {
let mut map = HashMap::new();
map.insert(
"gpt-4.1-nano-2025-04-14".to_string(),
"gpt-4.1-nano-2025-04-14".to_string(),
);
map
},
};
let cli = Cli {
action: Action::Recommend {
input_csv: PathBuf::from("input.csv"),
guidelines_csv: PathBuf::from("guidelines.csv"),
context_csv: None,
num_recommendations: None,
llm_provider: LlmProvider::Google,
model: LlmModel::GeminiFlash2,
max_tokens: None,
temperature: None,
},
};
assert!(
cli.validate_llm_provider_model_compatibility(&compat)
.is_ok()
);
Ok(())
}
#[test]
fn test_validate_llm_provider_model_compatibility_failure() -> Result<()> {
let compat = CompatibilityConfig {
google: HashMap::new(),
openai: HashMap::new(),
};
let cli = Cli {
action: Action::Recommend {
input_csv: PathBuf::from("input.csv"),
guidelines_csv: PathBuf::from("guidelines.csv"),
context_csv: None,
num_recommendations: None,
llm_provider: LlmProvider::Google,
model: LlmModel::GeminiFlash2,
max_tokens: None,
temperature: None,
},
};
let result = cli.validate_llm_provider_model_compatibility(&compat);
assert!(result.is_err());
Ok(())
}
}