use crate::config::LearningConfig;
const DEFAULT_ANALYSIS_INTERVAL: u64 = 5;
#[derive(Debug, Clone, Copy)]
pub(crate) struct RlRoutingConfig {
pub(super) enabled: bool,
pub(super) learning_rate: f32,
pub(super) persist_interval: u32,
}
pub(crate) struct LearningEngine {
pub(super) config: Option<LearningConfig>,
pub(super) rl_routing: Option<RlRoutingConfig>,
pub(super) reflection_used: bool,
turn_counter: u64,
last_analysis_turn: u64,
analysis_interval: u64,
pub(super) last_analyzed_correction_id: i64,
}
impl LearningEngine {
#[must_use]
pub(crate) fn new() -> Self {
Self {
config: None,
rl_routing: None,
reflection_used: false,
turn_counter: 0,
last_analysis_turn: 0,
analysis_interval: DEFAULT_ANALYSIS_INTERVAL,
last_analyzed_correction_id: 0,
}
}
pub(super) fn is_enabled(&self) -> bool {
self.config.as_ref().is_some_and(|c| c.enabled)
}
pub(super) fn should_analyze(&self) -> bool {
let Some(cfg) = self.config.as_ref() else {
return false;
};
cfg.correction_detection
&& self.turn_counter >= self.last_analysis_turn + self.analysis_interval
}
pub(super) fn tick(&mut self) {
self.turn_counter += 1;
}
pub(super) fn mark_analyzed(&mut self) {
self.last_analysis_turn = self.turn_counter;
}
pub(super) fn mark_reflection_used(&mut self) {
self.reflection_used = true;
}
pub(super) fn was_reflection_used(&self) -> bool {
self.reflection_used
}
pub(super) fn reset_reflection(&mut self) {
self.reflection_used = false;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_defaults() {
let e = LearningEngine::new();
assert!(e.config.is_none());
assert!(!e.reflection_used);
assert!(!e.is_enabled());
assert!(!e.should_analyze());
assert_eq!(e.last_analyzed_correction_id, 0);
}
#[test]
fn is_enabled_no_config() {
let e = LearningEngine::new();
assert!(!e.is_enabled());
}
#[test]
fn is_enabled_disabled_config() {
let mut e = LearningEngine::new();
e.config = Some(LearningConfig {
enabled: false,
..Default::default()
});
assert!(!e.is_enabled());
}
#[test]
fn is_enabled_enabled_config() {
let mut e = LearningEngine::new();
e.config = Some(LearningConfig {
enabled: true,
..Default::default()
});
assert!(e.is_enabled());
}
#[test]
fn reflection_lifecycle() {
let mut e = LearningEngine::new();
assert!(!e.was_reflection_used());
e.mark_reflection_used();
assert!(e.was_reflection_used());
e.reset_reflection();
assert!(!e.was_reflection_used());
}
#[test]
fn mark_reflection_idempotent() {
let mut e = LearningEngine::new();
e.mark_reflection_used();
e.mark_reflection_used();
assert!(e.was_reflection_used());
e.reset_reflection();
assert!(!e.was_reflection_used());
}
#[test]
fn should_analyze_uses_correction_detection_not_enabled() {
let mut e = LearningEngine::new();
e.config = Some(LearningConfig {
enabled: false,
correction_detection: true,
..Default::default()
});
for _ in 0..DEFAULT_ANALYSIS_INTERVAL {
e.tick();
}
assert!(e.should_analyze());
}
#[test]
fn should_analyze_false_when_correction_detection_disabled() {
let mut e = LearningEngine::new();
e.config = Some(LearningConfig {
enabled: true,
correction_detection: false,
..Default::default()
});
for _ in 0..100 {
e.tick();
}
assert!(!e.should_analyze());
}
#[test]
fn tick_and_analyze_cycle() {
let mut e = LearningEngine::new();
e.config = Some(LearningConfig {
correction_detection: true,
..Default::default()
});
for i in 0..DEFAULT_ANALYSIS_INTERVAL {
assert!(!e.should_analyze(), "should not analyze at turn {i}");
e.tick();
}
assert!(e.should_analyze());
e.mark_analyzed();
assert!(!e.should_analyze());
for _ in 0..DEFAULT_ANALYSIS_INTERVAL {
e.tick();
}
assert!(e.should_analyze());
}
}