//! Helpers shared by the experiment binaries. //! //! Sandbox tooling for iterating on Whisper / LLM prompts against a real case //! without modifying production state. All outputs land under `case_/runs/` //! and `case_/compare/` — never in `tmpdata/`. //! //! **Security note**: The Ionos bearer key flows through this code as a //! `String` inside `Settings.llm.api_key`. Helpers here MUST NOT persist or //! print the key. `RunMeta` deliberately stores only the URL/model/timestamp — //! never the key — so on-disk audit logs are safe to share. use std::path::Path; use anyhow::Context; use serde::Serialize; use time::OffsetDateTime; use time::format_description::well_known::Iso8601; /// Generate a run identifier in the form /// `_l-_run`. Used as the directory name under /// `case_/runs/`. pub fn run_id(llm_variant: &str, run_index: u32) -> String { let now = OffsetDateTime::now_utc(); let stamp = now .format(&Iso8601::DEFAULT) .unwrap_or_else(|_| "unknown".into()); let safe_stamp = stamp.replace(':', "-").replace('.', "-"); format!("{safe_stamp}_l-{llm_variant}_run{run_index}") } /// Read a prompt file. Returns empty string for an empty file (used as /// "no prompt" signal by `run_full_case`'s settings override). pub fn load_prompt(path: &Path) -> anyhow::Result { let content = std::fs::read_to_string(path) .with_context(|| format!("reading prompt file {}", path.display()))?; Ok(content.trim_end_matches('\n').to_string()) } /// Atomically write content to `path` via a sibling tempfile + rename. /// Same pattern the server uses for `document.md` — ensures readers never /// observe a partial file. pub fn atomic_write(path: &Path, content: &str) -> anyhow::Result<()> { let parent = path .parent() .with_context(|| format!("path {} has no parent", path.display()))?; std::fs::create_dir_all(parent)?; let mut tmp = tempfile::Builder::new() .prefix(".tmp-") .tempfile_in(parent)?; use std::io::Write; tmp.write_all(content.as_bytes())?; tmp.flush()?; tmp.persist(path) .map_err(|e| anyhow::anyhow!("persist failed: {}", e.error))?; Ok(()) } /// Snapshot of variant identities + provenance for one run. Serialized /// as `meta.json` next to the produced `document.md`. **Never contains /// the API key** — only the LLM endpoint URL and model name. #[derive(Debug, Serialize)] pub struct RunMeta { pub run_id: String, pub created_at_utc: String, pub case: String, pub llm_variant: String, pub whisper_url: String, pub llm_url: String, pub llm_model: String, pub llm_temperature: f32, pub vocab_dir: String, pub vocab_loaded: usize, pub gazetteer_dict: GazetteerDictMode, pub timings_ms: RunTimings, pub recordings: Vec, } #[derive(Debug, Serialize)] pub enum GazetteerDictMode { NoDict, SpellbookHunspell { stem: String }, } #[derive(Debug, Default, Serialize)] pub struct RunTimings { pub whisper_total_ms: u64, pub gazetteer_total_ms: u64, pub llm_ms: u64, pub wallclock_ms: u64, } #[derive(Debug, Serialize)] pub struct RecordingMeta { pub recorded_at: String, pub audio_path: String, pub whisper_chars_raw: usize, pub whisper_chars_after_gazetteer: usize, }