mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
fix(reasoning): refuse SPARQL query execution instead of returning empty results (#1087)
* fix(reasoning): refuse SPARQL query execution instead of returning empty results (#1083) SPARQLReasoner.execute_query() never executed the query: both branches returned an empty SPARQLQueryResult, with or without a triplet store, so callers that trust an empty result as "no matches" silently drew wrong conclusions. Until a real triplet-store execution path lands, the method raises NotImplementedError with an explanation, per the issue's suggestion. The dead cache/inference scaffolding after the execution point is removed along with it. Co-Authored-By: Claude <noreply@anthropic.com> * docs(reasoning): align execute_query() docs with the NotImplementedError contract (#1087) Review feedback: the docstring still carried a "Returns" section and the reasoning guide showed execute_query() returning bindings, both of which now mislead. The docstring documents Raises only, the guide demonstrates expand_query() and points to rdflib for execution until the triplet-store path lands, and query_cache/clear_cache() are marked as reserved for that future execution path. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)")
|
||||
|
||||
Reference in New Issue
Block a user