Skip to main content

ai_model_vault/rag/
rules.rs

1//! Rule-based system support for RAG pipelines.
2
3use crate::error::Result;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// Rule for rule-based systems
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Rule {
10    /// Rule ID
11    pub id: String,
12
13    /// Rule name
14    pub name: String,
15
16    /// Rule conditions (key-value pairs)
17    pub conditions: HashMap<String, RuleCondition>,
18
19    /// Rule actions
20    pub actions: Vec<RuleAction>,
21
22    /// Rule priority (higher = more priority)
23    pub priority: i32,
24
25    /// Is rule enabled
26    pub enabled: bool,
27}
28
29/// Rule condition
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub enum RuleCondition {
32    /// Exact match
33    Equals(String),
34
35    /// Contains substring
36    Contains(String),
37
38    /// Matches regex pattern
39    Matches(String),
40
41    /// Numeric comparison
42    GreaterThan(f64),
43    LessThan(f64),
44
45    /// In list
46    In(Vec<String>),
47
48    /// Custom condition (serialized as string)
49    Custom(String),
50}
51
52/// Rule action
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub enum RuleAction {
55    /// Set a value
56    SetValue { key: String, value: String },
57
58    /// Add to list
59    AddToList { key: String, value: String },
60
61    /// Log message
62    Log { level: String, message: String },
63
64    /// Call function (by name)
65    CallFunction { function: String, args: Vec<String> },
66
67    /// Stop rule processing
68    Stop,
69}
70
71/// Rule engine for rule-based systems
72pub struct RuleEngine {
73    rules: Vec<Rule>,
74    context: HashMap<String, String>,
75}
76
77impl RuleEngine {
78    /// Create a new rule engine
79    pub fn new() -> Self {
80        Self {
81            rules: Vec::new(),
82            context: HashMap::new(),
83        }
84    }
85
86    /// Add a rule
87    pub fn add_rule(&mut self, rule: Rule) {
88        self.rules.push(rule);
89        // Sort by priority (descending)
90        self.rules.sort_by_key(|r| std::cmp::Reverse(r.priority));
91    }
92
93    /// Set context value
94    pub fn set_context(&mut self, key: String, value: String) {
95        self.context.insert(key, value);
96    }
97
98    /// Get context value
99    pub fn get_context(&self, key: &str) -> Option<&String> {
100        self.context.get(key)
101    }
102
103    /// Evaluate a rule condition
104    fn evaluate_condition(&self, key: &str, condition: &RuleCondition) -> bool {
105        let value = match self.context.get(key) {
106            Some(v) => v,
107            None => return false,
108        };
109
110        match condition {
111            RuleCondition::Equals(expected) => value == expected,
112            RuleCondition::Contains(substring) => value.contains(substring),
113            RuleCondition::Matches(pattern) => {
114                // Simple pattern matching (could use regex crate for more complex patterns)
115                value.contains(pattern)
116            }
117            RuleCondition::GreaterThan(threshold) => value
118                .parse::<f64>()
119                .map(|v| v > *threshold)
120                .unwrap_or(false),
121            RuleCondition::LessThan(threshold) => value
122                .parse::<f64>()
123                .map(|v| v < *threshold)
124                .unwrap_or(false),
125            RuleCondition::In(list) => list.contains(value),
126            RuleCondition::Custom(_) => {
127                // Custom conditions would need specialized handling
128                false
129            }
130        }
131    }
132
133    /// Execute rule actions
134    fn execute_actions(&mut self, actions: &[RuleAction]) -> Result<bool> {
135        for action in actions {
136            match action {
137                RuleAction::SetValue { key, value } => {
138                    self.context.insert(key.clone(), value.clone());
139                }
140                RuleAction::AddToList { key, value } => {
141                    let current = self.context.get(key).cloned().unwrap_or_default();
142                    let new_value = if current.is_empty() {
143                        value.clone()
144                    } else {
145                        format!("{},{}", current, value)
146                    };
147                    self.context.insert(key.clone(), new_value);
148                }
149                RuleAction::Log {
150                    level: _,
151                    message: _,
152                } => {
153                    // Logging would be handled by tracing/logging framework
154                }
155                RuleAction::CallFunction {
156                    function: _,
157                    args: _,
158                } => {
159                    // Function calls would need to be handled by caller
160                }
161                RuleAction::Stop => {
162                    return Ok(true); // Stop processing
163                }
164            }
165        }
166        Ok(false) // Continue processing
167    }
168
169    /// Execute all matching rules
170    pub fn execute(&mut self) -> Result<Vec<String>> {
171        let mut executed_rules = Vec::new();
172
173        for rule in &self.rules.clone() {
174            if !rule.enabled {
175                continue;
176            }
177
178            // Check all conditions
179            let all_conditions_met = rule
180                .conditions
181                .iter()
182                .all(|(key, condition)| self.evaluate_condition(key, condition));
183
184            if all_conditions_met {
185                executed_rules.push(rule.id.clone());
186                let should_stop = self.execute_actions(&rule.actions)?;
187                if should_stop {
188                    break;
189                }
190            }
191        }
192
193        Ok(executed_rules)
194    }
195
196    /// Clear all rules
197    pub fn clear_rules(&mut self) {
198        self.rules.clear();
199    }
200
201    /// Get all rules
202    pub fn get_rules(&self) -> &[Rule] {
203        &self.rules
204    }
205}
206
207impl Default for RuleEngine {
208    fn default() -> Self {
209        Self::new()
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    fn make_rule(id: &str, key: &str, cond: RuleCondition, actions: Vec<RuleAction>) -> Rule {
218        let mut conditions = HashMap::new();
219        conditions.insert(key.to_string(), cond);
220        Rule {
221            id: id.to_string(),
222            name: format!("rule_{}", id),
223            conditions,
224            actions,
225            priority: 0,
226            enabled: true,
227        }
228    }
229
230    #[test]
231    fn test_rule_engine_default() {
232        let engine = RuleEngine::default();
233        assert!(engine.get_rules().is_empty());
234    }
235
236    #[test]
237    fn test_rule_engine_set_get_context() {
238        let mut engine = RuleEngine::new();
239        engine.set_context("key".to_string(), "value".to_string());
240        assert_eq!(engine.get_context("key"), Some(&"value".to_string()));
241        assert_eq!(engine.get_context("missing"), None);
242    }
243
244    #[test]
245    fn test_evaluate_condition_equals() {
246        let mut engine = RuleEngine::new();
247        engine.set_context("status".to_string(), "active".to_string());
248
249        let rule = make_rule(
250            "r1",
251            "status",
252            RuleCondition::Equals("active".to_string()),
253            vec![RuleAction::SetValue {
254                key: "result".to_string(),
255                value: "matched".to_string(),
256            }],
257        );
258        engine.add_rule(rule);
259        let executed = engine.execute().unwrap();
260        assert_eq!(executed, vec!["r1"]);
261        assert_eq!(engine.get_context("result"), Some(&"matched".to_string()));
262    }
263
264    #[test]
265    fn test_evaluate_condition_contains() {
266        let mut engine = RuleEngine::new();
267        engine.set_context("text".to_string(), "hello world".to_string());
268
269        let rule = make_rule(
270            "r1",
271            "text",
272            RuleCondition::Contains("world".to_string()),
273            vec![],
274        );
275        engine.add_rule(rule);
276        let executed = engine.execute().unwrap();
277        assert_eq!(executed, vec!["r1"]);
278    }
279
280    #[test]
281    fn test_evaluate_condition_matches() {
282        let mut engine = RuleEngine::new();
283        engine.set_context("name".to_string(), "test-model-v2".to_string());
284
285        let rule = make_rule(
286            "r1",
287            "name",
288            RuleCondition::Matches("model".to_string()),
289            vec![],
290        );
291        engine.add_rule(rule);
292        assert_eq!(engine.execute().unwrap(), vec!["r1"]);
293    }
294
295    #[test]
296    fn test_evaluate_condition_greater_than() {
297        let mut engine = RuleEngine::new();
298        engine.set_context("score".to_string(), "85.5".to_string());
299
300        let rule = make_rule("r1", "score", RuleCondition::GreaterThan(80.0), vec![]);
301        engine.add_rule(rule);
302        assert_eq!(engine.execute().unwrap(), vec!["r1"]);
303    }
304
305    #[test]
306    fn test_evaluate_condition_less_than() {
307        let mut engine = RuleEngine::new();
308        engine.set_context("latency".to_string(), "50".to_string());
309
310        let rule = make_rule("r1", "latency", RuleCondition::LessThan(100.0), vec![]);
311        engine.add_rule(rule);
312        assert_eq!(engine.execute().unwrap(), vec!["r1"]);
313    }
314
315    #[test]
316    fn test_evaluate_condition_in_list() {
317        let mut engine = RuleEngine::new();
318        engine.set_context("format".to_string(), "onnx".to_string());
319
320        let rule = make_rule(
321            "r1",
322            "format",
323            RuleCondition::In(vec![
324                "safetensors".to_string(),
325                "onnx".to_string(),
326                "gguf".to_string(),
327            ]),
328            vec![],
329        );
330        engine.add_rule(rule);
331        assert_eq!(engine.execute().unwrap(), vec!["r1"]);
332    }
333
334    #[test]
335    fn test_evaluate_condition_custom() {
336        let mut engine = RuleEngine::new();
337        engine.set_context("x".to_string(), "y".to_string());
338
339        let rule = make_rule(
340            "r1",
341            "x",
342            RuleCondition::Custom("custom_fn".to_string()),
343            vec![],
344        );
345        engine.add_rule(rule);
346        // Custom always returns false
347        assert!(engine.execute().unwrap().is_empty());
348    }
349
350    #[test]
351    fn test_action_add_to_list() {
352        let mut engine = RuleEngine::new();
353        engine.set_context("status".to_string(), "go".to_string());
354
355        let rule = make_rule(
356            "r1",
357            "status",
358            RuleCondition::Equals("go".to_string()),
359            vec![
360                RuleAction::AddToList {
361                    key: "log".to_string(),
362                    value: "first".to_string(),
363                },
364                RuleAction::AddToList {
365                    key: "log".to_string(),
366                    value: "second".to_string(),
367                },
368            ],
369        );
370        engine.add_rule(rule);
371        engine.execute().unwrap();
372        assert_eq!(engine.get_context("log"), Some(&"first,second".to_string()));
373    }
374
375    #[test]
376    fn test_action_log_and_call_function() {
377        let mut engine = RuleEngine::new();
378        engine.set_context("x".to_string(), "y".to_string());
379
380        let rule = make_rule(
381            "r1",
382            "x",
383            RuleCondition::Equals("y".to_string()),
384            vec![
385                RuleAction::Log {
386                    level: "info".to_string(),
387                    message: "matched".to_string(),
388                },
389                RuleAction::CallFunction {
390                    function: "noop".to_string(),
391                    args: vec![],
392                },
393            ],
394        );
395        engine.add_rule(rule);
396        assert_eq!(engine.execute().unwrap(), vec!["r1"]);
397    }
398
399    #[test]
400    fn test_action_stop() {
401        let mut engine = RuleEngine::new();
402        engine.set_context("a".to_string(), "1".to_string());
403
404        let mut rule1 = make_rule(
405            "r1",
406            "a",
407            RuleCondition::Equals("1".to_string()),
408            vec![RuleAction::Stop],
409        );
410        rule1.priority = 10;
411
412        let mut rule2 = make_rule("r2", "a", RuleCondition::Equals("1".to_string()), vec![]);
413        rule2.priority = 5;
414
415        engine.add_rule(rule1);
416        engine.add_rule(rule2);
417
418        // Only r1 should execute since it has Stop
419        let executed = engine.execute().unwrap();
420        assert_eq!(executed, vec!["r1"]);
421    }
422
423    #[test]
424    fn test_disabled_rule_skipped() {
425        let mut engine = RuleEngine::new();
426        engine.set_context("x".to_string(), "y".to_string());
427
428        let mut rule = make_rule("r1", "x", RuleCondition::Equals("y".to_string()), vec![]);
429        rule.enabled = false;
430        engine.add_rule(rule);
431
432        assert!(engine.execute().unwrap().is_empty());
433    }
434
435    #[test]
436    fn test_clear_rules() {
437        let mut engine = RuleEngine::new();
438        engine.add_rule(make_rule(
439            "r1",
440            "x",
441            RuleCondition::Equals("y".to_string()),
442            vec![],
443        ));
444        engine.clear_rules();
445        assert!(engine.get_rules().is_empty());
446    }
447
448    #[test]
449    fn test_missing_context_key() {
450        let mut engine = RuleEngine::new();
451        // No context set — condition on "missing_key" should fail
452        let rule = make_rule(
453            "r1",
454            "missing_key",
455            RuleCondition::Equals("val".to_string()),
456            vec![],
457        );
458        engine.add_rule(rule);
459        assert!(engine.execute().unwrap().is_empty());
460    }
461
462    #[test]
463    fn test_priority_ordering() {
464        let mut engine = RuleEngine::new();
465        engine.set_context("x".to_string(), "1".to_string());
466
467        let mut r1 = make_rule("low", "x", RuleCondition::Equals("1".to_string()), vec![]);
468        r1.priority = 1;
469        let mut r2 = make_rule("high", "x", RuleCondition::Equals("1".to_string()), vec![]);
470        r2.priority = 10;
471
472        engine.add_rule(r1);
473        engine.add_rule(r2);
474
475        let rules = engine.get_rules();
476        assert_eq!(rules[0].id, "high");
477        assert_eq!(rules[1].id, "low");
478    }
479}