use std::path::PathBuf; use clap::Parser; /// Default system prompt, compiled into the binary as a fallback. const DEFAULT_SYSTEM_PROMPT: &str = include_str!("../prompts/normalize.txt"); const DEFAULT_MERGE_PROMPT: &str = include_str!("../prompts/merge.txt"); #[derive(Parser, Debug)] #[command(version, about = "Furumi Agent: music metadata ingest and normalization")] pub struct Args { /// IP address and port for the admin web UI #[arg(long, env = "FURUMI_AGENT_BIND", default_value = "0.0.0.0:8090")] pub bind: String, /// Directory to watch for new music files #[arg(long, env = "FURUMI_AGENT_INBOX_DIR")] pub inbox_dir: PathBuf, /// Directory for permanently stored and organized music files #[arg(long, env = "FURUMI_AGENT_STORAGE_DIR")] pub storage_dir: PathBuf, /// PostgreSQL connection URL #[arg(long, env = "FURUMI_AGENT_DATABASE_URL")] pub database_url: String, /// Ollama API base URL #[arg(long, env = "FURUMI_AGENT_OLLAMA_URL", default_value = "http://localhost:11434")] pub ollama_url: String, /// Ollama model name #[arg(long, env = "FURUMI_AGENT_OLLAMA_MODEL", default_value = "qwen3:14b")] pub ollama_model: String, /// Authorization header value for Ollama API (e.g. "Bearer " or "Basic ") #[arg(long, env = "FURUMI_AGENT_OLLAMA_AUTH")] pub ollama_auth: Option, /// Inbox scan interval in seconds #[arg(long, env = "FURUMI_AGENT_POLL_INTERVAL_SECS", default_value_t = 30)] pub poll_interval_secs: u64, /// Confidence threshold for auto-approval (0.0 - 1.0) #[arg(long, env = "FURUMI_AGENT_CONFIDENCE_THRESHOLD", default_value_t = 0.85)] pub confidence_threshold: f64, /// Path to a custom system prompt file (overrides the built-in default) #[arg(long, env = "FURUMI_AGENT_SYSTEM_PROMPT_FILE")] pub system_prompt_file: Option, /// Path to a custom merge prompt file (overrides the built-in default) #[arg(long, env = "FURUMI_AGENT_MERGE_PROMPT_FILE")] pub merge_prompt_file: Option, } impl Args { pub fn validate(&self) -> Result<(), Box> { if !self.inbox_dir.exists() || !self.inbox_dir.is_dir() { return Err(format!("Inbox directory {:?} does not exist or is not a directory", self.inbox_dir).into()); } if !self.storage_dir.exists() || !self.storage_dir.is_dir() { return Err(format!("Storage directory {:?} does not exist or is not a directory", self.storage_dir).into()); } if !(0.0..=1.0).contains(&self.confidence_threshold) { return Err("Confidence threshold must be between 0.0 and 1.0".into()); } Ok(()) } /// Load the system prompt from a custom file or use the built-in default. pub fn load_system_prompt(&self) -> Result> { match &self.system_prompt_file { Some(path) => { tracing::info!("Loading system prompt from {:?}", path); Ok(std::fs::read_to_string(path)?) } None => { tracing::info!("Using built-in default system prompt"); Ok(DEFAULT_SYSTEM_PROMPT.to_owned()) } } } pub fn load_merge_prompt(&self) -> Result> { match &self.merge_prompt_file { Some(path) => { tracing::info!("Loading merge prompt from {:?}", path); Ok(std::fs::read_to_string(path)?) } None => Ok(DEFAULT_MERGE_PROMPT.to_owned()), } } }