echo_agent 0.1.3

Production-grade AI Agent framework for Rust — ReAct engine, multi-agent, memory, streaming, MCP, IM channels, workflows
Documentation
//! Critic trait — evaluates the quality of Agent output

use crate::agent::self_reflection::types::Critique;
use crate::error::Result;
use futures::future::BoxFuture;

/// Critic trait — evaluates the response generated by an Agent
///
/// Symmetrical to `Planner`, this is a pluggable evaluation component in the Self-Reflection pattern.
/// Implement `Critic` to customize the evaluation strategy.
pub trait Critic: Send + Sync {
    /// Evaluate a response
    ///
    /// - `task`: Original task description
    /// - `answer`: Response generated by the Agent
    /// - `context`: Additional context (e.g., episodic memory, previous results, etc.)
    fn critique<'a>(
        &'a self,
        task: &'a str,
        answer: &'a str,
        context: &'a str,
    ) -> BoxFuture<'a, Result<Critique>>;

    /// Human-readable name of the Critic
    fn name(&self) -> &str {
        "anonymous"
    }
}

// ── StaticCritic ────────────────────────────────────────────────────────────

/// Static Critic: always returns a fixed result (for testing)
pub struct StaticCritic {
    score: f64,
    passed: bool,
    feedback: String,
    suggestions: Vec<String>,
}

impl StaticCritic {
    /// Create a static evaluator
    ///
    /// # Parameters
    /// * `score` - Evaluation score (range typically 0.0-10.0)
    /// * `passed` - Whether the evaluation passed
    /// * `feedback` - Evaluation feedback text
    ///
    /// # Description
    /// The static evaluator ignores the task content and response, always returning the same evaluation result.
    /// Primarily used for testing or simulating specific evaluation scenarios.
    pub fn new(score: f64, passed: bool, feedback: impl Into<String>) -> Self {
        Self {
            score,
            passed,
            feedback: feedback.into(),
            suggestions: Vec::new(),
        }
    }

    /// Set improvement suggestions list
    ///
    /// # Parameters
    /// * `suggestions` - List of improvement suggestion strings, used to guide response optimization
    ///
    /// # Description
    /// Suggestions are returned with the evaluation results for use in the reflection phase.
    pub fn with_suggestions(mut self, suggestions: Vec<String>) -> Self {
        self.suggestions = suggestions;
        self
    }

    /// Create a Critic that always passes
    pub fn always_pass() -> Self {
        Self::new(9.0, true, "Response quality is excellent")
    }

    /// Create a Critic that always fails
    pub fn always_fail() -> Self {
        Self::new(3.0, false, "Response does not meet requirements")
    }
}

impl Critic for StaticCritic {
    fn critique<'a>(
        &'a self,
        _task: &'a str,
        _answer: &'a str,
        _context: &'a str,
    ) -> BoxFuture<'a, Result<Critique>> {
        Box::pin(async move {
            Ok(Critique {
                score: self.score,
                passed: self.passed,
                feedback: self.feedback.clone(),
                suggestions: self.suggestions.clone(),
            })
        })
    }

    fn name(&self) -> &str {
        "static"
    }
}

// ── ThresholdCritic ──────────────────────────────────────────────────────────

/// Threshold Critic: evaluates via another Critic, then determines pass/fail based on threshold
///
/// Used to wrap other Critics, overriding their `passed` judgment.
pub struct ThresholdCritic<C: Critic> {
    inner: C,
    threshold: f64,
}

impl<C: Critic> ThresholdCritic<C> {
    /// Create a threshold evaluator
    ///
    /// # Parameters
    /// * `inner` - Inner evaluator, used for actual scoring and generating feedback
    /// * `threshold` - Pass threshold, result is marked as passed when score >= threshold
    ///
    /// # Description
    /// The threshold evaluator calls the inner evaluator to get a score and feedback,
    /// then recalculates the pass status based on the threshold.
    /// This allows threshold adjustments on existing evaluators without modifying their internal logic.
    pub fn new(inner: C, threshold: f64) -> Self {
        Self { inner, threshold }
    }
}

impl<C: Critic> Critic for ThresholdCritic<C> {
    fn critique<'a>(
        &'a self,
        task: &'a str,
        answer: &'a str,
        context: &'a str,
    ) -> BoxFuture<'a, Result<Critique>> {
        Box::pin(async move {
            let mut critique = self.inner.critique(task, answer, context).await?;
            critique.passed = critique.score >= self.threshold;
            Ok(critique)
        })
    }

    fn name(&self) -> &str {
        self.inner.name()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_static_critic_always_pass() {
        let critic = StaticCritic::always_pass();
        let critique = critic.critique("task", "answer", "").await.unwrap();
        assert!(critique.passed);
        assert!(critique.score >= 8.0);
    }

    #[tokio::test]
    async fn test_static_critic_always_fail() {
        let critic = StaticCritic::always_fail();
        let critique = critic.critique("task", "answer", "").await.unwrap();
        assert!(!critique.passed);
    }

    #[tokio::test]
    async fn test_threshold_critic() {
        let inner = StaticCritic::new(6.0, true, "OK");
        let critic = ThresholdCritic::new(inner, 8.0);
        let critique = critic.critique("task", "answer", "").await.unwrap();
        assert!(!critique.passed); // 6.0 < 8.0
        assert_eq!(critique.score, 6.0);
    }

    #[tokio::test]
    async fn test_threshold_critic_passes() {
        let inner = StaticCritic::new(9.0, false, "Very good");
        let critic = ThresholdCritic::new(inner, 8.0);
        let critique = critic.critique("task", "answer", "").await.unwrap();
        assert!(critique.passed); // 9.0 >= 8.0
    }
}