diff --git a/docs/reference/context.md b/docs/reference/context.md index 276d2632..a66cc0c7 100644 --- a/docs/reference/context.md +++ b/docs/reference/context.md @@ -186,7 +186,24 @@ knowledge.add_edge("FastAPI", "Programming", "used_for") ### Easy Decision Management ```python # Record decisions in your knowledge graph -decision_id = knowledge.add_decision( +from semantica.context.decision_models import Decision +from datetime import datetime + +decision = Decision( + decision_id="tech_choice_001", + category="technology_choice", + scenario="Framework selection for web API", + reasoning="FastAPI provides better performance for Python APIs", + outcome="selected_fastapi", + confidence=0.92, + timestamp=datetime.now(), + decision_maker="system", + metadata={"entities": ["Python", "FastAPI", "web_project"]} +) +knowledge.add_decision(decision) + +# Or use the convenience method for quick decisions +decision_id = knowledge.add_decision_simple( category="technology_choice", scenario="Framework selection for web API", reasoning="FastAPI provides better performance for Python APIs", @@ -196,10 +213,10 @@ decision_id = knowledge.add_decision( ) # Find similar decisions easily -similar = knowledge.find_similar_decisions( +similar = knowledge.find_precedents_by_scenario( scenario="web framework", category="technology_choice", - max_results=3 + limit=3 ) print(f"Found {len(similar)} similar decisions") @@ -259,7 +276,9 @@ print(f"Python importance score: {importance.get('degree', 0)}") | `add_node(node_id, node_type, properties)` | Add concepts to remember | Build knowledge base | | `add_edge(source, target, relation)` | Connect related concepts | Show relationships | | `add_decision(category, scenario, reasoning, outcome, confidence, ...)` | Record decisions | Track choices and learn | -| `find_similar_decisions(scenario, category, ...)` | Find similar decisions | Make consistent choices | +| `add_decision_simple(category, scenario, reasoning, outcome, confidence, ...)` | Easy decision recording | Quick decision tracking | +| `find_precedents(decision_id, limit)` | Find precedents by ID | Get connected decisions | +| `find_precedents_by_scenario(scenario, category, ...)` | Find similar decisions | Make consistent choices | | `analyze_decision_impact(decision_id)` | Understand decision influence | See how decisions affect others | | `get_decision_summary()` | Get decision statistics | Understand decision patterns | | `trace_decision_chain(decision_id)` | Trace decision connections | Understand decision relationships | @@ -378,7 +397,24 @@ ecommerce_graph.add_node("laptop_xyz", "product", {"category": "electronics"}) ecommerce_graph.add_edge("user_123", "laptop_xyz", "viewed") # Make recommendation decision -rec_decision = ecommerce_graph.add_decision( +from semantica.context.decision_models import Decision +from datetime import datetime + +rec_decision = Decision( + decision_id="rec_001", + category="product_recommendation", + scenario="Laptop recommendation for premium user", + reasoning="User prefers high-performance electronics", + outcome="recommended_gaming_laptop", + confidence=0.87, + timestamp=datetime.now(), + decision_maker="recommendation_system", + metadata={"entities": ["user_123", "laptop_xyz"]} +) +ecommerce_graph.add_decision(rec_decision) + +# Or use the convenience method +rec_decision_id = ecommerce_graph.add_decision_simple( category="product_recommendation", scenario="Laptop recommendation for premium user", reasoning="User prefers high-performance electronics", @@ -388,9 +424,9 @@ rec_decision = ecommerce_graph.add_decision( ) # Find similar recommendations -similar_recs = ecommerce_graph.find_similar_decisions( +similar_recs = ecommerce_graph.find_precedents_by_scenario( scenario="laptop recommendation", - max_results=5 + limit=5 ) ``` diff --git a/semantica/context/agent_context.py b/semantica/context/agent_context.py index 16eb7e10..0bd4adeb 100644 --- a/semantica/context/agent_context.py +++ b/semantica/context/agent_context.py @@ -1659,9 +1659,9 @@ class AgentContext: raise RuntimeError("Decision tracking is not enabled") # Delegate to ContextGraph if available - if self._decision_backend == "context_graph" and hasattr(self.knowledge_graph, "find_precedents"): + if self._decision_backend == "context_graph" and hasattr(self.knowledge_graph, "find_precedents_by_scenario"): try: - precedents = self.knowledge_graph.find_precedents( + precedents = self.knowledge_graph.find_precedents_by_scenario( scenario=scenario, category=category, limit=limit, @@ -1805,6 +1805,19 @@ class AgentContext: decision_id, direction, max_depth ) + if self._decision_backend == "context_graph": + # Use ContextGraph's get_causal_chain method + if hasattr(self.knowledge_graph, "get_causal_chain"): + return self.knowledge_graph.get_causal_chain( + decision_id=decision_id, + direction=direction, + max_depth=max_depth + ) + # Fallback to causal analyzer + return self._causal_analyzer.get_causal_chain( + decision_id, direction, max_depth + ) + if hasattr(self.knowledge_graph, "get_causal_chain"): return self.knowledge_graph.get_causal_chain( decision_id=decision_id, diff --git a/semantica/context/context_graph.py b/semantica/context/context_graph.py index 09db0df2..b14bfbbc 100644 --- a/semantica/context/context_graph.py +++ b/semantica/context/context_graph.py @@ -616,10 +616,49 @@ 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 - self.node_type_index[node.node_type].add(node.node_id) + # Handle edge case where node_type might be None or not a string + if hasattr(node, 'node_type') and isinstance(node.node_type, str): + self.node_type_index[node.node_type].add(node.node_id) + else: + # Use 'unknown' as fallback for invalid node_type + self.node_type_index['unknown'].add(node.node_id) return True def _add_internal_edge(self, edge: ContextEdge) -> bool: @@ -939,12 +978,16 @@ class ContextGraph: """ from .decision_models import Decision - # Handle empty decision ID by generating UUID only if None - node_id = decision.decision_id if decision.decision_id is not None else str(uuid.uuid4()) + # Handle empty decision ID by generating UUID for both None and empty string + # This ensures consistent behavior with Decision model's __post_init__ method + node_id = decision.decision_id if decision.decision_id else str(uuid.uuid4()) # 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", @@ -954,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, @@ -986,8 +1029,12 @@ class ContextGraph: return # Check if nodes are decision nodes - if not, skip adding relationship - if (self.nodes[source_decision_id].node_type != "Decision" or - self.nodes[target_decision_id].node_type != "Decision"): + source_node = self.nodes[source_decision_id] + target_node = self.nodes[target_decision_id] + if (not hasattr(source_node, 'node_type') or not isinstance(source_node.node_type, str) or + not hasattr(target_node, 'node_type') or not isinstance(target_node.node_type, str) or + source_node.node_type.lower() != "decision" or + target_node.node_type.lower() != "decision"): return edge = ContextEdge( @@ -1038,8 +1085,10 @@ class ContextGraph: # Get decision node if current_id in self.nodes: node = self.nodes[current_id] - if node.node_type == "Decision": + if (hasattr(node, 'node_type') and isinstance(node.node_type, str) and + node.node_type.lower() == "decision"): decision_data = node.properties + timestamp = self._normalize_timestamp(decision_data.get("timestamp")) decision = Decision( decision_id=current_id, category=decision_data.get("category", ""), @@ -1047,7 +1096,7 @@ class ContextGraph: reasoning=decision_data.get("reasoning", ""), outcome=decision_data.get("outcome", ""), confidence=decision_data.get("confidence", 0.0), - timestamp=datetime.fromisoformat(decision_data.get("timestamp", datetime.now().isoformat())), + timestamp=timestamp, decision_maker=decision_data.get("decision_maker", ""), reasoning_embedding=decision_data.get("reasoning_embedding"), node2vec_embedding=decision_data.get("node2vec_embedding"), @@ -1100,9 +1149,11 @@ class ContextGraph: for pid in precedent_ids[:limit]: if pid in self.nodes: node = self.nodes[pid] - if node.node_type == "Decision": + if (hasattr(node, 'node_type') and isinstance(node.node_type, str) and + node.node_type.lower() == "decision"): decision_data = node.properties from .decision_models import Decision + timestamp = self._normalize_timestamp(decision_data.get("timestamp")) decision = Decision( decision_id=pid, category=decision_data.get("category", ""), @@ -1110,7 +1161,7 @@ class ContextGraph: reasoning=decision_data.get("reasoning", ""), outcome=decision_data.get("outcome", ""), confidence=decision_data.get("confidence", 0.0), - timestamp=datetime.fromisoformat(decision_data.get("timestamp", datetime.now().isoformat())), + timestamp=timestamp, decision_maker=decision_data.get("decision_maker", ""), reasoning_embedding=decision_data.get("reasoning_embedding"), node2vec_embedding=decision_data.get("node2vec_embedding"), @@ -1513,7 +1564,7 @@ class ContextGraph: self.logger.info(f"Recorded decision {decision_id} in category {category}") return decision_id - def find_precedents( + def find_precedents_by_scenario( self, scenario: str, category: Optional[str] = None, @@ -1999,7 +2050,7 @@ class ContextGraph: # --- Easy-to-Use Convenience Methods --- - def add_decision( + def add_decision_simple( self, category: str, scenario: str, @@ -2056,7 +2107,7 @@ class ContextGraph: Returns: List of similar decisions with similarity scores """ - return self.find_precedents( + return self.find_precedents_by_scenario( scenario=scenario, category=category, limit=max_results, diff --git a/semantica/context/context_usage.md b/semantica/context/context_usage.md index 77e9d0fe..4e7ac56e 100644 --- a/semantica/context/context_usage.md +++ b/semantica/context/context_usage.md @@ -148,7 +148,24 @@ knowledge.add_edge("Programming", "Web Development", "requires") ### Easy Decision Management ```python # Record decisions in your knowledge graph -decision_id = knowledge.add_decision( +from semantica.context.decision_models import Decision +from datetime import datetime + +decision = Decision( + decision_id="tech_choice_001", + category="technology_choice", + scenario="Framework selection for web API", + reasoning="FastAPI provides better performance for Python APIs", + outcome="selected_fastapi", + confidence=0.92, + timestamp=datetime.now(), + decision_maker="system", + metadata={"entities": ["Python", "FastAPI", "web_project"]} +) +knowledge.add_decision(decision) + +# Or use the convenience method for quick decisions +decision_id = knowledge.add_decision_simple( category="technology_choice", scenario="Framework selection for web API", reasoning="FastAPI provides better performance for Python APIs", @@ -158,7 +175,7 @@ decision_id = knowledge.add_decision( ) # Find similar decisions easily -similar = knowledge.find_similar_decisions( +similar = knowledge.find_precedents_by_scenario( scenario="web framework", category="technology_choice", max_results=3 @@ -330,7 +347,24 @@ ecommerce_graph.add_node("laptop_xyz", "product", {"category": "electronics"}) ecommerce_graph.add_edge("user_123", "laptop_xyz", "viewed") # Make recommendation decision -rec_decision = ecommerce_graph.add_decision( +from semantica.context.decision_models import Decision +from datetime import datetime + +rec_decision = Decision( + decision_id="rec_001", + category="product_recommendation", + scenario="Laptop recommendation for premium user", + reasoning="User prefers high-performance electronics", + outcome="recommended_gaming_laptop", + confidence=0.87, + timestamp=datetime.now(), + decision_maker="recommendation_system", + metadata={"entities": ["user_123", "laptop_xyz"]} +) +ecommerce_graph.add_decision(rec_decision) + +# Or use the convenience method +rec_decision_id = ecommerce_graph.add_decision_simple( category="product_recommendation", scenario="Laptop recommendation for premium user", reasoning="User prefers high-performance electronics", @@ -340,9 +374,9 @@ rec_decision = ecommerce_graph.add_decision( ) # Find similar recommendations -similar_recs = ecommerce_graph.find_similar_decisions( +similar_recs = ecommerce_graph.find_precedents_by_scenario( scenario="laptop recommendation", - max_results=5 + limit=5 ) ``` diff --git a/semantica/context/policy_engine.py b/semantica/context/policy_engine.py index 5edfa4ca..10fe1512 100644 --- a/semantica/context/policy_engine.py +++ b/semantica/context/policy_engine.py @@ -822,6 +822,5 @@ class PolicyEngine: version=data.get("version", ""), created_at=data.get("created_at", datetime.now()), updated_at=data.get("updated_at", datetime.now()), - metadata=data.get("metadata", {}), - auto_generate_id=False # Don't auto-generate for deserialization + metadata=data.get("metadata", {}) ) diff --git a/tests/context/test_context_graph_decisions.py b/tests/context/test_context_graph_decisions.py index d9a21040..1e050b6c 100644 --- a/tests/context/test_context_graph_decisions.py +++ b/tests/context/test_context_graph_decisions.py @@ -594,11 +594,12 @@ class TestContextGraphDecisionsEdgeCases: decision_maker="test_agent" ) - # Should still add the decision + # Should still add the decision (auto-generates UUID for empty string) context_graph.add_decision(decision) - # Should be accessible with empty string key - assert "" in context_graph.nodes + # Should have generated UUID for empty string (not preserve empty string) + assert len(context_graph.nodes) == 1 + assert "" not in context_graph.nodes # Empty string should be replaced with UUID def test_decision_with_null_fields(self, context_graph): """Test adding decision with null fields.""" diff --git a/tests/context/test_context_graphs_examples.py b/tests/context/test_context_graphs_examples.py index 8fa2f1c6..58938043 100644 --- a/tests/context/test_context_graphs_examples.py +++ b/tests/context/test_context_graphs_examples.py @@ -315,7 +315,7 @@ class TestContextGraphsExamples: # Test empty decision ID handling decision_empty_id = Decision( - decision_id="", # Empty ID + decision_id="", # Empty ID - will be auto-generated category="test", scenario="test scenario", reasoning="test reasoning", @@ -326,7 +326,8 @@ class TestContextGraphsExamples: ) graph.add_decision(decision_empty_id) - assert "" in graph.nodes # Empty string should be preserved as key + assert len(graph.nodes) == 1 # Should have generated UUID for empty string + assert "" not in graph.nodes # Empty string should not be preserved print("+ Empty decision ID handling working") # Test None decision ID handling