use super::PatternExtractor as PatternExtractorTrait;
use crate::episode::Episode;
use crate::pattern::Pattern;
use crate::types::OutcomeStats;
use anyhow::Result;
use async_trait::async_trait;
use uuid::Uuid;
pub struct DecisionPointExtractor {
confidence_threshold: f32,
}
impl Default for DecisionPointExtractor {
fn default() -> Self {
Self::new()
}
}
impl DecisionPointExtractor {
#[must_use]
pub fn new() -> Self {
Self {
confidence_threshold: 0.6,
}
}
#[must_use]
pub fn with_threshold(threshold: f32) -> Self {
Self {
confidence_threshold: threshold,
}
}
fn is_decision_action(action: &str) -> bool {
let action_lower = action.to_lowercase();
action_lower.contains("if ")
|| action_lower.contains("when ")
|| action_lower.contains("check ")
|| action_lower.contains("verify ")
|| action_lower.contains("validate ")
|| action_lower.contains("ensure ")
|| action_lower.starts_with("decide ")
|| action_lower.starts_with("determine ")
}
}
#[async_trait]
impl PatternExtractorTrait for DecisionPointExtractor {
async fn extract(&self, episode: &Episode) -> Result<Vec<Pattern>> {
let mut patterns = Vec::new();
if !episode.is_complete() {
return Ok(patterns);
}
for step in &episode.steps {
if Self::is_decision_action(&step.action) {
let outcome_stats = OutcomeStats {
success_count: usize::from(step.is_success()),
failure_count: usize::from(!step.is_success()),
total_count: 1,
avg_duration_secs: step.latency_ms as f32 / 1000.0,
};
patterns.push(Pattern::DecisionPoint {
id: Uuid::new_v4(),
condition: step.action.clone(),
action: step.tool.clone(),
outcome_stats,
context: episode.context.clone(),
effectiveness: crate::pattern::PatternEffectiveness::new(),
});
}
}
Ok(patterns)
}
fn name(&self) -> &'static str {
"DecisionPointExtractor"
}
fn confidence_threshold(&self) -> f32 {
self.confidence_threshold
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::episode::ExecutionStep;
use crate::patterns::extractors::tests::{complete_episode_successfully, create_test_episode};
use crate::types::ExecutionResult;
#[tokio::test]
async fn test_extract_decision_point() {
let extractor = DecisionPointExtractor::new();
let mut episode = create_test_episode();
let mut step = ExecutionStep::new(
1,
"validator".to_string(),
"Check if input is valid".to_string(),
);
step.result = Some(ExecutionResult::Success {
output: "Valid".to_string(),
});
step.latency_ms = 50;
episode.add_step(step);
complete_episode_successfully(&mut episode);
let patterns = extractor.extract(&episode).await.unwrap();
assert_eq!(patterns.len(), 1);
if let Pattern::DecisionPoint {
condition, action, ..
} = &patterns[0]
{
assert!(condition.contains("Check"));
assert_eq!(action, "validator");
} else {
panic!("Expected DecisionPoint pattern");
}
}
#[tokio::test]
async fn test_multiple_decision_points() {
let extractor = DecisionPointExtractor::new();
let mut episode = create_test_episode();
let decision_keywords = ["Check if", "Verify that", "Validate"];
for (i, keyword) in decision_keywords.iter().enumerate() {
let mut step =
ExecutionStep::new(i + 1, format!("tool_{i}"), format!("{keyword} something"));
step.result = Some(ExecutionResult::Success {
output: "OK".to_string(),
});
episode.add_step(step);
}
complete_episode_successfully(&mut episode);
let patterns = extractor.extract(&episode).await.unwrap();
assert_eq!(patterns.len(), 3);
}
#[tokio::test]
async fn test_no_decision_points() {
let extractor = DecisionPointExtractor::new();
let mut episode = create_test_episode();
let mut step = ExecutionStep::new(1, "reader".to_string(), "Read file".to_string());
step.result = Some(ExecutionResult::Success {
output: "OK".to_string(),
});
episode.add_step(step);
complete_episode_successfully(&mut episode);
let patterns = extractor.extract(&episode).await.unwrap();
assert!(patterns.is_empty());
}
}