mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
Merge pull request #327 from Hawksight-AI/context
Fix Context Graph Features - Resolve Method Conflicts and Integration Issues
This commit is contained in:
@@ -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
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user