//! An agent store is a standard HDF5 file: h5py can open it and read every //! dataset. //! //! It could not: the float datatype's sign-bit position was hard-coded for //! f64, so every f32 dataset (embeddings, norms, activation weights) made //! libhdf5 refuse the file with "sign bit position out of bounds". use std::process::Command; use clawhdf5_agent::{AgentMemory, HDF5Memory, MemoryConfig, MemoryEntry}; fn python() -> String { std::env::var("CLAWHDF5_PYTHON").unwrap_or_else(|_| "python3".to_string()) } fn h5py_available() -> bool { Command::new(python()) .args(["-c", "import h5py"]) .output() .map(|o| o.status.success()) .unwrap_or(false) } #[test] fn h5py_reads_every_dataset_of_an_agent_store() { if !h5py_available() { assert!( std::env::var("CLAWHDF5_REQUIRE_INTEROP").as_deref() != Ok("1"), "CLAWHDF5_REQUIRE_INTEROP=1 but python3 with h5py is not available" ); eprintln!("SKIP: python3 with h5py not available"); return; } let dir = tempfile::tempdir().unwrap(); for float16 in [false, true] { let path = dir.path().join(format!("store_{float16}.h5")); let mut cfg = MemoryConfig::new(path.clone(), "agent", 8); cfg.float16 = float16; let mut m = HDF5Memory::create(cfg).unwrap(); // save_batch checkpoints, so the records are in the .h5, not the WAL. m.save_batch( (0..20) .map(|i| MemoryEntry { chunk: format!("memory {i}"), embedding: (0..8).map(|j| ((i * 8 + j) as f32).sin()).collect(), source_channel: "test".into(), timestamp: i as f64, session_id: "s".into(), tags: String::new(), }) .collect(), ) .unwrap(); drop(m); // Exact expected values, as bits: numpy's sin need not match Rust's // to the last place. let bits = (0..160) .map(|k| (k as f32).sin().to_bits().to_string()) .collect::>() .join(","); let script = format!( r#" import h5py, numpy as np want = np.float16 if {py_bool} else np.float32 with h5py.File("{path}", "r") as f: names = [] f.visititems(lambda n, o: names.append(n) if isinstance(o, h5py.Dataset) else None) for n in names: f[n][()] # every dataset must decode e = f["memory/embeddings"] assert e.dtype == want, e.dtype assert e.shape == (20, 8), e.shape ref = np.array([{bits}], dtype=np.uint32).view(np.float32).astype(want).reshape(20, 8) assert (e[()] == ref).all() assert f["memory/norms"].dtype == np.float32 print(len(names)) "#, py_bool = if float16 { "True" } else { "False" }, path = path.display() ); let out = Command::new(python()) .args(["-c", &script]) .output() .unwrap(); assert!( out.status.success(), "float16={float16}: {}", String::from_utf8_lossy(&out.stderr) ); let n: usize = String::from_utf8_lossy(&out.stdout).trim().parse().unwrap(); assert!(n >= 10, "only {n} datasets"); } }