From 49c60387c5bf023028cbf1145265ed0a9ddbf6df Mon Sep 17 00:00:00 2001 From: KaifAhmad1 Date: Tue, 17 Feb 2026 14:29:47 +0530 Subject: [PATCH] Fix timestamp normalization: Prevent float timestamps from breaking Decision serialization - Add _normalize_timestamp helper to handle various timestamp formats - Support datetime, int/float (epoch), str (ISO with optional Z), None/invalid - Update get_causal_chain to use timestamp normalization - Update find_precedents to use timestamp normalization - Update add_decision to normalize timestamps before storage - Prevent float timestamps from breaking Decision.to_dict() and .isoformat() - Ensure consistent datetime objects in all Decision instances - All 62 tests still passing successfully --- semantica/context/context_graph.py | 51 +++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/semantica/context/context_graph.py b/semantica/context/context_graph.py index 05470197..b14bfbbc 100644 --- a/semantica/context/context_graph.py +++ b/semantica/context/context_graph.py @@ -616,6 +616,40 @@ class ContextGraph: # --- Internal Helpers --- + def _normalize_timestamp(self, timestamp_value) -> datetime: + """ + Normalize timestamp value to datetime object. + + Handles various timestamp formats: + - datetime: returns as-is + - int/float: converts from epoch seconds + - str: parses ISO format (with optional Z) + - None/invalid: returns current datetime + + Args: + timestamp_value: Timestamp value in various formats + + Returns: + datetime: Normalized datetime object + """ + from datetime import datetime + + if isinstance(timestamp_value, datetime): + return timestamp_value + elif isinstance(timestamp_value, (int, float)): + return datetime.fromtimestamp(timestamp_value) + elif isinstance(timestamp_value, str): + # Handle ISO format with optional Z suffix + timestamp_str = timestamp_value.rstrip('Z') # Remove Z if present + try: + return datetime.fromisoformat(timestamp_str) + except ValueError: + # Fallback to current datetime if parsing fails + return datetime.now() + else: + # Fallback for None or other types + return datetime.now() + def _add_internal_node(self, node: ContextNode) -> bool: """Internal method to add a node.""" self.nodes[node.node_id] = node @@ -951,6 +985,9 @@ class ContextGraph: # Handle None metadata metadata = decision.metadata or {} + # Normalize timestamp to ensure consistent storage format + normalized_timestamp = self._normalize_timestamp(decision.timestamp) + node = ContextNode( node_id=node_id, node_type="Decision", @@ -960,7 +997,7 @@ class ContextGraph: "reasoning": decision.reasoning, "outcome": decision.outcome, "confidence": decision.confidence, - "timestamp": decision.timestamp.isoformat(), + "timestamp": normalized_timestamp.isoformat(), "decision_maker": decision.decision_maker, "reasoning_embedding": decision.reasoning_embedding, "node2vec_embedding": decision.node2vec_embedding, @@ -1051,11 +1088,7 @@ class ContextGraph: if (hasattr(node, 'node_type') and isinstance(node.node_type, str) and node.node_type.lower() == "decision"): decision_data = node.properties - timestamp_str = decision_data.get("timestamp", datetime.now().isoformat()) - if isinstance(timestamp_str, str): - timestamp = datetime.fromisoformat(timestamp_str) - else: - timestamp = timestamp_str + timestamp = self._normalize_timestamp(decision_data.get("timestamp")) decision = Decision( decision_id=current_id, category=decision_data.get("category", ""), @@ -1120,11 +1153,7 @@ class ContextGraph: node.node_type.lower() == "decision"): decision_data = node.properties from .decision_models import Decision - timestamp_str = decision_data.get("timestamp", datetime.now().isoformat()) - if isinstance(timestamp_str, str): - timestamp = datetime.fromisoformat(timestamp_str) - else: - timestamp = timestamp_str + timestamp = self._normalize_timestamp(decision_data.get("timestamp")) decision = Decision( decision_id=pid, category=decision_data.get("category", ""),