pylitmus 1.1.0__py3-none-any.whl → 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pylitmus/__init__.py CHANGED
@@ -28,7 +28,7 @@ from .storage import (
28
28
  from .strategies import MaxStrategy, ScoringStrategy, SumStrategy, WeightedStrategy
29
29
  from .types import AssessmentResult, DecisionTier, Operator, Rule, RuleResult, Severity
30
30
 
31
- __version__ = "1.1.0"
31
+ __version__ = "1.2.0"
32
32
 
33
33
  __all__ = [
34
34
  # Main
pylitmus/engine.py CHANGED
@@ -10,6 +10,7 @@ from .exceptions import EvaluationError
10
10
  from .types import AssessmentResult, DecisionTier, Rule, RuleResult
11
11
 
12
12
  if TYPE_CHECKING:
13
+ from .rete import RETENetwork, RuleCompiler
13
14
  from .storage.base import RuleRepository
14
15
  from .strategies.base import ScoringStrategy
15
16
 
@@ -41,6 +42,13 @@ class RuleEngine:
41
42
  ]
42
43
  )
43
44
 
45
+ # With RETE algorithm for optimized evaluation (recommended for 50+ rules)
46
+ engine = RuleEngine(
47
+ repository=repo,
48
+ scoring_strategy=WeightedStrategy(),
49
+ use_rete=True # Enable RETE optimization
50
+ )
51
+
44
52
  result = engine.evaluate(claim, context)
45
53
  """
46
54
 
@@ -50,6 +58,7 @@ class RuleEngine:
50
58
  scoring_strategy: Optional["ScoringStrategy"] = None,
51
59
  decision_tiers: Optional[List[DecisionTier]] = None,
52
60
  condition_builder: Optional[Any] = None,
61
+ use_rete: bool = False,
53
62
  ):
54
63
  """
55
64
  Initialize the rule engine.
@@ -60,6 +69,9 @@ class RuleEngine:
60
69
  decision_tiers: User-defined decision tiers with score ranges.
61
70
  If not provided, decision will be None.
62
71
  condition_builder: Builder for creating conditions from dicts
72
+ use_rete: Enable RETE algorithm for optimized rule evaluation.
73
+ Recommended for rule sets with 50+ rules or significant
74
+ condition sharing. Default is False (use standard evaluation).
63
75
  """
64
76
  self.repository = repository
65
77
  self._scoring_strategy = scoring_strategy
@@ -67,7 +79,26 @@ class RuleEngine:
67
79
  self._condition_builder = condition_builder
68
80
  self._rules_cache: Optional[List[Rule]] = None
69
81
 
70
- logger.info("RuleEngine initialized")
82
+ # RETE configuration
83
+ self._use_rete = use_rete
84
+ self._rete_network: Optional["RETENetwork"] = None
85
+ self._rete_compiled = False
86
+
87
+ logger.info(f"RuleEngine initialized (use_rete={use_rete})")
88
+
89
+ @property
90
+ def use_rete(self) -> bool:
91
+ """Check if RETE algorithm is enabled."""
92
+ return self._use_rete
93
+
94
+ @use_rete.setter
95
+ def use_rete(self, value: bool) -> None:
96
+ """Enable or disable RETE algorithm."""
97
+ if value != self._use_rete:
98
+ self._use_rete = value
99
+ self._rete_network = None
100
+ self._rete_compiled = False
101
+ logger.info(f"RETE algorithm {'enabled' if value else 'disabled'}")
71
102
 
72
103
  @property
73
104
  def scoring_strategy(self) -> "ScoringStrategy":
@@ -87,6 +118,30 @@ class RuleEngine:
87
118
  self._condition_builder = ConditionBuilder()
88
119
  return self._condition_builder
89
120
 
121
+ def _ensure_rete_compiled(self, filters: Optional[Dict[str, Any]] = None) -> None:
122
+ """
123
+ Ensure the RETE network is compiled with current rules.
124
+
125
+ Args:
126
+ filters: Filters to apply when fetching rules
127
+ """
128
+ if self._rete_compiled and self._rete_network is not None:
129
+ return
130
+
131
+ from .rete import RuleCompiler
132
+
133
+ rules = self.repository.get_enabled(filters)
134
+ compiler = RuleCompiler()
135
+ self._rete_network = compiler.compile(rules)
136
+ self._rete_compiled = True
137
+
138
+ stats = self._rete_network.get_stats()
139
+ logger.info(
140
+ f"RETE network compiled: {stats['alpha_nodes']} alpha nodes, "
141
+ f"{stats['shared_conditions']} shared conditions, "
142
+ f"{stats['terminal_nodes']} rules"
143
+ )
144
+
90
145
  def evaluate(
91
146
  self,
92
147
  data: Dict[str, Any],
@@ -96,6 +151,104 @@ class RuleEngine:
96
151
  """
97
152
  Evaluate data against all applicable rules.
98
153
 
154
+ Args:
155
+ data: Data to evaluate
156
+ context: Additional context for evaluation
157
+ filters: Filters to apply when fetching rules
158
+
159
+ Returns:
160
+ AssessmentResult with score, decision, and triggered rules
161
+ """
162
+ if self._use_rete:
163
+ return self._evaluate_with_rete(data, context, filters)
164
+ else:
165
+ return self._evaluate_standard(data, context, filters)
166
+
167
+ def _evaluate_with_rete(
168
+ self,
169
+ data: Dict[str, Any],
170
+ context: Optional[Dict[str, Any]] = None,
171
+ filters: Optional[Dict[str, Any]] = None,
172
+ ) -> AssessmentResult:
173
+ """
174
+ Evaluate data using the RETE algorithm.
175
+
176
+ Args:
177
+ data: Data to evaluate
178
+ context: Additional context for evaluation
179
+ filters: Filters to apply when fetching rules
180
+
181
+ Returns:
182
+ AssessmentResult with score, decision, and triggered rules
183
+ """
184
+ start_time = time.time()
185
+ context = context or {}
186
+
187
+ logger.debug(f"Starting RETE evaluation with {len(data)} data fields")
188
+
189
+ # Ensure RETE network is compiled
190
+ self._ensure_rete_compiled(filters)
191
+
192
+ try:
193
+ # Evaluate through RETE network
194
+ triggered_results = self._rete_network.evaluate(data)
195
+
196
+ # Convert severity strings back to Severity enum for consistency
197
+ from .types import Severity
198
+
199
+ for result in triggered_results:
200
+ if isinstance(result.severity, str):
201
+ try:
202
+ result.severity = Severity(result.severity)
203
+ except ValueError:
204
+ pass # Keep as string if not a valid Severity
205
+
206
+ except Exception as e:
207
+ logger.error(f"Error in RETE evaluation: {e}")
208
+ raise EvaluationError(f"RETE evaluation failed: {e}") from e
209
+
210
+ # Calculate total score using scoring strategy
211
+ total_score = self.scoring_strategy.calculate(triggered_results)
212
+
213
+ # Determine decision
214
+ decision = self._make_decision(total_score)
215
+
216
+ processing_time = (time.time() - start_time) * 1000
217
+
218
+ # Get total rules count from network stats
219
+ stats = self._rete_network.get_stats()
220
+ total_rules = stats["terminal_nodes"]
221
+
222
+ result = AssessmentResult(
223
+ total_score=total_score,
224
+ decision=decision,
225
+ triggered_rules=triggered_results,
226
+ all_rules_evaluated=total_rules,
227
+ processing_time_ms=processing_time,
228
+ metadata={
229
+ "context": context,
230
+ "filters": filters,
231
+ "evaluation_method": "rete",
232
+ "rete_stats": stats,
233
+ },
234
+ )
235
+
236
+ logger.info(
237
+ f"RETE evaluation complete: score={total_score}, decision={decision}, "
238
+ f"triggered={len(triggered_results)}/{total_rules}, time={processing_time:.2f}ms"
239
+ )
240
+
241
+ return result
242
+
243
+ def _evaluate_standard(
244
+ self,
245
+ data: Dict[str, Any],
246
+ context: Optional[Dict[str, Any]] = None,
247
+ filters: Optional[Dict[str, Any]] = None,
248
+ ) -> AssessmentResult:
249
+ """
250
+ Evaluate data using standard (non-RETE) algorithm.
251
+
99
252
  Args:
100
253
  data: Data to evaluate
101
254
  context: Additional context for evaluation
@@ -107,7 +260,7 @@ class RuleEngine:
107
260
  start_time = time.time()
108
261
  context = context or {}
109
262
 
110
- logger.debug(f"Starting evaluation with {len(data)} data fields")
263
+ logger.debug(f"Starting standard evaluation with {len(data)} data fields")
111
264
 
112
265
  # Get applicable rules
113
266
  rules = self.repository.get_enabled(filters)
@@ -146,11 +299,15 @@ class RuleEngine:
146
299
  triggered_rules=triggered_results,
147
300
  all_rules_evaluated=len(rules),
148
301
  processing_time_ms=processing_time,
149
- metadata={"context": context, "filters": filters},
302
+ metadata={
303
+ "context": context,
304
+ "filters": filters,
305
+ "evaluation_method": "standard",
306
+ },
150
307
  )
151
308
 
152
309
  logger.info(
153
- f"Evaluation complete: score={total_score}, decision={decision}, "
310
+ f"Standard evaluation complete: score={total_score}, decision={decision}, "
154
311
  f"triggered={len(triggered_results)}/{len(rules)}, time={processing_time:.2f}ms"
155
312
  )
156
313
 
@@ -224,6 +381,10 @@ class RuleEngine:
224
381
  """Force reload rules from repository."""
225
382
  self._rules_cache = None
226
383
 
384
+ # Invalidate RETE network to force recompilation
385
+ self._rete_network = None
386
+ self._rete_compiled = False
387
+
227
388
  # If repository has cache, invalidate it
228
389
  if hasattr(self.repository, "invalidate"):
229
390
  self.repository.invalidate()
@@ -253,3 +414,14 @@ class RuleEngine:
253
414
  Rule if found, None otherwise
254
415
  """
255
416
  return self.repository.get_by_code(code)
417
+
418
+ def get_rete_stats(self) -> Optional[Dict[str, int]]:
419
+ """
420
+ Get RETE network statistics.
421
+
422
+ Returns:
423
+ Dictionary with stats if RETE is enabled and compiled, None otherwise
424
+ """
425
+ if self._rete_network is not None:
426
+ return self._rete_network.get_stats()
427
+ return None
pylitmus/factory.py CHANGED
@@ -36,6 +36,8 @@ def create_engine(
36
36
  scoring_strategy: Union[str, ScoringStrategy] = "sum",
37
37
  # Decision
38
38
  decision_tiers: Optional[List[DecisionTier]] = None,
39
+ # RETE optimization
40
+ use_rete: bool = False,
39
41
  ) -> RuleEngine:
40
42
  """
41
43
  Create a configured RuleEngine instance.
@@ -55,6 +57,9 @@ def create_engine(
55
57
  scoring_strategy: 'sum', 'weighted', 'max', or ScoringStrategy instance
56
58
  decision_tiers: User-defined decision tiers with score ranges.
57
59
  If not provided, decision will be None in results.
60
+ use_rete: Enable RETE algorithm for optimized rule evaluation.
61
+ Recommended for rule sets with 50+ rules or significant
62
+ condition sharing across rules. Default is False.
58
63
 
59
64
  Returns:
60
65
  Configured RuleEngine instance
@@ -92,6 +97,13 @@ def create_engine(
92
97
  DecisionTier("AUTO_REJECT", 95, 101, "Very high risk"),
93
98
  ]
94
99
  )
100
+
101
+ # With RETE algorithm optimization (for large rule sets)
102
+ engine = create_engine(
103
+ storage_backend='database',
104
+ database_url='postgresql://localhost/mydb',
105
+ use_rete=True # Enable RETE for performance
106
+ )
95
107
  """
96
108
 
97
109
  # Build or use provided repository
@@ -122,6 +134,7 @@ def create_engine(
122
134
  repository=repo,
123
135
  scoring_strategy=strategy,
124
136
  decision_tiers=decision_tiers,
137
+ use_rete=use_rete,
125
138
  )
126
139
 
127
140
 
@@ -2,11 +2,12 @@
2
2
  Flask extension for cmap-rules-engine.
3
3
  """
4
4
 
5
- from typing import Any, Dict, Optional
5
+ from typing import Any, Dict, List, Optional
6
6
 
7
7
  from ...engine import RuleEngine
8
8
  from ...storage import CachedRuleRepository, InMemoryRuleRepository
9
9
  from ...strategies import MaxStrategy, SumStrategy, WeightedStrategy
10
+ from ...types import DecisionTier
10
11
 
11
12
  try:
12
13
  from flask import Flask, current_app
@@ -83,6 +84,47 @@ def create_strategy(config: Dict[str, Any]) -> Any:
83
84
  return strategies[strategy_name]()
84
85
 
85
86
 
87
+ def create_decision_tiers(config: Dict[str, Any]) -> Optional[List[DecisionTier]]:
88
+ """
89
+ Create decision tiers from Flask config.
90
+
91
+ Supports two formats:
92
+ 1. Legacy dict format: {"approve": 30, "review": 70}
93
+ - Creates APPROVE (0-30), REVIEW (30-70), FLAG (70-101) tiers
94
+ 2. List format: [DecisionTier(...), ...]
95
+
96
+ Args:
97
+ config: Flask app configuration
98
+
99
+ Returns:
100
+ List of DecisionTier objects or None
101
+ """
102
+ thresholds = config.get("CMAP_RULES_THRESHOLDS")
103
+ decision_tiers = config.get("CMAP_RULES_DECISION_TIERS")
104
+
105
+ # If explicit decision tiers are provided, use them
106
+ if decision_tiers is not None:
107
+ return decision_tiers
108
+
109
+ # Convert legacy thresholds format to decision tiers
110
+ if thresholds is not None:
111
+ approve_threshold = thresholds.get("approve", 30)
112
+ review_threshold = thresholds.get("review", 70)
113
+
114
+ return [
115
+ DecisionTier("APPROVE", 0, approve_threshold),
116
+ DecisionTier("REVIEW", approve_threshold, review_threshold),
117
+ DecisionTier("FLAG", review_threshold, 101),
118
+ ]
119
+
120
+ # Default decision tiers
121
+ return [
122
+ DecisionTier("APPROVE", 0, 30),
123
+ DecisionTier("REVIEW", 30, 70),
124
+ DecisionTier("FLAG", 70, 101),
125
+ ]
126
+
127
+
86
128
  class CmapRulesEngine:
87
129
  """
88
130
  Flask extension for cmap-rules-engine.
@@ -119,7 +161,7 @@ class CmapRulesEngine:
119
161
  if not HAS_FLASK:
120
162
  raise ImportError(
121
163
  "Flask is required for the Flask integration. "
122
- "Install it with: pip install cmap-rules-engine[flask]"
164
+ "Install it with: pip install pylitmus[flask]"
123
165
  )
124
166
 
125
167
  self._engine: Optional[RuleEngine] = None
@@ -137,7 +179,7 @@ class CmapRulesEngine:
137
179
  # Store reference in extensions
138
180
  if not hasattr(app, "extensions"):
139
181
  app.extensions = {}
140
- app.extensions["cmap_rules_engine"] = self
182
+ app.extensions["pylitmus"] = self
141
183
 
142
184
  # Set default configuration
143
185
  app.config.setdefault("CMAP_RULES_STORAGE", "memory")
@@ -181,13 +223,13 @@ class CmapRulesEngine:
181
223
  # Create strategy
182
224
  strategy = create_strategy(config)
183
225
 
184
- # Get thresholds
185
- thresholds = config.get("CMAP_RULES_THRESHOLDS")
226
+ # Create decision tiers from config
227
+ decision_tiers = create_decision_tiers(config)
186
228
 
187
229
  return RuleEngine(
188
230
  repository=repository,
189
231
  scoring_strategy=strategy,
190
- decision_thresholds=thresholds,
232
+ decision_tiers=decision_tiers,
191
233
  )
192
234
 
193
235
  @property
@@ -209,7 +251,7 @@ def get_engine() -> RuleEngine:
209
251
  Get rule engine from current Flask app.
210
252
 
211
253
  Usage:
212
- from cmap_rules_engine.integrations.flask import get_engine
254
+ from pylitmus.integrations.flask import get_engine
213
255
 
214
256
  @app.route('/assess')
215
257
  def assess():
@@ -226,9 +268,9 @@ def get_engine() -> RuleEngine:
226
268
  if not HAS_FLASK:
227
269
  raise ImportError("Flask is required for the Flask integration")
228
270
 
229
- if "cmap_rules_engine" not in current_app.extensions:
271
+ if "pylitmus" not in current_app.extensions:
230
272
  raise RuntimeError(
231
273
  "CmapRulesEngine not initialized for this app. Did you call init_app()?"
232
274
  )
233
275
 
234
- return current_app.extensions["cmap_rules_engine"].engine
276
+ return current_app.extensions["pylitmus"].engine
@@ -0,0 +1,26 @@
1
+ """
2
+ RETE Algorithm Implementation for PyLitmus.
3
+
4
+ This module provides a lightweight RETE network implementation optimized
5
+ for the PyLitmus rule engine. It maintains the same user-facing API while
6
+ providing performance benefits for large rule sets through:
7
+
8
+ 1. Condition sharing - identical conditions across rules are evaluated once
9
+ 2. Alpha network - single-condition tests with memory
10
+ 3. Beta network - join nodes for combining conditions (AND/OR logic)
11
+
12
+ Usage:
13
+ from pylitmus.rete import RETENetwork, RuleCompiler
14
+
15
+ # Compile rules into RETE network
16
+ compiler = RuleCompiler()
17
+ network = compiler.compile(rules)
18
+
19
+ # Evaluate data
20
+ triggered_rules = network.evaluate(data)
21
+ """
22
+
23
+ from .compiler import RuleCompiler
24
+ from .network import RETENetwork
25
+
26
+ __all__ = ["RETENetwork", "RuleCompiler"]