pylitmus 1.0.0__tar.gz → 1.2.0__tar.gz

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.
Files changed (65) hide show
  1. {pylitmus-1.0.0 → pylitmus-1.2.0}/PKG-INFO +1 -1
  2. {pylitmus-1.0.0 → pylitmus-1.2.0}/docs/flask-integration.md +11 -11
  3. {pylitmus-1.0.0 → pylitmus-1.2.0}/docs/quickstart.md +8 -8
  4. {pylitmus-1.0.0 → pylitmus-1.2.0}/pyproject.toml +1 -1
  5. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/__init__.py +3 -2
  6. pylitmus-1.2.0/src/pylitmus/engine.py +427 -0
  7. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/factory.py +29 -7
  8. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/integrations/flask/extension.py +51 -9
  9. pylitmus-1.2.0/src/pylitmus/rete/__init__.py +26 -0
  10. pylitmus-1.2.0/src/pylitmus/rete/compiler.py +356 -0
  11. pylitmus-1.2.0/src/pylitmus/rete/network.py +269 -0
  12. pylitmus-1.2.0/src/pylitmus/rete/nodes.py +318 -0
  13. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/types.py +29 -3
  14. pylitmus-1.2.0/tests/test_phase10_backward_compat.py +1421 -0
  15. pylitmus-1.2.0/tests/test_phase11_rete_spec.py +1154 -0
  16. {pylitmus-1.0.0 → pylitmus-1.2.0}/tests/test_phase1_core.py +50 -21
  17. {pylitmus-1.0.0 → pylitmus-1.2.0}/tests/test_phase2_conditions.py +4 -4
  18. {pylitmus-1.0.0 → pylitmus-1.2.0}/tests/test_phase3_evaluators.py +5 -5
  19. {pylitmus-1.0.0 → pylitmus-1.2.0}/tests/test_phase4_strategies.py +10 -10
  20. {pylitmus-1.0.0 → pylitmus-1.2.0}/tests/test_phase5_storage.py +4 -4
  21. {pylitmus-1.0.0 → pylitmus-1.2.0}/tests/test_phase6_patterns.py +7 -7
  22. {pylitmus-1.0.0 → pylitmus-1.2.0}/tests/test_phase7_flask.py +38 -27
  23. {pylitmus-1.0.0 → pylitmus-1.2.0}/tests/test_phase8_factory.py +51 -14
  24. pylitmus-1.2.0/tests/test_phase9_decision_tiers.py +566 -0
  25. pylitmus-1.0.0/src/pylitmus/engine.py +0 -244
  26. {pylitmus-1.0.0 → pylitmus-1.2.0}/CHANGELOG.md +0 -0
  27. {pylitmus-1.0.0 → pylitmus-1.2.0}/LICENSE +0 -0
  28. {pylitmus-1.0.0 → pylitmus-1.2.0}/README.md +0 -0
  29. {pylitmus-1.0.0 → pylitmus-1.2.0}/docs/api-reference.md +0 -0
  30. {pylitmus-1.0.0 → pylitmus-1.2.0}/docs/rules-format.md +0 -0
  31. {pylitmus-1.0.0 → pylitmus-1.2.0}/examples/basic_usage.py +0 -0
  32. {pylitmus-1.0.0 → pylitmus-1.2.0}/examples/flask_app/app.py +0 -0
  33. {pylitmus-1.0.0 → pylitmus-1.2.0}/examples/flask_app/requirements.txt +0 -0
  34. {pylitmus-1.0.0 → pylitmus-1.2.0}/examples/flask_app/rules.yaml +0 -0
  35. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/conditions/__init__.py +0 -0
  36. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/conditions/base.py +0 -0
  37. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/conditions/builder.py +0 -0
  38. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/conditions/composite.py +0 -0
  39. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/conditions/simple.py +0 -0
  40. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/evaluators/__init__.py +0 -0
  41. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/evaluators/base.py +0 -0
  42. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/evaluators/factory.py +0 -0
  43. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/exceptions.py +0 -0
  44. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/integrations/__init__.py +0 -0
  45. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/integrations/flask/__init__.py +0 -0
  46. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/patterns/__init__.py +0 -0
  47. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/patterns/base.py +0 -0
  48. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/patterns/engine.py +0 -0
  49. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/patterns/exact.py +0 -0
  50. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/patterns/fuzzy.py +0 -0
  51. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/patterns/glob.py +0 -0
  52. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/patterns/range.py +0 -0
  53. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/patterns/regex.py +0 -0
  54. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/storage/__init__.py +0 -0
  55. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/storage/base.py +0 -0
  56. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/storage/cached.py +0 -0
  57. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/storage/database.py +0 -0
  58. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/storage/file.py +0 -0
  59. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/storage/memory.py +0 -0
  60. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/strategies/__init__.py +0 -0
  61. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/strategies/base.py +0 -0
  62. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/strategies/max.py +0 -0
  63. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/strategies/sum.py +0 -0
  64. {pylitmus-1.0.0 → pylitmus-1.2.0}/src/pylitmus/strategies/weighted.py +0 -0
  65. {pylitmus-1.0.0 → pylitmus-1.2.0}/tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pylitmus
3
- Version: 1.0.0
3
+ Version: 1.2.0
4
4
  Summary: A high-performance rules engine for Python - evaluate data against configurable rules and get clear verdicts
5
5
  Project-URL: Homepage, https://github.com/yourorg/pylitmus
6
6
  Project-URL: Documentation, https://pylitmus.readthedocs.io/
@@ -1,11 +1,11 @@
1
1
  # Flask Integration Guide
2
2
 
3
- This guide covers integrating cmap-rules-engine with Flask applications.
3
+ This guide covers integrating pylitmus with Flask applications.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- pip install cmap-rules-engine[flask]
8
+ pip install pylitmus[flask]
9
9
  ```
10
10
 
11
11
  ## Basic Setup
@@ -14,7 +14,7 @@ pip install cmap-rules-engine[flask]
14
14
 
15
15
  ```python
16
16
  from flask import Flask
17
- from cmap_rules_engine.integrations.flask import CmapRulesEngine
17
+ from pylitmus.integrations.flask import CmapRulesEngine
18
18
 
19
19
  rules_engine = CmapRulesEngine()
20
20
 
@@ -35,7 +35,7 @@ def create_app():
35
35
 
36
36
  ```python
37
37
  from flask import Flask
38
- from cmap_rules_engine.integrations.flask import CmapRulesEngine
38
+ from pylitmus.integrations.flask import CmapRulesEngine
39
39
 
40
40
  app = Flask(__name__)
41
41
  app.config['CMAP_RULES_FILE'] = 'rules.yaml'
@@ -62,7 +62,7 @@ rules_engine = CmapRulesEngine(app)
62
62
 
63
63
  ```python
64
64
  from flask import Flask, request, jsonify
65
- from cmap_rules_engine.integrations.flask import CmapRulesEngine, get_engine
65
+ from pylitmus.integrations.flask import CmapRulesEngine, get_engine
66
66
 
67
67
  app = Flask(__name__)
68
68
  app.config['CMAP_RULES_FILE'] = 'rules.yaml'
@@ -98,7 +98,7 @@ def evaluate():
98
98
 
99
99
  ```python
100
100
  from flask import Blueprint, request, jsonify
101
- from cmap_rules_engine.integrations.flask import get_engine
101
+ from pylitmus.integrations.flask import get_engine
102
102
 
103
103
  api = Blueprint('api', __name__)
104
104
 
@@ -201,7 +201,7 @@ rules:
201
201
 
202
202
  ```python
203
203
  from flask import Flask, request, jsonify
204
- from cmap_rules_engine.integrations.flask import CmapRulesEngine, get_engine
204
+ from pylitmus.integrations.flask import CmapRulesEngine, get_engine
205
205
 
206
206
  def create_app(config_class='config.Config'):
207
207
  app = Flask(__name__)
@@ -326,7 +326,7 @@ curl -X POST http://localhost:5000/evaluate \
326
326
  ```python
327
327
  from flask import Flask
328
328
  from flask_sqlalchemy import SQLAlchemy
329
- from cmap_rules_engine.integrations.flask import CmapRulesEngine
329
+ from pylitmus.integrations.flask import CmapRulesEngine
330
330
 
331
331
  app = Flask(__name__)
332
332
  app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://localhost/mydb'
@@ -341,7 +341,7 @@ rules_engine = CmapRulesEngine(app)
341
341
 
342
342
  ```python
343
343
  from flask import Flask
344
- from cmap_rules_engine.integrations.flask import CmapRulesEngine
344
+ from pylitmus.integrations.flask import CmapRulesEngine
345
345
 
346
346
  app = Flask(__name__)
347
347
 
@@ -357,8 +357,8 @@ rules_engine = CmapRulesEngine(app)
357
357
 
358
358
  ```python
359
359
  from flask import Flask, jsonify
360
- from cmap_rules_engine.integrations.flask import CmapRulesEngine, get_engine
361
- from cmap_rules_engine import RuleEngineError, EvaluationError
360
+ from pylitmus.integrations.flask import CmapRulesEngine, get_engine
361
+ from pylitmus import RuleEngineError, EvaluationError
362
362
 
363
363
  app = Flask(__name__)
364
364
  rules_engine = CmapRulesEngine(app)
@@ -1,27 +1,27 @@
1
1
  # Quick Start Guide
2
2
 
3
- This guide will help you get started with cmap-rules-engine in minutes.
3
+ This guide will help you get started with pylitmus in minutes.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- pip install cmap-rules-engine
8
+ pip install pylitmus
9
9
  ```
10
10
 
11
11
  For additional features:
12
12
 
13
13
  ```bash
14
14
  # Database support (PostgreSQL, MySQL, SQLite)
15
- pip install cmap-rules-engine[database]
15
+ pip install pylitmus[database]
16
16
 
17
17
  # Redis caching
18
- pip install cmap-rules-engine[redis]
18
+ pip install pylitmus[redis]
19
19
 
20
20
  # Flask integration
21
- pip install cmap-rules-engine[flask]
21
+ pip install pylitmus[flask]
22
22
 
23
23
  # All extras
24
- pip install cmap-rules-engine[all]
24
+ pip install pylitmus[all]
25
25
  ```
26
26
 
27
27
  ## Basic Usage
@@ -31,7 +31,7 @@ pip install cmap-rules-engine[all]
31
31
  The simplest way to create a rules engine:
32
32
 
33
33
  ```python
34
- from cmap_rules_engine import create_engine
34
+ from pylitmus import create_engine
35
35
 
36
36
  # Simple in-memory engine
37
37
  engine = create_engine()
@@ -40,7 +40,7 @@ engine = create_engine()
40
40
  ### Defining Rules in Python
41
41
 
42
42
  ```python
43
- from cmap_rules_engine import create_engine, Rule, Severity
43
+ from pylitmus import create_engine, Rule, Severity
44
44
 
45
45
  rules = [
46
46
  Rule(
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pylitmus"
7
- version = "1.0.0"
7
+ version = "1.2.0"
8
8
  description = "A high-performance rules engine for Python - evaluate data against configurable rules and get clear verdicts"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -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.0.0"
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
@@ -0,0 +1,427 @@
1
+ """
2
+ Main RuleEngine class for evaluating data against configured rules.
3
+ """
4
+
5
+ import logging
6
+ import time
7
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional
8
+
9
+ from .exceptions import EvaluationError
10
+ from .types import AssessmentResult, DecisionTier, Rule, RuleResult
11
+
12
+ if TYPE_CHECKING:
13
+ from .rete import RETENetwork, RuleCompiler
14
+ from .storage.base import RuleRepository
15
+ from .strategies.base import ScoringStrategy
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class RuleEngine:
21
+ """
22
+ Main rules engine for evaluating data against configured rules.
23
+
24
+ Usage:
25
+ # Without decision tiers (returns None for decision)
26
+ engine = RuleEngine(
27
+ repository=DatabaseRuleRepository(db_url),
28
+ scoring_strategy=WeightedStrategy()
29
+ )
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
+
52
+ result = engine.evaluate(claim, context)
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ repository: "RuleRepository",
58
+ scoring_strategy: Optional["ScoringStrategy"] = None,
59
+ decision_tiers: Optional[List[DecisionTier]] = None,
60
+ condition_builder: Optional[Any] = None,
61
+ use_rete: bool = False,
62
+ ):
63
+ """
64
+ Initialize the rule engine.
65
+
66
+ Args:
67
+ repository: Rule storage backend
68
+ scoring_strategy: Strategy for calculating scores
69
+ decision_tiers: User-defined decision tiers with score ranges.
70
+ If not provided, decision will be None.
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).
75
+ """
76
+ self.repository = repository
77
+ self._scoring_strategy = scoring_strategy
78
+ self._decision_tiers = decision_tiers
79
+ self._condition_builder = condition_builder
80
+ self._rules_cache: Optional[List[Rule]] = None
81
+
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'}")
102
+
103
+ @property
104
+ def scoring_strategy(self) -> "ScoringStrategy":
105
+ """Get the scoring strategy, creating default if needed."""
106
+ if self._scoring_strategy is None:
107
+ from .strategies import SumStrategy
108
+
109
+ self._scoring_strategy = SumStrategy()
110
+ return self._scoring_strategy
111
+
112
+ @property
113
+ def condition_builder(self) -> Any:
114
+ """Get the condition builder, creating default if needed."""
115
+ if self._condition_builder is None:
116
+ from .conditions import ConditionBuilder
117
+
118
+ self._condition_builder = ConditionBuilder()
119
+ return self._condition_builder
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
+
145
+ def evaluate(
146
+ self,
147
+ data: Dict[str, Any],
148
+ context: Optional[Dict[str, Any]] = None,
149
+ filters: Optional[Dict[str, Any]] = None,
150
+ ) -> AssessmentResult:
151
+ """
152
+ Evaluate data against all applicable rules.
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
+
252
+ Args:
253
+ data: Data to evaluate
254
+ context: Additional context for evaluation
255
+ filters: Filters to apply when fetching rules
256
+
257
+ Returns:
258
+ AssessmentResult with score, decision, and triggered rules
259
+ """
260
+ start_time = time.time()
261
+ context = context or {}
262
+
263
+ logger.debug(f"Starting standard evaluation with {len(data)} data fields")
264
+
265
+ # Get applicable rules
266
+ rules = self.repository.get_enabled(filters)
267
+
268
+ triggered_results: List[RuleResult] = []
269
+ all_results: List[RuleResult] = []
270
+
271
+ for rule in rules:
272
+ try:
273
+ result = self.evaluate_rule(rule, data, context)
274
+ all_results.append(result)
275
+
276
+ if result.triggered:
277
+ triggered_results.append(result)
278
+ logger.debug(
279
+ f"Rule {rule.code} triggered with score {result.score}"
280
+ )
281
+
282
+ except Exception as e:
283
+ logger.error(f"Error evaluating rule {rule.code}: {e}")
284
+ raise EvaluationError(
285
+ f"Failed to evaluate rule {rule.code}: {e}"
286
+ ) from e
287
+
288
+ # Calculate total score
289
+ total_score = self.scoring_strategy.calculate(triggered_results)
290
+
291
+ # Determine decision
292
+ decision = self._make_decision(total_score)
293
+
294
+ processing_time = (time.time() - start_time) * 1000
295
+
296
+ result = AssessmentResult(
297
+ total_score=total_score,
298
+ decision=decision,
299
+ triggered_rules=triggered_results,
300
+ all_rules_evaluated=len(rules),
301
+ processing_time_ms=processing_time,
302
+ metadata={
303
+ "context": context,
304
+ "filters": filters,
305
+ "evaluation_method": "standard",
306
+ },
307
+ )
308
+
309
+ logger.info(
310
+ f"Standard evaluation complete: score={total_score}, decision={decision}, "
311
+ f"triggered={len(triggered_results)}/{len(rules)}, time={processing_time:.2f}ms"
312
+ )
313
+
314
+ return result
315
+
316
+ def evaluate_rule(
317
+ self, rule: Rule, data: Dict[str, Any], context: Optional[Dict[str, Any]] = None
318
+ ) -> RuleResult:
319
+ """
320
+ Evaluate a single rule against data.
321
+
322
+ Args:
323
+ rule: Rule to evaluate
324
+ data: Data to evaluate against
325
+ context: Additional context
326
+
327
+ Returns:
328
+ RuleResult indicating if rule triggered
329
+ """
330
+ context = context or {}
331
+
332
+ # Check if rule is effective
333
+ if not rule.is_effective():
334
+ return RuleResult(
335
+ rule_code=rule.code,
336
+ rule_name=rule.name,
337
+ triggered=False,
338
+ score=0,
339
+ severity=rule.severity,
340
+ category=rule.category,
341
+ explanation="Rule is not currently effective",
342
+ )
343
+
344
+ # Build condition from rule definition
345
+ condition = self.condition_builder.build(rule.conditions)
346
+
347
+ # Evaluate condition
348
+ triggered = condition.evaluate(data)
349
+
350
+ return RuleResult(
351
+ rule_code=rule.code,
352
+ rule_name=rule.name,
353
+ triggered=triggered,
354
+ score=rule.score if triggered else 0,
355
+ severity=rule.severity,
356
+ category=rule.category,
357
+ explanation=rule.description if triggered else "Condition not met",
358
+ )
359
+
360
+ def _make_decision(self, score: int) -> Optional[str]:
361
+ """
362
+ Make a decision based on the total score and configured decision tiers.
363
+
364
+ Args:
365
+ score: Total calculated score
366
+
367
+ Returns:
368
+ Decision string if a matching tier is found, None otherwise.
369
+ If no decision tiers are configured, returns None.
370
+ """
371
+ if not self._decision_tiers:
372
+ return None
373
+
374
+ for tier in self._decision_tiers:
375
+ if tier.matches(score):
376
+ return tier.name
377
+
378
+ return None
379
+
380
+ def reload_rules(self) -> None:
381
+ """Force reload rules from repository."""
382
+ self._rules_cache = None
383
+
384
+ # Invalidate RETE network to force recompilation
385
+ self._rete_network = None
386
+ self._rete_compiled = False
387
+
388
+ # If repository has cache, invalidate it
389
+ if hasattr(self.repository, "invalidate"):
390
+ self.repository.invalidate()
391
+
392
+ logger.info("Rules cache invalidated")
393
+
394
+ def get_rules(self, filters: Optional[Dict[str, Any]] = None) -> List[Rule]:
395
+ """
396
+ Get all rules, optionally filtered.
397
+
398
+ Args:
399
+ filters: Optional filters to apply
400
+
401
+ Returns:
402
+ List of rules
403
+ """
404
+ return self.repository.get_all(filters)
405
+
406
+ def get_rule(self, code: str) -> Optional[Rule]:
407
+ """
408
+ Get a specific rule by code.
409
+
410
+ Args:
411
+ code: Rule code
412
+
413
+ Returns:
414
+ Rule if found, None otherwise
415
+ """
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
@@ -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, Dict, List, Optional, Union
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
- decision_thresholds: Optional[Dict[str, int]] = None,
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
- decision_thresholds: Custom thresholds {'approve': 30, 'review': 70}
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 weighted scoring
88
+ # With user-defined decision tiers
82
89
  engine = create_engine(
83
90
  scoring_strategy='weighted',
84
- decision_thresholds={'approve': 25, 'review': 60}
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
- decision_thresholds=decision_thresholds,
136
+ decision_tiers=decision_tiers,
137
+ use_rete=use_rete,
116
138
  )
117
139
 
118
140