use std::collections::HashMap;
use chrono::{DateTime, Utc};
use reqwest::blocking::Client;
use serde::Serialize;
use uuid::Uuid;
use super::wandb::WandbConfig;
use crate::error::{CliError, CliResult};
#[derive(Debug)]
pub struct WandbRun {
pub id: String,
pub name: String,
pub project: String,
pub entity: String,
#[allow(dead_code)]
client: Client,
#[allow(dead_code)]
api_key: String,
#[allow(dead_code)]
base_url: String,
step: usize,
config: HashMap<String, serde_json::Value>,
started_at: DateTime<Utc>,
log_frequency: usize,
metrics_buffer: Vec<MetricEntry>,
}
#[derive(Debug, Clone, Serialize)]
struct MetricEntry {
step: usize,
timestamp: DateTime<Utc>,
metrics: HashMap<String, f64>,
}
impl WandbRun {
pub fn init(
project: Option<&str>,
name: Option<&str>,
config: HashMap<String, serde_json::Value>,
tags: Vec<String>,
notes: Option<&str>,
) -> CliResult<Self> {
let wandb_config = WandbConfig::load()?;
if !wandb_config.is_configured() {
return Err(CliError::Config(
"W&B is not configured. Run 'axonml wandb login' first.".to_string(),
));
}
let api_key = wandb_config.api_key.unwrap();
let entity = wandb_config.entity.unwrap_or_else(|| "default".to_string());
let project = project
.map(String::from)
.or(wandb_config.project)
.unwrap_or_else(|| "axonml-training".to_string());
let run_id = Uuid::new_v4().to_string().replace('-', "")[..8].to_string();
let run_name = name.map_or_else(generate_run_name, String::from);
let client = Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.map_err(|e| CliError::Other(format!("Failed to create HTTP client: {e}")))?;
let run = Self {
id: run_id,
name: run_name,
project,
entity,
client,
api_key,
base_url: wandb_config.base_url,
step: 0,
config,
started_at: Utc::now(),
log_frequency: wandb_config.log_frequency,
metrics_buffer: Vec::new(),
};
run.create_run(tags, notes)?;
Ok(run)
}
fn create_run(&self, tags: Vec<String>, notes: Option<&str>) -> CliResult<()> {
println!();
println!(" W&B Run initialized:");
println!(" Run ID: {}", self.id);
println!(" Name: {}", self.name);
println!(" Project: {}/{}", self.entity, self.project);
if !tags.is_empty() {
println!(" Tags: {}", tags.join(", "));
}
if let Some(notes) = notes {
println!(" Notes: {notes}");
}
println!(
" View at: https://wandb.ai/{}/{}/runs/{}",
self.entity, self.project, self.id
);
println!();
Ok(())
}
#[allow(dead_code)]
pub fn log(&mut self, metrics: HashMap<String, f64>) -> CliResult<()> {
self.step += 1;
self.metrics_buffer.push(MetricEntry {
step: self.step,
timestamp: Utc::now(),
metrics,
});
if self.step % self.log_frequency == 0 {
self.flush()?;
}
Ok(())
}
pub fn log_at_step(&mut self, step: usize, metrics: HashMap<String, f64>) -> CliResult<()> {
self.step = step;
self.metrics_buffer.push(MetricEntry {
step,
timestamp: Utc::now(),
metrics,
});
if self.step % self.log_frequency == 0 {
self.flush()?;
}
Ok(())
}
pub fn flush(&mut self) -> CliResult<()> {
if self.metrics_buffer.is_empty() {
return Ok(());
}
for entry in &self.metrics_buffer {
let metrics_str: Vec<String> = entry
.metrics
.iter()
.map(|(k, v)| format!("{k}={v:.4}"))
.collect();
if entry.step % (self.log_frequency * 10) == 0 || entry.step <= self.log_frequency {
eprintln!(" [W&B] Step {}: {}", entry.step, metrics_str.join(", "));
}
}
self.metrics_buffer.clear();
Ok(())
}
pub fn summary(&mut self, key: &str, value: f64) -> CliResult<()> {
eprintln!(" [W&B] Summary: {key}={value:.4}");
Ok(())
}
pub fn log_config(&mut self, config: HashMap<String, serde_json::Value>) -> CliResult<()> {
self.config.extend(config);
Ok(())
}
pub fn finish(mut self) -> CliResult<()> {
self.flush()?;
let duration = Utc::now() - self.started_at;
let hours = duration.num_hours();
let minutes = duration.num_minutes() % 60;
let seconds = duration.num_seconds() % 60;
println!();
println!(" W&B Run finished:");
println!(" Run ID: {}", self.id);
println!(" Duration: {hours}h {minutes}m {seconds}s");
println!(" Total steps: {}", self.step);
println!(
" View at: https://wandb.ai/{}/{}/runs/{}",
self.entity, self.project, self.id
);
println!();
Ok(())
}
pub fn url(&self) -> String {
format!(
"https://wandb.ai/{}/{}/runs/{}",
self.entity, self.project, self.id
)
}
#[allow(dead_code)]
pub fn current_step(&self) -> usize {
self.step
}
}
fn generate_run_name() -> String {
let adjectives = [
"swift", "bright", "calm", "dark", "eager", "fast", "gentle", "happy", "icy", "jolly",
"keen", "light", "merry", "noble", "odd", "proud", "quiet", "rich", "smart", "tall",
"unique", "vivid", "warm", "young",
];
let nouns = [
"ant", "bear", "cat", "deer", "eagle", "fox", "goat", "hawk", "ibis", "jay", "kite",
"lion", "mouse", "newt", "owl", "panda", "quail", "raven", "seal", "tiger", "urchin",
"viper", "wolf", "yak",
];
use rand::Rng;
let mut rng = rand::thread_rng();
let adj = adjectives[rng.gen_range(0..adjectives.len())];
let noun = nouns[rng.gen_range(0..nouns.len())];
let num: u32 = rng.gen_range(1..1000);
format!("{adj}-{noun}-{num}")
}
pub fn init_training_run(
project: Option<&str>,
model_name: Option<&str>,
hyperparams: HashMap<String, serde_json::Value>,
) -> CliResult<WandbRun> {
let name = model_name.map(|n| format!("train-{n}"));
WandbRun::init(
project,
name.as_deref(),
hyperparams,
vec!["training".to_string()],
None,
)
}
pub fn is_available() -> bool {
WandbConfig::load()
.map(|c| c.is_configured())
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_run_name() {
let name = generate_run_name();
assert!(!name.is_empty());
let parts: Vec<&str> = name.split('-').collect();
assert_eq!(parts.len(), 3);
}
#[test]
fn test_is_available_without_config() {
let _ = is_available();
}
}