feat: CLI suggest/eval/index; Mode C baseline runnable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 18:31:35 +02:00
parent 377fc84875
commit 41debc4f60
2 changed files with 120 additions and 2 deletions
+106 -2
View File
@@ -1,3 +1,107 @@
fn main() { use alpha_id::eval::{inject_errors, recall_at_k};
println!("alpha-id CLI — see `alpha-id --help`"); use alpha_id::model::{Config, Filter, Mode};
use alpha_id::pipeline::Pipeline;
use clap::{Parser, Subcommand};
use std::io::Read;
#[derive(Parser)]
#[command(name = "alpha-id")]
struct Cli {
#[arg(long, default_value = "config/default.toml", global = true)]
config: String,
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
/// Build indexes (lexical now; embeddings added in Phase A)
Index { #[arg(long)] sample: Option<usize>, #[arg(long)] full: bool, #[arg(long)] confirm: bool },
/// Suggest ICD codes for a dictation (FILE or - for stdin)
Suggest {
input: String,
#[arg(long, default_value = "c")] mode: String,
#[arg(long, default_value_t = 10)] top: usize,
#[arg(long)] billable_only: bool,
#[arg(long)] valid_only: bool,
#[arg(long)] exclude_exotic: bool,
#[arg(long)] chapters: Option<String>,
#[arg(long)] json: bool,
},
/// Run the eval harness
Eval {
#[arg(long, default_value = "c")] mode: String,
#[arg(long, default_value_t = 200)] cases: usize,
#[arg(long, default_value_t = 42)] seed: u64,
},
}
fn read_input(arg: &str) -> String {
if let Ok(v) = std::env::var("ALPHA_ID_STDIN") { return v; }
if arg == "-" {
let mut s = String::new();
std::io::stdin().read_to_string(&mut s).ok();
s
} else {
std::fs::read_to_string(arg).unwrap_or_default()
}
}
fn main() {
let cli = Cli::parse();
let cfg = Config::load(&cli.config).expect("config");
match cli.cmd {
Cmd::Index { sample, full, confirm } => {
let p = Pipeline::load(&cfg).expect("load");
eprintln!("Loaded {} entries, {} ICD metas. (sample={:?} full={} confirm={})",
p.entries.len(), p.meta.len(), sample, full, confirm);
}
Cmd::Suggest { input, mode, top, billable_only, valid_only, exclude_exotic, chapters, json } => {
let p = Pipeline::load(&cfg).expect("load");
let m: Mode = mode.parse().expect("mode");
let filter = Filter {
billable_only, valid_only, exclude_exotic,
chapters: chapters.map(|c| c.split(',').map(|s| s.trim().to_string()).collect()),
};
let text = read_input(&input);
let res = p.suggest(&text, m, &filter, top);
if json {
println!("{}", serde_json::to_string_pretty(&res).unwrap());
} else {
for s in &res.suggestions {
println!("{:<8} {:>5.3} {}", s.icd_code, s.score, s.description);
}
eprintln!("[{} segments, mode {}, {} ms, degraded={}]",
res.diagnostics.segment_count, res.diagnostics.mode,
res.diagnostics.millis, res.diagnostics.degraded);
}
}
Cmd::Eval { mode, cases, seed } => {
let p = Pipeline::load(&cfg).expect("load");
let m: Mode = mode.parse().expect("mode");
let mut sum = 0.0;
let mut billable_sum = 0.0;
let mut billable_n: f64 = 0.0;
let billable: Vec<_> = p.entries.iter().enumerate()
.filter(|(_, e)| e.valid)
.filter(|(_, e)| {
alpha_id::corpus::primary_code(e)
.and_then(|c| p.meta.get(c))
.map(|mt| mt.para295 == "P").unwrap_or(false)
})
.take(cases).collect();
for (k, (_, e)) in billable.iter().enumerate() {
let code = alpha_id::corpus::primary_code(e).unwrap().to_string();
let noisy = inject_errors(&e.text, 0.15, seed.wrapping_add(k as u64));
let res = p.suggest(&noisy, m, &Filter::default(), 10);
let preds: Vec<String> = res.suggestions.iter().map(|s| s.icd_code.clone()).collect();
sum += recall_at_k(&preds, &code, 10);
billable_sum += recall_at_k(&preds, &code, 10);
billable_n += 1.0;
}
let n = billable.len().max(1) as f64;
println!("mode={} cases={} Recall@10={:.3} BillableRecall@10={:.3}",
mode, billable.len(), sum / n, billable_sum / billable_n.max(1.0));
}
}
} }
+14
View File
@@ -0,0 +1,14 @@
use std::process::Command;
#[test]
fn cli_suggest_outputs_json_with_codes() {
let out = Command::new(env!("CARGO_BIN_EXE_alpha-id"))
.args(["suggest", "--mode", "c", "--json", "-"])
.arg("--config").arg("config/default.toml")
.env("ALPHA_ID_STDIN", "Diabetes mellitus Typ 2")
.output().unwrap();
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let s = String::from_utf8_lossy(&out.stdout);
assert!(s.contains("\"icd_code\""));
assert!(s.contains("E11"));
}