How Expert System Rules Work in Animals.ipynb: A Deep Dive into Symbolic AI

Expert system rules in Animals.ipynb work through two inference paradigms: a custom backward-chaining shell that recursively evaluates If/AND/OR rule trees, and a forward-chaining engine using Experta's @Rule decorators that automatically fires rules as facts are asserted.

The Animals.ipynb notebook in Microsoft's AI-For-Beginners repository demonstrates classical symbolic AI through a practical animal identification system. Located at lessons/2-Symbolic/Animals.ipynb, this educational implementation shows how domain knowledge can be encoded as explicit rules and reasoned over without any machine learning—core principles of expert systems that dominated AI research before the neural network revolution.

Backward-Chaining: Goal-Driven Inference

Rule Representation with Python Objects

The backward-chaining implementation defines rules using lightweight Python classes that store structural information without embedded logic. Four primitives form the foundation:

  • Ask – Wraps user prompts for yes/no or multiple-choice questions (lines 38–50)
  • If – Contains a list of sub-goals that must all be satisfied
  • AND / OR – Logical combinators inherited from a Content base class

These classes appear in lessons/2-Symbolic/Animals.ipynb around lines 55–62. They function purely as containers; the actual reasoning logic resides in the KnowledgeBase evaluator.

The Rules Dictionary Structure

Rules map goal names to expression trees in a plain Python dictionary. Each key follows the pattern animal:species or names intermediate attributes:

rules = {
    'animal:giraffe': If(['ungulate', 'long neck', 'long legs', 'pattern:dark spots']),
    'animal:tiger': If(['mammal', 'carnivor', 'color:red-brown', 'pattern:dark stripes']),
    'animal:cheetah': If(['mammal', 'carnivor', 'color:tawny', 'pattern:dark spots', 'fast']),
    'ungulate': If(['mammal', 'hooves']),
    'ungulate': If(['mammal', 'chew cud']),
    'carnivor': If(['mammal', Ask('has sharp teeth?'), Ask('has claws?'), Ask('has forward-looking eyes?')]),
    'carnivor': If(['mammal', Ask('eats meat?')])
}

This structure appears around lines 80–95. Multiple rules can share the same key (e.g., two paths to ungulate and carnivor), enabling disjunctive reasoning.

KnowledgeBase Implementation

The KnowledgeBase class (lines 119–146) provides the inference engine through two methods:

Method Purpose
get(name) Retrieves facts from working memory or triggers rule evaluation
eval(expr, field=None) Recursively interprets expression trees

The core evaluation loop (lines 148–166) implements classic backward chaining:

class KnowledgeBase:
    def __init__(self, rules):
        self.rules = rules
        self.memory = {}  # Working memory for cached facts

        
    def get(self, name):
        if name in self.memory:           # Return cached result

            return self.memory[name]
        
        # Find matching rules for this goal

        for key, value in self.rules.items():
            if key.startswith(name):
                result = self.eval(value, field=name.split(':')[1] if ':' in name else None)
                if result:
                    self.memory[name] = result
                    return result
        return None
    
    def eval(self, expr, field=None):
        if isinstance(expr, Ask):
            return expr.ask()              # Prompt user interactively

        elif isinstance(expr, If):
            return all(self.eval(x) for x in expr.parts)  # AND semantics

        elif isinstance(expr, str):
            return self.get(expr)          # Recursively solve sub-goal

        elif isinstance(expr, list):
            return any(self.get(x) for x in expr)  # OR semantics

When kb.get('animal') is called, the engine:

  1. Discovers all animal:* keys in the rules dictionary
  2. Attempts each in order until one succeeds
  3. Recursively evaluates prerequisites, asking the user only when encountering Ask nodes
  4. Caches proven facts in self.memory to avoid redundant queries

Running the Backward-Chaining System

from ipywidgets import interact_manual

# Initialize with rule set

kb = KnowledgeBase(rules)

# Trigger inference—system will prompt for needed information

@interact_manual()
def find_animal():
    animal = kb.get('animal')
    print(f"\n*** Inferred animal: {animal} ***")

The interactive session prompts for characteristics only when required to distinguish between possible animals, demonstrating lazy, goal-driven reasoning.

Forward-Chaining: Data-Driven Inference with Experta

Declarative Rule Syntax

The same knowledge base is reimplemented using Experta, a Python production rule engine inspired by CLIPS. Rules are declared as decorated methods within a KnowledgeEngine subclass starting at line 304:

from experta import KnowledgeEngine, Fact, Rule, OR, AND, MATCH

class Animals(KnowledgeEngine):
    
    @Rule(OR(
           AND(Fact('sharp teeth'), Fact('claws'), Fact('forward looking eyes')),
           Fact('eats meat')))
    def carnivor(self):
        self.declare(Fact('carnivor'))
    
    @Rule(Fact('mammal'), Fact('carnivor'),
          Fact(color='red-brown'), Fact(pattern='dark stripes'))
    def tiger(self):
        self.declare(Fact(animal='tiger'))
    
    @Rule(Fact(animal=MATCH.a))
    def print_result(self, a):
        print(f"Animal is {a}")

Key Experta constructs include:

  • @Rule(...) – Decorator specifying activation conditions
  • Fact(pattern) – Matches asserted facts; can use key=value for fields or MATCH.variable for binding
  • AND/OR – Logical combinations of conditions
  • self.declare(Fact(...)) – Asserts new facts, potentially triggering additional rules

Forward Inference Execution

The engine maintains a FactList and executes the matching-selecting-acting cycle:


# Initialize engine

engine = Animals()
engine.reset()  # Clear and prepare fact base

# Assert initial observations (data-driven start)

engine.declare(
    Fact('gives milk'),           # From observation

    Fact('sharp teeth'),          # From observation

    Fact('claws'),                # From observation

    Fact('forward looking eyes'), # From observation

    color='red-brown',            # Field-style fact

    pattern='dark stripes'        # Field-style fact

)

# Run inference—rules fire automatically until quiescence

engine.run()

Output:


Animal is tiger

The run() method (implemented in Experta's base class) performs:

  1. Matching: Compare all rules against current facts
  2. Selection: Resolve conflicts using salience and recency
  3. Acting: Execute selected rule's method body, potentially asserting new facts
  4. Iteration: Repeat until no rules match (fixed-point reached)

Comparison: Backward vs. Forward Chaining

Aspect Custom Backward-Chaining Experta Forward-Chaining
Rule format Python objects (If, AND, OR, Ask) Decorated methods with @Rule
Control flow Goal-driven; asks what's needed Data-driven; processes all consequences
User interaction Embedded via Ask nodes External; facts must be pre-asserted
Implementation ~150 lines custom code Library-based (experta dependency)
Traceability Explicit recursive evaluation Engine-managed matching cycle
Extensibility Modify rules dictionary Add methods to KnowledgeEngine subclass

Both implementations occupy the same notebook to illustrate that inference direction is an architectural choice, not inherent to the knowledge itself.

Extending the Rule Base

Adding a New Animal: Lion

Backward-chaining approach—extend the dictionary:

rules['animal:lion'] = If([
    'mammal',
    'carnivor', 
    'color:red-brown',
    'pattern:dark spots',
    Ask('has a mane?')
])

Forward-chaining approach—add a method to the Animals class:

@Rule(Fact('mammal'), Fact('carnivor'),
      Fact(color='red-brown'), Fact(pattern='dark spots'),
      Fact('mane'))
def lion(self):
    self.declare(Fact(animal='lion'))

The structural equivalence demonstrates how expert systems separate knowledge (rules about lions) from control (how to apply those rules).

Source Locations and Dependencies

Component File Location Lines
Core primitives (Ask, If, AND, OR) lessons/2-Symbolic/Animals.ipynb 38–62
Rules dictionary definition lessons/2-Symbolic/Animals.ipynb 80–95
KnowledgeBase class lessons/2-Symbolic/Animals.ipynb 119–166
Experta Animals engine lessons/2-Symbolic/Animals.ipynb 304–339
experta dependency environment.yml package list

The repository requires experta for forward-chaining functionality, specified in the project's environment configuration.

Summary

  • Expert system rules in Animals.ipynb encode domain knowledge as structured data—either as nested Python objects for backward chaining or as decorated methods for forward chaining.

  • Backward chaining uses a lightweight KnowledgeBase class that recursively evaluates If/AND/OR expressions, prompting users only for information needed to prove a specific goal.

  • Forward chaining leverages the experta library's production rule engine, where @Rule decorators declaratively specify conditions and run() automatically propagates inferences from asserted facts.

  • Both approaches demonstrate symbolic AI fundamentals: explicit knowledge representation, logical inference, and separation of domain rules from control procedures.

Frequently Asked Questions

What is the difference between backward and forward chaining in expert systems?

Backward chaining is goal-driven: it starts with a hypothesis (e.g., "is it a tiger?") and works backward to find supporting evidence, asking questions only when necessary. Forward chaining is data-driven: it starts with known facts and applies rules to derive new conclusions, continuing until no more inferences are possible. Animals.ipynb implements both to illustrate this architectural trade-off.

How does the KnowledgeBase.eval() method handle logical combinations?

The eval() method recursively interprets expression trees by type: Ask nodes trigger user interaction, If nodes require all sub-expressions to be true (AND semantics), and plain lists are evaluated with OR semantics (any element true). This simple dispatch logic—around 20 lines in lessons/2-Symbolic/Animals.ipynb lines 148–166—powers the entire backward-chaining inference.

Can the rule base be modified without changing the inference engine?

Yes. In the backward-chaining implementation, adding or modifying entries in the rules dictionary immediately changes system behavior without touching KnowledgeBase. Similarly, in the Experta forward-chaining version, adding new @Rule methods extends the engine's capabilities. This knowledge-engineering separation is a defining characteristic of expert systems.

Why does the notebook include two implementations of the same knowledge base?

The dual implementation serves pedagogical purposes: the custom backward-chaining shell (~150 lines) reveals how inference engines work internally, while the Experta version demonstrates production-grade features like automatic matching and conflict resolution. Students can compare explicit control flow against declarative specification, understanding that the same logical rules support multiple execution strategies.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →