-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrule_engine.py
More file actions
340 lines (273 loc) · 10 KB
/
Copy pathrule_engine.py
File metadata and controls
340 lines (273 loc) · 10 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
"""
Rule-based inference engine with forward chaining and conflict resolution.
"""
from typing import List, Set, Dict, Any, Callable, Optional, Tuple
from collections import defaultdict
import copy
class Fact:
"""Represents a fact in the knowledge base."""
def __init__(self, name: str, **attributes: Any) -> None:
"""
Initialize a fact.
Args:
name: The name/type of the fact
**attributes: Key-value pairs representing fact attributes
"""
self.name = name
self.attributes = attributes
def __eq__(self, other: object) -> bool:
if not isinstance(other, Fact):
return False
return self.name == other.name and self.attributes == other.attributes
def __hash__(self) -> int:
return hash((self.name, tuple(sorted(self.attributes.items()))))
def __repr__(self) -> str:
if not self.attributes:
return f"Fact('{self.name}')"
attrs = ', '.join(f"{k}={v}" for k, v in self.attributes.items())
return f"Fact('{self.name}', {attrs})"
class Rule:
"""Represents an inference rule with conditions and conclusions."""
def __init__(
self,
name: str,
conditions: List[Fact],
conclusions: List[Fact],
priority: int = 0
) -> None:
"""
Initialize a rule.
Args:
name: Name of the rule
conditions: List of facts that must be true for rule to fire
conclusions: List of facts that become true when rule fires
priority: Priority for conflict resolution (higher fires first)
"""
self.name = name
self.conditions = conditions
self.conclusions = conclusions
self.priority = priority
self.fired = False
def can_fire(self, facts: Set[Fact]) -> bool:
"""
Check if rule can fire given current facts.
Args:
facts: Current set of known facts
Returns:
True if all conditions are met, False otherwise
"""
if self.fired:
return False
for condition in self.conditions:
if not self._fact_matches(condition, facts):
return False
return True
def _fact_matches(self, condition: Fact, facts: Set[Fact]) -> bool:
"""Check if a condition fact matches any known fact."""
for fact in facts:
if fact.name == condition.name:
# Check if all condition attributes match
match = True
for key, value in condition.attributes.items():
if key not in fact.attributes or fact.attributes[key] != value:
match = False
break
if match:
return True
return False
def fire(self, facts: Set[Fact]) -> List[Fact]:
"""
Fire the rule and return new facts.
Args:
facts: Current set of known facts
Returns:
List of new facts generated by this rule
"""
if not self.can_fire(facts):
return []
self.fired = True
new_facts = []
for conclusion in self.conclusions:
# Only add if not already present
if conclusion not in facts:
new_facts.append(conclusion)
return new_facts
def reset(self) -> None:
"""Reset the rule's fired status."""
self.fired = False
def __repr__(self) -> str:
return f"Rule('{self.name}', priority={self.priority})"
class KnowledgeBase:
"""Manages facts and rules for inference."""
def __init__(self) -> None:
"""Initialize an empty knowledge base."""
self.facts: Set[Fact] = set()
self.rules: List[Rule] = []
self._rule_index: Dict[str, List[Rule]] = defaultdict(list)
def add_fact(self, fact: Fact) -> None:
"""
Add a fact to the knowledge base.
Args:
fact: The fact to add
"""
self.facts.add(fact)
def add_rule(self, rule: Rule) -> None:
"""
Add a rule to the knowledge base.
Args:
rule: The rule to add
"""
self.rules.append(rule)
# Index rules by condition names for efficiency
for condition in rule.conditions:
self._rule_index[condition.name].append(rule)
def get_applicable_rules(self) -> List[Rule]:
"""
Get all rules that can currently fire.
Returns:
List of applicable rules sorted by priority
"""
applicable = [rule for rule in self.rules if rule.can_fire(self.facts)]
# Sort by priority (higher first) then by name for deterministic ordering
applicable.sort(key=lambda r: (-r.priority, r.name))
return applicable
def forward_chain(self) -> Tuple[int, int]:
"""
Perform forward chaining until no more rules can fire.
Returns:
Tuple of (iterations, facts_added) during inference
"""
iterations = 0
facts_added = 0
while True:
applicable_rules = self.get_applicable_rules()
if not applicable_rules:
break
iterations += 1
fired_in_iteration = 0
for rule in applicable_rules:
new_facts = rule.fire(self.facts)
if new_facts:
fired_in_iteration += 1
for fact in new_facts:
self.facts.add(fact)
facts_added += len(new_facts)
# If no rules fired in this iteration, we're done
if fired_in_iteration == 0:
break
return iterations, facts_added
def query(self, fact: Fact) -> bool:
"""
Check if a fact can be derived from the knowledge base.
Args:
fact: The fact to query
Returns:
True if fact exists or can be derived, False otherwise
"""
# First check if already known
for known_fact in self.facts:
if known_fact.name == fact.name:
match = True
for key, value in fact.attributes.items():
if key not in known_fact.attributes or known_fact.attributes[key] != value:
match = False
break
if match:
return True
return False
def reset(self) -> None:
"""Reset all rules to unfired state."""
for rule in self.rules:
rule.reset()
def __repr__(self) -> str:
return f"KnowledgeBase({len(self.facts)} facts, {len(self.rules)} rules)"
def main() -> None:
"""Demo of the inference engine with a simple medical diagnosis system."""
# Create knowledge base
kb = KnowledgeBase()
# Add initial facts
kb.add_fact(Fact("patient", fever=True))
kb.add_fact(Fact("patient", headache=True))
kb.add_fact(Fact("patient", age="adult"))
# Add rules
kb.add_rule(Rule(
"flu_diagnosis",
[Fact("patient", fever=True), Fact("patient", headache=True)],
[Fact("diagnosis", disease="flu")],
priority=10
))
kb.add_rule(Rule(
"migraine_diagnosis",
[Fact("patient", headache=True), Fact("patient", age="adult")],
[Fact("diagnosis", disease="migraine")],
priority=5
))
kb.add_rule(Rule(
"fever_treatment",
[Fact("diagnosis", disease="flu")],
[Fact("treatment", medication="antipyretic")],
priority=15
))
kb.add_rule(Rule(
"headache_treatment",
[Fact("diagnosis", disease="migraine")],
[Fact("treatment", medication="analgesic")],
priority=15
))
print("Initial state:")
print(f"Facts: {len(kb.facts)}")
print(f"Rules: {len(kb.rules)}")
print()
# Perform inference
iterations, facts_added = kb.forward_chain()
print(f"Forward chaining completed in {iterations} iterations")
print(f"Added {facts_added} new facts")
print()
# Check results
print("Derived facts:")
for fact in kb.facts:
print(f" {fact}")
print()
# Query specific facts
print("Queries:")
flu_query = kb.query(Fact("diagnosis", disease="flu"))
migraine_query = kb.query(Fact("diagnosis", disease="migraine"))
treatment_query = kb.query(Fact("treatment", medication="antipyretic"))
print(f"Flu diagnosis: {flu_query}")
print(f"Migraine diagnosis: {migraine_query}")
print(f"Antipyretic treatment: {treatment_query}")
# Test conflict resolution with another example
print("\n" + "="*50)
print("Testing conflict resolution:")
kb2 = KnowledgeBase()
# Facts
kb2.add_fact(Fact("animal", has_fur=True))
kb2.add_fact(Fact("animal", says="meow"))
kb2.add_fact(Fact("animal", has_tail=True))
# Rules with different priorities
kb2.add_rule(Rule(
"cat_id",
[Fact("animal", has_fur=True), Fact("animal", says="meow")],
[Fact("classification", species="cat")],
priority=5
))
kb2.add_rule(Rule(
"mammal_id",
[Fact("animal", has_fur=True)],
[Fact("classification", species="mammal")],
priority=1 # Lower priority
))
# This rule should fire first due to higher priority
kb2.add_rule(Rule(
"pet_id",
[Fact("animal", says="meow"), Fact("animal", has_tail=True)],
[Fact("classification", category="pet")],
priority=10
))
iterations2, facts_added2 = kb2.forward_chain()
print(f"Completed in {iterations2} iterations, added {facts_added2} facts")
print("Final facts:")
for fact in kb2.facts:
print(f" {fact}")
if __name__ == "__main__":
main()