pylitmus 1.0.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 +3 -2
- pylitmus/engine.py +207 -24
- pylitmus/factory.py +29 -7
- pylitmus/integrations/flask/extension.py +51 -9
- pylitmus/rete/__init__.py +26 -0
- pylitmus/rete/compiler.py +356 -0
- pylitmus/rete/network.py +269 -0
- pylitmus/rete/nodes.py +318 -0
- pylitmus/types.py +29 -3
- {pylitmus-1.0.0.dist-info → pylitmus-1.2.0.dist-info}/METADATA +1 -1
- {pylitmus-1.0.0.dist-info → pylitmus-1.2.0.dist-info}/RECORD +13 -9
- {pylitmus-1.0.0.dist-info → pylitmus-1.2.0.dist-info}/WHEEL +0 -0
- {pylitmus-1.0.0.dist-info → pylitmus-1.2.0.dist-info}/licenses/LICENSE +0 -0
pylitmus/__init__.py
CHANGED
|
@@ -26,9 +26,9 @@ from .storage import (
|
|
|
26
26
|
RuleRepository,
|
|
27
27
|
)
|
|
28
28
|
from .strategies import MaxStrategy, ScoringStrategy, SumStrategy, WeightedStrategy
|
|
29
|
-
from .types import AssessmentResult, Operator, Rule, RuleResult, Severity
|
|
29
|
+
from .types import AssessmentResult, DecisionTier, Operator, Rule, RuleResult, Severity
|
|
30
30
|
|
|
31
|
-
__version__ = "1.
|
|
31
|
+
__version__ = "1.2.0"
|
|
32
32
|
|
|
33
33
|
__all__ = [
|
|
34
34
|
# Main
|
|
@@ -40,6 +40,7 @@ __all__ = [
|
|
|
40
40
|
"Rule",
|
|
41
41
|
"RuleResult",
|
|
42
42
|
"AssessmentResult",
|
|
43
|
+
"DecisionTier",
|
|
43
44
|
"Operator",
|
|
44
45
|
"Severity",
|
|
45
46
|
# Conditions
|
pylitmus/engine.py
CHANGED
|
@@ -7,9 +7,10 @@ import time
|
|
|
7
7
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
8
8
|
|
|
9
9
|
from .exceptions import EvaluationError
|
|
10
|
-
from .types import AssessmentResult, Rule, RuleResult
|
|
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
|
|
|
@@ -21,25 +22,43 @@ class RuleEngine:
|
|
|
21
22
|
Main rules engine for evaluating data against configured rules.
|
|
22
23
|
|
|
23
24
|
Usage:
|
|
25
|
+
# Without decision tiers (returns None for decision)
|
|
24
26
|
engine = RuleEngine(
|
|
25
27
|
repository=DatabaseRuleRepository(db_url),
|
|
26
28
|
scoring_strategy=WeightedStrategy()
|
|
27
29
|
)
|
|
28
30
|
|
|
31
|
+
# With user-defined decision tiers
|
|
32
|
+
engine = RuleEngine(
|
|
33
|
+
repository=DatabaseRuleRepository(db_url),
|
|
34
|
+
scoring_strategy=WeightedStrategy(),
|
|
35
|
+
decision_tiers=[
|
|
36
|
+
DecisionTier("AUTO_APPROVE", 0, 20, "Very low risk"),
|
|
37
|
+
DecisionTier("APPROVE", 20, 40, "Low risk"),
|
|
38
|
+
DecisionTier("SOFT_REVIEW", 40, 60, "Medium risk"),
|
|
39
|
+
DecisionTier("HARD_REVIEW", 60, 80, "Higher risk"),
|
|
40
|
+
DecisionTier("FLAG", 80, 95, "High risk"),
|
|
41
|
+
DecisionTier("AUTO_REJECT", 95, 101, "Very high risk"),
|
|
42
|
+
]
|
|
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
|
+
|
|
29
52
|
result = engine.evaluate(claim, context)
|
|
30
53
|
"""
|
|
31
54
|
|
|
32
|
-
DEFAULT_THRESHOLDS = {
|
|
33
|
-
"approve": 30,
|
|
34
|
-
"review": 70,
|
|
35
|
-
}
|
|
36
|
-
|
|
37
55
|
def __init__(
|
|
38
56
|
self,
|
|
39
57
|
repository: "RuleRepository",
|
|
40
58
|
scoring_strategy: Optional["ScoringStrategy"] = None,
|
|
41
|
-
|
|
59
|
+
decision_tiers: Optional[List[DecisionTier]] = None,
|
|
42
60
|
condition_builder: Optional[Any] = None,
|
|
61
|
+
use_rete: bool = False,
|
|
43
62
|
):
|
|
44
63
|
"""
|
|
45
64
|
Initialize the rule engine.
|
|
@@ -47,16 +66,39 @@ class RuleEngine:
|
|
|
47
66
|
Args:
|
|
48
67
|
repository: Rule storage backend
|
|
49
68
|
scoring_strategy: Strategy for calculating scores
|
|
50
|
-
|
|
69
|
+
decision_tiers: User-defined decision tiers with score ranges.
|
|
70
|
+
If not provided, decision will be None.
|
|
51
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).
|
|
52
75
|
"""
|
|
53
76
|
self.repository = repository
|
|
54
77
|
self._scoring_strategy = scoring_strategy
|
|
55
|
-
self.
|
|
78
|
+
self._decision_tiers = decision_tiers
|
|
56
79
|
self._condition_builder = condition_builder
|
|
57
80
|
self._rules_cache: Optional[List[Rule]] = None
|
|
58
81
|
|
|
59
|
-
|
|
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'}")
|
|
60
102
|
|
|
61
103
|
@property
|
|
62
104
|
def scoring_strategy(self) -> "ScoringStrategy":
|
|
@@ -76,6 +118,30 @@ class RuleEngine:
|
|
|
76
118
|
self._condition_builder = ConditionBuilder()
|
|
77
119
|
return self._condition_builder
|
|
78
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
|
+
|
|
79
145
|
def evaluate(
|
|
80
146
|
self,
|
|
81
147
|
data: Dict[str, Any],
|
|
@@ -85,6 +151,104 @@ class RuleEngine:
|
|
|
85
151
|
"""
|
|
86
152
|
Evaluate data against all applicable rules.
|
|
87
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
|
+
|
|
88
252
|
Args:
|
|
89
253
|
data: Data to evaluate
|
|
90
254
|
context: Additional context for evaluation
|
|
@@ -96,7 +260,7 @@ class RuleEngine:
|
|
|
96
260
|
start_time = time.time()
|
|
97
261
|
context = context or {}
|
|
98
262
|
|
|
99
|
-
logger.debug(f"Starting evaluation with {len(data)} data fields")
|
|
263
|
+
logger.debug(f"Starting standard evaluation with {len(data)} data fields")
|
|
100
264
|
|
|
101
265
|
# Get applicable rules
|
|
102
266
|
rules = self.repository.get_enabled(filters)
|
|
@@ -135,11 +299,15 @@ class RuleEngine:
|
|
|
135
299
|
triggered_rules=triggered_results,
|
|
136
300
|
all_rules_evaluated=len(rules),
|
|
137
301
|
processing_time_ms=processing_time,
|
|
138
|
-
metadata={
|
|
302
|
+
metadata={
|
|
303
|
+
"context": context,
|
|
304
|
+
"filters": filters,
|
|
305
|
+
"evaluation_method": "standard",
|
|
306
|
+
},
|
|
139
307
|
)
|
|
140
308
|
|
|
141
309
|
logger.info(
|
|
142
|
-
f"
|
|
310
|
+
f"Standard evaluation complete: score={total_score}, decision={decision}, "
|
|
143
311
|
f"triggered={len(triggered_results)}/{len(rules)}, time={processing_time:.2f}ms"
|
|
144
312
|
)
|
|
145
313
|
|
|
@@ -189,30 +357,34 @@ class RuleEngine:
|
|
|
189
357
|
explanation=rule.description if triggered else "Condition not met",
|
|
190
358
|
)
|
|
191
359
|
|
|
192
|
-
def _make_decision(self, score: int) -> str:
|
|
360
|
+
def _make_decision(self, score: int) -> Optional[str]:
|
|
193
361
|
"""
|
|
194
|
-
Make a decision based on the total score.
|
|
362
|
+
Make a decision based on the total score and configured decision tiers.
|
|
195
363
|
|
|
196
364
|
Args:
|
|
197
365
|
score: Total calculated score
|
|
198
366
|
|
|
199
367
|
Returns:
|
|
200
|
-
Decision string
|
|
368
|
+
Decision string if a matching tier is found, None otherwise.
|
|
369
|
+
If no decision tiers are configured, returns None.
|
|
201
370
|
"""
|
|
202
|
-
|
|
203
|
-
|
|
371
|
+
if not self._decision_tiers:
|
|
372
|
+
return None
|
|
204
373
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
return "FLAG"
|
|
374
|
+
for tier in self._decision_tiers:
|
|
375
|
+
if tier.matches(score):
|
|
376
|
+
return tier.name
|
|
377
|
+
|
|
378
|
+
return None
|
|
211
379
|
|
|
212
380
|
def reload_rules(self) -> None:
|
|
213
381
|
"""Force reload rules from repository."""
|
|
214
382
|
self._rules_cache = None
|
|
215
383
|
|
|
384
|
+
# Invalidate RETE network to force recompilation
|
|
385
|
+
self._rete_network = None
|
|
386
|
+
self._rete_compiled = False
|
|
387
|
+
|
|
216
388
|
# If repository has cache, invalidate it
|
|
217
389
|
if hasattr(self.repository, "invalidate"):
|
|
218
390
|
self.repository.invalidate()
|
|
@@ -242,3 +414,14 @@ class RuleEngine:
|
|
|
242
414
|
Rule if found, None otherwise
|
|
243
415
|
"""
|
|
244
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
|
@@ -4,7 +4,7 @@ Factory functions for creating RuleEngine instances with sensible defaults.
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
from typing import TYPE_CHECKING,
|
|
7
|
+
from typing import TYPE_CHECKING, List, Optional, Union
|
|
8
8
|
|
|
9
9
|
from .engine import RuleEngine
|
|
10
10
|
from .storage import (
|
|
@@ -15,6 +15,7 @@ from .storage import (
|
|
|
15
15
|
RuleRepository,
|
|
16
16
|
)
|
|
17
17
|
from .strategies import MaxStrategy, ScoringStrategy, SumStrategy, WeightedStrategy
|
|
18
|
+
from .types import DecisionTier
|
|
18
19
|
|
|
19
20
|
if TYPE_CHECKING:
|
|
20
21
|
from .types import Rule
|
|
@@ -34,7 +35,9 @@ def create_engine(
|
|
|
34
35
|
# Scoring
|
|
35
36
|
scoring_strategy: Union[str, ScoringStrategy] = "sum",
|
|
36
37
|
# Decision
|
|
37
|
-
|
|
38
|
+
decision_tiers: Optional[List[DecisionTier]] = None,
|
|
39
|
+
# RETE optimization
|
|
40
|
+
use_rete: bool = False,
|
|
38
41
|
) -> RuleEngine:
|
|
39
42
|
"""
|
|
40
43
|
Create a configured RuleEngine instance.
|
|
@@ -52,13 +55,17 @@ def create_engine(
|
|
|
52
55
|
cache_url: Redis URL (for 'redis' cache)
|
|
53
56
|
cache_ttl: Cache TTL in seconds
|
|
54
57
|
scoring_strategy: 'sum', 'weighted', 'max', or ScoringStrategy instance
|
|
55
|
-
|
|
58
|
+
decision_tiers: User-defined decision tiers with score ranges.
|
|
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.
|
|
56
63
|
|
|
57
64
|
Returns:
|
|
58
65
|
Configured RuleEngine instance
|
|
59
66
|
|
|
60
67
|
Examples:
|
|
61
|
-
# Simple in-memory engine
|
|
68
|
+
# Simple in-memory engine (no decision tiers - decision will be None)
|
|
62
69
|
engine = create_engine()
|
|
63
70
|
|
|
64
71
|
# In-memory with predefined rules
|
|
@@ -78,10 +85,24 @@ def create_engine(
|
|
|
78
85
|
rules_file='./rules/fraud_rules.yaml'
|
|
79
86
|
)
|
|
80
87
|
|
|
81
|
-
# With
|
|
88
|
+
# With user-defined decision tiers
|
|
82
89
|
engine = create_engine(
|
|
83
90
|
scoring_strategy='weighted',
|
|
84
|
-
|
|
91
|
+
decision_tiers=[
|
|
92
|
+
DecisionTier("AUTO_APPROVE", 0, 20, "Very low risk"),
|
|
93
|
+
DecisionTier("APPROVE", 20, 40, "Low risk"),
|
|
94
|
+
DecisionTier("SOFT_REVIEW", 40, 60, "Medium risk"),
|
|
95
|
+
DecisionTier("HARD_REVIEW", 60, 80, "Higher risk"),
|
|
96
|
+
DecisionTier("FLAG", 80, 95, "High risk"),
|
|
97
|
+
DecisionTier("AUTO_REJECT", 95, 101, "Very high risk"),
|
|
98
|
+
]
|
|
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
|
|
85
106
|
)
|
|
86
107
|
"""
|
|
87
108
|
|
|
@@ -112,7 +133,8 @@ def create_engine(
|
|
|
112
133
|
return RuleEngine(
|
|
113
134
|
repository=repo,
|
|
114
135
|
scoring_strategy=strategy,
|
|
115
|
-
|
|
136
|
+
decision_tiers=decision_tiers,
|
|
137
|
+
use_rete=use_rete,
|
|
116
138
|
)
|
|
117
139
|
|
|
118
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
|
|
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["
|
|
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
|
-
#
|
|
185
|
-
|
|
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
|
-
|
|
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
|
|
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 "
|
|
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["
|
|
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"]
|