diff --git a/README.md b/README.md index 5db9030d..0b0ae144 100644 --- a/README.md +++ b/README.md @@ -354,7 +354,7 @@ from semantica.context import AgentContext, AgentMemory from semantica.vector_store import VectorStore context = AgentContext( - vector_store=VectorStore(backend="faiss", dimension=768), + vector_store=VectorStore(backend="inmemory"), knowledge_graph=ContextGraph(advanced_analytics=True), decision_tracking=True, graph_expansion=True, diff --git a/docs/getting-started.md b/docs/getting-started.md index dcec03a9..deb69915 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -44,7 +44,7 @@ from semantica.context import AgentContext, ContextGraph from semantica.vector_store import VectorStore context = AgentContext( - vector_store=VectorStore(backend="faiss", dimension=768), + vector_store=VectorStore(backend="inmemory"), knowledge_graph=ContextGraph(advanced_analytics=True), decision_tracking=True, ) diff --git a/docs/index.md b/docs/index.md index 63a0ecf7..42a2255b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -65,7 +65,7 @@ from semantica.context import AgentContext, ContextGraph from semantica.vector_store import VectorStore context = AgentContext( - vector_store=VectorStore(backend="faiss", dimension=768), + vector_store=VectorStore(backend="inmemory"), knowledge_graph=ContextGraph(advanced_analytics=True), decision_tracking=True, ) diff --git a/docs/reference/context.md b/docs/reference/context.md index a66cc0c7..f0d0fb74 100644 --- a/docs/reference/context.md +++ b/docs/reference/context.md @@ -275,7 +275,7 @@ 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 | +| `add_decision(decision)` or `add_decision(category, scenario, reasoning, outcome, ...)` | Record decisions | Track choices and learn | | `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 | diff --git a/semantica/context/context_graph.py b/semantica/context/context_graph.py index b471d04e..0bdf2be7 100644 --- a/semantica/context/context_graph.py +++ b/semantica/context/context_graph.py @@ -1478,25 +1478,85 @@ class ContextGraph: } # Decision Support Methods - def add_decision(self, decision: "Decision") -> None: + def add_decision( + self, + decision: "Decision" = None, + *, + category: str = None, + scenario: str = None, + reasoning: str = None, + outcome: str = None, + confidence: float = 0.5, + entities: Optional[List[str]] = None, + decision_maker: Optional[str] = "system", + valid_from=None, + valid_until=None, + **kwargs, + ) -> str: """ Add decision node to graph. - + + Accepts either a Decision object or keyword arguments: + + # From a Decision object + graph.add_decision(Decision(category="x", scenario="y", ...)) + + # From keyword arguments (convenience form) + graph.add_decision(category="x", scenario="y", reasoning="z", + outcome="o", confidence=0.9) + Args: - decision: Decision object to add + decision: Decision object to add (mutually exclusive with kwargs) + category: Decision category + scenario: Decision scenario description + reasoning: Reasoning behind the decision + outcome: Decision outcome + confidence: Confidence score (0.0–1.0) + entities: Related entity labels + decision_maker: Who made the decision + valid_from: Start of validity window (ISO string or datetime) + valid_until: End of validity window (ISO string or datetime) + **kwargs: Extra metadata stored on the decision node + + Returns: + Decision ID """ from .decision_models import Decision - + + if decision is not None and ( + any(v is not None for v in ( + category, scenario, reasoning, outcome, entities, valid_from, valid_until, + )) or kwargs + ): + raise ValueError( + "Pass either a Decision object or keyword arguments, not both." + ) + + if decision is None: + # Build from kwargs — delegate to record_decision which handles ID gen + return self.record_decision( + category=category, + scenario=scenario, + reasoning=reasoning, + outcome=outcome, + confidence=confidence, + entities=entities, + decision_maker=decision_maker, + valid_from=valid_from, + valid_until=valid_until, + metadata=kwargs, + ) + # 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", @@ -1516,6 +1576,7 @@ class ContextGraph: valid_until=decision.valid_until, ) self._add_internal_node(node) + return node_id def add_causal_relationship( self, diff --git a/tests/context/test_agent_context_smoke.py b/tests/context/test_agent_context_smoke.py index 51b03250..07813c47 100644 --- a/tests/context/test_agent_context_smoke.py +++ b/tests/context/test_agent_context_smoke.py @@ -39,6 +39,24 @@ def test_agent_context_minimal_decisions_and_chain(): assert len(chain) >= 1 +def test_agent_context_inmemory_store_and_retrieve(): + """VectorStore(backend="inmemory") stores memories without faiss-cpu.""" + vs = VectorStore(backend="inmemory") + ctx = AgentContext( + vector_store=vs, + knowledge_graph=ContextGraph(), + decision_tracking=True, + kg_algorithms=False, + vector_store_features=False, + ) + memory_id = ctx.store( + "GPT-4 outperforms GPT-3.5 on reasoning benchmarks by 40%", + conversation_id="test_session", + ) + assert isinstance(memory_id, str) + assert len(memory_id) > 0 + + def test_agent_context_policy_engine_with_graph_backend(): vs = VectorStore(backend="inmemory", dimension=64) graph = ContextGraph() diff --git a/tests/context/test_context_graph_decisions.py b/tests/context/test_context_graph_decisions.py index 801634a7..f8dc9b4e 100644 --- a/tests/context/test_context_graph_decisions.py +++ b/tests/context/test_context_graph_decisions.py @@ -49,6 +49,38 @@ class TestContextGraphDecisions: assert node.properties["confidence"] == sample_decision.confidence assert node.properties["decision_maker"] == sample_decision.decision_maker + def test_add_decision_kwargs_form(self, context_graph): + """add_decision() accepts kwargs directly (no Decision object required).""" + decision_id = context_graph.add_decision( + category="loan_approval", + scenario="Mortgage application — 780 credit score", + reasoning="Strong credit history, low DTI", + outcome="approved", + confidence=0.95, + ) + + assert isinstance(decision_id, str) + assert len(decision_id) > 0 + node = context_graph.nodes[decision_id] + assert node.node_type in ("Decision", "decision") + assert node.properties["category"] == "loan_approval" + assert node.properties["outcome"] == "approved" + assert node.properties["confidence"] == 0.95 + + def test_add_decision_kwargs_and_object_both_return_id(self, context_graph, sample_decision): + """Both call forms return a non-empty decision ID string.""" + id_from_object = context_graph.add_decision(sample_decision) + id_from_kwargs = context_graph.add_decision( + category="test", + scenario="test scenario", + reasoning="test reasoning", + outcome="approved", + confidence=0.8, + ) + + assert isinstance(id_from_object, str) and len(id_from_object) > 0 + assert isinstance(id_from_kwargs, str) and len(id_from_kwargs) > 0 + def test_add_decision_with_embeddings(self, context_graph): """Test adding decision with embeddings.""" decision = Decision( diff --git a/tests/test_395_temporal_semantics_comprehensive.py b/tests/test_395_temporal_semantics_comprehensive.py index 1b1bd78a..81ac6026 100644 --- a/tests/test_395_temporal_semantics_comprehensive.py +++ b/tests/test_395_temporal_semantics_comprehensive.py @@ -1062,6 +1062,8 @@ class TestFindPrecedentsAsOf: # Bob's decision should be reachable; Alice's should not appear # (implementation may not filter on valid_from, just check it doesn't crash) assert isinstance(precedents, list) + assert "approve loan for Bob" in scenarios + assert "approve loan for Alice" not in scenarios def test_find_precedents_no_as_of_returns_list(self): self.graph.record_decision( diff --git a/tests/test_unreleased_changelog_comprehensive.py b/tests/test_unreleased_changelog_comprehensive.py index 25830f3c..ca228746 100644 --- a/tests/test_unreleased_changelog_comprehensive.py +++ b/tests/test_unreleased_changelog_comprehensive.py @@ -873,6 +873,7 @@ class TestOllamaProviderBaseURLGap: ollama_mock.Client = MagicMock(return_value=MagicMock()) with patch.dict("sys.modules", {"ollama": ollama_mock}): from semantica.semantic_extract.providers import OllamaProvider + OllamaProvider( provider = OllamaProvider( model_name="llama3", base_url="http://192.168.1.10:11434",