Files
hermes-agent/tests/test_temporal_kg.py
Allegro ae6f3e9a95 feat: Issue #39 - temporal knowledge graph with versioning and reasoning
Implement Phase 28: Sovereign Knowledge Graph 'Time Travel'

- agent/temporal_knowledge_graph.py: SQLite-backed temporal triple store
  with versioning, validity periods, and temporal query operators
  (BEFORE, AFTER, DURING, OVERLAPS, AT)

- agent/temporal_reasoning.py: Temporal reasoning engine supporting
  historical queries, fact evolution tracking, and worldview snapshots

- tools/temporal_kg_tool.py: Tool integration with functions for
  storing facts with time, querying historical state, generating
  temporal summaries, and natural language temporal queries

- tests/test_temporal_kg.py: Comprehensive test coverage including
  storage tests, query operators, historical summaries, and integration tests
2026-04-01 02:08:20 +00:00

474 lines
18 KiB
Python

"""Tests for Temporal Knowledge Graph implementation.
Tests cover:
- Temporal storage tests
- Query operator tests (BEFORE, AFTER, DURING, OVERLAPS)
- Historical summary tests
- Integration with tools
"""
import pytest
import tempfile
import os
from datetime import datetime, timedelta
from agent.temporal_knowledge_graph import (
TemporalTripleStore, TemporalTriple, TemporalOperator
)
from agent.temporal_reasoning import (
TemporalReasoner, ChangeType, HistoricalSummary
)
from tools.temporal_kg_tool import (
store_fact_with_time,
query_historical_state,
get_fact_history,
generate_temporal_summary,
when_did_we_learn,
how_has_it_changed,
query_with_temporal_operator,
get_worldview_at_time
)
class TestTemporalTripleStore:
"""Tests for the TemporalTripleStore class."""
@pytest.fixture
def store(self):
"""Create a temporary store for testing."""
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
db_path = f.name
store = TemporalTripleStore(db_path)
yield store
# Cleanup
os.unlink(db_path)
def test_store_fact(self, store):
"""Test storing a basic fact."""
triple = store.store_fact("Timmy", "has_feature", "sovereignty")
assert triple.subject == "Timmy"
assert triple.predicate == "has_feature"
assert triple.object == "sovereignty"
assert triple.version == 1
assert triple.valid_until is None
def test_store_fact_with_validity_period(self, store):
"""Test storing a fact with validity bounds."""
valid_from = "2026-01-01T00:00:00"
valid_until = "2026-12-31T23:59:59"
triple = store.store_fact(
"Hermes",
"status",
"active",
valid_from=valid_from,
valid_until=valid_until
)
assert triple.valid_from == valid_from
assert triple.valid_until == valid_until
def test_fact_versioning(self, store):
"""Test that facts are properly versioned."""
# Store initial fact
triple1 = store.store_fact("Timmy", "version", "1.0")
assert triple1.version == 1
# Store updated fact
triple2 = store.store_fact("Timmy", "version", "2.0")
assert triple2.version == 2
# Check that first fact was superseded
history = store.get_fact_history("Timmy", "version")
assert len(history) == 2
assert history[0].superseded_by == triple2.id
def test_query_at_time(self, store):
"""Test querying facts at a specific time."""
# Store facts at different times
store.store_fact("Timmy", "status", "alpha", valid_from="2026-01-01T00:00:00")
store.store_fact("Timmy", "status", "beta", valid_from="2026-03-01T00:00:00")
store.store_fact("Timmy", "status", "stable", valid_from="2026-06-01T00:00:00")
# Query at different points
feb_facts = store.query_at_time("2026-02-01T00:00:00", subject="Timmy")
assert len(feb_facts) == 1
assert feb_facts[0].object == "alpha"
may_facts = store.query_at_time("2026-05-01T00:00:00", subject="Timmy")
assert len(may_facts) == 1
assert may_facts[0].object == "beta"
jul_facts = store.query_at_time("2026-07-01T00:00:00", subject="Timmy")
assert len(jul_facts) == 1
assert jul_facts[0].object == "stable"
def test_query_temporal_operators(self, store):
"""Test temporal query operators."""
# Store some facts
store.store_fact("A", "rel", "1", valid_from="2026-01-01T00:00:00")
store.store_fact("B", "rel", "2", valid_from="2026-03-01T00:00:00")
store.store_fact("C", "rel", "3", valid_from="2026-06-01T00:00:00")
# Test BEFORE
before_april = store.query_temporal(
TemporalOperator.BEFORE, "2026-04-01T00:00:00"
)
assert len(before_april) == 2 # A and B
# Test AFTER
after_feb = store.query_temporal(
TemporalOperator.AFTER, "2026-02-01T00:00:00"
)
assert len(after_feb) == 2 # B and C
# Test DURING (at a specific time)
during_may = store.query_temporal(
TemporalOperator.DURING, "2026-05-01T00:00:00"
)
assert len(during_may) == 1 # Only B is valid in May
assert during_may[0].object == "2"
def test_get_fact_history(self, store):
"""Test retrieving fact version history."""
# Create multiple versions
store.store_fact("Feature", "status", "planned", valid_from="2026-01-01T00:00:00")
store.store_fact("Feature", "status", "in_progress", valid_from="2026-02-01T00:00:00")
store.store_fact("Feature", "status", "completed", valid_from="2026-03-01T00:00:00")
history = store.get_fact_history("Feature", "status")
assert len(history) == 3
assert history[0].object == "planned"
assert history[1].object == "in_progress"
assert history[2].object == "completed"
# Check versions
assert history[0].version == 1
assert history[1].version == 2
assert history[2].version == 3
def test_get_entity_changes(self, store):
"""Test getting entity changes in a time range."""
store.store_fact("Codebase", "feature", "auth", valid_from="2026-01-01T00:00:00")
store.store_fact("Codebase", "feature", "logging", valid_from="2026-02-01T00:00:00")
store.store_fact("Codebase", "feature", "metrics", valid_from="2026-03-01T00:00:00")
changes = store.get_entity_changes(
"Codebase",
"2026-01-15T00:00:00",
"2026-03-15T00:00:00"
)
# Should include logging and metrics
assert len(changes) >= 2
def test_export_import(self, store):
"""Test exporting and importing data."""
# Store some data
store.store_fact("Test", "data", "value1")
store.store_fact("Test", "data", "value2")
# Export
json_data = store.export_to_json()
assert "Test" in json_data
assert "value1" in json_data
assert "value2" in json_data
# Create new store and import
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
db_path2 = f.name
try:
store2 = TemporalTripleStore(db_path2)
store2.import_from_json(json_data)
# Verify imported data
facts = store2.query_at_time(datetime.now().isoformat(), subject="Test")
assert len(facts) >= 1
finally:
os.unlink(db_path2)
class TestTemporalReasoner:
"""Tests for the TemporalReasoner class."""
@pytest.fixture
def reasoner(self):
"""Create a temporary reasoner for testing."""
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
db_path = f.name
store = TemporalTripleStore(db_path)
reasoner = TemporalReasoner(store)
yield reasoner
os.unlink(db_path)
def test_what_did_we_believe(self, reasoner):
"""Test "what did we believe" queries."""
# Set up facts
reasoner.store.store_fact("Timmy", "view", "optimistic", valid_from="2026-01-01T00:00:00")
reasoner.store.store_fact("Timmy", "view", "cautious", valid_from="2026-03-01T00:00:00")
# Query before March
beliefs = reasoner.what_did_we_believe("Timmy", "2026-02-15T00:00:00")
assert len(beliefs) == 1
assert beliefs[0].object == "optimistic"
def test_when_did_we_learn(self, reasoner):
"""Test "when did we learn" queries."""
timestamp = "2026-02-15T10:30:00"
reasoner.store.store_fact(
"MLX",
"integrated_with",
"Hermes",
valid_from=timestamp
)
when = reasoner.when_did_we_learn("MLX", "integrated_with")
assert when == timestamp
def test_how_has_it_changed(self, reasoner):
"""Test "how has it changed" queries."""
reasoner.store.store_fact("Security", "level", "low", valid_from="2026-01-01T00:00:00")
reasoner.store.store_fact("Security", "level", "medium", valid_from="2026-02-01T00:00:00")
reasoner.store.store_fact("Security", "level", "high", valid_from="2026-03-01T00:00:00")
changes = reasoner.how_has_it_changed("Security", "2026-01-15T00:00:00")
assert len(changes) >= 2
# Check that changes are properly categorized
change_types = [c.change_type for c in changes]
assert ChangeType.MODIFIED in change_types or ChangeType.ADDED in change_types
def test_generate_temporal_summary(self, reasoner):
"""Test generating historical summaries."""
# Create a history of changes
reasoner.store.store_fact("Project", "status", "planning", valid_from="2026-01-01T00:00:00")
reasoner.store.store_fact("Project", "status", "development", valid_from="2026-02-01T00:00:00")
reasoner.store.store_fact("Project", "milestone", "alpha", valid_from="2026-02-15T00:00:00")
reasoner.store.store_fact("Project", "status", "testing", valid_from="2026-03-01T00:00:00")
summary = reasoner.generate_temporal_summary(
"Project",
"2026-01-01T00:00:00",
"2026-04-01T00:00:00"
)
assert summary.entity == "Project"
assert summary.total_changes >= 3
assert len(summary.evolution_timeline) >= 3
assert len(summary.current_state) >= 1
def test_get_worldview_at_time(self, reasoner):
"""Test getting complete worldview at a time."""
reasoner.store.store_fact("Timmy", "mood", "happy", valid_from="2026-01-01T00:00:00")
reasoner.store.store_fact("Timmy", "task", "coding", valid_from="2026-01-01T00:00:00")
reasoner.store.store_fact("Hermes", "status", "active", valid_from="2026-01-01T00:00:00")
worldview = reasoner.get_worldview_at_time("2026-01-15T00:00:00")
assert "Timmy" in worldview
assert "Hermes" in worldview
assert len(worldview["Timmy"]) == 2
def test_infer_temporal_relationship(self, reasoner):
"""Test temporal relationship inference."""
triple_a = reasoner.store.store_fact("A", "rel", "1", valid_from="2026-01-01T00:00:00")
triple_a.valid_until = "2026-02-01T00:00:00"
triple_b = reasoner.store.store_fact("B", "rel", "2", valid_from="2026-02-15T00:00:00")
rel = reasoner.infer_temporal_relationship(triple_a, triple_b)
assert "before" in rel.lower()
class TestTemporalKGTools:
"""Tests for the temporal KG tool functions."""
@pytest.fixture(autouse=True)
def reset_singleton(self):
"""Reset singleton instances before each test."""
import tools.temporal_kg_tool as tool_module
tool_module._store = None
tool_module._reasoner = None
yield
tool_module._store = None
tool_module._reasoner = None
def test_store_fact_with_time(self):
"""Test the store_fact_with_time tool function."""
result = store_fact_with_time(
subject="Hermes Agent",
predicate="has_feature",
object="input_sanitizer",
valid_from="2026-04-01T01:00:00"
)
assert result["success"] is True
assert result["triple"]["subject"] == "Hermes Agent"
assert result["triple"]["predicate"] == "has_feature"
assert result["triple"]["object"] == "input_sanitizer"
def test_query_historical_state(self):
"""Test the query_historical_state tool function."""
# Store a fact first
store_fact_with_time(
subject="Timmy",
predicate="view_on_sovereignty",
object="strong",
valid_from="2026-02-01T00:00:00"
)
# Query it
result = query_historical_state("Timmy", "2026-03-01T00:00:00")
assert result["success"] is True
assert result["subject"] == "Timmy"
assert result["fact_count"] == 1
assert result["facts"][0]["object"] == "strong"
def test_get_fact_history(self):
"""Test the get_fact_history tool function."""
# Create version history
store_fact_with_time("Feature", "status", "planned", valid_from="2026-01-01T00:00:00")
store_fact_with_time("Feature", "status", "done", valid_from="2026-02-01T00:00:00")
result = get_fact_history("Feature", "status")
assert result["success"] is True
assert result["version_count"] == 2
assert len(result["versions"]) == 2
def test_when_did_we_learn(self):
"""Test the when_did_we_learn tool function."""
store_fact_with_time(
"MLX",
"integrated_with",
"Hermes",
valid_from="2026-03-15T12:00:00"
)
result = when_did_we_learn("MLX", "integrated_with")
assert result["success"] is True
assert result["first_known"] == "2026-03-15T12:00:00"
def test_how_has_it_changed(self):
"""Test the how_has_it_changed tool function."""
store_fact_with_time("Codebase", "feature_count", "10", valid_from="2026-01-01T00:00:00")
store_fact_with_time("Codebase", "feature_count", "20", valid_from="2026-02-01T00:00:00")
result = how_has_it_changed("Codebase", "2026-01-15T00:00:00")
assert result["success"] is True
assert result["change_count"] >= 1
def test_query_with_temporal_operator(self):
"""Test the query_with_temporal_operator tool function."""
store_fact_with_time("A", "rel", "1", valid_from="2026-01-01T00:00:00")
store_fact_with_time("B", "rel", "2", valid_from="2026-03-01T00:00:00")
result = query_with_temporal_operator("BEFORE", "2026-02-01T00:00:00")
assert result["success"] is True
assert result["fact_count"] == 1
assert result["facts"][0]["subject"] == "A"
def test_get_worldview_at_time(self):
"""Test the get_worldview_at_time tool function."""
store_fact_with_time("Timmy", "mood", "good", valid_from="2026-01-01T00:00:00")
store_fact_with_time("Hermes", "status", "running", valid_from="2026-01-01T00:00:00")
result = get_worldview_at_time("2026-01-15T00:00:00")
assert result["success"] is True
assert result["entity_count"] == 2
def test_generate_temporal_summary(self):
"""Test the generate_temporal_summary tool function."""
store_fact_with_time("Security", "level", "low", valid_from="2026-01-01T00:00:00")
store_fact_with_time("Security", "level", "high", valid_from="2026-03-01T00:00:00")
result = generate_temporal_summary("Security", "2026-01-01T00:00:00", "2026-04-01T00:00:00")
assert result["success"] is True
assert result["entity"] == "Security"
assert result["summary"]["total_changes"] >= 1
class TestIntegration:
"""Integration tests for the complete temporal KG system."""
@pytest.fixture
def system(self):
"""Create a complete temporal KG system."""
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
db_path = f.name
store = TemporalTripleStore(db_path)
reasoner = TemporalReasoner(store)
yield {"store": store, "reasoner": reasoner}
os.unlink(db_path)
def test_full_workflow(self, system):
"""Test a complete temporal knowledge workflow."""
store = system["store"]
reasoner = system["reasoner"]
# 1. Store initial facts about a security audit
store.store_fact("SecurityAudit", "status", "scheduled", valid_from="2026-01-01T00:00:00")
store.store_fact("SecurityAudit", "auditor", "ExternalFirm", valid_from="2026-01-01T00:00:00")
# 2. Update as audit progresses
store.store_fact("SecurityAudit", "status", "in_progress", valid_from="2026-02-01T00:00:00")
store.store_fact("SecurityAudit", "findings", "none_yet", valid_from="2026-02-01T00:00:00")
# 3. Complete audit
store.store_fact("SecurityAudit", "status", "completed", valid_from="2026-03-01T00:00:00")
store.store_fact("SecurityAudit", "findings", "5_minor_issues", valid_from="2026-03-01T00:00:00")
store.store_fact("SecurityAudit", "recommendation", "address_within_30_days", valid_from="2026-03-01T00:00:00")
# 4. Query historical state
jan_state = reasoner.get_worldview_at_time("2026-01-15T00:00:00", ["SecurityAudit"])
assert jan_state["SecurityAudit"][0]["predicate"] == "status"
assert jan_state["SecurityAudit"][0]["object"] == "scheduled"
feb_state = reasoner.get_worldview_at_time("2026-02-15T00:00:00", ["SecurityAudit"])
status_fact = [f for f in feb_state["SecurityAudit"] if f["predicate"] == "status"][0]
assert status_fact["object"] == "in_progress"
# 5. Generate summary
summary = reasoner.generate_temporal_summary(
"SecurityAudit",
"2026-01-01T00:00:00",
"2026-04-01T00:00:00"
)
assert summary.total_changes >= 5
assert any(f["predicate"] == "status" for f in summary.key_facts)
# 6. Check when we learned about findings
when = reasoner.when_did_we_learn("SecurityAudit", "findings")
assert when is not None
def test_temporal_inference(self, system):
"""Test temporal inference capabilities."""
store = system["store"]
reasoner = system["reasoner"]
# Store facts with temporal relationships
triple_a = store.store_fact("EventA", "happened", "yes", valid_from="2026-01-01T00:00:00")
triple_a.valid_until = "2026-01-31T23:59:59"
triple_b = store.store_fact("EventB", "happened", "yes", valid_from="2026-02-01T00:00:00")
# Infer relationship
rel = reasoner.infer_temporal_relationship(triple_a, triple_b)
assert "before" in rel.lower()
if __name__ == "__main__":
pytest.main([__file__, "-v"])