diff --git a/semantica/context/causal_analyzer.py b/semantica/context/causal_analyzer.py index 9ad64215..0f18242c 100644 --- a/semantica/context/causal_analyzer.py +++ b/semantica/context/causal_analyzer.py @@ -420,7 +420,7 @@ class CausalChainAnalyzer: data["timestamp"] = datetime.fromisoformat(data["timestamp"]) return Decision( - decision_id=data.get("decision_id", ""), + decision_id=data["decision_id"], # Required field category=data.get("category", ""), scenario=data.get("scenario", ""), reasoning=data.get("reasoning", ""), @@ -430,5 +430,6 @@ class CausalChainAnalyzer: decision_maker=data.get("decision_maker", ""), reasoning_embedding=data.get("reasoning_embedding"), node2vec_embedding=data.get("node2vec_embedding"), - metadata=data.get("metadata", {}) + metadata=data.get("metadata", {}), + auto_generate_id=False # Don't auto-generate for deserialization ) diff --git a/semantica/context/decision_models.py b/semantica/context/decision_models.py index ca2de217..d068b86d 100644 --- a/semantica/context/decision_models.py +++ b/semantica/context/decision_models.py @@ -99,10 +99,12 @@ class Decision: node2vec_embedding: Optional[List[float]] = None metadata: Dict[str, Any] = field(default_factory=dict) - def __post_init__(self): + def __post_init__(self, auto_generate_id: bool = True): """Validate decision data.""" - if self.decision_id is None: + if auto_generate_id and not self.decision_id: # Handle both None and empty string self.decision_id = str(uuid.uuid4()) + elif not self.decision_id and not auto_generate_id: + raise ValueError("decision_id is required when auto_generate_id=False") if not 0 <= self.confidence <= 1: raise ValueError("Confidence must be between 0 and 1") @@ -141,10 +143,12 @@ class DecisionContext: cross_system_inputs: Dict[str, Any] = field(default_factory=dict) metadata: Dict[str, Any] = field(default_factory=dict) - def __post_init__(self): - """Validate context data.""" - if self.context_id is None: + def __post_init__(self, auto_generate_id: bool = True): + """Validate decision context data.""" + if auto_generate_id and not self.context_id: # Handle both None and empty string self.context_id = str(uuid.uuid4()) + elif not self.context_id and not auto_generate_id: + raise ValueError("context_id is required when auto_generate_id=False") def to_dict(self) -> Dict[str, Any]: """Convert context to dictionary.""" @@ -177,10 +181,12 @@ class Policy: updated_at: datetime metadata: Dict[str, Any] = field(default_factory=dict) - def __post_init__(self): + def __post_init__(self, auto_generate_id: bool = True): """Validate policy data.""" - if self.policy_id is None: + if auto_generate_id and not self.policy_id: # Handle both None and empty string self.policy_id = str(uuid.uuid4()) + elif not self.policy_id and not auto_generate_id: + raise ValueError("policy_id is required when auto_generate_id=False") def to_dict(self) -> Dict[str, Any]: """Convert policy to dictionary.""" @@ -218,10 +224,12 @@ class PolicyException: justification: str metadata: Dict[str, Any] = field(default_factory=dict) - def __post_init__(self): - """Validate exception data.""" - if self.exception_id is None: + def __post_init__(self, auto_generate_id: bool = True): + """Validate policy exception data.""" + if auto_generate_id and not self.exception_id: # Handle both None and empty string self.exception_id = str(uuid.uuid4()) + elif not self.exception_id and not auto_generate_id: + raise ValueError("exception_id is required when auto_generate_id=False") def to_dict(self) -> Dict[str, Any]: """Convert exception to dictionary.""" @@ -254,10 +262,12 @@ class Precedent: relationship_type: str # "similar_scenario", "same_policy", "exception_precedent" metadata: Dict[str, Any] = field(default_factory=dict) - def __post_init__(self): + def __post_init__(self, auto_generate_id: bool = True): """Validate precedent data.""" - if self.precedent_id is None: + if auto_generate_id and not self.precedent_id: # Handle both None and empty string self.precedent_id = str(uuid.uuid4()) + elif not self.precedent_id and not auto_generate_id: + raise ValueError("precedent_id is required when auto_generate_id=False") if not 0 <= self.similarity_score <= 1: raise ValueError("Similarity score must be between 0 and 1") valid_types = ["similar_scenario", "same_policy", "exception_precedent"] @@ -292,10 +302,12 @@ class ApprovalChain: timestamp: datetime metadata: Dict[str, Any] = field(default_factory=dict) - def __post_init__(self): - """Validate approval data.""" - if self.approval_id is None: + def __post_init__(self, auto_generate_id: bool = True): + """Validate approval chain data.""" + if auto_generate_id and not self.approval_id: # Handle both None and empty string self.approval_id = str(uuid.uuid4()) + elif not self.approval_id and not auto_generate_id: + raise ValueError("approval_id is required when auto_generate_id=False") valid_methods = ["slack_dm", "zoom_call", "email", "system"] if self.approval_method not in valid_methods: raise ValueError(f"Approval method must be one of: {valid_methods}") diff --git a/semantica/context/decision_query.py b/semantica/context/decision_query.py index 9ecf1322..f020ab7b 100644 --- a/semantica/context/decision_query.py +++ b/semantica/context/decision_query.py @@ -641,7 +641,7 @@ class DecisionQuery: data["timestamp"] = datetime.fromisoformat(data["timestamp"]) return Decision( - decision_id=data.get("decision_id", ""), + decision_id=data["decision_id"], # Required field category=data.get("category", ""), scenario=data.get("scenario", ""), reasoning=data.get("reasoning", ""), @@ -651,7 +651,8 @@ class DecisionQuery: decision_maker=data.get("decision_maker", ""), reasoning_embedding=data.get("reasoning_embedding"), node2vec_embedding=data.get("node2vec_embedding"), - metadata=data.get("metadata", {}) + metadata=data.get("metadata", {}), + auto_generate_id=False # Don't auto-generate for deserialization ) def _dict_to_exception(self, data: Dict[str, Any]) -> PolicyException: @@ -661,14 +662,15 @@ class DecisionQuery: data["approval_timestamp"] = datetime.fromisoformat(data["approval_timestamp"]) return PolicyException( - exception_id=data.get("exception_id", ""), - decision_id=data.get("decision_id", ""), - policy_id=data.get("policy_id", ""), + exception_id=data["exception_id"], # Required field + decision_id=data["decision_id"], # Required field + policy_id=data["policy_id"], # Required field reason=data.get("reason", ""), approver=data.get("approver", ""), approval_timestamp=data.get("approval_timestamp", datetime.now()), justification=data.get("justification", ""), - metadata=data.get("metadata", {}) + metadata=data.get("metadata", {}), + auto_generate_id=False # Don't auto-generate for deserialization ) def _cosine_similarity(self, vec1: List[float], vec2: List[float]) -> float: diff --git a/semantica/context/decision_recorder.py b/semantica/context/decision_recorder.py index 19093b86..7d340c6c 100644 --- a/semantica/context/decision_recorder.py +++ b/semantica/context/decision_recorder.py @@ -231,7 +231,7 @@ class DecisionRecorder: Exception ID """ try: - exception = Exception( + exception = PolicyException( exception_id=str(uuid.uuid4()), decision_id=decision_id, policy_id=policy_id, @@ -423,7 +423,7 @@ class DecisionRecorder: "metadata": decision.metadata }) - def _store_exception_node(self, exception: Exception) -> None: + def _store_exception_node(self, exception: PolicyException) -> None: """Store exception node in graph database.""" query = """ CREATE (e:Exception { diff --git a/semantica/context/policy_engine.py b/semantica/context/policy_engine.py index a69830c9..5edfa4ca 100644 --- a/semantica/context/policy_engine.py +++ b/semantica/context/policy_engine.py @@ -814,7 +814,7 @@ class PolicyEngine: data[field] = datetime.fromisoformat(data[field]) return Policy( - policy_id=data.get("policy_id", ""), + policy_id=data["policy_id"], # Required field name=data.get("name", ""), description=data.get("description", ""), rules=data.get("rules", {}), @@ -822,5 +822,6 @@ 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", {}) + metadata=data.get("metadata", {}), + auto_generate_id=False # Don't auto-generate for deserialization ) diff --git a/tests/context/test_decision_models.py b/tests/context/test_decision_models.py index 92ec20fd..71cb13d3 100644 --- a/tests/context/test_decision_models.py +++ b/tests/context/test_decision_models.py @@ -10,7 +10,7 @@ from datetime import datetime from typing import List, Dict, Any from semantica.context.decision_models import ( - Decision, DecisionContext, Policy, Exception, Precedent, ApprovalChain, + Decision, DecisionContext, Policy, PolicyException, Precedent, ApprovalChain, validate_decision, validate_policy, serialize_decision, deserialize_decision, serialize_policy, deserialize_policy ) @@ -213,12 +213,12 @@ class TestPolicy: assert len(policy.policy_id) > 0 -class TestException: - """Test Exception data model.""" +class TestPolicyException: + """Test PolicyException data model.""" - def test_exception_creation(self): - """Test basic exception creation.""" - exception = Exception( + def test_policy_exception_creation(self): + """Test basic policy exception creation.""" + policy_exception = PolicyException( exception_id="exc_001", decision_id="decision_001", policy_id="policy_001", @@ -229,17 +229,17 @@ class TestException: metadata={"override_type": "vip_exception"} ) - assert exception.exception_id == "exc_001" - assert exception.decision_id == "decision_001" - assert exception.policy_id == "policy_001" - assert exception.reason == "Customer is VIP with special arrangements" - assert exception.approver == "manager_001" - assert exception.justification == "Long-term customer with excellent history" - assert exception.metadata["override_type"] == "vip_exception" + assert policy_exception.exception_id == "exc_001" + assert policy_exception.decision_id == "decision_001" + assert policy_exception.policy_id == "policy_001" + assert policy_exception.reason == "Customer is VIP with special arrangements" + assert policy_exception.approver == "manager_001" + assert policy_exception.justification == "Long-term customer with excellent history" + assert policy_exception.metadata["override_type"] == "vip_exception" - def test_exception_auto_id(self): + def test_policy_exception_auto_id(self): """Test automatic ID generation.""" - exception = Exception( + policy_exception = PolicyException( exception_id="", decision_id="decision_001", policy_id="policy_001", @@ -249,8 +249,8 @@ class TestException: justification="test justification" ) - assert exception.exception_id != "" - assert len(exception.exception_id) > 0 + assert policy_exception.exception_id != "" + assert len(policy_exception.exception_id) > 0 class TestPrecedent: diff --git a/tests/context/test_decision_query.py b/tests/context/test_decision_query.py index 4611dbba..f4bd6c8e 100644 --- a/tests/context/test_decision_query.py +++ b/tests/context/test_decision_query.py @@ -10,7 +10,7 @@ from datetime import datetime, timedelta from unittest.mock import Mock, patch from typing import List, Dict, Any -from semantica.context.decision_models import Decision, Policy, Exception +from semantica.context.decision_models import Decision, Policy, PolicyException from semantica.context.decision_query import DecisionQuery diff --git a/tests/context/test_decision_recorder.py b/tests/context/test_decision_recorder.py index 6ce81e05..a4328501 100644 --- a/tests/context/test_decision_recorder.py +++ b/tests/context/test_decision_recorder.py @@ -239,9 +239,9 @@ class TestDecisionRecorder: def test_store_exception_node(self, decision_recorder, mock_graph_store): """Test storing exception node in graph.""" - from semantica.context.decision_models import Exception + from semantica.context.decision_models import PolicyException - exception = Exception( + exception = PolicyException( exception_id="exc_001", decision_id="decision_001", policy_id="policy_001", diff --git a/tests/context/test_policy_engine.py b/tests/context/test_policy_engine.py index e0ff9077..2728a7bb 100644 --- a/tests/context/test_policy_engine.py +++ b/tests/context/test_policy_engine.py @@ -10,7 +10,7 @@ from datetime import datetime, timedelta from unittest.mock import Mock, patch from typing import List, Dict, Any -from semantica.context.decision_models import Policy, Exception +from semantica.context.decision_models import Policy, PolicyException from semantica.context.policy_engine import PolicyEngine