semantica.reasoning derives new knowledge from existing facts using logical rules:
  • Six reasoning engines: forward chaining, Rete, SPARQL, Datalog, temporal, and LLM-powered GraphReasoner
  • Every engine produces explainable inference paths: traceable chains of rules and facts
  • DatalogReasoner guarantees termination via semi-naive fixpoint evaluation
  • TemporalReasoningEngine implements all 13 Allen interval algebra relations
  • ExplanationGenerator produces step-by-step natural-language justifications

Exported Classes

ClassRole
ReasonerForward-chaining inference: add_fact, add_rule, forward_chain, backward_chain, infer_facts
GraphReasonerLLM-powered reasoning over a KG dict: answers natural language queries via reason(graph, query)
ReteEngineRete pattern matching: build_network, add_fact, match_patterns, execute_matches
SPARQLReasonerRule-based SPARQL query expansion via execute_query, expand_query, infer_results
DatalogReasonerRecursive Horn clause rules with semi-naive fixpoint: add_fact, add_rule, derive_all, query
TemporalReasoningEngineAll 13 Allen interval algebra relations: relation(a, b), overlaps, contains, active_at
ExplanationGeneratorStep-by-step explanations via generate_explanation(inference_result)
RuleIF/THEN rule: {rule_id, name, conditions, conclusion, rule_type, confidence, priority}
FactWorking-memory fact: {fact_id, predicate, arguments}
InferenceResultSingle derived conclusion: {conclusion, rule_used, premises, confidence}

Which Engine Should I Use?

Reasoner

IF/THEN rules, forward and backward chaining. Start here: covers 90% of use cases. No query language required.

GraphReasoner

Natural language queries over a knowledge graph via LLM. No SPARQL or rules: just ask a question.

DatalogReasoner

Recursive Horn clause rules with guaranteed termination. Use for complex multi-hop transitive rules.

ReteEngine

Rete pattern matching for high-frequency inference. Use when you need to match many facts against many rules simultaneously.

SPARQLReasoner

SPARQL query expansion and rule-based inference. Use when you’re working with RDF/OWL data.

TemporalReasoningEngine

All 13 Allen interval algebra relations. Use for time-aware reasoning: overlaps, before/after, during, contains.

Getting Started

The most common pattern is the Reasoner for IF/THEN forward-chaining:
from semantica.reasoning import Reasoner, Rule, RuleType

reasoner = Reasoner()

# Add facts as strings in predicate(args) form
reasoner.add_fact("Manager(Alice)")
reasoner.add_fact("Employee(Alice)")

# Add an IF-THEN rule using the string form
reasoner.add_rule("IF Manager(?x) THEN HasAuthority(?x)")

# Run forward chaining: returns List[InferenceResult]
results = reasoner.forward_chain()
for r in results:
    print(r.conclusion)         # "HasAuthority(Alice)"
    print(r.confidence)         # 1.0
    if r.rule_used:
        print(r.rule_used.name) # name of the rule applied
Or build rules programmatically using the Rule dataclass:
from semantica.reasoning import Rule, RuleType

rule = Rule(
    rule_id="rule_001",
    name="manager_authority",
    conditions=["Manager(?x)"],
    conclusion="HasAuthority(?x)",
    rule_type=RuleType.IMPLICATION,
    confidence=0.9,
)
reasoner.add_rule(rule)

Reasoner (Forward/Backward Chaining)

Reasoner is the unified entry point for rule-based inference: iterates facts and rules to a fixpoint, then optionally proves a specific goal via backward chaining:
from semantica.reasoning import Reasoner, Rule, RuleType, InferenceResult

reasoner = Reasoner()

# Facts can be strings, KG entity dicts, or KG relationship dicts
reasoner.add_fact("Manager(John)")
reasoner.add_fact("Employee(John)")

# IF-THEN string form
reasoner.add_rule("IF Manager(?x) AND Employee(?x) THEN SeniorStaff(?x)")

# Forward chaining: iterates until fixpoint
results = reasoner.forward_chain()
for r in results:
    print(r.conclusion)   # e.g. "SeniorStaff(John)"
    print(r.premises)     # list of premise strings matched
    print(r.confidence)   # float

# Backward chaining: prove a specific goal
result = reasoner.backward_chain("SeniorStaff(John)", max_depth=10)
if result:
    print(f"Proven: {result.conclusion}")
    print(f"Premises: {result.premises}")

# infer_facts() loads facts and rules in one call, returns conclusion strings
conclusions = reasoner.infer_facts(
    facts=["Manager(Alice)", "Employee(Alice)"],
    rules=["IF Manager(?x) THEN HasAuthority(?x)"],
)
# → ["HasAuthority(Alice)"]

Reasoner Methods

MethodReturnsDescription
add_fact(fact)NoneAdd a string, entity dict, or relationship dict to working memory
add_rule(rule)RuleAdd a Rule object or IF-THEN string; rules are sorted by priority descending
forward_chain()List[InferenceResult]Derive all possible conclusions iteratively until fixpoint
backward_chain(goal, max_depth)InferenceResult | NoneProve a specific goal string, returns None if unprovable
infer_facts(facts, rules)List[str]Load facts and rules then run forward_chain(), returns conclusion strings
clear()NoneClear all facts and rules
reset()NoneAlias for clear()

Rule and Fact dataclass fields

from semantica.reasoning import Rule, Fact, RuleType

# Rule: all fields
rule = Rule(
    rule_id="rule_001",            # required: unique identifier
    name="manager_authority",      # required: display name
    conditions=["Manager(?x)"],    # list of condition strings
    conclusion="HasAuthority(?x)", # conclusion string
    rule_type=RuleType.IMPLICATION, # IMPLICATION | EQUIVALENCE | CONSTRAINT | TRANSFORMATION
    confidence=1.0,                 # default 1.0
    priority=0,                     # higher priority rules run first
)

# Fact: for working with the Rete engine directly
from semantica.reasoning import Fact
fact = Fact(
    fact_id="f001",                # required: unique identifier
    predicate="Manager",
    arguments=["John"],
    metadata={},
)

GraphReasoner

GraphReasoner uses an LLM to answer natural language queries over a knowledge graph dict: no SPARQL or rule authoring required:
from semantica.reasoning import GraphReasoner

# Initialize: uses openai by default; override via kwargs
reasoner = GraphReasoner(provider="openai", model="gpt-4o-mini")

kg = {
    "entities": [
        {"id": "alice",   "name": "Alice",   "type": "Person",       "properties": {"role": "CEO"}},
        {"id": "acme",    "name": "Acme Inc", "type": "Organization"},
    ],
    "relationships": [
        {"source": "alice", "target": "acme", "type": "leads"}
    ],
}

answer: str = reasoner.reason(
    graph=kg,
    query="Who leads Acme Inc. and what is their role?"
)
print(answer)
reason() converts the graph to a text context and calls the LLM with a structured prompt. Returns a plain string answer.

ReteEngine

High-performance Rete pattern matching for large rule sets:
from semantica.reasoning import ReteEngine, Rule, Fact, RuleType

engine = ReteEngine()

# Build the Rete network from a list of Rule objects
rules = [
    Rule(
        rule_id="r1",
        name="manager_authority",
        conditions=["Manager(?x)"],
        conclusion="HasAuthority(?x)",
    )
]
engine.build_network(rules)

# Add facts to working memory
engine.add_fact(Fact(fact_id="f1", predicate="Manager", arguments=["Alice"]))

# Match patterns and execute
matches = engine.match_patterns()
results = engine.execute_matches(matches)
# results is a list of conclusion values from matched rules

# Network statistics
stats = engine.get_network_stats()
# → {"total_nodes": N, "alpha_nodes": A, "beta_nodes": B, "terminal_nodes": T, "facts": F}

engine.reset()

ReteEngine Methods

MethodReturnsDescription
build_network(rules)NoneBuild the Rete network from a list of Rule objects
add_fact(fact)NoneAdd a Fact to working memory and propagate through the network
match_patterns(facts)List[Match]Match all patterns; optionally add facts before matching
execute_matches(matches)List[Any]Execute matched rules and return their conclusion values
reset()NoneClear facts and all node activation state
get_network_stats()dictReturn counts of alpha, beta, terminal nodes and facts

SPARQLReasoner

SPARQLReasoner extends SPARQL with inference rule expansion: add IF-THEN rules and they are automatically woven into queries before execution:
from semantica.reasoning import SPARQLReasoner

reasoner = SPARQLReasoner()

# Add an inference rule (IF-THEN string form)
reasoner.add_inference_rule("IF is_a(?x, Manager) THEN has_authority(?x)")

# Execute a query: returns SPARQLQueryResult
result = reasoner.execute_query("""
    PREFIX ex: <http://example.org/>
    SELECT ?person ?company WHERE {
        ?person ex:founded ?company .
        ?company ex:located_in ex:SiliconValley .
    }
""")

for row in result.bindings:
    print(row)   # each row is a dict of variable → {"value": ..., "type": ...}

# Expand a query with inference rules (returns modified query string)
expanded = reasoner.expand_query("SELECT ?x WHERE { ?x a :Manager }")

# Infer additional bindings from existing results
enriched = reasoner.infer_results(result)

SPARQLReasoner Constructor

SPARQLReasoner(
    config=None,         # optional config dict
    triplet_store=None,  # optional TripletStore instance for live query execution
    enable_inference=True,
)
execute_query() returns empty bindings when no triplet_store is configured. Pass a TripletStore instance via the triplet_store= kwarg to execute queries against a live backend.

DatalogReasoner

Pure-Python bottom-up semi-naive fixpoint evaluation for recursive Horn clause rules. Termination is guaranteed: the engine detects fixpoint convergence and stops:
from semantica.reasoning import DatalogReasoner, DatalogFact

datalog = DatalogReasoner()

# Add base facts: string form is the simplest
datalog.add_fact("parent(alice, bob)")
datalog.add_fact("parent(bob, charlie)")

# Or use DatalogFact directly (args is a tuple of strings)
datalog.add_fact(DatalogFact(predicate="parent", args=("charlie", "dave")))

# Add recursive rules using Horn clause syntax
datalog.add_rule("ancestor(X, Y) :- parent(X, Y).")
datalog.add_rule("ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z).")

# Evaluate to fixpoint: returns all derived fact strings
all_facts = datalog.derive_all()
# e.g. ["parent(alice, bob)", "parent(bob, charlie)", ..., "ancestor(alice, bob)", ...]

# Query with variable pattern: variables start with uppercase or ?
results = datalog.query("ancestor(alice, ?Z)")
# → [{"Z": "bob"}, {"Z": "charlie"}, {"Z": "dave"}]

# Clear and start over
datalog.clear()

DatalogFact, DatalogRule fields

from semantica.reasoning import DatalogFact, DatalogRule

# DatalogFact: ground fact; args must all be constants (lowercase start)
fact = DatalogFact(predicate="parent", args=("alice", "bob"))

# DatalogRule: parsed from string; head and body are set by the parser
# Use add_rule("head(X, Y) :- body(X, Z), body2(Z, Y)."): do not construct directly

DatalogReasoner Methods

MethodReturnsDescription
add_fact(fact)NoneAdd string, dict, or DatalogFact; constants must be lowercase-starting
add_rule(rule_str)NoneParse and add a Horn clause string like "ancestor(X,Y) :- parent(X,Y)."
derive_all()List[str]Run semi-naive fixpoint evaluation; returns all facts as strings
query(pattern)List[dict]Query derived facts: auto-runs derive_all() if needed
load_from_graph(graph)intLoad a ContextGraph’s nodes/edges as Datalog facts; returns count added
clear()NoneClear all facts and rules

TemporalReasoningEngine

Pure-Python Allen interval algebra: all 13 relations, no LLM calls:
from datetime import datetime
from semantica.reasoning import TemporalReasoningEngine, TemporalInterval, IntervalRelation

engine = TemporalReasoningEngine()

ceo_tenure   = TemporalInterval(start=datetime(1997, 9, 16), end=datetime(2011, 8, 24))
board_member = TemporalInterval(start=datetime(2000, 1,  1), end=datetime(2012, 6,  1))

# Compute Allen relation: method is relation(), not get_relation()
rel = engine.relation(ceo_tenure, board_member)
# → IntervalRelation.DURING  (ceo_tenure is fully inside board_member)

# Other helpers
engine.overlaps(ceo_tenure, board_member)   # bool
engine.contains(board_member, ceo_tenure)   # bool

# Is a given point in time inside an interval?
engine.active_at(ceo_tenure, datetime(2005, 6, 1))   # True
All 13 Allen interval algebra relations:
RelationMeaning
BEFOREA ends before B starts
MEETSA ends exactly when B starts
OVERLAPSA starts before B, ends inside B
DURINGA is fully inside B
STARTSA and B start together, A ends first
FINISHESA and B end together, A starts later
EQUALSIdentical intervals
AFTERInverse of BEFORE
MET_BYInverse of MEETS
OVERLAPPED_BYInverse of OVERLAPS
CONTAINSInverse of DURING
STARTED_BYInverse of STARTS
FINISHED_BYInverse of FINISHES
TemporalInterval.start expects a datetime object, not a string. Import datetime from the standard library and construct intervals with datetime(year, month, day).

ExplanationGenerator

Generate structured explanations for any InferenceResult:
from semantica.reasoning import ExplanationGenerator, Reasoner, Rule

reasoner = Reasoner()
reasoner.add_fact("Manager(John)")
reasoner.add_rule("IF Manager(?x) THEN HasAuthority(?x)")
results = reasoner.forward_chain()

# ExplanationGenerator takes no positional args
generator = ExplanationGenerator()

# Pass an InferenceResult object: not a dict
explanation = generator.generate_explanation(results[0])

print(f"Type:       {explanation.explanation_type}")   # "inference"
print(f"Conclusion: {explanation.conclusion}")
print(f"NL:         {explanation.natural_language}")

if explanation.reasoning_path:
    for step in explanation.reasoning_path.steps:
        print(f"  Step {step.step_id}: {step.description}")
        if step.rule_applied:
            print(f"    Rule: {step.rule_applied.name}")

# Justify a conclusion with a reasoning path
path = generator.show_reasoning_path(results[0])
justification = generator.justify_conclusion(results[0].conclusion, path)
print(justification.explanation_text)

ExplanationGenerator Methods

MethodReturnsDescription
generate_explanation(reasoning)ExplanationGenerate structured explanation for an InferenceResult, Proof, or abductive result
show_reasoning_path(reasoning)ReasoningPathExtract and return the reasoning path from any result
justify_conclusion(conclusion, path)JustificationBuild a Justification with evidence and NL text for a conclusion

Key dataclass fields

# Explanation
explanation.explanation_id    # str
explanation.explanation_type  # "inference" | "proof" | "abductive" | "generic"
explanation.conclusion        # conclusion value
explanation.reasoning_path    # ReasoningPath | None
explanation.natural_language  # NL string (when generate_nl=True, the default)

# ReasoningStep
step.step_id        # str
step.description    # str
step.rule_applied   # Rule | None  (NOT rule_name)
step.input_facts    # List[Any]
step.output_fact    # Any
step.confidence     # float

Engine Selection Guide

EngineBest ForTerminationKey Method
ReasonerSimple IF/THEN rulesAlways (with max_iterations cap)forward_chain()
GraphReasonerNL queries over a KG via LLMAlwaysreason(graph, query)
ReteEngineLarge rule sets with many factsAlwaysmatch_patterns()
SPARQLReasonerRule-augmented SPARQL queriesAlwaysexecute_query()
DatalogReasonerRecursive rules (ancestry, reachability)Guaranteed fixpointderive_all() / query()
TemporalReasoningEngineTime interval relationshipsAlwaysrelation(a, b)
For recursive rules (e.g. ancestor, reachability, transitivity), use DatalogReasoner: it guarantees termination via semi-naive bottom-up fixpoint evaluation. Reasoner.forward_chain() has a max_iterations cap (default 50) and will silently stop early with deep recursion.
GraphReasoner requires a configured LLM provider. If the provider fails to initialize, reason() returns an error string instead of raising. Check reasoner.provider is not None before calling if you need to surface failures explicitly.

Knowledge Graph

The knowledge graph being reasoned over.

Ontology

Ontology axioms and SHACL constraints for logical reasoning.

Triplet Store

RDF backend for SPARQL-based reasoning.

Context

Reasoning integrated into agent decision intelligence.