use crate::diff::ModuleDiff;
use crate::recursive::Iteration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReviewTrigger {
Interval,
ScoreDrop,
Convergence,
FirstIteration,
Manual,
Keyword,
}
impl ReviewTrigger {
pub fn description(&self) -> &'static str {
match self {
Self::Interval => "Scheduled review interval",
Self::ScoreDrop => "Score decreased",
Self::Convergence => "About to accept result",
Self::FirstIteration => "First iteration",
Self::Manual => "Manual review requested",
Self::Keyword => "Keyword triggered",
}
}
pub fn is_warning(&self) -> bool {
matches!(self, Self::ScoreDrop)
}
}
#[derive(Debug)]
pub struct ReviewContext<'a> {
pub iteration: u32,
pub max_iterations: u32,
pub diff: ModuleDiff<'a>,
pub score: f64,
pub prev_score: f64,
pub feedback: Option<&'a str>,
pub output: &'a str,
pub prev_output: Option<&'a str>,
pub history: &'a [Iteration],
pub trigger: ReviewTrigger,
pub domain: &'a str,
pub question: &'a str,
}
impl<'a> ReviewContext<'a> {
pub fn new(
iteration: u32,
max_iterations: u32,
score: f64,
output: &'a str,
trigger: ReviewTrigger,
) -> Self {
Self {
iteration,
max_iterations,
diff: ModuleDiff::new(),
score,
prev_score: 0.0,
feedback: None,
output,
prev_output: None,
history: &[],
trigger,
domain: "",
question: "",
}
}
pub fn with_diff(mut self, diff: ModuleDiff<'a>) -> Self {
self.diff = diff;
self
}
pub fn with_prev_score(mut self, score: f64) -> Self {
self.prev_score = score;
self
}
pub fn with_feedback(mut self, feedback: &'a str) -> Self {
self.feedback = Some(feedback);
self
}
pub fn with_prev_output(mut self, output: &'a str) -> Self {
self.prev_output = Some(output);
self
}
pub fn with_history(mut self, history: &'a [Iteration]) -> Self {
self.history = history;
self
}
pub fn with_domain(mut self, domain: &'a str) -> Self {
self.domain = domain;
self
}
pub fn with_question(mut self, question: &'a str) -> Self {
self.question = question;
self
}
pub fn score_change(&self) -> f64 {
self.score - self.prev_score
}
pub fn score_improved(&self) -> bool {
self.score > self.prev_score
}
pub fn progress(&self) -> f64 {
if self.max_iterations == 0 {
0.0
} else {
self.iteration as f64 / self.max_iterations as f64
}
}
pub fn is_final(&self) -> bool {
self.iteration >= self.max_iterations || matches!(self.trigger, ReviewTrigger::Convergence)
}
}
#[derive(Debug, Clone)]
pub enum ReviewDecision {
Accept,
Reject,
Edit {
instruction: Option<String>,
output: Option<String>,
guidance: Option<String>,
},
Stop,
AcceptFinal,
Rollback {
to_iteration: u32,
},
SkipNext {
count: u32,
},
Pause {
duration: std::time::Duration,
},
RequestInfo {
query: String,
},
}
impl ReviewDecision {
pub fn edit_output(output: String) -> Self {
Self::Edit {
instruction: None,
output: Some(output),
guidance: None,
}
}
pub fn with_guidance(guidance: String) -> Self {
Self::Edit {
instruction: None,
output: None,
guidance: Some(guidance),
}
}
pub fn continues_iteration(&self) -> bool {
matches!(
self,
Self::Accept | Self::Edit { .. } | Self::SkipNext { .. } | Self::Rollback { .. }
)
}
pub fn stops_iteration(&self) -> bool {
matches!(self, Self::Stop | Self::AcceptFinal)
}
pub fn skip_count(&self) -> u32 {
match self {
Self::SkipNext { count } => *count,
_ => 0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_review_trigger_description() {
assert!(!ReviewTrigger::Interval.description().is_empty());
assert!(ReviewTrigger::ScoreDrop.is_warning());
assert!(!ReviewTrigger::Interval.is_warning());
}
#[test]
fn test_review_context_basic() {
let ctx = ReviewContext::new(3, 10, 0.75, "output", ReviewTrigger::Interval);
assert_eq!(ctx.iteration, 3);
assert_eq!(ctx.max_iterations, 10);
assert_eq!(ctx.score, 0.75);
assert_eq!(ctx.output, "output");
}
#[test]
fn test_review_context_score_change() {
let ctx =
ReviewContext::new(1, 10, 0.75, "output", ReviewTrigger::Interval).with_prev_score(0.5);
assert_eq!(ctx.score_change(), 0.25);
assert!(ctx.score_improved());
}
#[test]
fn test_review_context_progress() {
let ctx = ReviewContext::new(5, 10, 0.5, "output", ReviewTrigger::Interval);
assert_eq!(ctx.progress(), 0.5);
let ctx2 = ReviewContext::new(10, 10, 0.9, "output", ReviewTrigger::Convergence);
assert!(ctx2.is_final());
}
#[test]
fn test_review_decision_continues() {
assert!(ReviewDecision::Accept.continues_iteration());
assert!(ReviewDecision::SkipNext { count: 5 }.continues_iteration());
assert!(!ReviewDecision::Stop.continues_iteration());
assert!(!ReviewDecision::AcceptFinal.continues_iteration());
}
#[test]
fn test_review_decision_stops() {
assert!(ReviewDecision::Stop.stops_iteration());
assert!(ReviewDecision::AcceptFinal.stops_iteration());
assert!(!ReviewDecision::Accept.stops_iteration());
}
#[test]
fn test_review_decision_skip_count() {
assert_eq!(ReviewDecision::SkipNext { count: 3 }.skip_count(), 3);
assert_eq!(ReviewDecision::Accept.skip_count(), 0);
}
#[test]
fn test_edit_decision_helpers() {
let decision = ReviewDecision::edit_output("new output".to_string());
if let ReviewDecision::Edit { output, .. } = decision {
assert_eq!(output, Some("new output".to_string()));
} else {
panic!("Expected Edit decision");
}
}
}