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
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rule Compiler for RETE Network.
|
|
3
|
+
|
|
4
|
+
The RuleCompiler transforms PyLitmus Rule definitions into a RETE network
|
|
5
|
+
structure, handling condition extraction, sharing detection, and network
|
|
6
|
+
construction.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
11
|
+
|
|
12
|
+
from ..types import Rule
|
|
13
|
+
from .network import RETENetwork
|
|
14
|
+
from .nodes import AlphaNode, BetaNode, OrNode, TerminalNode
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RuleCompiler:
|
|
20
|
+
"""
|
|
21
|
+
Compiles PyLitmus rules into a RETE network.
|
|
22
|
+
|
|
23
|
+
The compiler:
|
|
24
|
+
1. Extracts conditions from rule definitions
|
|
25
|
+
2. Identifies opportunities for condition sharing
|
|
26
|
+
3. Builds alpha nodes for simple conditions
|
|
27
|
+
4. Builds beta nodes for AND combinations
|
|
28
|
+
5. Builds OR nodes for ANY combinations
|
|
29
|
+
6. Connects terminal nodes for rule activation
|
|
30
|
+
|
|
31
|
+
Usage:
|
|
32
|
+
compiler = RuleCompiler()
|
|
33
|
+
network = compiler.compile(rules)
|
|
34
|
+
results = network.evaluate(data)
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self):
|
|
38
|
+
"""Initialize the compiler."""
|
|
39
|
+
self._network: Optional[RETENetwork] = None
|
|
40
|
+
|
|
41
|
+
def compile(self, rules: List[Rule]) -> RETENetwork:
|
|
42
|
+
"""
|
|
43
|
+
Compile a list of rules into a RETE network.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
rules: List of PyLitmus Rule objects
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Compiled RETENetwork ready for evaluation
|
|
50
|
+
"""
|
|
51
|
+
self._network = RETENetwork()
|
|
52
|
+
|
|
53
|
+
for rule in rules:
|
|
54
|
+
if not rule.is_effective():
|
|
55
|
+
logger.debug(f"Skipping ineffective rule: {rule.code}")
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
self._compile_rule(rule)
|
|
59
|
+
|
|
60
|
+
logger.info(
|
|
61
|
+
f"Compiled {len(rules)} rules into RETE network: "
|
|
62
|
+
f"{self._network.get_stats()}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return self._network
|
|
66
|
+
|
|
67
|
+
def _compile_rule(self, rule: Rule) -> None:
|
|
68
|
+
"""
|
|
69
|
+
Compile a single rule into the network.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
rule: The rule to compile
|
|
73
|
+
"""
|
|
74
|
+
# Create terminal node for this rule
|
|
75
|
+
terminal = self._network.create_terminal(rule)
|
|
76
|
+
|
|
77
|
+
# Parse and compile the conditions
|
|
78
|
+
conditions = rule.conditions
|
|
79
|
+
|
|
80
|
+
if not conditions:
|
|
81
|
+
# Rule with no conditions always triggers
|
|
82
|
+
terminal.activated = True
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
# Compile the condition tree and get the root nodes
|
|
86
|
+
root_nodes = self._compile_conditions(conditions, terminal)
|
|
87
|
+
|
|
88
|
+
logger.debug(
|
|
89
|
+
f"Compiled rule {rule.code} with {len(root_nodes)} root conditions"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def _compile_conditions(
|
|
93
|
+
self, conditions: Dict[str, Any], terminal: TerminalNode
|
|
94
|
+
) -> List[Union[AlphaNode, BetaNode, OrNode]]:
|
|
95
|
+
"""
|
|
96
|
+
Compile a conditions dictionary into network nodes.
|
|
97
|
+
|
|
98
|
+
Handles:
|
|
99
|
+
- Simple conditions: {"field": "x", "operator": "equals", "value": 1}
|
|
100
|
+
- AND conditions: {"all": [...]}
|
|
101
|
+
- OR conditions: {"any": [...]}
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
conditions: The conditions dictionary
|
|
105
|
+
terminal: The terminal node to connect to
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
List of root nodes created
|
|
109
|
+
"""
|
|
110
|
+
# Check for composite conditions
|
|
111
|
+
if "all" in conditions:
|
|
112
|
+
return self._compile_all(conditions["all"], terminal)
|
|
113
|
+
elif "any" in conditions:
|
|
114
|
+
return self._compile_any(conditions["any"], terminal)
|
|
115
|
+
else:
|
|
116
|
+
# Simple condition
|
|
117
|
+
return self._compile_simple(conditions, terminal)
|
|
118
|
+
|
|
119
|
+
def _compile_simple(
|
|
120
|
+
self, condition: Dict[str, Any], terminal: TerminalNode
|
|
121
|
+
) -> List[AlphaNode]:
|
|
122
|
+
"""
|
|
123
|
+
Compile a simple condition.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
condition: Simple condition dict with field, operator, value
|
|
127
|
+
terminal: Terminal node to connect to
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
List containing the alpha node
|
|
131
|
+
"""
|
|
132
|
+
field = condition.get("field")
|
|
133
|
+
operator = condition.get("operator")
|
|
134
|
+
value = condition.get("value")
|
|
135
|
+
|
|
136
|
+
if not all([field, operator]):
|
|
137
|
+
logger.warning(f"Invalid simple condition: {condition}")
|
|
138
|
+
return []
|
|
139
|
+
|
|
140
|
+
# Get or create alpha node (enables sharing)
|
|
141
|
+
alpha = self._network.get_or_create_alpha(field, operator, value)
|
|
142
|
+
|
|
143
|
+
# For a single condition, create a simple beta that just wraps it
|
|
144
|
+
beta = self._network.create_beta([alpha])
|
|
145
|
+
beta.children.append(terminal)
|
|
146
|
+
|
|
147
|
+
return [alpha]
|
|
148
|
+
|
|
149
|
+
def _compile_all(
|
|
150
|
+
self, conditions: List[Dict[str, Any]], terminal: TerminalNode
|
|
151
|
+
) -> List[Union[AlphaNode, BetaNode]]:
|
|
152
|
+
"""
|
|
153
|
+
Compile an ALL (AND) composite condition.
|
|
154
|
+
|
|
155
|
+
All conditions must pass for the rule to trigger.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
conditions: List of condition dictionaries
|
|
159
|
+
terminal: Terminal node to connect to
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
List of nodes created
|
|
163
|
+
"""
|
|
164
|
+
all_alphas: List[AlphaNode] = []
|
|
165
|
+
nested_nodes: List[Union[BetaNode, OrNode]] = []
|
|
166
|
+
|
|
167
|
+
for cond in conditions:
|
|
168
|
+
if "all" in cond:
|
|
169
|
+
# Nested ALL - flatten it
|
|
170
|
+
nested = self._compile_all_without_terminal(cond["all"])
|
|
171
|
+
all_alphas.extend(nested)
|
|
172
|
+
elif "any" in cond:
|
|
173
|
+
# Nested ANY - create OR node
|
|
174
|
+
or_node = self._compile_any_as_or_node(cond["any"])
|
|
175
|
+
if or_node:
|
|
176
|
+
nested_nodes.append(or_node)
|
|
177
|
+
else:
|
|
178
|
+
# Simple condition
|
|
179
|
+
field = cond.get("field")
|
|
180
|
+
operator = cond.get("operator")
|
|
181
|
+
value = cond.get("value")
|
|
182
|
+
|
|
183
|
+
if field and operator:
|
|
184
|
+
alpha = self._network.get_or_create_alpha(field, operator, value)
|
|
185
|
+
all_alphas.append(alpha)
|
|
186
|
+
|
|
187
|
+
# Create beta node joining all alpha conditions
|
|
188
|
+
if all_alphas:
|
|
189
|
+
beta = self._network.create_beta(all_alphas)
|
|
190
|
+
|
|
191
|
+
# If there are nested OR nodes, we need special handling
|
|
192
|
+
if nested_nodes:
|
|
193
|
+
# Create a combined check that requires both beta AND or_nodes
|
|
194
|
+
# For now, connect terminal to both
|
|
195
|
+
beta.children.append(terminal)
|
|
196
|
+
for node in nested_nodes:
|
|
197
|
+
node.children.append(terminal)
|
|
198
|
+
else:
|
|
199
|
+
beta.children.append(terminal)
|
|
200
|
+
elif nested_nodes:
|
|
201
|
+
# Only OR nodes, no direct alphas
|
|
202
|
+
for node in nested_nodes:
|
|
203
|
+
node.children.append(terminal)
|
|
204
|
+
|
|
205
|
+
return all_alphas
|
|
206
|
+
|
|
207
|
+
def _compile_all_without_terminal(
|
|
208
|
+
self, conditions: List[Dict[str, Any]]
|
|
209
|
+
) -> List[AlphaNode]:
|
|
210
|
+
"""
|
|
211
|
+
Compile ALL conditions without connecting to a terminal.
|
|
212
|
+
Used for nested ALL conditions.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
conditions: List of condition dictionaries
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
List of alpha nodes
|
|
219
|
+
"""
|
|
220
|
+
alphas = []
|
|
221
|
+
|
|
222
|
+
for cond in conditions:
|
|
223
|
+
if "all" in cond:
|
|
224
|
+
# Recursively flatten
|
|
225
|
+
alphas.extend(self._compile_all_without_terminal(cond["all"]))
|
|
226
|
+
elif "any" in cond:
|
|
227
|
+
# Skip OR in nested ALL for now (complex case)
|
|
228
|
+
logger.warning("Nested ANY inside ALL not fully supported in RETE")
|
|
229
|
+
else:
|
|
230
|
+
field = cond.get("field")
|
|
231
|
+
operator = cond.get("operator")
|
|
232
|
+
value = cond.get("value")
|
|
233
|
+
|
|
234
|
+
if field and operator:
|
|
235
|
+
alpha = self._network.get_or_create_alpha(field, operator, value)
|
|
236
|
+
alphas.append(alpha)
|
|
237
|
+
|
|
238
|
+
return alphas
|
|
239
|
+
|
|
240
|
+
def _compile_any(
|
|
241
|
+
self, conditions: List[Dict[str, Any]], terminal: TerminalNode
|
|
242
|
+
) -> List[Union[AlphaNode, OrNode]]:
|
|
243
|
+
"""
|
|
244
|
+
Compile an ANY (OR) composite condition.
|
|
245
|
+
|
|
246
|
+
Any one condition passing triggers the rule.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
conditions: List of condition dictionaries
|
|
250
|
+
terminal: Terminal node to connect to
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
List of nodes created
|
|
254
|
+
"""
|
|
255
|
+
# For OR, each branch independently can trigger the terminal
|
|
256
|
+
branches: List[List[AlphaNode]] = []
|
|
257
|
+
|
|
258
|
+
for cond in conditions:
|
|
259
|
+
if "all" in cond:
|
|
260
|
+
# Branch is an AND - collect its alphas
|
|
261
|
+
branch_alphas = self._compile_all_without_terminal(cond["all"])
|
|
262
|
+
if branch_alphas:
|
|
263
|
+
branches.append(branch_alphas)
|
|
264
|
+
elif "any" in cond:
|
|
265
|
+
# Nested OR - flatten by adding each as separate branch
|
|
266
|
+
for nested_cond in cond["any"]:
|
|
267
|
+
nested_alphas = self._extract_alphas_from_condition(nested_cond)
|
|
268
|
+
if nested_alphas:
|
|
269
|
+
branches.append(nested_alphas)
|
|
270
|
+
else:
|
|
271
|
+
# Simple condition is its own branch
|
|
272
|
+
field = cond.get("field")
|
|
273
|
+
operator = cond.get("operator")
|
|
274
|
+
value = cond.get("value")
|
|
275
|
+
|
|
276
|
+
if field and operator:
|
|
277
|
+
alpha = self._network.get_or_create_alpha(field, operator, value)
|
|
278
|
+
branches.append([alpha])
|
|
279
|
+
|
|
280
|
+
if branches:
|
|
281
|
+
# Create OR node
|
|
282
|
+
or_node = self._network.create_or_node(branches)
|
|
283
|
+
or_node.children.append(terminal)
|
|
284
|
+
return [or_node]
|
|
285
|
+
|
|
286
|
+
return []
|
|
287
|
+
|
|
288
|
+
def _compile_any_as_or_node(
|
|
289
|
+
self, conditions: List[Dict[str, Any]]
|
|
290
|
+
) -> Optional[OrNode]:
|
|
291
|
+
"""
|
|
292
|
+
Compile ANY conditions into an OR node without terminal.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
conditions: List of condition dictionaries
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
OrNode or None
|
|
299
|
+
"""
|
|
300
|
+
branches: List[List[AlphaNode]] = []
|
|
301
|
+
|
|
302
|
+
for cond in conditions:
|
|
303
|
+
alphas = self._extract_alphas_from_condition(cond)
|
|
304
|
+
if alphas:
|
|
305
|
+
branches.append(alphas)
|
|
306
|
+
|
|
307
|
+
if branches:
|
|
308
|
+
return self._network.create_or_node(branches)
|
|
309
|
+
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
def _extract_alphas_from_condition(
|
|
313
|
+
self, condition: Dict[str, Any]
|
|
314
|
+
) -> List[AlphaNode]:
|
|
315
|
+
"""
|
|
316
|
+
Extract alpha nodes from a condition dict.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
condition: Condition dictionary
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
List of alpha nodes
|
|
323
|
+
"""
|
|
324
|
+
if "all" in condition:
|
|
325
|
+
return self._compile_all_without_terminal(condition["all"])
|
|
326
|
+
elif "any" in condition:
|
|
327
|
+
# For extraction, just get the first branch
|
|
328
|
+
# (full OR handling is done elsewhere)
|
|
329
|
+
alphas = []
|
|
330
|
+
for cond in condition["any"]:
|
|
331
|
+
alphas.extend(self._extract_alphas_from_condition(cond))
|
|
332
|
+
return alphas
|
|
333
|
+
else:
|
|
334
|
+
field = condition.get("field")
|
|
335
|
+
operator = condition.get("operator")
|
|
336
|
+
value = condition.get("value")
|
|
337
|
+
|
|
338
|
+
if field and operator:
|
|
339
|
+
alpha = self._network.get_or_create_alpha(field, operator, value)
|
|
340
|
+
return [alpha]
|
|
341
|
+
|
|
342
|
+
return []
|
|
343
|
+
|
|
344
|
+
def recompile(self, rules: List[Rule]) -> RETENetwork:
|
|
345
|
+
"""
|
|
346
|
+
Recompile rules into a fresh network.
|
|
347
|
+
|
|
348
|
+
Use this when rules have changed.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
rules: Updated list of rules
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
New RETENetwork
|
|
355
|
+
"""
|
|
356
|
+
return self.compile(rules)
|
pylitmus/rete/network.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RETE Network Implementation.
|
|
3
|
+
|
|
4
|
+
The RETENetwork class is the main entry point for RETE-based rule evaluation.
|
|
5
|
+
It manages the alpha and beta networks and coordinates rule evaluation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any, Dict, List, Optional, Set
|
|
10
|
+
|
|
11
|
+
from ..types import Rule, RuleResult
|
|
12
|
+
from .nodes import AlphaNode, BetaNode, ConditionKey, OrNode, TerminalNode
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RETENetwork:
|
|
18
|
+
"""
|
|
19
|
+
Main RETE network for rule evaluation.
|
|
20
|
+
|
|
21
|
+
The network consists of:
|
|
22
|
+
- Alpha network: Single-condition tests with sharing
|
|
23
|
+
- Beta network: Join nodes for combining conditions
|
|
24
|
+
- Terminal nodes: Rule activation points
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
network = RETENetwork()
|
|
28
|
+
network.add_rule(rule) # Add rules one by one
|
|
29
|
+
# Or use RuleCompiler.compile(rules) to build the network
|
|
30
|
+
|
|
31
|
+
results = network.evaluate(data)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self):
|
|
35
|
+
"""Initialize an empty RETE network."""
|
|
36
|
+
# Alpha nodes indexed by condition key for sharing
|
|
37
|
+
self._alpha_nodes: Dict[ConditionKey, AlphaNode] = {}
|
|
38
|
+
|
|
39
|
+
# All alpha nodes in order of creation
|
|
40
|
+
self._alpha_list: List[AlphaNode] = []
|
|
41
|
+
|
|
42
|
+
# Beta nodes (AND joins)
|
|
43
|
+
self._beta_nodes: List[BetaNode] = []
|
|
44
|
+
|
|
45
|
+
# OR nodes
|
|
46
|
+
self._or_nodes: List[OrNode] = []
|
|
47
|
+
|
|
48
|
+
# Terminal nodes (one per rule)
|
|
49
|
+
self._terminal_nodes: List[TerminalNode] = []
|
|
50
|
+
|
|
51
|
+
# Rule code to terminal node mapping
|
|
52
|
+
self._rule_terminals: Dict[str, TerminalNode] = {}
|
|
53
|
+
|
|
54
|
+
# Track if network has been compiled
|
|
55
|
+
self._compiled = False
|
|
56
|
+
|
|
57
|
+
# Statistics
|
|
58
|
+
self._stats = {
|
|
59
|
+
"alpha_nodes": 0,
|
|
60
|
+
"shared_conditions": 0,
|
|
61
|
+
"beta_nodes": 0,
|
|
62
|
+
"or_nodes": 0,
|
|
63
|
+
"terminal_nodes": 0,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
def get_or_create_alpha(self, field: str, operator: str, value: Any) -> AlphaNode:
|
|
67
|
+
"""
|
|
68
|
+
Get an existing alpha node or create a new one.
|
|
69
|
+
|
|
70
|
+
This is the key to condition sharing - if an identical condition
|
|
71
|
+
already exists, we reuse its alpha node.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
field: Field path to test
|
|
75
|
+
operator: Comparison operator
|
|
76
|
+
value: Value to compare against
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Alpha node for this condition
|
|
80
|
+
"""
|
|
81
|
+
key = ConditionKey(field, operator, value)
|
|
82
|
+
|
|
83
|
+
if key in self._alpha_nodes:
|
|
84
|
+
self._stats["shared_conditions"] += 1
|
|
85
|
+
logger.debug(f"Reusing alpha node for {field} {operator} {value}")
|
|
86
|
+
return self._alpha_nodes[key]
|
|
87
|
+
|
|
88
|
+
# Create new alpha node
|
|
89
|
+
alpha = AlphaNode(field, operator, value, key)
|
|
90
|
+
self._alpha_nodes[key] = alpha
|
|
91
|
+
self._alpha_list.append(alpha)
|
|
92
|
+
self._stats["alpha_nodes"] += 1
|
|
93
|
+
|
|
94
|
+
logger.debug(f"Created alpha node for {field} {operator} {value}")
|
|
95
|
+
return alpha
|
|
96
|
+
|
|
97
|
+
def create_beta(self, alphas: List[AlphaNode]) -> BetaNode:
|
|
98
|
+
"""
|
|
99
|
+
Create a beta node joining multiple alpha nodes.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
alphas: List of alpha nodes to join (AND logic)
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
New beta node
|
|
106
|
+
"""
|
|
107
|
+
beta = BetaNode(alphas)
|
|
108
|
+
|
|
109
|
+
# Register beta with its alpha nodes
|
|
110
|
+
for alpha in alphas:
|
|
111
|
+
alpha.children.append(beta)
|
|
112
|
+
|
|
113
|
+
self._beta_nodes.append(beta)
|
|
114
|
+
self._stats["beta_nodes"] += 1
|
|
115
|
+
|
|
116
|
+
return beta
|
|
117
|
+
|
|
118
|
+
def create_or_node(self, branches: List[List[AlphaNode]]) -> OrNode:
|
|
119
|
+
"""
|
|
120
|
+
Create an OR node with multiple branches.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
branches: List of branches, each branch is a list of alpha nodes
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
New OR node
|
|
127
|
+
"""
|
|
128
|
+
or_node = OrNode(branches)
|
|
129
|
+
self._or_nodes.append(or_node)
|
|
130
|
+
self._stats["or_nodes"] += 1
|
|
131
|
+
return or_node
|
|
132
|
+
|
|
133
|
+
def create_terminal(self, rule: Rule) -> TerminalNode:
|
|
134
|
+
"""
|
|
135
|
+
Create a terminal node for a rule.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
rule: The rule this terminal represents
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
New terminal node
|
|
142
|
+
"""
|
|
143
|
+
terminal = TerminalNode(
|
|
144
|
+
rule_code=rule.code,
|
|
145
|
+
rule_name=rule.name,
|
|
146
|
+
score=rule.score,
|
|
147
|
+
severity=rule.severity.value
|
|
148
|
+
if hasattr(rule.severity, "value")
|
|
149
|
+
else str(rule.severity),
|
|
150
|
+
category=rule.category,
|
|
151
|
+
description=rule.description,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
self._terminal_nodes.append(terminal)
|
|
155
|
+
self._rule_terminals[rule.code] = terminal
|
|
156
|
+
self._stats["terminal_nodes"] += 1
|
|
157
|
+
|
|
158
|
+
return terminal
|
|
159
|
+
|
|
160
|
+
def evaluate(self, data: Dict[str, Any]) -> List[RuleResult]:
|
|
161
|
+
"""
|
|
162
|
+
Evaluate data through the RETE network.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
data: The fact/data to evaluate
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
List of RuleResult for triggered rules
|
|
169
|
+
"""
|
|
170
|
+
# Clear previous evaluation state
|
|
171
|
+
self._clear_evaluation_state()
|
|
172
|
+
|
|
173
|
+
# Phase 1: Activate alpha network
|
|
174
|
+
for alpha in self._alpha_list:
|
|
175
|
+
alpha.activate(data)
|
|
176
|
+
|
|
177
|
+
# Phase 2: Check beta nodes (AND conditions)
|
|
178
|
+
for beta in self._beta_nodes:
|
|
179
|
+
beta.activate(data)
|
|
180
|
+
|
|
181
|
+
# Phase 3: Check OR nodes
|
|
182
|
+
for or_node in self._or_nodes:
|
|
183
|
+
# Check each branch
|
|
184
|
+
for i, branch in enumerate(or_node.branches):
|
|
185
|
+
# A branch is satisfied if all its alphas passed
|
|
186
|
+
branch_satisfied = all(alpha._last_result for alpha in branch)
|
|
187
|
+
if branch_satisfied:
|
|
188
|
+
or_node.branch_satisfied(i)
|
|
189
|
+
|
|
190
|
+
or_node.activate(data)
|
|
191
|
+
|
|
192
|
+
# Phase 4: Collect triggered rules
|
|
193
|
+
results = []
|
|
194
|
+
for terminal in self._terminal_nodes:
|
|
195
|
+
if terminal.activated:
|
|
196
|
+
results.append(
|
|
197
|
+
RuleResult(
|
|
198
|
+
rule_code=terminal.rule_code,
|
|
199
|
+
rule_name=terminal.rule_name,
|
|
200
|
+
triggered=True,
|
|
201
|
+
score=terminal.score,
|
|
202
|
+
severity=terminal.severity,
|
|
203
|
+
category=terminal.category,
|
|
204
|
+
explanation=terminal.description,
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
return results
|
|
209
|
+
|
|
210
|
+
def _clear_evaluation_state(self) -> None:
|
|
211
|
+
"""Clear all evaluation state for a fresh evaluation."""
|
|
212
|
+
# Clear alpha memories and last results
|
|
213
|
+
for alpha in self._alpha_list:
|
|
214
|
+
alpha.clear_memory()
|
|
215
|
+
|
|
216
|
+
# Clear beta nodes
|
|
217
|
+
for beta in self._beta_nodes:
|
|
218
|
+
beta.clear()
|
|
219
|
+
|
|
220
|
+
# Clear OR nodes
|
|
221
|
+
for or_node in self._or_nodes:
|
|
222
|
+
or_node.clear()
|
|
223
|
+
|
|
224
|
+
# Clear terminal nodes
|
|
225
|
+
for terminal in self._terminal_nodes:
|
|
226
|
+
terminal.clear()
|
|
227
|
+
|
|
228
|
+
def get_stats(self) -> Dict[str, int]:
|
|
229
|
+
"""Get network statistics."""
|
|
230
|
+
return self._stats.copy()
|
|
231
|
+
|
|
232
|
+
def dump(self) -> str:
|
|
233
|
+
"""
|
|
234
|
+
Dump network structure for debugging.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
String representation of the network
|
|
238
|
+
"""
|
|
239
|
+
lines = ["RETE Network Structure:", "=" * 40]
|
|
240
|
+
|
|
241
|
+
lines.append(f"\nAlpha Nodes ({len(self._alpha_list)}):")
|
|
242
|
+
for alpha in self._alpha_list:
|
|
243
|
+
shared = "SHARED" if self._stats["shared_conditions"] > 0 else ""
|
|
244
|
+
lines.append(f" - {alpha} {shared}")
|
|
245
|
+
|
|
246
|
+
lines.append(f"\nBeta Nodes ({len(self._beta_nodes)}):")
|
|
247
|
+
for beta in self._beta_nodes:
|
|
248
|
+
lines.append(f" - {beta}")
|
|
249
|
+
|
|
250
|
+
lines.append(f"\nOR Nodes ({len(self._or_nodes)}):")
|
|
251
|
+
for or_node in self._or_nodes:
|
|
252
|
+
lines.append(f" - {or_node}")
|
|
253
|
+
|
|
254
|
+
lines.append(f"\nTerminal Nodes ({len(self._terminal_nodes)}):")
|
|
255
|
+
for terminal in self._terminal_nodes:
|
|
256
|
+
lines.append(f" - {terminal}")
|
|
257
|
+
|
|
258
|
+
lines.append("\nStatistics:")
|
|
259
|
+
for key, value in self._stats.items():
|
|
260
|
+
lines.append(f" {key}: {value}")
|
|
261
|
+
|
|
262
|
+
return "\n".join(lines)
|
|
263
|
+
|
|
264
|
+
def __repr__(self) -> str:
|
|
265
|
+
return (
|
|
266
|
+
f"RETENetwork(alphas={len(self._alpha_list)}, "
|
|
267
|
+
f"betas={len(self._beta_nodes)}, "
|
|
268
|
+
f"terminals={len(self._terminal_nodes)})"
|
|
269
|
+
)
|