How to Implement Rule-Based Reasoning with Semantica's ReteEngine: A Complete Guide
Semantica's ReteEngine implements incremental pattern matching through a network of AlphaNode, BetaNode, and TerminalNode classes to perform efficient forward-chaining inference over working memory facts.
Rule-based reasoning enables AI systems to derive new knowledge from existing facts using declarative if-then logic. The semantica-agi/semantica repository provides a production-ready ReteEngine in semantica/reasoning/rete_engine.py that implements the classic Rete algorithm for efficient pattern matching. This guide demonstrates how to construct inference networks, propagate facts, and execute rules using the engine's core APIs.
Understanding the Rete Network Architecture
The Rete algorithm optimizes rule evaluation by sharing nodes across multiple patterns and maintaining partial matches in working memory. Semantica's implementation organizes the network into three distinct node types that collaborate to filter, join, and activate rule conclusions.
AlphaNode (Condition Filter)
AlphaNode instances represent the first layer of the network, storing individual rule conditions and the facts that satisfy them. According to the source code in semantica/reasoning/rete_engine.py, each alpha node maintains a condition pattern and a collection of matching facts【/semantica/reasoning/rete_engine.py#L64-L78】. When facts enter the system, the engine evaluates them against alpha nodes to determine which conditions they satisfy.
BetaNode (Conjunctive Join)
BetaNode objects perform the critical join operation that enables conjunctive rule bodies. These nodes take inputs from two predecessor nodes—typically alpha nodes or other beta nodes—and produce combined matches when their constraints align. The implementation in semantica/reasoning/rete_engine.py handles the cross-product of partial matches to build complex logical conjunctions【/semantica/reasoning/rete_engine.py#L85-L100】.
TerminalNode (Rule Activation)
TerminalNode instances represent the rule head and trigger when all conditions in a rule's body are satisfied. These nodes store the rule's conclusion and record Match instances whenever their predecessor chain produces valid combinations【/semantica/reasoning/rete_engine.py#L107-L115】. Terminal nodes serve as the output collection points for the inference engine.
Building and Initializing the ReteEngine
The ReteEngine class serves as the primary interface for rule-based reasoning. Initialize the engine to create an empty working memory and an unpopulated network structure【/semantica/reasoning/rete_engine.py#L120-L135】.
from semantica.reasoning import ReteEngine, Rule, Fact
# Initialize the engine with empty working memory
engine = ReteEngine()
# The engine now contains:
# - self.network: empty node structure
# - self.facts: list for working memory storage
# - self.tracker: progress monitoring for diagnostics
The engine also provides reset() to clear working memory and all node caches, plus get_network_stats() to retrieve node counts and fact storage metrics【/semantica/reasoning/rete_engine.py#L62-L73】.
Defining Rules and Facts
Rules in Semantica follow a declarative pattern using the Rule dataclass defined in semantica/reasoning/reasoner.py. Each rule requires a unique identifier, name, list of conditions, and a conclusion pattern【/semantica/reasoning/reasoner.py#L24-L33】.
# Define a rule: IF Person(x) AND HasPet(x, y) THEN OwnerOfPet(x, y)
rule = Rule(
rule_id="r1",
name="Owner inference",
conditions=[
("Person", ["?x"]), # First condition with variable binding
("HasPet", ["?x", "?y"]), # Second condition sharing variable ?x
],
conclusion=("OwnerOfPet", ["?x", "?y"]),
)
# Create facts to populate working memory
fact1 = Fact("f1", "Person", ["Alice"])
fact2 = Fact("f2", "HasPet", ["Alice", "Fluffy"])
Fact objects encapsulate assertions with unique identifiers, predicate types, and argument lists that ground the logic to specific values.
Constructing the Network
Before the engine can perform inference, rules must be compiled into the Rete network structure. The build_network() method iterates over rule conditions to instantiate the appropriate node topology【/semantica/reasoning/rete_engine.py#L92-L122】.
# Compile rules into alpha/beta/terminal node structure
engine.build_network([rule])
Internally, _add_rule_to_network performs the following construction steps:
- Creates one AlphaNode for each condition in the rule body
- Chains BetaNode instances between alpha nodes when multiple conditions exist (handling the conjunctive logic)
- Attaches a TerminalNode to the final join node to store the rule's conclusion
Alternatively, rules can be added incrementally using add_rule(), which invokes the same network construction logic for individual rules.
Propagating Facts and Pattern Matching
Fact insertion triggers the incremental pattern matching that makes the Rete algorithm efficient. The add_fact() method stores facts in working memory and initiates network propagation through _propagate_fact()【/semantica/reasoning/rete_engine.py#L24-L65】.
# Insert facts into working memory and propagate through network
engine.add_fact(fact1)
engine.add_fact(fact2)
The propagation algorithm executes recursively:
- Alpha evaluation: The fact visits all alpha nodes; matching nodes store the fact in their local memory
- Beta joining: Alpha nodes with new facts notify successor beta nodes, which attempt joins with counterparts
- Terminal activation: When beta joins succeed, they propagate to terminal nodes, which instantiate Match objects
This incremental approach ensures the engine never re-evaluates conditions against the entire fact base, maintaining performance even as working memory grows.
Executing Inferred Conclusions
After propagation completes, retrieve and execute rule activations using the engine's query methods. match_patterns() collects all Match instances from terminal nodes across the network, while execute_matches() evaluates the associated conclusions【/semantica/reasoning/rete_engine.py#L66-L80】【/semantica/reasoning/rete_engine.py#L14-L27】.
# Retrieve all pattern matches from terminal nodes
matches = engine.match_patterns()
# Execute matched rules to obtain conclusions
results = engine.execute_matches(matches)
print("Inferred conclusions:", results)
# Output: [('OwnerOfPet', ['Alice', 'Fluffy'])]
The current implementation returns the stored conclusion directly, though the architecture supports custom handler logic for complex action execution.
Advanced Customization
The engine's design separates generic Rete mechanics from domain-specific logic through method overriding. For custom condition evaluation, override the _matches() method in AlphaNode subclasses. For specialized join semantics, override _can_join() in BetaNode implementations.
The Reasoner façade in semantica/reasoning/reasoner.py provides a higher-level interface that orchestrates rule parsing, fact management, and forward-chaining invocation【/semantica/reasoning/reasoner.py#L24-L38】, though direct ReteEngine usage offers maximum control over network topology and execution semantics.
Summary
- ReteEngine in
semantica/reasoning/rete_engine.pyprovides the core forward-chaining implementation using alpha, beta, and terminal nodes. - AlphaNode filters individual facts against single conditions, while BetaNode joins partial matches to handle conjunctive rule bodies.
- Rule and Fact dataclasses in
reasoner.pydefine the declarative logic and ground assertions respectively. build_network()compiles rules into the Rete topology;add_fact()triggers incremental pattern matching via_propagate_fact().match_patterns()andexecute_matches()retrieve and materialize inferred conclusions from terminal node activations.
Frequently Asked Questions
How does Semantica's ReteEngine handle complex multi-condition rules?
The engine chains BetaNode instances between AlphaNode outputs to handle conjunctions. Each beta node joins matches from its two predecessors, enabling the network to represent arbitrary logical AND combinations across rule conditions【/semantica/reasoning/rete_engine.py#L85-L100】.
What is the difference between build_network() and add_rule()?
build_network() accepts a list of rules and compiles them into the node structure in a single operation, while add_rule() processes individual rules incrementally. Both methods invoke _add_rule_to_network() internally to create the alpha-beta-terminal node chains【/semantica/reasoning/rete_engine.py#L92-L122】.
Can the ReteEngine handle negation or disjunction in rules?
The current implementation focuses on positive conjunctions through BetaNode joins. Custom logic would require extending the node classes; override _can_join() in BetaNode for negation-as-failure semantics, or create multiple terminal paths from different alpha nodes for disjunctive logic.
How do I reset the engine state between inference sessions?
Call engine.reset() to clear the working memory (self.facts) and purge all cached partial matches from alpha and beta nodes. This returns the network to its initial state without requiring reconstruction of the rule topology【/semantica/reasoning/rete_engine.py#L62-L73】.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →