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

> Explore how expert system rules work in symbolic AI within Animals.ipynb. Discover backward-chaining and forward-chaining engines for intelligent rule evaluation.

- Repository: [Microsoft/AI-For-Beginners](https://github.com/microsoft/AI-For-Beginners)
- Tags: deep-dive
- Published: 2026-08-20

---

**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:

```python
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:

```python
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

```python
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:

```python
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**:

```python

# 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:

```python
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:

```python
@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`](https://github.com/microsoft/AI-For-Beginners/blob/main/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.