rust_rule_engine/rete/
agenda.rs

1//! Advanced Agenda System (Drools-style)
2//!
3//! This module implements advanced agenda features similar to Drools:
4//! - Activation Groups: Only one rule in a group can fire
5//! - Agenda Groups: Sequential execution of rule groups
6//! - Ruleflow Groups: Workflow-based execution
7//! - Auto-focus: Automatic agenda group switching
8//! - Lock-on-active: Prevent re-activation during rule firing
9
10use std::collections::{HashMap, HashSet, BinaryHeap};
11use std::cmp::Ordering;
12
13/// Activation represents a rule that is ready to fire
14#[derive(Debug, Clone)]
15pub struct Activation {
16    /// Rule name
17    pub rule_name: String,
18    /// Priority/salience (higher fires first)
19    pub salience: i32,
20    /// Activation group (only one rule in group can fire)
21    pub activation_group: Option<String>,
22    /// Agenda group (for sequential execution)
23    pub agenda_group: String,
24    /// Ruleflow group (for workflow execution)
25    pub ruleflow_group: Option<String>,
26    /// No-loop flag
27    pub no_loop: bool,
28    /// Lock-on-active flag
29    pub lock_on_active: bool,
30    /// Auto-focus flag
31    pub auto_focus: bool,
32    /// Creation timestamp (for conflict resolution)
33    pub created_at: std::time::Instant,
34    /// Internal ID
35    id: usize,
36}
37
38impl Activation {
39    /// Create a new activation
40    pub fn new(rule_name: String, salience: i32) -> Self {
41        Self {
42            rule_name,
43            salience,
44            activation_group: None,
45            agenda_group: "MAIN".to_string(),
46            ruleflow_group: None,
47            no_loop: true,
48            lock_on_active: false,
49            auto_focus: false,
50            created_at: std::time::Instant::now(),
51            id: 0,
52        }
53    }
54
55    /// Builder: Set activation group
56    pub fn with_activation_group(mut self, group: String) -> Self {
57        self.activation_group = Some(group);
58        self
59    }
60
61    /// Builder: Set agenda group
62    pub fn with_agenda_group(mut self, group: String) -> Self {
63        self.agenda_group = group;
64        self
65    }
66
67    /// Builder: Set ruleflow group
68    pub fn with_ruleflow_group(mut self, group: String) -> Self {
69        self.ruleflow_group = Some(group);
70        self
71    }
72
73    /// Builder: Set no-loop
74    pub fn with_no_loop(mut self, no_loop: bool) -> Self {
75        self.no_loop = no_loop;
76        self
77    }
78
79    /// Builder: Set lock-on-active
80    pub fn with_lock_on_active(mut self, lock: bool) -> Self {
81        self.lock_on_active = lock;
82        self
83    }
84
85    /// Builder: Set auto-focus
86    pub fn with_auto_focus(mut self, auto_focus: bool) -> Self {
87        self.auto_focus = auto_focus;
88        self
89    }
90}
91
92// Implement ordering for priority queue (higher salience first)
93impl PartialEq for Activation {
94    fn eq(&self, other: &Self) -> bool {
95        self.id == other.id
96    }
97}
98
99impl Eq for Activation {}
100
101impl PartialOrd for Activation {
102    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
103        Some(self.cmp(other))
104    }
105}
106
107impl Ord for Activation {
108    fn cmp(&self, other: &Self) -> Ordering {
109        // First compare by salience (higher is better)
110        match self.salience.cmp(&other.salience) {
111            Ordering::Equal => {
112                // Then by creation time (earlier is better = reverse)
113                other.created_at.cmp(&self.created_at)
114            }
115            other_order => other_order,
116        }
117    }
118}
119
120/// Advanced Agenda (Drools-style)
121pub struct AdvancedAgenda {
122    /// All activations by agenda group
123    activations: HashMap<String, BinaryHeap<Activation>>,
124    /// Current focus (agenda group)
125    focus: String,
126    /// Focus stack
127    focus_stack: Vec<String>,
128    /// Fired rules (for no-loop)
129    fired_rules: HashSet<String>,
130    /// Fired activation groups
131    fired_activation_groups: HashSet<String>,
132    /// Locked groups (lock-on-active)
133    locked_groups: HashSet<String>,
134    /// Active ruleflow groups
135    active_ruleflow_groups: HashSet<String>,
136    /// Next activation ID
137    next_id: usize,
138}
139
140impl AdvancedAgenda {
141    /// Create a new agenda with "MAIN" as default focus
142    pub fn new() -> Self {
143        let mut agenda = Self {
144            activations: HashMap::new(),
145            focus: "MAIN".to_string(),
146            focus_stack: Vec::new(),
147            fired_rules: HashSet::new(),
148            fired_activation_groups: HashSet::new(),
149            locked_groups: HashSet::new(),
150            active_ruleflow_groups: HashSet::new(),
151            next_id: 0,
152        };
153        agenda.activations.insert("MAIN".to_string(), BinaryHeap::new());
154        agenda
155    }
156
157    /// Add an activation to the agenda
158    pub fn add_activation(&mut self, mut activation: Activation) {
159        // Auto-focus: switch to this agenda group if requested
160        if activation.auto_focus && activation.agenda_group != self.focus {
161            self.set_focus(activation.agenda_group.clone());
162        }
163
164        // Check activation group: if group already fired, skip
165        if let Some(ref group) = activation.activation_group {
166            if self.fired_activation_groups.contains(group) {
167                return; // Skip this activation
168            }
169        }
170
171        // Check ruleflow group: if not active, skip
172        if let Some(ref group) = activation.ruleflow_group {
173            if !self.active_ruleflow_groups.contains(group) {
174                return; // Skip this activation
175            }
176        }
177
178        // Assign ID
179        activation.id = self.next_id;
180        self.next_id += 1;
181
182        // Add to appropriate agenda group
183        self.activations
184            .entry(activation.agenda_group.clone())
185            .or_insert_with(BinaryHeap::new)
186            .push(activation);
187    }
188
189    /// Get the next activation to fire (from current focus)
190    pub fn get_next_activation(&mut self) -> Option<Activation> {
191        loop {
192            // Try to get from current focus
193            if let Some(heap) = self.activations.get_mut(&self.focus) {
194                while let Some(activation) = heap.pop() {
195                    // Check no-loop
196                    if activation.no_loop && self.fired_rules.contains(&activation.rule_name) {
197                        continue;
198                    }
199
200                    // Check lock-on-active
201                    if activation.lock_on_active && self.locked_groups.contains(&activation.agenda_group) {
202                        continue;
203                    }
204
205                    // Check activation group
206                    if let Some(ref group) = activation.activation_group {
207                        if self.fired_activation_groups.contains(group) {
208                            continue;
209                        }
210                    }
211
212                    return Some(activation);
213                }
214            }
215
216            // No more activations in current focus, try to pop focus stack
217            if let Some(prev_focus) = self.focus_stack.pop() {
218                self.focus = prev_focus;
219            } else {
220                return None; // Agenda is empty
221            }
222        }
223    }
224
225    /// Mark a rule as fired
226    pub fn mark_rule_fired(&mut self, activation: &Activation) {
227        self.fired_rules.insert(activation.rule_name.clone());
228
229        // If has activation group, mark group as fired (no other rules in group can fire)
230        if let Some(ref group) = activation.activation_group {
231            self.fired_activation_groups.insert(group.clone());
232        }
233
234        // Lock the agenda group if lock-on-active
235        if activation.lock_on_active {
236            self.locked_groups.insert(activation.agenda_group.clone());
237        }
238    }
239
240    /// Set focus to a specific agenda group
241    pub fn set_focus(&mut self, group: String) {
242        if group != self.focus {
243            self.focus_stack.push(self.focus.clone());
244            self.focus = group;
245        }
246    }
247
248    /// Get current focus
249    pub fn get_focus(&self) -> &str {
250        &self.focus
251    }
252
253    /// Clear all agenda groups
254    pub fn clear(&mut self) {
255        self.activations.clear();
256        self.activations.insert("MAIN".to_string(), BinaryHeap::new());
257        self.focus = "MAIN".to_string();
258        self.focus_stack.clear();
259        self.fired_rules.clear();
260        self.fired_activation_groups.clear();
261        self.locked_groups.clear();
262    }
263
264    /// Reset fired flags (for re-evaluation)
265    pub fn reset_fired_flags(&mut self) {
266        self.fired_rules.clear();
267        self.fired_activation_groups.clear();
268        self.locked_groups.clear();
269    }
270
271    /// Activate a ruleflow group (make rules in this group eligible to fire)
272    pub fn activate_ruleflow_group(&mut self, group: String) {
273        self.active_ruleflow_groups.insert(group);
274    }
275
276    /// Deactivate a ruleflow group
277    pub fn deactivate_ruleflow_group(&mut self, group: &str) {
278        self.active_ruleflow_groups.remove(group);
279    }
280
281    /// Check if ruleflow group is active
282    pub fn is_ruleflow_group_active(&self, group: &str) -> bool {
283        self.active_ruleflow_groups.contains(group)
284    }
285
286    /// Get agenda statistics
287    pub fn stats(&self) -> AgendaStats {
288        let total_activations: usize = self.activations.values().map(|heap| heap.len()).sum();
289        let groups = self.activations.len();
290
291        AgendaStats {
292            total_activations,
293            groups,
294            focus: self.focus.clone(),
295            fired_rules: self.fired_rules.len(),
296            fired_activation_groups: self.fired_activation_groups.len(),
297            active_ruleflow_groups: self.active_ruleflow_groups.len(),
298        }
299    }
300}
301
302impl Default for AdvancedAgenda {
303    fn default() -> Self {
304        Self::new()
305    }
306}
307
308/// Agenda statistics
309#[derive(Debug, Clone)]
310pub struct AgendaStats {
311    pub total_activations: usize,
312    pub groups: usize,
313    pub focus: String,
314    pub fired_rules: usize,
315    pub fired_activation_groups: usize,
316    pub active_ruleflow_groups: usize,
317}
318
319impl std::fmt::Display for AgendaStats {
320    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
321        write!(
322            f,
323            "Agenda Stats: {} activations, {} groups, focus='{}', {} fired rules",
324            self.total_activations, self.groups, self.focus, self.fired_rules
325        )
326    }
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    #[test]
334    fn test_basic_activation() {
335        let mut agenda = AdvancedAgenda::new();
336
337        let act1 = Activation::new("Rule1".to_string(), 10);
338        let act2 = Activation::new("Rule2".to_string(), 20);
339
340        agenda.add_activation(act1);
341        agenda.add_activation(act2);
342
343        // Higher salience fires first
344        let next = agenda.get_next_activation().unwrap();
345        assert_eq!(next.rule_name, "Rule2");
346    }
347
348    #[test]
349    fn test_activation_groups() {
350        let mut agenda = AdvancedAgenda::new();
351
352        let act1 = Activation::new("Rule1".to_string(), 10)
353            .with_activation_group("group1".to_string());
354        let act2 = Activation::new("Rule2".to_string(), 20)
355            .with_activation_group("group1".to_string());
356
357        agenda.add_activation(act1);
358        agenda.add_activation(act2);
359
360        // First activation fires
361        let first = agenda.get_next_activation().unwrap();
362        agenda.mark_rule_fired(&first);
363
364        // Second activation should be skipped (same group)
365        let second = agenda.get_next_activation();
366        assert!(second.is_none());
367    }
368
369    #[test]
370    fn test_agenda_groups() {
371        let mut agenda = AdvancedAgenda::new();
372
373        let act1 = Activation::new("Rule1".to_string(), 10)
374            .with_agenda_group("group_a".to_string());
375        let act2 = Activation::new("Rule2".to_string(), 20)
376            .with_agenda_group("group_b".to_string());
377
378        agenda.add_activation(act1);
379        agenda.add_activation(act2);
380
381        // MAIN is empty, nothing fires
382        assert!(agenda.get_next_activation().is_none());
383
384        // Set focus to group_a
385        agenda.set_focus("group_a".to_string());
386        let next = agenda.get_next_activation().unwrap();
387        assert_eq!(next.rule_name, "Rule1");
388    }
389
390    #[test]
391    fn test_auto_focus() {
392        let mut agenda = AdvancedAgenda::new();
393
394        let act = Activation::new("Rule1".to_string(), 10)
395            .with_agenda_group("special".to_string())
396            .with_auto_focus(true);
397
398        agenda.add_activation(act);
399
400        // Auto-focus should switch to "special"
401        assert_eq!(agenda.get_focus(), "special");
402    }
403
404    #[test]
405    fn test_ruleflow_groups() {
406        let mut agenda = AdvancedAgenda::new();
407
408        let act = Activation::new("Rule1".to_string(), 10)
409            .with_ruleflow_group("flow1".to_string());
410
411        // Without activating ruleflow group, activation is not added
412        agenda.add_activation(act.clone());
413        assert_eq!(agenda.stats().total_activations, 0);
414
415        // Activate ruleflow group
416        agenda.activate_ruleflow_group("flow1".to_string());
417        agenda.add_activation(act);
418        assert_eq!(agenda.stats().total_activations, 1);
419    }
420}