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
This commit is contained in:
491
tools/temporal_kg_tool.py
Normal file
491
tools/temporal_kg_tool.py
Normal file
@@ -0,0 +1,491 @@
|
||||
"""Temporal Knowledge Graph Tool for Hermes Agent.
|
||||
|
||||
Provides tool functions for storing and querying temporal facts,
|
||||
enabling Timmy to track how knowledge evolves over time.
|
||||
|
||||
Functions:
|
||||
- store_fact_with_time: Store a fact with temporal bounds
|
||||
- query_historical_state: Query facts valid at a specific time
|
||||
- get_fact_history: Get the version history of a fact
|
||||
- generate_temporal_summary: Generate a historical summary
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List, Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from agent.temporal_knowledge_graph import TemporalTripleStore, TemporalOperator
|
||||
from agent.temporal_reasoning import TemporalReasoner
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Global instances (singleton pattern)
|
||||
_store: Optional[TemporalTripleStore] = None
|
||||
_reasoner: Optional[TemporalReasoner] = None
|
||||
|
||||
|
||||
def _get_store() -> TemporalTripleStore:
|
||||
"""Get or create the temporal triple store singleton."""
|
||||
global _store
|
||||
if _store is None:
|
||||
_store = TemporalTripleStore()
|
||||
return _store
|
||||
|
||||
|
||||
def _get_reasoner() -> TemporalReasoner:
|
||||
"""Get or create the temporal reasoner singleton."""
|
||||
global _reasoner
|
||||
if _reasoner is None:
|
||||
_reasoner = TemporalReasoner(_get_store())
|
||||
return _reasoner
|
||||
|
||||
|
||||
def store_fact_with_time(
|
||||
subject: str,
|
||||
predicate: str,
|
||||
object: str,
|
||||
valid_from: Optional[str] = None,
|
||||
valid_until: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Store a fact with temporal metadata.
|
||||
|
||||
Args:
|
||||
subject: The subject of the fact (e.g., "Hermes Agent")
|
||||
predicate: The predicate/relationship (e.g., "has_feature")
|
||||
object: The object/value (e.g., "input_sanitizer")
|
||||
valid_from: When this fact becomes valid (ISO 8601). Defaults to now.
|
||||
valid_until: When this fact expires (ISO 8601). None means still valid.
|
||||
|
||||
Returns:
|
||||
Dictionary containing the stored triple details
|
||||
|
||||
Example:
|
||||
>>> store_fact_with_time(
|
||||
... subject="Hermes Agent",
|
||||
... predicate="has_feature",
|
||||
... object="input_sanitizer",
|
||||
... valid_from="2026-04-01T01:00:00"
|
||||
... )
|
||||
"""
|
||||
try:
|
||||
store = _get_store()
|
||||
triple = store.store_fact(subject, predicate, object, valid_from, valid_until)
|
||||
|
||||
logger.info(f"Stored temporal fact: {subject} {predicate} {object}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"triple": {
|
||||
"id": triple.id,
|
||||
"subject": triple.subject,
|
||||
"predicate": triple.predicate,
|
||||
"object": triple.object,
|
||||
"valid_from": triple.valid_from,
|
||||
"valid_until": triple.valid_until,
|
||||
"timestamp": triple.timestamp,
|
||||
"version": triple.version
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to store temporal fact: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
|
||||
def query_historical_state(
|
||||
subject: str,
|
||||
timestamp: str,
|
||||
predicate: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Query what was known about a subject at a specific point in time.
|
||||
|
||||
Args:
|
||||
subject: The entity to query (e.g., "Timmy")
|
||||
timestamp: The point in time (ISO 8601, e.g., "2026-03-01T00:00:00")
|
||||
predicate: Optional predicate filter
|
||||
|
||||
Returns:
|
||||
Dictionary containing the facts valid at that time
|
||||
|
||||
Example:
|
||||
>>> query_historical_state("Timmy", "2026-03-01T00:00:00")
|
||||
# Returns facts valid at that time
|
||||
"""
|
||||
try:
|
||||
store = _get_store()
|
||||
facts = store.query_at_time(timestamp, subject=subject, predicate=predicate)
|
||||
|
||||
logger.info(f"Queried historical state for {subject} at {timestamp}: {len(facts)} facts")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"subject": subject,
|
||||
"timestamp": timestamp,
|
||||
"fact_count": len(facts),
|
||||
"facts": [
|
||||
{
|
||||
"predicate": f.predicate,
|
||||
"object": f.object,
|
||||
"valid_from": f.valid_from,
|
||||
"valid_until": f.valid_until,
|
||||
"version": f.version
|
||||
}
|
||||
for f in facts
|
||||
]
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to query historical state: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
|
||||
def get_fact_history(
|
||||
subject: str,
|
||||
predicate: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Get the complete version history of a fact.
|
||||
|
||||
Args:
|
||||
subject: The subject to query
|
||||
predicate: The predicate to query
|
||||
|
||||
Returns:
|
||||
Dictionary containing the version history
|
||||
|
||||
Example:
|
||||
>>> get_fact_history("Timmy", "view_on_sovereignty")
|
||||
# Returns all versions of this fact
|
||||
"""
|
||||
try:
|
||||
store = _get_store()
|
||||
history = store.get_fact_history(subject, predicate)
|
||||
|
||||
logger.info(f"Retrieved history for {subject} {predicate}: {len(history)} versions")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"subject": subject,
|
||||
"predicate": predicate,
|
||||
"version_count": len(history),
|
||||
"versions": [
|
||||
{
|
||||
"object": h.object,
|
||||
"valid_from": h.valid_from,
|
||||
"valid_until": h.valid_until,
|
||||
"timestamp": h.timestamp,
|
||||
"version": h.version,
|
||||
"superseded_by": h.superseded_by
|
||||
}
|
||||
for h in history
|
||||
]
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get fact history: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
|
||||
def generate_temporal_summary(
|
||||
entity: str,
|
||||
start_time: str,
|
||||
end_time: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Generate a historical summary of an entity's evolution.
|
||||
|
||||
Args:
|
||||
entity: The entity to summarize (e.g., "security_audit")
|
||||
start_time: Start of time range (ISO 8601)
|
||||
end_time: End of time range (ISO 8601)
|
||||
|
||||
Returns:
|
||||
Dictionary containing the historical summary
|
||||
|
||||
Example:
|
||||
>>> generate_temporal_summary("security_audit", "2026-03-01", "2026-04-01")
|
||||
# Returns evolution of security posture
|
||||
"""
|
||||
try:
|
||||
reasoner = _get_reasoner()
|
||||
summary = reasoner.generate_temporal_summary(entity, start_time, end_time)
|
||||
|
||||
logger.info(f"Generated temporal summary for {entity}: {summary.total_changes} changes")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"entity": entity,
|
||||
"start_time": start_time,
|
||||
"end_time": end_time,
|
||||
"summary": summary.to_dict()
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to generate temporal summary: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
|
||||
def when_did_we_learn(
|
||||
subject: str,
|
||||
predicate: Optional[str] = None,
|
||||
object: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Query when we first learned about something.
|
||||
|
||||
Args:
|
||||
subject: The subject to search for
|
||||
predicate: Optional predicate filter
|
||||
object: Optional object filter
|
||||
|
||||
Returns:
|
||||
Dictionary containing the timestamp of first knowledge
|
||||
|
||||
Example:
|
||||
>>> when_did_we_learn("MLX", predicate="integrated_with")
|
||||
# Returns when MLX integration was first recorded
|
||||
"""
|
||||
try:
|
||||
reasoner = _get_reasoner()
|
||||
timestamp = reasoner.when_did_we_learn(subject, predicate, object)
|
||||
|
||||
if timestamp:
|
||||
logger.info(f"Found first knowledge of {subject} at {timestamp}")
|
||||
return {
|
||||
"success": True,
|
||||
"subject": subject,
|
||||
"predicate": predicate,
|
||||
"object": object,
|
||||
"first_known": timestamp
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": True,
|
||||
"subject": subject,
|
||||
"predicate": predicate,
|
||||
"object": object,
|
||||
"first_known": None,
|
||||
"message": "No knowledge found for this subject"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to query when we learned: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
|
||||
def how_has_it_changed(
|
||||
subject: str,
|
||||
since_time: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Query how something has changed since a specific time.
|
||||
|
||||
Args:
|
||||
subject: The entity to analyze
|
||||
since_time: The starting time (ISO 8601)
|
||||
|
||||
Returns:
|
||||
Dictionary containing the list of changes
|
||||
|
||||
Example:
|
||||
>>> how_has_it_changed("codebase", "2026-03-01T00:00:00")
|
||||
# Returns changes since the security audit
|
||||
"""
|
||||
try:
|
||||
reasoner = _get_reasoner()
|
||||
changes = reasoner.how_has_it_changed(subject, since_time)
|
||||
|
||||
logger.info(f"Found {len(changes)} changes for {subject} since {since_time}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"subject": subject,
|
||||
"since_time": since_time,
|
||||
"change_count": len(changes),
|
||||
"changes": [
|
||||
{
|
||||
"change_type": c.change_type.value,
|
||||
"predicate": c.predicate,
|
||||
"old_value": c.old_value,
|
||||
"new_value": c.new_value,
|
||||
"timestamp": c.timestamp,
|
||||
"version": c.version
|
||||
}
|
||||
for c in changes
|
||||
]
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to query changes: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
|
||||
def query_with_temporal_operator(
|
||||
operator: str,
|
||||
timestamp: str,
|
||||
subject: Optional[str] = None,
|
||||
predicate: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Query using temporal operators (BEFORE, AFTER, DURING, OVERLAPS).
|
||||
|
||||
Args:
|
||||
operator: Temporal operator (BEFORE, AFTER, DURING, OVERLAPS, AT)
|
||||
timestamp: Reference timestamp (ISO 8601)
|
||||
subject: Optional subject filter
|
||||
predicate: Optional predicate filter
|
||||
|
||||
Returns:
|
||||
Dictionary containing matching facts
|
||||
|
||||
Example:
|
||||
>>> query_with_temporal_operator("BEFORE", "2026-04-01T00:00:00", subject="Timmy")
|
||||
# Returns facts about Timmy before April 2026
|
||||
"""
|
||||
try:
|
||||
store = _get_store()
|
||||
|
||||
# Map string to enum
|
||||
op_map = {
|
||||
"BEFORE": TemporalOperator.BEFORE,
|
||||
"AFTER": TemporalOperator.AFTER,
|
||||
"DURING": TemporalOperator.DURING,
|
||||
"OVERLAPS": TemporalOperator.OVERLAPS,
|
||||
"AT": TemporalOperator.AT
|
||||
}
|
||||
|
||||
if operator.upper() not in op_map:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Invalid operator: {operator}. Use BEFORE, AFTER, DURING, OVERLAPS, or AT"
|
||||
}
|
||||
|
||||
op = op_map[operator.upper()]
|
||||
facts = store.query_temporal(op, timestamp, subject, predicate)
|
||||
|
||||
logger.info(f"Queried with operator {operator}: {len(facts)} facts")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"operator": operator,
|
||||
"timestamp": timestamp,
|
||||
"subject": subject,
|
||||
"predicate": predicate,
|
||||
"fact_count": len(facts),
|
||||
"facts": [
|
||||
{
|
||||
"subject": f.subject,
|
||||
"predicate": f.predicate,
|
||||
"object": f.object,
|
||||
"valid_from": f.valid_from,
|
||||
"valid_until": f.valid_until,
|
||||
"version": f.version
|
||||
}
|
||||
for f in facts
|
||||
]
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to query with temporal operator: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
|
||||
def get_worldview_at_time(
|
||||
timestamp: str,
|
||||
subjects: Optional[List[str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Get Timmy's complete worldview at a specific point in time.
|
||||
|
||||
Args:
|
||||
timestamp: The point in time (ISO 8601)
|
||||
subjects: Optional list of subjects to include. If None, includes all.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping subjects to their facts at that time
|
||||
|
||||
Example:
|
||||
>>> get_worldview_at_time("2026-03-01T00:00:00", ["Timmy", "Hermes"])
|
||||
"""
|
||||
try:
|
||||
reasoner = _get_reasoner()
|
||||
worldview = reasoner.get_worldview_at_time(timestamp, subjects)
|
||||
|
||||
logger.info(f"Retrieved worldview at {timestamp}: {len(worldview)} entities")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"timestamp": timestamp,
|
||||
"entity_count": len(worldview),
|
||||
"worldview": worldview
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get worldview: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
|
||||
# Convenience function for natural language queries
|
||||
def ask_temporal_question(question: str, **kwargs) -> Dict[str, Any]:
|
||||
"""Parse and answer a temporal question.
|
||||
|
||||
This is a higher-level interface that can parse simple temporal questions
|
||||
and route them to the appropriate function.
|
||||
|
||||
Args:
|
||||
question: Natural language temporal question
|
||||
**kwargs: Additional context parameters
|
||||
|
||||
Returns:
|
||||
Dictionary containing the answer
|
||||
|
||||
Example:
|
||||
>>> ask_temporal_question("What was Timmy's view on sovereignty before March 2026?")
|
||||
"""
|
||||
question_lower = question.lower()
|
||||
|
||||
# Simple pattern matching for common question types
|
||||
if "what did we believe" in question_lower or "what was" in question_lower:
|
||||
if "before" in question_lower:
|
||||
# Extract subject and time
|
||||
subject = kwargs.get("subject")
|
||||
before_time = kwargs.get("before_time")
|
||||
if subject and before_time:
|
||||
return query_historical_state(subject, before_time)
|
||||
|
||||
elif "when did we first learn" in question_lower or "when did we learn" in question_lower:
|
||||
subject = kwargs.get("subject")
|
||||
predicate = kwargs.get("predicate")
|
||||
if subject:
|
||||
return when_did_we_learn(subject, predicate)
|
||||
|
||||
elif "how has" in question_lower and "changed" in question_lower:
|
||||
subject = kwargs.get("subject")
|
||||
since_time = kwargs.get("since_time")
|
||||
if subject and since_time:
|
||||
return how_has_it_changed(subject, since_time)
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Could not parse temporal question. Use specific function calls instead.",
|
||||
"available_functions": [
|
||||
"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"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user