diff --git a/docs/guides/reasoning.md b/docs/guides/reasoning.md index 6e43e23f..4df1d010 100644 --- a/docs/guides/reasoning.md +++ b/docs/guides/reasoning.md @@ -269,7 +269,7 @@ print("Loaded {} facts from graph".format(count)) ## Step 5 — SPARQL queries over enriched working memory -After forward chaining has derived new facts, `SPARQLReasoner` lets you query the enriched working memory using SPARQL triple-pattern matching with optional inference expansion: +After forward chaining has derived new facts, `SPARQLReasoner` prepares SPARQL queries over the enriched working memory with optional inference expansion: ```python from semantica.reasoning import SPARQLReasoner @@ -288,22 +288,13 @@ query = """ } """ -# execute_query() runs: expansion → inference → deduplication -result = sparql.execute_query(query) - -for binding in result.bindings: - print("Actor: {:15s} CVE: {}".format( - binding.get("actor", "?"), - binding.get("cve", "?"), - )) - -# metadata shows how many results came from inference vs ground facts -print("Original: {} Inferred: {}".format( - result.metadata.get("original_count", 0), - result.metadata.get("inferred_count", 0), -)) +# expand_query() applies inference rules to the query text: +expanded = sparql.expand_query(query) +print(expanded) ``` +`execute_query()` is not implemented yet: no triplet-store execution path exists, so it raises `NotImplementedError` rather than returning an empty result set that callers would misread as "no matches". Until execution lands, run the expanded query against your RDF store directly (for example with `rdflib`). + Inspect the expanded query before running it: ```python diff --git a/semantica/reasoning/sparql_reasoner.py b/semantica/reasoning/sparql_reasoner.py index 58e0c351..16be2a9f 100644 --- a/semantica/reasoning/sparql_reasoner.py +++ b/semantica/reasoning/sparql_reasoner.py @@ -84,6 +84,9 @@ class SPARQLReasoner: self.triplet_store = self.config.get("triplet_store") self.enable_inference = self.config.get("enable_inference", True) + # Reserved for query caching once a triplet-store execution path + # lands. execute_query() raises NotImplementedError until then, so + # the cache cannot be populated through any public path yet. self.query_cache: Dict[str, Any] = {} def expand_query(self, query: str, **options) -> str: @@ -330,79 +333,32 @@ class SPARQLReasoner: """ Execute SPARQL query with reasoning. + Not implemented: no triplet-store execution path exists yet, so the + query is refused loudly instead of returning an empty result set + that callers would read as "no matches" (issue #1083). + Args: query: SPARQL query string **options: Additional options - Returns: - Query results + Raises: + NotImplementedError: always, until a triplet-store execution + path lands. """ - tracking_id = self.progress_tracker.start_tracking( - module="reasoning", - submodule="SPARQLReasoner", - message="Executing SPARQL query with reasoning", + raise NotImplementedError( + "SPARQLReasoner.execute_query() is not implemented: no " + "triplet-store execution path exists yet. Returning an empty " + "result set would be misread as 'no matches', so the query " + "is refused instead." ) - try: - # Check cache - self.progress_tracker.update_tracking( - tracking_id, message="Checking query cache..." - ) - if query in self.query_cache: - self.logger.debug("Returning cached query result") - self.progress_tracker.stop_tracking( - tracking_id, - status="completed", - message="Returned cached query result", - ) - return self.query_cache[query] - - # Expand query - self.progress_tracker.update_tracking( - tracking_id, message="Expanding query with inference rules..." - ) - expanded_query = self.expand_query(query, **options) - - # Execute query (if triplet store available) - self.progress_tracker.update_tracking( - tracking_id, message="Executing query..." - ) - if self.triplet_store: - # This would call the triplet store's query method - # For now, return empty result - result = SPARQLQueryResult(bindings=[], variables=[]) - else: - # Mock result for testing - result = SPARQLQueryResult(bindings=[], variables=[]) - - # Infer additional results - if self.enable_inference: - self.progress_tracker.update_tracking( - tracking_id, message="Inferring additional results..." - ) - result = self.infer_results(result, **options) - - # Cache result - self.progress_tracker.update_tracking( - tracking_id, message="Caching query result..." - ) - self.query_cache[query] = result - - self.progress_tracker.stop_tracking( - tracking_id, - status="completed", - message=f"Query executed: {len(result.bindings)} results", - ) - return result - - except Exception as e: - self.progress_tracker.stop_tracking( - tracking_id, status="failed", message=str(e) - ) - raise - def clear_cache(self) -> None: - """Clear query cache.""" + """Clear query cache. + + Reserved for when a triplet-store execution path lands: until then, + ``execute_query()`` raises ``NotImplementedError`` and nothing can + populate the cache. + """ self.query_cache.clear() def add_inference_rule(self, rule_definition: str, **options) -> Rule: diff --git a/tests/reasoning/test_specialized_reasoners.py b/tests/reasoning/test_specialized_reasoners.py index 3a519171..51dbdc9a 100644 --- a/tests/reasoning/test_specialized_reasoners.py +++ b/tests/reasoning/test_specialized_reasoners.py @@ -30,6 +30,29 @@ class TestSpecializedReasoners(unittest.TestCase): binding_types = [b.get("x_type") for b in inferred.bindings] self.assertIn("Human", binding_types) + def test_execute_query_raises_not_implemented(self): + """Empty results must not pass as a valid answer (issue #1083). + + Both branches returned ``SPARQLQueryResult(bindings=[], variables=[])`` + -- with or without a triplet store -- so callers that trust an empty + result as "no matches" silently drew wrong conclusions. Until a real + execution path lands, refusing loudly is safer. + """ + reasoner = SPARQLReasoner() + with self.assertRaises(NotImplementedError): + reasoner.execute_query("SELECT ?s ?p ?o WHERE { ?s ?p ?o }") + + def test_execute_query_with_triplet_store_raises_not_implemented(self): + reasoner = SPARQLReasoner(triplet_store=object()) + with self.assertRaises(NotImplementedError): + reasoner.execute_query("SELECT ?s ?p ?o WHERE { ?s ?p ?o }") + + def test_execute_query_error_explains_why_the_query_is_refused(self): + reasoner = SPARQLReasoner() + with self.assertRaises(NotImplementedError) as ctx: + reasoner.execute_query("SELECT ?s WHERE { ?s ?p ?o }") + self.assertIn("not implemented", str(ctx.exception)) + def test_abductive_reasoner_generate_hypotheses(self): reasoner = AbductiveReasoner() reasoner.reasoner.add_rule("IF Disease(Flu) THEN Symptom(Fever)")