Merge pull request #327 from Hawksight-AI/context

Fix Context Graph Features - Resolve Method Conflicts and Integration Issues
This commit is contained in:
Mohd Kaif
2026-02-17 15:13:43 +05:30
committed by GitHub
7 changed files with 169 additions and 34 deletions
+43 -7
View File
@@ -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
)
```
+15 -2
View File
@@ -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,
+64 -13
View File
@@ -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,
+39 -5
View File
@@ -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
)
```
+1 -2
View File
@@ -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", {})
)
@@ -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."""
@@ -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