How to Implement Bi-Temporal Facts with BiTemporalFact for Time-Travel Queries in Semantica
Use the BiTemporalFact class in semantica/kg/temporal_model.py to capture both valid time and transaction time, enabling immutable historical auditing and time-travel queries via TemporalGraphQuery.
The Semantica knowledge graph framework supports audit-ready temporal reasoning through bi-temporal data modeling. By tracking when facts are true in reality (valid time) and when they are recorded in the system (transaction time), you can execute precise time-travel queries against any historical state of the graph. The core implementation resides in the semantica-agi/semantica repository, specifically within the temporal knowledge graph modules.
Understanding Bi-Temporal Fact Structure
A BiTemporalFact consists of six core components defined in semantica/kg/temporal_model.py:
- entity – The subject node identifier (e.g.,
"Person:123"). - relationship – The predicate or edge name (e.g.,
"employedAt"). - value – The object node or literal value.
- valid_from / valid_to –
TemporalBoundobjects describing when the fact is true in the real world. - transaction_from / transaction_to –
TemporalBoundobjects describing when the fact exists in the database.
TemporalBound is a lightweight wrapper that accepts either a concrete datetime object or the special markers OPEN and CLOSED to represent unbounded intervals. Helper functions parse_temporal_bound and parse_temporal_value normalize ISO-8601 strings, epoch milliseconds, or custom period specifications into these bound objects.
Constructing Bi-Temporal Facts
The preferred factory method is BiTemporalFact.from_relationship(), which accepts a dictionary matching the canonical relationship representation used throughout Semantica’s pipelines:
from semantica.kg import BiTemporalFact
fact = BiTemporalFact.from_relationship({
"entity": "Person:123",
"relationship": "employedAt",
"value": "Company:XYZ",
"valid_from": "2020-01-01",
"valid_to": "2022-12-31",
"transaction_from": "2020-02-01T10:00:00Z",
"transaction_to": "OPEN"
})
Internally, this method normalizes the supplied string values, instantiates TemporalBound objects for each bound, and returns a fully-typed, immutable BiTemporalFact instance. This construction pattern is validated in the comprehensive test suite, particularly in test_395_temporal_semantics_comprehensive.py and semantic_extract/test_temporal_extraction.py.
Executing Time-Travel Queries
Semantica’s query engine relies on three abstractions to filter historical states:
- TemporalGraphQuery – A query object that filters on valid and/or transaction intervals.
- TemporalInterval – Utility class computing overlaps between bounds.
- TemporalReasoning – Functions coercing inputs into
BiTemporalFactobjects and applying interval logic.
To query the graph "as of" a specific transaction timestamp, instantiate TemporalGraphQuery with the as_of_transaction parameter:
from semantica.kg import TemporalGraphQuery
query = TemporalGraphQuery(
entity="Person:123",
as_of_transaction="2021-06-01T00:00:00Z"
)
# Returns only facts where transaction_from <= as_of_transaction < transaction_to
facts = graph.search(query)
The engine validates each BiTemporalFact’s transaction bounds using TemporalInterval utilities, ensuring that only the active revision at the requested time is returned.
Managing Fact Revisions
Semantica implements an immutable-by-design versioning pattern. Rather than mutating existing facts, the system creates new BiTemporalFact instances with fresh transaction_from timestamps while automatically closing the prior fact’s transaction_to bound.
The revision logic resides in semantica/kg/temporal_reasoning.py, specifically within the apply_revision helper (approximately lines 180–210). This function uses _coerce_fact to normalize input data, then adjusts the old fact’s transaction_to to the new transaction’s start time before persisting the new fact. This guarantees complete auditability and enables reliable replay of the graph at any past point.
from semantica.kg import BiTemporalFact
# Existing fact (retrieved from storage)
old_fact = kg.get_fact(entity="Person:42", relationship="livesIn")
# Define new revision
new_fact = BiTemporalFact.from_relationship({
"entity": "Person:42",
"relationship": "livesIn",
"value": "City:Berlin",
"valid_from": "2023-06-01",
"valid_to": "OPEN",
"transaction_from": "2023-06-01T09:30:00Z",
"transaction_to": "OPEN"
})
# Backend handles closing old_fact.transaction_to automatically
kg.update_fact(old_fact, new_fact)
Summary
- BiTemporalFact captures both valid time (real-world truth) and transaction time (system record) using
TemporalBoundobjects defined insemantica/kg/temporal_model.py. - Use
BiTemporalFact.from_relationship()to construct facts from dictionary representations, with parsing handled byparse_temporal_bound. - Execute historical queries via
TemporalGraphQueryfiltered byas_of_transaction, leveragingTemporalIntervalfor bound comparison. - Fact updates create immutable revisions;
apply_revisioninsemantica/kg/temporal_reasoning.pymanages transaction interval closure automatically.
Frequently Asked Questions
What is the difference between valid time and transaction time in Semantica?
Valid time represents when a fact is true in the real world (e.g., an employee actually worked at a company from 2020 to 2022), while transaction time represents when the fact was recorded in the knowledge graph (e.g., the system learned about this employment on February 1, 2020). BiTemporalFact stores both as separate TemporalBound pairs to enable independent querying of historical reality versus historical database states.
How do I query the state of the graph at a specific point in history?
Instantiate TemporalGraphQuery with the as_of_transaction parameter set to the desired timestamp. The query engine filters BiTemporalFact instances where the transaction interval contains the specified time, effectively returning the graph state as it existed at that moment. This is implemented in semantica/kg/temporal_query.py.
Can I update an existing BiTemporalFact, or is it immutable?
BiTemporalFact objects are immutable. To "update" a fact, you insert a new BiTemporalFact with an updated transaction_from timestamp, while the system automatically sets the previous fact’s transaction_to to the new transaction’s start time. This append-only pattern, handled by apply_revision in semantica/kg/temporal_reasoning.py, preserves complete audit trails.
What temporal formats does Semantica support for bound parsing?
The parse_temporal_bound and parse_temporal_value functions in semantica/kg/temporal_model.py accept ISO-8601 formatted strings (e.g., "2023-01-01T00:00:00Z"), Unix epoch milliseconds, or the special string literals "OPEN" and "CLOSED" for unbounded intervals. These parsers normalize all inputs into consistent TemporalBound objects.
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 →