Skip to main content

ai_agents_reasoning/
lib.rs

1//! Reasoning and reflection capabilities for AI Agents framework
2
3mod config;
4mod evaluation;
5mod metadata;
6mod mode;
7mod plan;
8mod planning;
9
10pub use config::{ReasoningConfig, ReflectionConfig};
11pub use evaluation::{CriterionResult, EvaluationResult, ReflectionAttempt};
12pub use metadata::{ReasoningMetadata, ReflectionMetadata};
13pub use mode::{ReasoningMode, ReasoningOutput, ReflectionMode};
14pub use plan::{Plan, PlanAction, PlanStatus, PlanStep, StepStatus};
15pub use planning::{
16    PlanAvailableActions, PlanReflectionConfig, PlanningConfig, StepFailureAction, StringOrList,
17};
18
19#[cfg(test)]
20mod tests {
21    use super::*;
22
23    #[test]
24    fn test_full_reasoning_config() {
25        let yaml = r#"
26mode: plan_and_execute
27judge_llm: router
28output: tagged
29max_iterations: 10
30planning:
31  planner_llm: router
32  max_steps: 15
33  available:
34    tools: all
35    skills:
36      - analyze
37      - summarize
38  reflection:
39    enabled: true
40    on_step_failure: replan
41    max_replans: 3
42"#;
43        let config: ReasoningConfig = serde_yaml::from_str(yaml).unwrap();
44        assert_eq!(config.mode, ReasoningMode::PlanAndExecute);
45        assert!(config.is_enabled());
46        assert!(config.needs_planning());
47
48        let planning = config.get_planning().unwrap();
49        assert_eq!(planning.max_steps, 15);
50        assert!(planning.available.tools.is_all());
51        assert!(!planning.available.skills.is_all());
52        assert!(planning.available.skills.allows("analyze"));
53        assert!(planning.reflection.enabled);
54    }
55
56    #[test]
57    fn test_full_reflection_config() {
58        let yaml = r#"
59enabled: auto
60evaluator_llm: router
61max_retries: 3
62criteria:
63  - "Response addresses the question"
64  - "Response is helpful"
65  - "Response is accurate"
66pass_threshold: 0.75
67"#;
68        let config: ReflectionConfig = serde_yaml::from_str(yaml).unwrap();
69        assert!(config.is_auto());
70        assert!(config.requires_evaluation());
71        assert_eq!(config.max_retries, 3);
72        assert_eq!(config.criteria.len(), 3);
73        assert_eq!(config.pass_threshold, 0.75);
74    }
75
76    #[test]
77    fn test_plan_workflow() {
78        let mut plan = Plan::new("Analyze weather and make recommendation");
79
80        let step1 = PlanStep::new(
81            "Get weather for Seoul",
82            PlanAction::tool("weather", serde_json::json!({"city": "Seoul"})),
83        )
84        .with_id("get_seoul");
85
86        let step2 = PlanStep::new(
87            "Get weather for Tokyo",
88            PlanAction::tool("weather", serde_json::json!({"city": "Tokyo"})),
89        )
90        .with_id("get_tokyo");
91
92        let step3 = PlanStep::new(
93            "Compare weather data",
94            PlanAction::think("Compare the weather"),
95        )
96        .with_id("compare")
97        .with_dependencies(vec!["get_seoul".to_string(), "get_tokyo".to_string()]);
98
99        let step4 = PlanStep::new(
100            "Make recommendation",
101            PlanAction::respond("Based on {{ compare }}..."),
102        )
103        .with_id("respond")
104        .with_dependencies(vec!["compare".to_string()]);
105
106        plan.add_step(step1);
107        plan.add_step(step2);
108        plan.add_step(step3);
109        plan.add_step(step4);
110
111        assert_eq!(plan.steps.len(), 4);
112
113        // Both get_seoul and get_tokyo can run in parallel (no dependencies)
114        let executable: Vec<_> = plan
115            .steps
116            .iter()
117            .filter(|s| {
118                s.status.is_pending()
119                    && s.dependencies
120                        .iter()
121                        .all(|dep| plan.is_step_completed_pub(dep))
122            })
123            .collect();
124        assert_eq!(executable.len(), 2);
125    }
126
127    #[test]
128    fn test_evaluation_workflow() {
129        let criteria = vec![
130            CriterionResult::pass("Addresses question"),
131            CriterionResult::fail("Complete response", "Missing conclusion"),
132        ];
133
134        let evaluation = EvaluationResult::new(false, 0.6).with_criteria(criteria);
135
136        assert!(!evaluation.passed);
137        assert!(!evaluation.passed_all());
138        assert_eq!(evaluation.failed_criteria().count(), 1);
139        assert!((evaluation.pass_rate() - 0.5).abs() < 0.01);
140
141        let attempt = ReflectionAttempt::new("Initial response", evaluation.clone())
142            .with_feedback("Add a conclusion to complete the response");
143
144        assert!(!attempt.passed());
145        assert!(attempt.feedback.is_some());
146
147        let mut metadata = ReflectionMetadata::new(evaluation);
148        metadata.add_attempt(attempt);
149        assert_eq!(metadata.attempts, 1);
150    }
151
152    #[test]
153    fn test_reasoning_metadata() {
154        let metadata = ReasoningMetadata::new(ReasoningMode::CoT)
155            .with_thinking("Step 1: Understand\nStep 2: Analyze\nStep 3: Conclude")
156            .with_iterations(1)
157            .with_auto_detected(false);
158
159        assert_eq!(metadata.mode_used, ReasoningMode::CoT);
160        assert!(metadata.has_thinking());
161        assert!(!metadata.auto_detected);
162    }
163
164    impl Plan {
165        fn is_step_completed_pub(&self, step_id: &str) -> bool {
166            self.steps
167                .iter()
168                .find(|s| s.id == step_id)
169                .map(|s| s.status.is_completed())
170                .unwrap_or(false)
171        }
172    }
173}