Expert System Lab Walkthrough
Unit 6: Knowledge-Based Agents and Inference — Lab Walkthrough
Try the lab yourself first! This page contains complete solutions. You will learn far more by struggling with the implementation yourself than by reading the answers. If you are stuck on a specific part, read only that section. Understand the solution before looking at the next one.
Part 1: Knowledge Base Implementation
The Rule Class
A rule has two components: a list of conditions (all must be true) and a single conclusion.
class Rule:
"""
Represents a logical rule: IF all conditions THEN conclusion.
"""
def __init__(self, conditions, conclusion):
# conditions: list of strings, each a fact that must be true
# conclusion: the single fact that becomes true when all conditions met
self.conditions = conditions # e.g., ['fever', 'cough']
self.conclusion = conclusion # e.g., 'respiratory_issue'
def can_fire(self, known_facts):
"""
Returns True if all conditions are in known_facts.
Uses Python's all() for concise AND-check over a list.
"""
return all(condition in known_facts for condition in self.conditions)
def __str__(self):
conds = ' ∧ '.join(self.conditions)
return f"IF {conds} THEN {self.conclusion}"
Design decisions:
-
conditionsis a list because all conditions must be true (AND semantics). -
can_fire()uses Python’sall()— returns True only if every element satisfies the condition. -
str()provides readable output for debugging and the explanation facility.
The KnowledgeBase Class
class KnowledgeBase:
"""
Stores facts and rules; implements the Tell-Ask interface.
"""
def __init__(self):
self.facts = set() # Use set for O(1) membership testing
self.rules = [] # Ordered list of Rule objects
self.fired_rules = [] # Track in-order for explanation facility
def tell(self, fact):
"""Add a fact to the knowledge base."""
self.facts.add(fact)
print(f"Told: {fact}")
def add_rule(self, rule):
"""Add a Rule object to the knowledge base."""
self.rules.append(rule)
print(f"Added rule: {rule}")
def ask(self, query):
"""Return True if query is a known fact."""
return query in self.facts
Why use a set for facts?
Set membership testing (x in my_set) is O(1) average case in Python.
If you used a list, x in my_list would be O(n) — slow for large KBs.
The fired_rules list is separate from rules so the original rule list stays intact for re-use.
Part 2: Forward Chaining Algorithm
The key structure is a while True outer loop that breaks when no new facts are produced.
def forward_chain(self):
"""
Apply forward chaining: fire rules until no new facts can be derived.
Returns the set of all newly inferred facts.
"""
inferred = set() # All facts derived during this call
iteration = 1
print("\n=== Forward Chaining Started ===")
print(f"Initial facts: {self.facts}\n")
while True:
print(f"--- Iteration {iteration} ---")
new_facts = set() # Facts derived in THIS iteration only
for rule in self.rules:
# Can this rule fire AND is its conclusion new?
if rule.can_fire(self.facts) and rule.conclusion not in self.facts:
print(f" Firing: {rule}")
new_facts.add(rule.conclusion)
self.fired_rules.append(rule)
if not new_facts:
print(" No new facts. Stopping.")
break
# Add new facts AFTER the inner loop (do not modify self.facts mid-scan)
print(f" New facts: {new_facts}")
self.facts.update(new_facts)
inferred.update(new_facts)
iteration += 1
print()
print(f"=== Forward Chaining Complete ===")
print(f"Inferred: {inferred}\n")
return inferred
# Attach to class (if defined outside)
KnowledgeBase.forward_chain = forward_chain
Why use new_facts inside the loop rather than updating self.facts directly?
If you call self.facts.add(…) inside the for rule in self.rules loop, later rules in the same iteration can see the new fact.
This means a rule that should require two iterations (A → B, B → C) might fire in a single iteration, producing different behavior than the formal algorithm specifies.
Using new_facts and only updating self.facts.update(new_facts) after the full scan makes each iteration’s input state well-defined and consistent.
Part 3: Domain-Specific Expert System
Here is a complete medical diagnosis KB with 18 rules covering 4 diseases.
kb = KnowledgeBase()
# --- Stage 1: Symptom grouping ---
kb.add_rule(Rule(['fever', 'cough'], 'respiratory_issue'))
kb.add_rule(Rule(['runny_nose', 'itchy_eyes'], 'allergy_symptoms'))
# --- Flu pathway ---
kb.add_rule(Rule(['respiratory_issue', 'fatigue'], 'possible_flu'))
kb.add_rule(Rule(['possible_flu', 'body_aches'], 'flu'))
# --- COVID pathway ---
kb.add_rule(Rule(['respiratory_issue', 'loss_of_taste'], 'possible_covid'))
kb.add_rule(Rule(['possible_covid', 'loss_of_smell'], 'covid'))
# --- Common cold pathway ---
kb.add_rule(Rule(['respiratory_issue', 'sore_throat'], 'possible_cold'))
kb.add_rule(Rule(['possible_cold', 'runny_nose'], 'common_cold'))
# --- Allergy pathway ---
kb.add_rule(Rule(['allergy_symptoms', 'sneezing'], 'allergies'))
# --- Flu recommendations ---
kb.add_rule(Rule(['flu'], 'recommend_rest'))
kb.add_rule(Rule(['flu'], 'recommend_fluids'))
kb.add_rule(Rule(['flu'], 'consider_antiviral'))
# --- COVID recommendations ---
kb.add_rule(Rule(['covid'], 'recommend_isolation'))
kb.add_rule(Rule(['covid'], 'recommend_covid_test'))
kb.add_rule(Rule(['covid'], 'monitor_oxygen'))
# --- Cold recommendations ---
kb.add_rule(Rule(['common_cold'], 'recommend_rest'))
kb.add_rule(Rule(['common_cold'], 'recommend_fluids'))
# --- Allergy recommendations ---
kb.add_rule(Rule(['allergies'], 'recommend_antihistamine'))
kb.add_rule(Rule(['allergies'], 'recommend_avoid_triggers'))
Test Case 1: Flu
kb.tell('fever')
kb.tell('cough')
kb.tell('fatigue')
kb.tell('body_aches')
inferred = kb.forward_chain()
print("--- Diagnosis ---")
if kb.ask('flu'):
print("Diagnosis: Influenza")
if kb.ask('recommend_rest'):
print(" • Get plenty of rest")
if kb.ask('recommend_fluids'):
print(" • Drink lots of fluids")
Expected Forward Chaining Output
=== Forward Chaining Started ===
Initial facts: {'fever', 'cough', 'fatigue', 'body_aches'}
--- Iteration 1 ---
Firing: IF fever ∧ cough THEN respiratory_issue
New facts: {'respiratory_issue'}
--- Iteration 2 ---
Firing: IF respiratory_issue ∧ fatigue THEN possible_flu
New facts: {'possible_flu'}
--- Iteration 3 ---
Firing: IF possible_flu ∧ body_aches THEN flu
New facts: {'flu'}
--- Iteration 4 ---
Firing: IF flu THEN recommend_rest
Firing: IF flu THEN recommend_fluids
Firing: IF flu THEN consider_antiviral
New facts: {'recommend_rest', 'recommend_fluids', 'consider_antiviral'}
--- Iteration 5 ---
No new facts. Stopping.
=== Forward Chaining Complete ===
Inferred: {'respiratory_issue', 'possible_flu', 'flu', 'recommend_rest',
'recommend_fluids', 'consider_antiviral'}
Part 4: Explanation Facility
def explain(self, target_fact):
"""
Explain how the KB derived a particular fact.
Shows the complete rule chain from initial facts to conclusion.
"""
print(f"\n=== Explanation for: {target_fact} ===")
if not self.ask(target_fact):
print(f"Cannot explain: '{target_fact}' is not in the knowledge base.")
return
# Build chain of relevant rules
chain = self._build_chain(target_fact)
if not chain:
print(f"'{target_fact}' was given as an initial fact.")
return
for step, rule in enumerate(chain, 1):
print(f"\nStep {step}:")
print(f" Rule applied: {rule}")
print(f" Conditions: {', '.join(rule.conditions)}")
print(f" Inferred: {rule.conclusion}")
print(f"\n✓ Conclusion: {target_fact} = TRUE")
def _build_chain(self, target):
"""
Recursively find the chain of fired rules that established target.
Returns rules in order from earliest inference to the target.
"""
chain = []
for rule in self.fired_rules:
if rule.conclusion == target:
# Find rules that established this rule's conditions
for cond in rule.conditions:
chain.extend(self._build_chain(cond))
chain.append(rule)
break # Only need one proof path per conclusion
return chain
KnowledgeBase.explain = explain
KnowledgeBase._build_chain = _build_chain
Sample Explanation Output
After running forward chaining on the flu scenario:
=== Explanation for: flu === Step 1: Rule applied: IF fever ∧ cough THEN respiratory_issue Conditions: fever, cough Inferred: respiratory_issue Step 2: Rule applied: IF respiratory_issue ∧ fatigue THEN possible_flu Conditions: respiratory_issue, fatigue Inferred: possible_flu Step 3: Rule applied: IF possible_flu ∧ body_aches THEN flu Conditions: possible_flu, body_aches Inferred: flu ✓ Conclusion: flu = TRUE
Pre-Submission Checklist
Before Submitting — Run These Checks
-
Rule class has
conditionslist andconclusionstring -
KnowledgeBase stores facts in a
set, rules in alist -
tell()andask()work correctly on simple examples -
forward_chain()checks for new facts before firing -
forward_chain()stops when no new facts are derived -
Knowledge base has 15+ rules
-
Rules chain through at least 3 levels (symptoms → intermediate → diagnosis → recommendation)
-
Tested with 3 different scenarios
-
explain()prints a complete reasoning chain -
All cells run without errors after Runtime → Restart and run all
Extension Challenges (Optional, No Points)
If you finish early and want to extend your system:
Optional extensions to try:
-
Confidence factors: Add a
cfattribute to Rule; compute combined CFs when chaining. -
Backward chaining: Implement a
backward_chain(goal)method and compare its output to forward chaining on the same KB. -
Interactive mode: Ask the user for symptoms one at a time via
input()and build up the fact set interactively. -
Why-not explanations: Implement
explain_not(fact)that tells the user which conditions were missing to prevent a conclusion from being reached. -
Conflict resolution: When two rules lead to contradictory conclusions, apply a priority ordering to choose between them.
Code examples adapted from aima-python, MIT License, Copyright 2016 aima-python contributors.
This work is licensed under CC BY-SA 4.0.