Performing Time Travel Queries on Historical Graph States in Semantica: A Complete Guide

Semantica enables time-travel queries through bi-temporal validity tracking (valid_from, valid_until) on entities and relationships, allowing you to reconstruct historical graph states using TemporalGraphQuery and persist snapshots via TemporalVersionManager.

The Semantica knowledge graph engine stores facts with temporal boundaries, making performing time travel queries on historical graph states in Semantica a native capability rather than an afterthought. By leveraging bi-temporal metadata and dedicated query classes, you can retrieve precise graph snapshots from any point in time, analyze evolution patterns, and maintain immutable audit trails.

Understanding the Temporal Data Model

Semantica represents temporal knowledge through relationships (and optionally entities) that carry explicit validity intervals. These intervals are stored as BiTemporalFact objects defined in semantica/kg/temporal_model.py:


# In semantica/kg/temporal_model.py

@dataclass
class BiTemporalFact:
    valid_from: Optional[datetime] = None
    valid_until: Optional[datetime] = None
    transaction_time: Optional[datetime] = None   # optional audit timestamp

When the GraphBuilder ingests data or the MCP tool adds relationships, the temporal fields are normalized by deserialize_relationship_temporal_fields and stored unchanged in the underlying graph structure. This bi-temporal approach separates valid time (when a fact was true in the real world) from transaction time (when it was recorded in the system).

Point-in-Time Graph Reconstruction

The primary mechanism for retrieving historical states is the query_at_time method in semantica/kg/temporal_query.py, implemented by the TemporalGraphQuery class:


# semantica/kg/temporal_query.py → query_at_time

def query_at_time(self, graph, query, at_time, include_history=False, **options):
    query_time = self._parse_time(at_time)
    reconstructed_graph = self.reconstruct_at_time(graph, query_time)
    # …return entities + relationships active at `query_time`

The reconstruction process follows three distinct steps:

  1. Parse the supplied timestamp using _parse_time to handle various datetime formats.
  2. Reconstruct a sub-graph via reconstruct_at_time that contains only entities whose valid_from/valid_until span the target timestamp, and relationships that are temporally active according to _relationship_active_at_time.
  3. Validate referential integrity to ensure no dangling edges exist in the filtered result.

The heavy lifting occurs in reconstruct_at_time:

def reconstruct_at_time(self, graph, at_time, *, time_axis="valid"):
    query_time = at_time if isinstance(at_time, datetime) else self._parse_time(at_time)
    reconstructed = copy.deepcopy(graph)
    entity_index = {
        self._entity_id(e): e
        for e in graph.get("entities", [])
        if self._entity_active_at_time(e, query_time, time_axis=time_axis)
    }
    # Keep only relationships whose temporal axis is active at `query_time`

This method builds an entity index of active entities and filters relationships accordingly, guaranteeing that the resulting sub-graph obeys referential integrity.

Querying Across Time Ranges

For analyzing periods rather than specific moments, use query_time_range:

def query_time_range(self, graph, query, start_time, end_time, **options):
    start, end = map(self._parse_time, (start_time, end_time))
    # Scan relationships and keep those intersecting the interval [start, end]

This method scans every relationship in the graph and retains those whose validity interval overlaps the requested range. The result reflects all facts that were true anywhere within the specified interval, enabling trend analysis and period-based auditing.

Detecting Temporal Patterns and Evolution

Beyond simple retrieval, TemporalGraphQuery integrates TemporalPatternDetector and TemporalReasoningEngine for advanced analytics:

  • Pattern detection (query_temporal_pattern) uncovers recurring sequences, cycles, or trends across the temporal axis.
  • Evolution analysis (analyze_evolution) aggregates metrics such as new versus retired entities per interval to provide a high-level view of graph changes.

Both features rely on low-level helpers like _relationship_active_at_time and _compare_times. You can invoke them as follows:

tgq = TemporalGraphQuery()
patterns = tgq.query_temporal_pattern(kg, pattern_type="cycle")
evolution = tgq.analyze_evolution(kg, granularities=["year"])

Persisting and Comparing Graph Versions

While TemporalGraphQuery operates on in-memory graphs, TemporalVersionManager adds persistent versioning capabilities. Found in semantica/kg/temporal_query.py (and utilizing storage backends from semantica/change_management/managers.py), this component enables snapshot management:


# semantica/kg/temporal_query.py → TemporalVersionManager

manager = TemporalVersionManager(storage_path="versions.db")   # SQLite backend

snapshot = manager.create_version(graph, version_label="v2024-01")
versions = manager.list_versions()
diff = manager.compare_versions("v2023-12", "v2024-01")

Key capabilities include:

  • Snapshots: create_version copies current entities and relationships into a lightweight dictionary, adding a timestamp and optional metadata.
  • Persistent storage: When a storage_path is supplied, snapshots serialize to an SQLite database via SQLiteVersionStorage; otherwise, InMemoryVersionStorage maintains transient copies.
  • Diffing: compare_versions loads two snapshots (by label or dictionary) and produces detailed change reports—added, removed, or modified entities and relationships—essential for audit trails and change-impact analysis.

Visualizing Historical Graph States

For interactive exploration, the optional TemporalVisualizer class (located in semantica/visualization/temporal_visualizer.py) renders timelines and version histories. It wraps Plotly for interactive charts, with graceful degradation if dependencies are missing:

from semantica.visualization.temporal_visualizer import TemporalVisualizer

tgq = TemporalGraphQuery()
kg_snapshot = tgq.reconstruct_at_time(kg, "2023-06-15")
viz = TemporalVisualizer()
viz.visualize_timeline(kg_snapshot, output="interactive")   # interactive timeline

viz.visualize_version_history(version_manager.list_versions(), output="static")

If Plotly is unavailable, the visualizer raises a ProcessingError (covered by the test suite) rather than failing silently.

End-to-End Workflow Example

All modules are independently importable, enabling a pipeline-style workflow:

from semantica.ingest import FileIngestor
from semantica.kg import GraphBuilder, TemporalGraphQuery, TemporalVersionManager

# 1️⃣ Ingest data

docs = FileIngestor().ingest_directory("./contracts/")

# 2️⃣ Build KG (temporal fields are automatically normalised)

builder = GraphBuilder()
kg = builder.build(docs)

# 3️⃣ Take a point‑in‑time snapshot

tgq = TemporalGraphQuery()
snapshot_2022 = tgq.reconstruct_at_time(kg, "2022-12-31")

# 4️⃣ Persist the snapshot

manager = TemporalVersionManager(storage_path="kg_versions.db")
manager.create_version(snapshot_2022, version_label="v2022‑end")

This architecture cleanly separates querying, versioning, and visualization, making it straightforward to embed temporal reasoning into data pipelines, multi-agent workflows, or compliance-driven audit systems.

Summary

  • Bi-temporal modeling in semantica/kg/temporal_model.py stores valid_from and valid_until on every fact, enabling precise historical reconstruction.
  • TemporalGraphQuery provides query_at_time for point-in-time snapshots and query_time_range for interval-based analysis.
  • reconstruct_at_time performs the heavy lifting of filtering entities and relationships while maintaining referential integrity.
  • TemporalVersionManager persists named snapshots to SQLite or memory and supports diffing between versions for audit trails.
  • TemporalVisualizer offers optional Plotly-based timelines for human inspection of historical states.

Frequently Asked Questions

What is the difference between query_at_time and reconstruct_at_time?

query_at_time is the high-level API that parses the input timestamp, calls reconstruct_at_time to build the filtered sub-graph, and returns the result with metadata such as entity counts. reconstruct_at_time is the lower-level engine that performs the actual graph filtering, building an entity index of active nodes and removing relationships that fall outside the temporal boundary. You typically use query_at_time for standard retrieval, while reconstruct_at_time is useful when you need the raw graph object for further manipulation.

How does Semantica store temporal validity information?

Temporal validity is stored via the BiTemporalFact dataclass in semantica/kg/temporal_model.py, which attaches valid_from and valid_until fields to relationships and optionally to entities. When data is ingested through the GraphBuilder or MCP tools, the deserialize_relationship_temporal_fields function normalizes these values before storage, ensuring consistent datetime handling throughout the query engine.

Can I compare two different historical versions of a knowledge graph?

Yes. The TemporalVersionManager class provides compare_versions, which accepts two version labels (for example, "v2023-12" and "v2024-01") and produces a detailed diff report. This report identifies added, removed, and modified entities and relationships, making it ideal for audit trails and understanding change impact between any two points in history.

What storage backends are available for temporal version management?

TemporalVersionManager supports two storage backends: SQLiteVersionStorage for persistent, disk-based versioning (specified via the storage_path parameter), and InMemoryVersionStorage for transient, session-based snapshots when no path is provided. The SQLite backend is recommended for production audit requirements, while the in-memory option suits temporary analysis or testing scenarios.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →