forge-guardrails 0.1.2

Foundation types for an LLM-agent workflow framework
Documentation
use serde_json::Value;

use super::types::AlternativeProposal;

#[derive(Debug)]
pub(crate) struct ReviewProgress {
    pub(crate) total: usize,
    pub(crate) seen: usize,
    pub(crate) skipped: usize,
    pub(crate) accepted_real: usize,
    pub(crate) accepted_corrected: usize,
    pub(crate) accepted_alternatives: usize,
    pub(crate) rejected: usize,
    pub(crate) alternatives_seen: usize,
    pub(crate) alternatives_skipped: usize,
}

impl ReviewProgress {
    pub(crate) fn new(total: usize) -> Self {
        Self {
            total,
            seen: 0,
            skipped: 0,
            accepted_real: 0,
            accepted_corrected: 0,
            accepted_alternatives: 0,
            rejected: 0,
            alternatives_seen: 0,
            alternatives_skipped: 0,
        }
    }

    pub(crate) fn log_skip_if_needed(&self) {
        if self.skipped == 1 || self.skipped.is_multiple_of(250) {
            eprintln!(
                "review resume skipped={} seen={}/{}",
                self.skipped, self.seen, self.total
            );
        }
    }

    pub(crate) fn log_reviewing(&self, capture: &Value) {
        eprintln!(
            "review row {}/{} group={} domain={} scenario={} tool={}",
            self.seen,
            self.total,
            value_str(capture.get("example_group_id")),
            trace_str(capture, "domain"),
            trace_str(capture, "scenario"),
            candidate_tool(capture.get("candidate_call"))
        );
    }

    pub(crate) fn log_accepted(&self, row_index: usize, label: &str, source_bucket: &str) {
        eprintln!(
            "review row {}/{} accepted label={} source_bucket={} accepted_real={} accepted_corrected={} rejected={}",
            row_index,
            self.total,
            label,
            source_bucket,
            self.accepted_real,
            self.accepted_corrected,
            self.rejected
        );
    }

    pub(crate) fn log_rejected(&self, row_index: usize, reason: &str) {
        eprintln!(
            "review row {}/{} rejected reason={} accepted_real={} accepted_corrected={} rejected={}",
            row_index,
            self.total,
            reason,
            self.accepted_real,
            self.accepted_corrected,
            self.rejected
        );
    }

    pub(crate) fn log_alternative_skip_if_needed(&self) {
        if self.alternatives_skipped == 1 || self.alternatives_skipped.is_multiple_of(250) {
            eprintln!(
                "review alternative skipped={} seen={}",
                self.alternatives_skipped, self.alternatives_seen
            );
        }
    }

    pub(crate) fn log_alternative_reviewing(&self, proposal: &AlternativeProposal) {
        eprintln!(
            "review alternative {} group={} label={} tool={}",
            self.alternatives_seen,
            proposal.example_group_id,
            proposal.label,
            candidate_tool(Some(&proposal.candidate_call))
        );
    }

    pub(crate) fn log_alternative_accepted(&self, label: &str) {
        eprintln!(
            "review alternative {} accepted label={} accepted_alternatives={} rejected={}",
            self.alternatives_seen, label, self.accepted_alternatives, self.rejected
        );
    }

    pub(crate) fn log_alternative_rejected(&self, reason: &str) {
        eprintln!(
            "review alternative {} rejected reason={} accepted_alternatives={} rejected={}",
            self.alternatives_seen, reason, self.accepted_alternatives, self.rejected
        );
    }
}

pub(crate) fn value_str(value: Option<&Value>) -> &str {
    value.and_then(Value::as_str).unwrap_or("unknown")
}

pub(crate) fn trace_str<'a>(capture: &'a Value, key: &str) -> &'a str {
    capture
        .get("proxy_trace")
        .and_then(|trace| trace.get(key))
        .and_then(Value::as_str)
        .unwrap_or("unknown")
}

pub(crate) fn candidate_tool(candidate: Option<&Value>) -> &str {
    candidate
        .and_then(|candidate| candidate.get("name"))
        .and_then(Value::as_str)
        .unwrap_or("unknown")
}