use anyhow::Result;
use clap::Args;
use std::path::PathBuf;
use std::sync::Arc;
use dkp_gen_core::{CliOverrides, GenConfig, OpenAiClient, PipelineContext};
use crate::cli::CmdCtx;
#[derive(Args, Debug)]
pub struct FixUntilArgs {
pub pack: PathBuf,
#[arg(
long,
value_name = "KEY",
env = "DKP_GEN_API_KEY",
hide_env_values = true
)]
pub api_key: Option<String>,
#[arg(long, value_name = "URL")]
pub base_url: Option<String>,
#[arg(long, value_name = "MODEL")]
pub model: Option<String>,
#[arg(long)]
pub no_tools: bool,
#[arg(long)]
pub no_render_formats: bool,
#[arg(long, value_name = "TEXT")]
pub instructions: Option<String>,
#[arg(long, value_name = "N")]
pub max_tool_turns: Option<u32>,
#[arg(long, value_name = "F")]
pub threshold: Option<f64>,
#[arg(long, value_name = "N", default_value = "5")]
pub max_rounds: u32,
#[arg(long, value_name = "N")]
pub pairs: Option<usize>,
#[arg(long)]
pub baseline_only: bool,
}
pub async fn run(args: FixUntilArgs, ctx: &CmdCtx) -> Result<()> {
let pack = dkp_core::Pack::open(&args.pack)?;
let domain = pack.manifest.domain.clone();
let pack_name = pack.manifest.name.clone();
let pack_version = pack.manifest.version.clone();
let min_eval_delta = pack.manifest.min_eval_delta.unwrap_or(0.0);
let threshold = args.threshold.unwrap_or({
if min_eval_delta > 0.0 {
min_eval_delta
} else {
1.0
}
});
let fix_config = GenConfig::load(CliOverrides {
base_url: args.base_url.clone(),
api_key: args.api_key.clone(),
model: args.model.clone(),
overwrite: true,
no_tools: args.no_tools,
instructions: args.instructions.clone(),
no_render_formats: args.no_render_formats,
max_tool_turns: args.max_tool_turns,
})?;
if fix_config.api_key.is_empty() {
anyhow::bail!(
"API key required: pass --api-key, set DKP_GEN_API_KEY, \
or add api_key to ~/.dkp/gen.toml"
);
}
if fix_config.tools_enabled && fix_config.search_api_key.is_empty() {
anyhow::bail!(
"Tool use is enabled but no search API key is set: set DKP_GEN_SEARCH_API_KEY, \
add [search] api_key to ~/.dkp/gen.toml, or pass --no-tools."
);
}
let eval_config = GenConfig::load(CliOverrides {
base_url: args.base_url,
api_key: args.api_key,
model: args.model,
overwrite: true,
no_tools: true,
instructions: None,
no_render_formats: true,
max_tool_turns: None,
})?;
let client = Arc::new(OpenAiClient::new(&fix_config)?);
let client_ref = Arc::clone(&client);
let display_name = pack_name.clone();
let client_dyn: Arc<dyn dkp_gen_core::LlmClient> = client;
let eval_ctx = Arc::new(PipelineContext {
pack_dir: args.pack.clone(),
domain: domain.clone(),
pack_name: pack_name.clone(),
config: eval_config,
client: Arc::clone(&client_dyn),
progress: None,
verbose: !ctx.quiet,
});
let fix_ctx = PipelineContext {
pack_dir: args.pack.clone(),
domain,
pack_name,
config: fix_config,
client: Arc::clone(&client_dyn),
progress: None,
verbose: !ctx.quiet,
};
let mut last_passed = 0usize;
let mut last_total = 0usize;
for round in 0..=args.max_rounds {
if !ctx.quiet {
println!(
"[{}] Round {}/{}: running eval...",
display_name,
round + 1,
args.max_rounds
);
}
let (pre_eval_p, pre_eval_c) = client_ref.token_usage();
let report = dkp_gen_core::eval::run(
Arc::clone(&eval_ctx),
&pack_version,
min_eval_delta,
args.pairs,
args.baseline_only,
)
.await?;
let (post_eval_p, post_eval_c) = client_ref.token_usage();
let passed = report.summary.passed;
let total = report.summary.total;
let pct = (passed * 100).checked_div(total).unwrap_or(0);
let pass_rate = if total > 0 {
passed as f64 / total as f64
} else {
0.0
};
last_passed = passed;
last_total = total;
if !ctx.quiet {
let ep = post_eval_p - pre_eval_p;
let ec = post_eval_c - pre_eval_c;
println!(
"[{}] Eval: {}/{} passed ({}%) — {} prompt + {} completion tokens",
display_name, passed, total, pct, ep, ec
);
}
if pass_rate >= threshold {
if !ctx.quiet {
println!(
"[{}] Threshold met ({:.0}%). Stopping.",
display_name,
threshold * 100.0
);
}
break;
}
if round == args.max_rounds {
if !ctx.quiet {
println!(
"[{}] Max rounds ({}) reached. Final pass rate: {}%.",
display_name, args.max_rounds, pct
);
}
break;
}
if !ctx.quiet {
println!(
"[{}] Round {}/{}: running fix...",
display_name,
round + 1,
args.max_rounds
);
}
let (pre_fix_p, pre_fix_c) = client_ref.token_usage();
let fix_report = dkp_gen_core::fix::run(&fix_ctx).await?;
let (post_fix_p, post_fix_c) = client_ref.token_usage();
if !ctx.quiet {
let fp = post_fix_p - pre_fix_p;
let fc = post_fix_c - pre_fix_c;
println!(
"[{}] Fix: {} failures addressed, {} chunks written, {} eval cases written — {} prompt + {} completion tokens",
display_name,
fix_report.failed_count,
fix_report.chunks_written,
fix_report.eval_cases_written,
fp,
fc,
);
}
}
if !ctx.quiet {
let final_pct = (last_passed * 100).checked_div(last_total).unwrap_or(0);
let (prompt, completion) = client_ref.token_usage();
println!(
"[{}] fix-until complete: {}/{} passed ({}%) — {} prompt + {} completion tokens total",
display_name, last_passed, last_total, final_pct, prompt, completion,
);
}
Ok(())
}