Skip to main content

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//! - Conflict Resolution Strategies: Multiple ordering strategies
10
11use std::cmp::Ordering;
12use std::collections::{BinaryHeap, HashMap, HashSet};
13
14/// Conflict Resolution Strategy
15///
16/// Determines how conflicting activations are ordered in the agenda.
17/// Similar to CLIPS and Drools conflict resolution strategies.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum ConflictResolutionStrategy {
20    /// Salience-based ordering (default) - Higher salience fires first
21    Salience,
22
23    /// LEX (Recency) - Most recently inserted facts fire first
24    /// Sorts by the timestamp of the most recent fact used in the activation
25    LEX,
26
27    /// MEA (Recency + Specificity) - LEX + more specific rules first
28    /// Combines recency with condition count (more conditions = more specific)
29    MEA,
30
31    /// Depth-first - Fire rule immediately after insertion
32    /// Re-evaluates agenda after each rule fires
33    Depth,
34
35    /// Breadth-first - Collect all activations before firing (default)
36    /// Fires all activations in current cycle before re-evaluating
37    Breadth,
38
39    /// Simplicity - Rules with fewer conditions fire first
40    /// Simpler rules are prioritized
41    Simplicity,
42
43    /// Complexity - Rules with more conditions fire first
44    /// More complex/specific rules are prioritized
45    Complexity,
46
47    /// Random - Random ordering
48    /// Useful for testing non-deterministic behavior
49    Random,
50}
51
52/// Activation represents a rule that is ready to fire
53#[derive(Debug, Clone)]
54pub struct Activation {
55    /// Rule name
56    pub rule_name: String,
57    /// Priority/salience (higher fires first)
58    pub salience: i32,
59    /// Activation group (only one rule in group can fire)
60    pub activation_group: Option<String>,
61    /// Agenda group (for sequential execution)
62    pub agenda_group: String,
63    /// Ruleflow group (for workflow execution)
64    pub ruleflow_group: Option<String>,
65    /// No-loop flag
66    pub no_loop: bool,
67    /// Lock-on-active flag
68    pub lock_on_active: bool,
69    /// Auto-focus flag
70    pub auto_focus: bool,
71    /// Creation timestamp (for conflict resolution)
72    pub created_at: std::time::Instant,
73    /// Number of conditions in the rule (for complexity/simplicity strategies)
74    pub condition_count: usize,
75    /// Matched fact handle (which fact triggered this activation)
76    pub matched_fact_handle: Option<super::FactHandle>,
77    /// Internal ID
78    id: usize,
79}
80
81impl Activation {
82    /// Create a new activation
83    pub fn new(rule_name: String, salience: i32) -> Self {
84        Self {
85            rule_name,
86            salience,
87            activation_group: None,
88            agenda_group: "MAIN".to_string(),
89            ruleflow_group: None,
90            no_loop: true,
91            lock_on_active: false,
92            auto_focus: false,
93            created_at: std::time::Instant::now(),
94            condition_count: 1, // Default to 1
95            matched_fact_handle: None,
96            id: 0,
97        }
98    }
99
100    /// Builder: Set matched fact handle
101    pub fn with_matched_fact(mut self, handle: super::FactHandle) -> Self {
102        self.matched_fact_handle = Some(handle);
103        self
104    }
105
106    /// Builder: Set condition count
107    pub fn with_condition_count(mut self, count: usize) -> Self {
108        self.condition_count = count;
109        self
110    }
111
112    /// Builder: Set activation group
113    pub fn with_activation_group(mut self, group: String) -> Self {
114        self.activation_group = Some(group);
115        self
116    }
117
118    /// Builder: Set agenda group
119    pub fn with_agenda_group(mut self, group: String) -> Self {
120        self.agenda_group = group;
121        self
122    }
123
124    /// Builder: Set ruleflow group
125    pub fn with_ruleflow_group(mut self, group: String) -> Self {
126        self.ruleflow_group = Some(group);
127        self
128    }
129
130    /// Builder: Set no-loop
131    pub fn with_no_loop(mut self, no_loop: bool) -> Self {
132        self.no_loop = no_loop;
133        self
134    }
135
136    /// Builder: Set lock-on-active
137    pub fn with_lock_on_active(mut self, lock: bool) -> Self {
138        self.lock_on_active = lock;
139        self
140    }
141
142    /// Builder: Set auto-focus
143    pub fn with_auto_focus(mut self, auto_focus: bool) -> Self {
144        self.auto_focus = auto_focus;
145        self
146    }
147}
148
149// Implement ordering for priority queue (higher salience first)
150impl PartialEq for Activation {
151    fn eq(&self, other: &Self) -> bool {
152        self.id == other.id
153    }
154}
155
156impl Eq for Activation {}
157
158impl PartialOrd for Activation {
159    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
160        Some(self.cmp(other))
161    }
162}
163
164impl Ord for Activation {
165    fn cmp(&self, other: &Self) -> Ordering {
166        // First compare by salience (higher is better)
167        match self.salience.cmp(&other.salience) {
168            Ordering::Equal => {
169                // Then by creation time (earlier is better = reverse)
170                other.created_at.cmp(&self.created_at)
171            }
172            other_order => other_order,
173        }
174    }
175}
176
177/// Advanced Agenda (Drools-style)
178pub struct AdvancedAgenda {
179    /// All activations by agenda group
180    activations: HashMap<String, BinaryHeap<Activation>>,
181    /// Current focus (agenda group)
182    focus: String,
183    /// Focus stack
184    focus_stack: Vec<String>,
185    /// Fired rules (for no-loop)
186    fired_rules: HashSet<String>,
187    /// Fired activation groups
188    fired_activation_groups: HashSet<String>,
189    /// Locked groups (lock-on-active)
190    locked_groups: HashSet<String>,
191    /// Active ruleflow groups
192    active_ruleflow_groups: HashSet<String>,
193    /// Next activation ID
194    next_id: usize,
195    /// Conflict resolution strategy
196    strategy: ConflictResolutionStrategy,
197}
198
199impl AdvancedAgenda {
200    /// Create a new agenda with "MAIN" as default focus
201    pub fn new() -> Self {
202        let mut agenda = Self {
203            activations: HashMap::new(),
204            focus: "MAIN".to_string(),
205            focus_stack: Vec::new(),
206            fired_rules: HashSet::new(),
207            fired_activation_groups: HashSet::new(),
208            locked_groups: HashSet::new(),
209            active_ruleflow_groups: HashSet::new(),
210            next_id: 0,
211            strategy: ConflictResolutionStrategy::Salience, // Default strategy
212        };
213        agenda
214            .activations
215            .insert("MAIN".to_string(), BinaryHeap::new());
216        agenda
217    }
218
219    /// Set the conflict resolution strategy
220    pub fn set_strategy(&mut self, strategy: ConflictResolutionStrategy) {
221        self.strategy = strategy;
222        // Re-sort all existing activations with new strategy
223        let current_strategy = self.strategy; // Copy strategy to avoid borrow issues
224        for heap in self.activations.values_mut() {
225            let mut activations: Vec<_> = heap.drain().collect();
226            Self::sort_with_strategy(current_strategy, &mut activations);
227            *heap = activations.into_iter().collect();
228        }
229    }
230
231    /// Get current strategy
232    pub fn strategy(&self) -> ConflictResolutionStrategy {
233        self.strategy
234    }
235
236    /// Sort activations according to given strategy (static method)
237    fn sort_with_strategy(strategy: ConflictResolutionStrategy, activations: &mut [Activation]) {
238        match strategy {
239            ConflictResolutionStrategy::Salience => {
240                // Default: sort by salience (higher first), then by recency
241                activations.sort_by(|a, b| match b.salience.cmp(&a.salience) {
242                    Ordering::Equal => b.created_at.cmp(&a.created_at),
243                    other => other,
244                });
245            }
246            ConflictResolutionStrategy::LEX => {
247                // Recency: most recent first
248                // Sort by creation time descending (most recent first) using sort_by_key
249                activations.sort_by_key(|b| std::cmp::Reverse(b.created_at));
250            }
251            ConflictResolutionStrategy::MEA => {
252                // Recency + Specificity: recent first, then more conditions
253                activations.sort_by(|a, b| match b.created_at.cmp(&a.created_at) {
254                    Ordering::Equal => b.condition_count.cmp(&a.condition_count),
255                    other => other,
256                });
257            }
258            ConflictResolutionStrategy::Depth => {
259                // Depth-first: same as salience (handled in fire loop)
260                activations.sort_by(|a, b| match b.salience.cmp(&a.salience) {
261                    Ordering::Equal => b.created_at.cmp(&a.created_at),
262                    other => other,
263                });
264            }
265            ConflictResolutionStrategy::Breadth => {
266                // Breadth-first: same as salience (default behavior)
267                activations.sort_by(|a, b| match b.salience.cmp(&a.salience) {
268                    Ordering::Equal => b.created_at.cmp(&a.created_at),
269                    other => other,
270                });
271            }
272            ConflictResolutionStrategy::Simplicity => {
273                // Simpler rules (fewer conditions) first
274                activations.sort_by(|a, b| match a.condition_count.cmp(&b.condition_count) {
275                    Ordering::Equal => b.created_at.cmp(&a.created_at),
276                    other => other,
277                });
278            }
279            ConflictResolutionStrategy::Complexity => {
280                // More complex rules (more conditions) first
281                activations.sort_by(|a, b| match b.condition_count.cmp(&a.condition_count) {
282                    Ordering::Equal => b.created_at.cmp(&a.created_at),
283                    other => other,
284                });
285            }
286            ConflictResolutionStrategy::Random => {
287                // Random ordering using stdlib hash-based randomization
288                // Use addresses as pseudo-random source for deterministic tests
289                use std::collections::hash_map::RandomState;
290                use std::hash::{BuildHasher, Hash, Hasher};
291
292                let hasher_builder = RandomState::new();
293                activations.sort_by_cached_key(|a| {
294                    let mut hasher = hasher_builder.build_hasher();
295                    a.rule_name.hash(&mut hasher);
296                    a.created_at.hash(&mut hasher);
297                    hasher.finish()
298                });
299            }
300        }
301    }
302
303    /// Add an activation to the agenda
304    pub fn add_activation(&mut self, mut activation: Activation) {
305        // Auto-focus: switch to this agenda group if requested
306        if activation.auto_focus && activation.agenda_group != self.focus {
307            self.set_focus(activation.agenda_group.clone());
308        }
309
310        // Check activation group: if group already fired, skip
311        if let Some(ref group) = activation.activation_group {
312            if self.fired_activation_groups.contains(group) {
313                return; // Skip this activation
314            }
315        }
316
317        // Check ruleflow group: if not active, skip
318        if let Some(ref group) = activation.ruleflow_group {
319            if !self.active_ruleflow_groups.contains(group) {
320                return; // Skip this activation
321            }
322        }
323
324        // Assign ID
325        activation.id = self.next_id;
326        self.next_id += 1;
327
328        // Add to appropriate agenda group
329        self.activations
330            .entry(activation.agenda_group.clone())
331            .or_default()
332            .push(activation);
333    }
334
335    /// Get the next activation to fire (from current focus)
336    pub fn get_next_activation(&mut self) -> Option<Activation> {
337        loop {
338            // Try to get from current focus
339            if let Some(heap) = self.activations.get_mut(&self.focus) {
340                while let Some(activation) = heap.pop() {
341                    // Check no-loop
342                    if activation.no_loop && self.fired_rules.contains(&activation.rule_name) {
343                        continue;
344                    }
345
346                    // Check lock-on-active
347                    if activation.lock_on_active
348                        && self.locked_groups.contains(&activation.agenda_group)
349                    {
350                        continue;
351                    }
352
353                    // Check activation group
354                    if let Some(ref group) = activation.activation_group {
355                        if self.fired_activation_groups.contains(group) {
356                            continue;
357                        }
358                    }
359
360                    return Some(activation);
361                }
362            }
363
364            // No more activations in current focus, try to pop focus stack
365            if let Some(prev_focus) = self.focus_stack.pop() {
366                self.focus = prev_focus;
367            } else {
368                return None; // Agenda is empty
369            }
370        }
371    }
372
373    /// Mark a rule as fired
374    pub fn mark_rule_fired(&mut self, activation: &Activation) {
375        self.fired_rules.insert(activation.rule_name.clone());
376
377        // If has activation group, mark group as fired (no other rules in group can fire)
378        if let Some(ref group) = activation.activation_group {
379            self.fired_activation_groups.insert(group.clone());
380        }
381
382        // Lock the agenda group if lock-on-active
383        if activation.lock_on_active {
384            self.locked_groups.insert(activation.agenda_group.clone());
385        }
386    }
387
388    /// Check if a rule has already fired
389    pub fn has_fired(&self, rule_name: &str) -> bool {
390        self.fired_rules.contains(rule_name)
391    }
392
393    /// Set focus to a specific agenda group
394    pub fn set_focus(&mut self, group: String) {
395        if group != self.focus {
396            self.focus_stack.push(self.focus.clone());
397            self.focus = group;
398        }
399    }
400
401    /// Get current focus
402    pub fn get_focus(&self) -> &str {
403        &self.focus
404    }
405
406    /// Clear all agenda groups
407    pub fn clear(&mut self) {
408        self.activations.clear();
409        self.activations
410            .insert("MAIN".to_string(), BinaryHeap::new());
411        self.focus = "MAIN".to_string();
412        self.focus_stack.clear();
413        self.fired_rules.clear();
414        self.fired_activation_groups.clear();
415        self.locked_groups.clear();
416    }
417
418    /// Reset fired flags (for re-evaluation)
419    pub fn reset_fired_flags(&mut self) {
420        self.fired_rules.clear();
421        self.fired_activation_groups.clear();
422        self.locked_groups.clear();
423    }
424
425    /// Activate a ruleflow group (make rules in this group eligible to fire)
426    pub fn activate_ruleflow_group(&mut self, group: String) {
427        self.active_ruleflow_groups.insert(group);
428    }
429
430    /// Deactivate a ruleflow group
431    pub fn deactivate_ruleflow_group(&mut self, group: &str) {
432        self.active_ruleflow_groups.remove(group);
433    }
434
435    /// Check if ruleflow group is active
436    pub fn is_ruleflow_group_active(&self, group: &str) -> bool {
437        self.active_ruleflow_groups.contains(group)
438    }
439
440    /// Get agenda statistics
441    pub fn stats(&self) -> AgendaStats {
442        let total_activations: usize = self.activations.values().map(|heap| heap.len()).sum();
443        let groups = self.activations.len();
444
445        AgendaStats {
446            total_activations,
447            groups,
448            focus: self.focus.clone(),
449            fired_rules: self.fired_rules.len(),
450            fired_activation_groups: self.fired_activation_groups.len(),
451            active_ruleflow_groups: self.active_ruleflow_groups.len(),
452        }
453    }
454}
455
456impl Default for AdvancedAgenda {
457    fn default() -> Self {
458        Self::new()
459    }
460}
461
462/// Agenda statistics
463#[derive(Debug, Clone)]
464pub struct AgendaStats {
465    pub total_activations: usize,
466    pub groups: usize,
467    pub focus: String,
468    pub fired_rules: usize,
469    pub fired_activation_groups: usize,
470    pub active_ruleflow_groups: usize,
471}
472
473impl std::fmt::Display for AgendaStats {
474    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
475        write!(
476            f,
477            "Agenda Stats: {} activations, {} groups, focus='{}', {} fired rules",
478            self.total_activations, self.groups, self.focus, self.fired_rules
479        )
480    }
481}
482
483#[cfg(test)]
484mod tests {
485    use super::*;
486
487    #[test]
488    fn test_basic_activation() {
489        let mut agenda = AdvancedAgenda::new();
490
491        let act1 = Activation::new("Rule1".to_string(), 10);
492        let act2 = Activation::new("Rule2".to_string(), 20);
493
494        agenda.add_activation(act1);
495        agenda.add_activation(act2);
496
497        // Higher salience fires first
498        let next = agenda.get_next_activation().unwrap();
499        assert_eq!(next.rule_name, "Rule2");
500    }
501
502    #[test]
503    fn test_activation_groups() {
504        let mut agenda = AdvancedAgenda::new();
505
506        let act1 =
507            Activation::new("Rule1".to_string(), 10).with_activation_group("group1".to_string());
508        let act2 =
509            Activation::new("Rule2".to_string(), 20).with_activation_group("group1".to_string());
510
511        agenda.add_activation(act1);
512        agenda.add_activation(act2);
513
514        // First activation fires
515        let first = agenda.get_next_activation().unwrap();
516        agenda.mark_rule_fired(&first);
517
518        // Second activation should be skipped (same group)
519        let second = agenda.get_next_activation();
520        assert!(second.is_none());
521    }
522
523    #[test]
524    fn test_agenda_groups() {
525        let mut agenda = AdvancedAgenda::new();
526
527        let act1 =
528            Activation::new("Rule1".to_string(), 10).with_agenda_group("group_a".to_string());
529        let act2 =
530            Activation::new("Rule2".to_string(), 20).with_agenda_group("group_b".to_string());
531
532        agenda.add_activation(act1);
533        agenda.add_activation(act2);
534
535        // MAIN is empty, nothing fires
536        assert!(agenda.get_next_activation().is_none());
537
538        // Set focus to group_a
539        agenda.set_focus("group_a".to_string());
540        let next = agenda.get_next_activation().unwrap();
541        assert_eq!(next.rule_name, "Rule1");
542    }
543
544    #[test]
545    fn test_auto_focus() {
546        let mut agenda = AdvancedAgenda::new();
547
548        let act = Activation::new("Rule1".to_string(), 10)
549            .with_agenda_group("special".to_string())
550            .with_auto_focus(true);
551
552        agenda.add_activation(act);
553
554        // Auto-focus should switch to "special"
555        assert_eq!(agenda.get_focus(), "special");
556    }
557
558    #[test]
559    fn test_ruleflow_groups() {
560        let mut agenda = AdvancedAgenda::new();
561
562        let act = Activation::new("Rule1".to_string(), 10).with_ruleflow_group("flow1".to_string());
563
564        // Without activating ruleflow group, activation is not added
565        agenda.add_activation(act.clone());
566        assert_eq!(agenda.stats().total_activations, 0);
567
568        // Activate ruleflow group
569        agenda.activate_ruleflow_group("flow1".to_string());
570        agenda.add_activation(act);
571        assert_eq!(agenda.stats().total_activations, 1);
572    }
573}