mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
155 lines
5.1 KiB
Python
155 lines
5.1 KiB
Python
import pytest
|
|
from unittest.mock import MagicMock, patch
|
|
from semantica.reasoning import (
|
|
InferenceEngine,
|
|
RuleManager,
|
|
ExplanationGenerator,
|
|
InferenceResult,
|
|
Rule,
|
|
RuleType
|
|
)
|
|
|
|
# --- Fixtures ---
|
|
|
|
@pytest.fixture
|
|
def inference_engine():
|
|
return InferenceEngine()
|
|
|
|
@pytest.fixture
|
|
def rule_manager():
|
|
return RuleManager()
|
|
|
|
@pytest.fixture
|
|
def explanation_generator():
|
|
return ExplanationGenerator()
|
|
|
|
# --- Test Rule Parsing (RuleManager) ---
|
|
|
|
def test_rule_parsing_simple(rule_manager):
|
|
rule_def = "IF Person(?x) THEN Human(?x)"
|
|
rule = rule_manager.define_rule(rule_def, name="HumanRule")
|
|
|
|
assert rule.name == "HumanRule"
|
|
assert rule.rule_type == RuleType.IMPLICATION
|
|
assert len(rule.conditions) == 1
|
|
assert rule.conditions[0] == "Person(?x)"
|
|
assert rule.conclusion == "Human(?x)"
|
|
|
|
def test_rule_parsing_multiple_conditions(rule_manager):
|
|
rule_def = "IF Parent(?x, ?y) AND Parent(?y, ?z) THEN Grandparent(?x, ?z)"
|
|
rule = rule_manager.define_rule(rule_def)
|
|
|
|
assert len(rule.conditions) == 2
|
|
assert rule.conditions[0] == "Parent(?x, ?y)"
|
|
assert rule.conditions[1] == "Parent(?y, ?z)"
|
|
assert rule.conclusion == "Grandparent(?x, ?z)"
|
|
|
|
def test_rule_parsing_invalid(rule_manager):
|
|
with pytest.raises(Exception):
|
|
rule_manager.define_rule("INVALID RULE SYNTAX")
|
|
|
|
# --- Test InferenceEngine (Forward Chaining) ---
|
|
|
|
def test_forward_chaining_exact_match(inference_engine):
|
|
# Rule: IF A THEN B
|
|
inference_engine.add_rule("IF A THEN B")
|
|
inference_engine.add_fact("A")
|
|
|
|
results = inference_engine.forward_chain()
|
|
|
|
assert len(results) >= 1
|
|
inferred_facts = [res.conclusion for res in results]
|
|
assert "B" in inferred_facts
|
|
|
|
def test_forward_chaining_simple(inference_engine):
|
|
# Rule: IF Person(?x) THEN Human(?x)
|
|
inference_engine.add_rule("IF Person(?x) THEN Human(?x)")
|
|
|
|
# Fact: Person(Alice)
|
|
inference_engine.add_fact("Person(Alice)")
|
|
|
|
results = inference_engine.forward_chain()
|
|
|
|
assert len(results) >= 1
|
|
# Check if Human(Alice) is inferred
|
|
inferred_facts = [res.conclusion for res in results]
|
|
assert "Human(Alice)" in inferred_facts
|
|
|
|
def test_forward_chaining_transitive(inference_engine):
|
|
# Rule: IF Parent(?a, ?b) AND Parent(?b, ?c) THEN Grandparent(?a, ?c)
|
|
inference_engine.add_rule("IF Parent(?a, ?b) AND Parent(?b, ?c) THEN Grandparent(?a, ?c)")
|
|
|
|
inference_engine.add_fact("Parent(Alice, Bob)")
|
|
inference_engine.add_fact("Parent(Bob, Charlie)")
|
|
|
|
results = inference_engine.forward_chain()
|
|
|
|
inferred_facts = [res.conclusion for res in results]
|
|
assert "Grandparent(Alice, Charlie)" in inferred_facts
|
|
|
|
def test_forward_chaining_no_match(inference_engine):
|
|
inference_engine.add_rule("IF A(?x) THEN B(?x)")
|
|
inference_engine.add_fact("C(Item)")
|
|
|
|
results = inference_engine.forward_chain()
|
|
assert len(results) == 0
|
|
|
|
# --- Test InferenceEngine (Backward Chaining) ---
|
|
|
|
def test_backward_chaining_success(inference_engine):
|
|
# Rule: IF Parent(?a, ?b) AND Parent(?b, ?c) THEN Grandparent(?a, ?c)
|
|
inference_engine.add_rule("IF Parent(?a, ?b) AND Parent(?b, ?c) THEN Grandparent(?a, ?c)")
|
|
|
|
inference_engine.add_fact("Parent(Alice, Bob)")
|
|
inference_engine.add_fact("Parent(Bob, Charlie)")
|
|
|
|
# Goal: Prove Grandparent(Alice, Charlie)
|
|
proof = inference_engine.backward_chain("Grandparent(Alice, Charlie)")
|
|
|
|
assert proof is not None
|
|
# Depending on implementation, proof might be a boolean or a Proof object
|
|
# The notebook says: if proof: print(...)
|
|
assert bool(proof) is True
|
|
|
|
def test_backward_chaining_failure(inference_engine):
|
|
inference_engine.add_rule("IF A(?x) THEN B(?x)")
|
|
inference_engine.add_fact("A(1)")
|
|
|
|
proof = inference_engine.backward_chain("B(2)")
|
|
assert not proof
|
|
|
|
# --- Test ExplanationGenerator ---
|
|
|
|
def test_explanation_generation(explanation_generator):
|
|
# Create a dummy InferenceResult
|
|
rule = Rule("r1", "test_rule", ["A(?x)"], "B(?x)")
|
|
result = InferenceResult(
|
|
conclusion="B(1)",
|
|
premises=["A(1)"],
|
|
rule_used=rule,
|
|
confidence=1.0
|
|
)
|
|
|
|
explanation = explanation_generator.generate_explanation(result)
|
|
|
|
assert explanation is not None
|
|
assert explanation.conclusion == "B(1)"
|
|
# Check if natural language explanation is generated
|
|
assert isinstance(explanation.natural_language, str)
|
|
assert "B(1)" in explanation.natural_language
|
|
assert "test_rule" in explanation.natural_language or "r1" in explanation.natural_language
|
|
|
|
# --- Test Edge Cases ---
|
|
|
|
def test_duplicate_facts(inference_engine):
|
|
assert inference_engine.add_fact("A(1)") is True
|
|
assert inference_engine.add_fact("A(1)") is False # Duplicate
|
|
assert len(inference_engine.facts) == 1
|
|
|
|
def test_max_iterations(inference_engine):
|
|
# Create a rule that generates infinite facts if not controlled
|
|
# e.g., IF Num(?x) THEN Num(?x+1) - hard to simulate with string matching without eval
|
|
# Instead, let's just test config setting
|
|
engine = InferenceEngine(max_iterations=5)
|
|
assert engine.max_iterations == 5
|