In 06-01 we built a ten-line chaining engine and left two problems open: two rules that clash (the warranty versus "used and more than 14 days") and an opened hygiene toothbrush that the engine refunded and denied at the same time. Solving them takes more than logic: you have to decide which rule wins, remember what is known about the case, explain every conclusion, ask the user for what is missing and, before all that, get the rules out of Diego's head and check them. That set of pieces is an expert system, the technology that in the 1980s took AI into industry (01-01) and that survives today, under a new name, in the business rule engines of banks, insurers and online shops. In this lesson we will see what an expert system is and where it comes from, its architecture piece by piece (knowledge base, inference engine with conflict resolution, working memory, explanation, knowledge acquisition and interface), how one is built (knowledge engineering with the expert, who at NovaMarket is Diego), its advantages and limits, and how it compares with an ML model. And we will build, in pure Python, an expert system for returns and warranties (case 8) with priorities, an explanation trace and an interface that asks for what it needs. It matters because it is the pattern you will use every time a decision has to be explainable, auditable and changeable without retraining.
Contents
- What an expert system is and where it comes from
- Architecture of an expert system
- The inference engine: chaining and conflict resolution
- Explanation: why? and how?
- Building an expert system: knowledge engineering
- Advantages and disadvantages; comparison with an ML model
- Code: NovaMarket's returns and warranties expert system
- Real tools
- Common Mistakes and Tips
- Exercises
- Conclusion
- What an expert system is and where it comes from
An expert system is a program that solves problems in a specific domain at the level of a human expert, using explicit knowledge (facts and rules written by people) and a reasoning mechanism kept separate from that knowledge. The separation is the key idea: the engine knows nothing about returns; the rules know nothing about how they are executed. Changing the policy means changing rules, not code.
The three historical milestones we already cited in 01-01, with what each contributed:
- DENDRAL (Stanford, 1965-): identified molecular structures from mass spectra. It showed that domain-specific knowledge, not a general reasoner, was what produced results: "in the knowledge lies the power" (Feigenbaum).
- MYCIN (Stanford, 1970s): diagnosis of bacterial infections and antibiotic recommendation with some 600 IF-THEN rules, backward chaining that questioned the physician, a module that explained "why" it was asking and "how" it had reached the conclusion, and certainty factors for uncertainty (06-03). Its empty engine, EMYCIN, was the first reusable shell.
- XCON / R1 (Digital Equipment Corporation, 1980): configured orders of VAX computers with thousands of forward-chaining rules; it saved tens of millions of dollars a year and set off the commercial boom. Its maintenance problems (more than 10,000 rules by 1988, with whole teams devoted to maintaining them) foreshadowed the winter.
What NovaMarket wants for returns is closer to XCON (business rules applied to every order) than to MYCIN, but it will borrow from MYCIN the explanation and the questions to the user.
- Architecture of an expert system
flowchart LR
subgraph ES[Returns expert system]
KB[(Knowledge base<br/>general facts + rules R1..R8)]
WM[(Working memory<br/>facts of the case: order 47102)]
IE[Inference engine<br/>chaining + conflict resolution]
EX[Explanation module<br/>why? / how?]
AQ[Knowledge acquisition<br/>module]
end
UI[User interface<br/>customer-service agent]
D[Diego, returns<br/>expert]
KE[Knowledge engineer<br/>Marta]
UI <--> IE
UI <--> EX
IE <--> KB
IE <--> WM
EX --> WM
EX --> KB
D --> KE --> AQ --> KB
The pieces, one by one:
| Component | What it contains / does | At NovaMarket |
|---|---|---|
| Knowledge base | Permanent knowledge of the domain: IF-THEN rules and general facts. It does not change from one case to the next | The returns and warranties rules: 14-day period, 3-year warranty, hygiene/sealed exceptions, free shipping if defective |
| Working memory | Facts of the current case: the input data, the user's answers and the intermediate conclusions. It is emptied for each consultation | Order 47102: 40 days, used, defective; then decision = warranty |
| Inference engine | Applies the rules to the working memory (forward or backward chaining, 06-01) and decides which rule to fire when several are applicable (conflict resolution) | Our InferenceEngine class |
| Explanation module | Answers "why are you asking me this?" (which rule it is trying to apply) and "how did you reach that conclusion?" (chain of rules and facts) | The trace "R3 was applied because..." |
| Knowledge acquisition module | Tools for entering, editing and validating rules without programming (editors, consistency checking, testing against cases) | A spreadsheet or form where Diego edits rules and sees which test cases change |
| User interface | Collects the data, asks questions, shows the decision and the explanation | The customer-service agent's screen or the case 7 assistant |
Notice the line separating the knowledge base from the working memory: it is the same one that in 06-01 separated RULES (general) from facts_of(order) (particular). And notice that the expert (Diego) does not touch the base directly: he does it through the knowledge engineer (Marta) and the acquisition module.
- The inference engine: chaining and conflict resolution
The engine repeats a recognise-act cycle:
- Match: find all the rules whose conditions are met by the current working memory. The result is the conflict set.
- Resolve the conflict: choose one rule from the set (if there are several).
- Act: fire the chosen rule: add its conclusions to the working memory (or ask, or execute an action).
- Back to 1 until there are no applicable rules or the goal is reached.
In 06-01 step 2 did not exist: we fired them all in the order they were written, and that is where the inconsistencies came from. The classic conflict resolution strategies, which are usually combined in this order:
| Strategy | Criterion | Example in returns |
|---|---|---|
| Refractoriness | A rule does not fire twice on the same facts | Avoids infinite loops |
| Priority (salience) | The rule with the highest priority, assigned by the expert, wins | The warranty (R1, priority 10) beats Diego's rule (R8, priority 2): a defective product is dealt with even if 14 days have passed |
| Specificity | The rule with more conditions (more specific) wins: exceptions are more specific than the general rule | "Within the period, unused, with label and with 5+ previous returns → human review" (R5) beats "within the period, unused, with label → refund" (R4) |
| Recency | The rule that uses the most recent facts wins: it follows the thread of the reasoning | After the user answers a question, the rules that use it are preferred |
| Order | The first one written | Last resort; fragile |
There is also a design decision about when to stop: derive everything derivable (useful for computing all the consequences of an order) or stop once the goal (decision) is set, which is what we will do: once the winning rule has set the decision, the lower-priority ones can no longer contradict it. It is the simplest way to let the right rule "win" without negations in the premises.
- Explanation: why? and how?
This is the feature that distinguishes an expert system from a giant if and the reason this module exists after 05-05. MYCIN answered two questions, and we will implement both:
- Why? (during the consultation): "why are you asking me whether the product shows signs of use?" → "because I am trying to apply R5, which needs to know whether it is used". It gives the user context and lets them spot absurd questions (a symptom of badly written rules).
- How? (at the end): "how did you decide that the warranty applies?" → "R1 was applied because defective = True, new product = True and 40 days ≤ 1095; R1 says: 3-year legal guarantee; NovaMarket pays the shipping". It is the justification demanded by the customer, the auditor and, in 02-04, the GDPR for automated decisions.
Technically, the explanation comes almost for free if the engine records, for every fact in the working memory, who put it there (input data, user answer or rule name) and keeps a trace of the rules fired with the concrete values that satisfied their conditions. With that, "why?" is a matter of following the chain of origins backwards.
- Building an expert system: knowledge engineering
An expert system is not trained: it is built, and the process is called knowledge engineering. With NovaMarket as the example:
- Choose the domain and scope. Narrow, well defined, with an available expert and repetitive decisions: returns and warranties (case 8) qualify; "customer service in general" does not.
- Knowledge acquisition: interviews with the expert. Marta sits down with Diego. Techniques: open questions ("how do you decide whether it applies?"), concrete cases ("and this order from last week?"), think aloud (Diego solves cases out loud), document review (returns policy, consumer regulations, which, we insist, must be reviewed by a legal professional). It is the slowest phase: experts know more than they can say (the acquisition bottleneck), they forget the exceptions until they see a case, and they sometimes contradict themselves between sessions.
- Formalise: extract rules. Turn what was said into precise conditions and conclusions, with attribute names defined in a dictionary (does "used" mean "showing signs of use" or "unsealed"? Diego distinguishes the two and the system must too). Assign priorities and justifications. Detect incomplete rules ("and if it is used but defective?").
- Validate with cases. A set of test cases with the decision Diego considers correct (20-50 real cases from the returns history), run every time a rule changes: the equivalent of the test set of 04-05. Look for rules that never fire, cases with no decision, and decisions Diego would not sign off.
- Maintain. Policies change (a Christmas campaign with 30-day returns, a new category), regulations change and new cases appear. Every change goes through the acquisition module and the case suite. XCON died of this: thousands of rules with no clear owner. The recipe: few rules, named, with a written justification and an owner (Diego owns the returns rules, as we agreed in 02-03 for the data).
An observation that connects with module 4: step 4 resembles evaluating a model, and step 3 resembles what a decision tree does, but backwards: there the algorithm finds the rules in orders.csv; here the expert dictates them. And there is an obvious bridge: the export_text of the tree of 04-04 produces readable rules that can serve as a draft for the interview with Diego ("the tree says that returns with more than 3 previous ones and fewer than 5 days are suspicious; does that ring true?").
- Advantages and disadvantages; comparison with an ML model
| Advantages | Disadvantages |
|---|---|
| Explainable by construction: every decision has its chain of rules | Acquisition bottleneck: extracting and formalising the knowledge is slow and expensive |
| Modifiable without retraining: changing a rule is a policy change, not a code change | Fragile outside the domain: it knows nothing about what is not in the rules; it does not degrade gracefully |
| Needs no historical data: works from day one and with rare cases | Maintenance gets hard as it grows: interactions between hundreds of rules, tangled priorities |
| Consistent: same input, same output, always | Uncertainty poorly supported in its pure form (06-03) |
| Auditable and compliant with regulations (AI Act, GDPR, 02-04) | Does not learn from experience unless someone rewrites rules |
| Preserves and spreads the expert's knowledge (if Diego leaves, the rules stay) | Also encodes the expert's biases and mistakes |
Against an ML model (the tree or the logistic regression of 04-04):
| Aspect | Expert system (written rules) | ML model (learned rules) |
|---|---|---|
| Source of knowledge | An expert and the regulations | Labelled historical data |
| Needs data | No (but yes, test cases) | Yes, lots of it and representative |
| Explainability | Total | The tree, yes; the network, no |
| Changing the policy | Edit a rule | Collect new data and retrain |
| Rare cases and exceptions | Written once | Poorly covered if rare in the data |
| Subtle patterns in lots of data | Does not see them | Its strong point |
| Uncertainty | Added on (06-03) | Native: probabilities |
| NovaMarket example | Returns and warranties (case 8), policies set by regulation | Fraud risk (case 3), demand (case 2), reviews (case 4) |
When to use each: rules when the decision is defined by regulation or policy, must be justified one by one, there is little data or the edge cases matter; ML when nobody can write down the input-output relationship but it is in the data. And very often both: the ML estimates (probability of fraud, of a defect) and the rules decide with that estimate as one more fact. It is the neurosymbolic combination announced in 05-05 and the one that will close the module in 06-04.
- Code: NovaMarket's returns and warranties expert system
We build the pieces of section 2 in pure Python. The facts in working memory are attribute-value pairs (days_since_delivery = 40, used = True), and the conditions of a rule are tuples (attribute, operator, value), readable and therefore explainable. Business data: 14-day withdrawal; 3-year legal guarantee (1095 days) for new products; hygiene and sealed products not returnable once opened; original label; free return shipping if defective.
7.1 Knowledge base, rules and working memory
import operator
OPERATORS = {"==": operator.eq, "!=": operator.ne, "<=": operator.le, "<": operator.lt,
">=": operator.ge, ">": operator.gt, "in": lambda a, b: a in b}
class Rule:
"""Rule IF conditions THEN conclusions, with priority and justification text."""
def __init__(self, name, conditions, conclusions, priority=0, justification=""):
self.name = name
self.conditions = conditions # list of (attribute, operator, value)
self.conclusions = conclusions # dict attribute -> value
self.priority = priority
self.justification = justification
def evaluate(self, memory):
"""True if every condition holds, False if any fails,
None if some fact is missing to decide."""
result = True
for attribute, op, value in self.conditions:
if attribute not in memory.facts:
result = None # not known yet: keep going in case another fails
continue
if not OPERATORS[op](memory.facts[attribute], value):
return False
return result
def missing_facts(self, memory):
return [a for a, _, _ in self.conditions if a not in memory.facts]
def __repr__(self):
cond = " ∧ ".join(f"{a} {op} {v!r}" for a, op, v in self.conditions)
return f"{self.name} [prio {self.priority}]: IF {cond} THEN {self.conclusions}"
class KnowledgeBase:
def __init__(self):
self.rules = []
def add(self, rule):
self.rules.append(rule)
def rules_concluding(self, attribute):
return [r for r in self.rules if attribute in r.conclusions]
class WorkingMemory:
"""Facts of the current case + a record of who asserted them and when."""
def __init__(self, initial_facts=None):
self.facts = {}
self.origin = {} # attribute -> "input" | "user" | rule name
self.clock = 0
self.time = {} # attribute -> instant at which it was asserted (for recency)
for k, v in (initial_facts or {}).items():
self.assert_fact(k, v, "input")
def assert_fact(self, attribute, value, origin):
self.clock += 1
self.facts[attribute] = value
self.origin[attribute] = origin
self.time[attribute] = self.clock
def recency(self, rule):
return max((self.time.get(a, 0) for a, _, _ in rule.conditions), default=0)Explanation: Rule.evaluate distinguishes three results, not two: true, false and "unknown" (None) when a fact is missing; that third value is what will let the interface ask exactly for what is missing. WorkingMemory stores, besides the value of each fact, its origin (input data, user answer or the rule that deduced it) and the instant it was asserted: the origin feeds the explanation and the instant feeds recency.
7.2 Inference engine with conflict resolution and explanation
class InferenceEngine:
def __init__(self, base, memory):
self.base = base
self.memory = memory
self.fired = [] # names of rules already applied (refractoriness)
self.trace = [] # step-by-step explanation
# ---- forward chaining with conflict resolution ----
def conflict_set(self):
return [r for r in self.base.rules
if r.name not in self.fired and r.evaluate(self.memory) is True]
def select(self, candidates):
"""Priority > specificity (number of conditions) > recency of the facts used."""
return max(candidates, key=lambda r: (r.priority, len(r.conditions),
self.memory.recency(r)))
def fire(self, rule):
detail = ", ".join(f"{a}={self.memory.facts[a]!r} ({op} {v!r})"
for a, op, v in rule.conditions)
for attribute, value in rule.conclusions.items():
self.memory.assert_fact(attribute, value, rule.name)
self.fired.append(rule.name)
self.trace.append(f"Applied {rule.name} because {detail} "
f"→ {rule.conclusions}. Justification: {rule.justification}")
def run(self, goal="decision"):
while goal not in self.memory.facts:
candidates = self.conflict_set()
if not candidates:
self.trace.append(f"No applicable rule and '{goal}' not set")
return None
if len(candidates) > 1:
self.trace.append("Conflict between " + ", ".join(r.name for r in candidates)
+ f"; {self.select(candidates).name} wins")
self.fire(self.select(candidates))
return self.memory.facts[goal]
# ---- explanation module ----
def why(self, attribute):
origin = self.memory.origin.get(attribute)
if origin is None:
return f"'{attribute}' is not in working memory"
if origin in ("input", "user"):
return f"'{attribute}' = {self.memory.facts[attribute]!r} is {origin} data"
rule = next(r for r in self.base.rules if r.name == origin)
return (f"'{attribute}' = {self.memory.facts[attribute]!r} was set by {rule.name} "
f"({rule.justification}); its conditions: "
+ "; ".join(self.why(a) for a, _, _ in rule.conditions))
def how(self):
return "\n".join(f" {i+1}. {step}" for i, step in enumerate(self.trace))
# ---- goal-driven consultation interface (simplified backward chaining) ----
def consult(self, goal, questions, answer):
"""Go through the rules that conclude the goal by priority; if one is
missing askable facts, ask for them; fire the first one that holds."""
for rule in sorted(self.base.rules_concluding(goal),
key=lambda r: (-r.priority, -len(r.conditions))):
for attribute in rule.missing_facts(self.memory):
if rule.evaluate(self.memory) is False:
break # we already know it does not apply: stop asking
if attribute in questions:
value = answer(questions[attribute], rule)
self.memory.assert_fact(attribute, value, "user")
if rule.evaluate(self.memory) is True:
self.fire(rule)
return self.memory.facts[goal]
return NoneExplanation:
conflict_setis the "match" step of the cycle: rules not yet fired (refractoriness) whose conditions are all true.selectis conflict resolution in one line:maxwith a composite key (priority, number of conditions, recency), which applies the criteria in that order.fireasserts the conclusions in memory (with the rule as origin) and writes into the trace a full sentence with the actual values that satisfied each condition: the raw material of the "how?".runrepeats the cycle until the goal (decision) is set or the rules run out. When there is more than one candidate, it notes it in the trace: we will see "Conflict between R3, R4; R3 wins".whyfollows the chain of origins: if the fact was set by a rule, it explains the rule and, recursively, each of its conditions.howreturns the numbered trace.consultis simplified goal-driven backward chaining: it takes the rules that concludedecisionsorted the way the engine would sort them (priority, specificity), and for each one asks only for the facts it is missing, stopping as soon as an already-known condition fails (so as not to ask useless things). The first rule that turns out true is fired.answer(question, rule)is a function passed in: in production it will read from the keyboard or a form; in the lesson we will simulate it so we can run without a keyboard, and we pass it the rule so that it can answer "why?".
7.3 The returns and warranties rules
def returns_kb():
kb = KnowledgeBase()
kb.add(Rule("R1", [("defective", "==", True), ("new_product", "==", True),
("days_since_delivery", "<=", 1095)],
{"decision": "warranty", "return_shipping": "free"}, priority=10,
justification="3-year legal guarantee for new products; NovaMarket pays the shipping"))
kb.add(Rule("R2", [("defective", "==", True), ("days_since_delivery", ">", 1095)],
{"decision": "denied", "reason": "outside the legal guarantee period"}, priority=9,
justification="after 3 years, paid technical service is offered"))
kb.add(Rule("R3", [("category", "in", ("hygiene", "sealed")), ("opened", "==", True)],
{"decision": "denied", "reason": "opened hygiene or sealed product"}, priority=8,
justification="exception to withdrawal: sealed and hygiene products are not returnable once opened"))
kb.add(Rule("R4", [("days_since_delivery", "<=", 14), ("used", "==", False),
("original_label", "==", True)],
{"decision": "withdrawal_refund", "return_shipping": "paid by the customer"}, priority=5,
justification="14-day right of withdrawal with an unused, labelled product"))
kb.add(Rule("R5", [("days_since_delivery", "<=", 14), ("used", "==", False),
("original_label", "==", True), ("previous_returns", ">=", 5)],
{"decision": "human_review", "reason": "pattern of frequent returns"}, priority=5,
justification="more specific than R4: case 3 (fraud) calls for a human look"))
kb.add(Rule("R6", [("days_since_delivery", "<=", 14), ("used", "==", True)],
{"decision": "human_review", "reason": "used product within the period: assess depreciation"},
priority=4, justification="a person decides whether a full or partial refund applies"))
kb.add(Rule("R7", [("days_since_delivery", "<=", 14), ("original_label", "==", False)],
{"decision": "human_review", "reason": "original label missing"}, priority=4,
justification="the policy requires the label; a person assesses the exception"))
kb.add(Rule("R8", [("days_since_delivery", ">", 14), ("defective", "==", False)],
{"decision": "denied", "reason": "outside the withdrawal period and no defect"}, priority=2,
justification="Diego's rule: used or not, after 14 days without a defect nothing applies"))
return kbRead the priorities as the hierarchy of the policy: first the warranty (R1, R2: a defect is dealt with regardless of withdrawal, resolving the clash of 06-01), then the exceptions to withdrawal (R3), then ordinary withdrawal (R4) and its variants that call for human review (R5-R7, in line with the human-in-the-loop of 02-04: the denial or partial refund of a used product is not automated), and finally Diego's rule (R8). Note that R5 and R4 have the same priority: specificity will decide between them.
7.4 Test cases
CASES = {
"A NovaBrew coffee maker": dict(days_since_delivery=9, used=False, original_label=True, defective=False, new_product=True, category="general", opened=True, previous_returns=0),
"B NovaClean vacuum cleaner": dict(days_since_delivery=40, used=True, original_label=False, defective=True, new_product=True, category="general", opened=True, previous_returns=1),
"C NovaSmile toothbrush": dict(days_since_delivery=5, used=False, original_label=True, defective=False, new_product=True, category="hygiene", opened=True, previous_returns=0),
"D NovaSound headphones": dict(days_since_delivery=20, used=True, original_label=True, defective=False, new_product=True, category="general", opened=True, previous_returns=2),
"E NovaView monitor": dict(days_since_delivery=3, used=False, original_label=True, defective=False, new_product=True, category="general", opened=False, previous_returns=7),
}
for name, data in CASES.items():
engine = InferenceEngine(returns_kb(), WorkingMemory(data))
decision = engine.run("decision")
print(f"{name}: decision = {decision}, {engine.memory.facts.get('return_shipping') or engine.memory.facts.get('reason')}")
print(engine.how())
engine = InferenceEngine(returns_kb(), WorkingMemory(CASES["C NovaSmile toothbrush"]))
engine.run("decision")
print("Why decision?\n ", engine.why("decision"))Output:
A NovaBrew coffee maker: decision = withdrawal_refund, paid by the customer
1. Applied R4 because days_since_delivery=9 (<= 14), used=False (== False), original_label=True (== True) → {'decision': 'withdrawal_refund', 'return_shipping': 'paid by the customer'}. Justification: 14-day right of withdrawal with an unused, labelled product
B NovaClean vacuum cleaner: decision = warranty, free
1. Applied R1 because defective=True (== True), new_product=True (== True), days_since_delivery=40 (<= 1095) → {'decision': 'warranty', 'return_shipping': 'free'}. Justification: 3-year legal guarantee for new products; NovaMarket pays the shipping
C NovaSmile toothbrush: decision = denied, opened hygiene or sealed product
1. Conflict between R3, R4; R3 wins
2. Applied R3 because category='hygiene' (in ('hygiene', 'sealed')), opened=True (== True) → {'decision': 'denied', 'reason': 'opened hygiene or sealed product'}. Justification: exception to withdrawal: sealed and hygiene products are not returnable once opened
D NovaSound headphones: decision = denied, outside the withdrawal period and no defect
1. Applied R8 because days_since_delivery=20 (> 14), defective=False (== False) → {'decision': 'denied', 'reason': 'outside the withdrawal period and no defect'}. Justification: Diego's rule: used or not, after 14 days without a defect nothing applies
E NovaView monitor: decision = human_review, pattern of frequent returns
1. Conflict between R4, R5; R5 wins
2. Applied R5 because days_since_delivery=3 (<= 14), used=False (== False), original_label=True (== True), previous_returns=7 (>= 5) → {'decision': 'human_review', 'reason': 'pattern of frequent returns'}. Justification: more specific than R4: case 3 (fraud) calls for a human look
Why decision?
'decision' = 'denied' was set by R3 (exception to withdrawal: sealed and hygiene products are not returnable once opened); its conditions: 'category' = 'hygiene' is input data; 'opened' = True is input dataThe five cases cover the policy: A, a clean withdrawal (the customer pays the return shipping); B, the defective vacuum cleaner at 40 days, used and without its label, goes through the warranty with free shipping: R1 beats everything else, and the clash of 06-01 has disappeared by priority; C, the opened hygiene toothbrush: R3 and R4 are both applicable (conflict) and R3 wins by priority, so the inconsistency of exercise 3 of 06-01 is resolved and documented in the trace; D, Diego's rule; E, the same case as A but with 7 previous returns: R4 and R5 tie on priority and R5 wins by specificity, sending it to human review. The call to why shows the explanation chained all the way back to the input data.
7.5 Question interface: goal-driven consultation
QUESTIONS = {
"defective": "Does the product have a manufacturing or functional defect? (y/n)",
"new_product": "Was it sold as a new product (not refurbished)? (y/n)",
"days_since_delivery": "How many days have passed since delivery?",
"category": "Product category (general/hygiene/sealed)",
"opened": "Is it opened or unsealed? (y/n)",
"used": "Does it show signs of use? (y/n)",
"original_label": "Does it still have the original label? (y/n)",
"previous_returns": "How many previous returns does the customer have?",
}
def parse_answer(text):
t = text.strip().lower()
if t in ("y", "yes"): return True
if t in ("n", "no"): return False
try: return int(t)
except ValueError: return t
def simulated_answer(answers):
"""Return an 'answer' function that reads from a dict instead of input()
(so the session can run without a keyboard). With '?' it explains why it asks."""
def answer(question, rule):
attribute = next(k for k, v in QUESTIONS.items() if v == question)
print(f" System: {question}")
if answers.get(attribute + "?"):
print(f" User: why?\n System: because I am trying to apply {rule.name}: {rule}")
text = answers[attribute]
print(f" User: {text}")
return parse_answer(text)
return answer
# NovaBook laptop returned with partial data: the system asks for what it needs
session = {"defective": "n", "days_since_delivery": "10", "category": "general", "used": "y", "used?": True}
engine = InferenceEngine(returns_kb(), WorkingMemory({}))
decision = engine.consult("decision", QUESTIONS, simulated_answer(session))
print(f" Decision: {decision} ({engine.memory.facts.get('reason', '')})")
print(" How?\n" + engine.how())Output:
System: Does the product have a manufacturing or functional defect? (y/n)
User: n
System: Product category (general/hygiene/sealed)
User: general
System: How many days have passed since delivery?
User: 10
System: Does it show signs of use? (y/n)
User: why?
System: because I am trying to apply R5: R5 [prio 5]: IF days_since_delivery <= 14 ∧ used == False ∧ original_label == True ∧ previous_returns >= 5 THEN {'decision': 'human_review', 'reason': 'pattern of frequent returns'}
User: y
Decision: human_review (used product within the period: assess depreciation)
How?
1. Applied R6 because days_since_delivery=10 (<= 14), used=True (== True) → {'decision': 'human_review', 'reason': 'used product within the period: assess depreciation'}. Justification: a person decides whether a full or partial refund appliesFollow the thread: the system starts with the highest-priority rule (R1) and asks whether it is defective; with "no", R1 and R2 are ruled out with no further questions. It moves on to R3 and asks the category; "general" rules it out, and it does not ask whether it is opened (the already-known condition fails). It moves on to R5 (same priority as R4 but more specific): it asks the days and whether it is used; the user asks "why?" and the explanation module answers with the rule it is trying; "yes, used" rules out R5 and R4, and R6 holds: human review for a used product within the period. In total 4 questions out of a possible 8: goal-driven chaining asks only what is relevant, like MYCIN. To use it with a keyboard, just pass answer=lambda question, rule: parse_answer(input(question + " ")).
- Real tools
There is no need to write the engine every time. In production people use:
- CLIPS (NASA, 1980s, in C): the classic rule shell with forward chaining and the Rete algorithm, which matches rules and facts efficiently when there are thousands of both; Lisp-like syntax.
- Drools (Java, open source): a business rule engine (BRMS) widely used in banking and insurance, with decision tables and the DMN standard we will see in 06-04.
experta(Python, successor of PyKnow): rules as methods decorated with@Rule, facts asFactobjects, an engine with salience (priority). It is not in this course's environment, but our code translates almost line by line into its concepts:Rule↔@Rule,WorkingMemory↔declare,priority↔salience.
With what you have learned here, the documentation of any of them will feel familiar; 07-03 places them in the ecosystem.
Common Mistakes and Tips
- Rules with no priority and no thought-out order. It is the mistake of 06-01: two applicable rules and the result depends on the order they were written in. Decide the hierarchy explicitly (warranty > exceptions > withdrawal > general rule) and write it into the justification.
- Rules that "know" too much. A rule mixing eight conditions is unreadable and fragile. Break it down into intermediate facts (
under_warranty,within_period) as we did in 06-01, and into rules of two or three conditions. - Ambiguous attributes. Is
opened"unsealed" or "used"? Diego distinguishes; the data dictionary must distinguish. Expert systems die of imprecise vocabulary before they die of logic. - Forgetting the "no applicable rule" case. Our engine returns
None; in production that must translate into human review, not a denial by default (closed world, 06-01) nor an error. - Not maintaining the case suite. Every new rule can change old decisions. Cases A-E are the test set; add every real contentious case that appears.
- Entrusting the policy to the machine. The system applies rules; the person responsible for the rules is Diego, and for the regulations, a legal professional. Make sure the trace always says which rule and which version decided.
- Automating what requires a person. Partial refunds, doubtful denials and fraud patterns go to
human_review; that is design, not cowardice (02-04).
Exercises
Exercise 1. Diego remembers an exception: during the Christmas campaign the withdrawal period is extended to 30 days for orders delivered in December. Add to the working memory an attribute christmas_campaign (True/False) and write the rule or rules needed without breaking the existing ones. What priority do you give them and why? Check with case D (headphones, 20 days) with christmas_campaign=True and with False.
Exercise 2. Run consult with the session {"defective": "y", "new_product": "y", "days_since_delivery": "400"} and note how many questions it asks and what it decides. Then change days_since_delivery to "1200". Explain, looking at the order of the rules in consult, why in both cases the system asks nothing about category, use or label.
Exercise 3. Design, without code, the knowledge acquisition session with Diego to add to the system the extended commercial warranties that NovaMarket sells with some products (for example, 2 additional years for laptops). Indicate: (a) three questions you would ask him, (b) which new attributes the working memory would need, (c) which test cases you would add to the A-E suite and (d) what could go wrong in the interaction with R1 and R2.
Solutions
Solution 1. A clean way: R4c, priority 5, conditions christmas_campaign == True, days_since_delivery <= 30, used == False, original_label == True → withdrawal_refund; and so that Diego's rule (R8) does not deny between days 15 and 30 during the campaign, either add to it the condition christmas_campaign == False (and an R8c with christmas_campaign == True and days_since_delivery > 30), or, more simply, place R4c above R8, which already happens (priority 5 versus 2), because the engine stops once decision is set. With christmas_campaign=True, case D (used) still does not enter R4c (it requires unused) and needs an R6c analogous to R6 with 30 days (human review); with False, R8 denies as before. The lesson: every temporary exception touches several rules; it is best to group them and test them together.
Solution 2. With 400 days: it asks defective (yes), new_product (yes) and days_since_delivery (400): three questions, R1 holds → warranty, free shipping. With 1200 days: the same three questions; R1 fails on the third condition, it moves to R2 (defective == True, days > 1095), which already has all its facts and holds → denied, "outside the legal guarantee period". consult goes through the rules in descending priority and fires the first one that holds: since R1 and R2 (priorities 10 and 9) are settled with those three facts, it never reaches R3-R8, whose questions (category, use, label) are irrelevant for a defective product.
Solution 3. (a) "Which products carry an extended warranty and how is that known from the order?"; "does the extended one cover the same as the legal one (defects) or accidents too?"; "who pays the shipping and who repairs, NovaMarket or the manufacturer?". (b) extended_warranty_months (or extended_warranty_end), coverage_type, perhaps defect_cause (manufacturing/accident). (c) A NovaBook laptop defective at 4 years with a 2-year extension → warranty (extended); the same without the extension → denied by R2; accidental damage with an extension that does not cover it → denied or human review. (d) R2 denies from 1095 days before considering the extension: the new rule would need a priority between R1 and R2 (for example 9.5) or R2 would need the condition that no extension is in force; and the dictionary must make clear that new_product and extended_warranty are different attributes.
Conclusion
In this lesson we turned the engine of 06-01 into an expert system. We saw what it is (explicit knowledge separated from reasoning) and where it comes from (DENDRAL, MYCIN, XCON), and we walked through its architecture: knowledge base, working memory, inference engine with conflict resolution (refractoriness, priority, specificity, recency), explanation module (why? and how?), knowledge acquisition and interface. We described the knowledge engineering process (interviews with Diego, extraction and formalisation of rules, validation with cases, maintenance with an owner), and we compared advantages and drawbacks and the expert system against an ML model: written rules versus learned rules, with the tree of 04-04 as a bridge and the combination of both as the destination. In code, Rule, KnowledgeBase, WorkingMemory and InferenceEngine decided five NovaMarket returns and warranties cases with an explanation trace (the NovaClean warranty by priority, the NovaSmile toothbrush by priority over withdrawal, the NovaView monitor by specificity), and consult asked only what was necessary, "why?" included.
But notice how we handled uncertainty: we did not. defective is true or false; human_review is the escape hatch when we do not want to decide. For returns that is acceptable, because the policy is clear-cut. For the incident diagnosis of case 9 it is not: a dented package suggests transport damage but also a picking error with poor packaging; a delay points to Getafe but also to the courier; and incidents.csv has half its causes unfilled. There, crisp rules fail and we need to reason with degrees of belief: the certainty factors MYCIN invented, fuzzy logic and, above all, probability, Bayes' theorem and the Bayesian networks that relaunched AI in the 1990s (01-01). That is the topic of the next lesson, 06-03.
Fundamentals of Artificial Intelligence (AI)
Module 1: Introduction to Artificial Intelligence
Module 2: Basic Principles of AI
- Fundamental Concepts: Agents, Environments and Rationality
- Types of Artificial Intelligence
- Data as the Raw Material of AI
- Ethics and Considerations in AI
Module 3: Algorithms in AI
- Introduction to Algorithms
- Search Algorithms
- Adversarial Search: Games and Minimax
- Optimization Algorithms
Module 4: Machine Learning
- Basic Concepts of Machine Learning
- Types of Machine Learning
- Data Preparation and Feature Engineering
- Machine Learning Algorithms
- Model Evaluation and Validation
- Overfitting, Regularization and Hyperparameter Tuning
Module 5: Neural Networks and Deep Learning
- Introduction to Neural Networks
- Neural Network Architecture
- How a Network Learns: Gradient Descent and Backpropagation
- Deep Learning and Its Applications
- Transformers, Large Language Models and Generative AI
Module 6: Logic and Expert Systems
- Logic in AI
- Expert Systems
- Reasoning under Uncertainty: Probability and Bayesian Networks
- Applications of Expert Systems
Module 7: Tools and Programming Languages in AI
- Programming Languages for AI
- Scientific Python: NumPy, pandas and Matplotlib
- Popular Tools and Libraries
- Development Environments
Module 8: Projects and Case Studies
Module 9: Exercises and Practice
- Algorithm Exercises
- Machine Learning Practice
- Neural Network Projects
- Capstone Project: from Idea to Prototype
