mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
fix(export): normalize entity name to label on all RDF paths
convert_kg_to_rdf() maps an entity's 'name' to 'label'/'text' but was never invoked from export_to_rdf(), so graphs produced by GraphBuilder (which emit 'name') exported with an empty semantica:text on every RDF format (turtle, ntriples, rdfxml, jsonld). Call convert_kg_to_rdf() at the export boundary before validation/serialization so all formats benefit from a single normalization step. Add regression tests asserting a name-only entity exports a non-empty label across all four serializers and the file-writing entry point, plus that an explicit 'text' is not clobbered and an id tail is used as a fallback label. Closes #1097
This commit is contained in:
@@ -1054,6 +1054,12 @@ class RDFExporter:
|
|||||||
|
|
||||||
self.logger.debug(f"Exporting to RDF format: {format}")
|
self.logger.debug(f"Exporting to RDF format: {format}")
|
||||||
|
|
||||||
|
# Normalize the graph before serialization so every format benefits
|
||||||
|
# from field normalization (e.g. mapping 'name' -> 'label'/'text').
|
||||||
|
# Without this, graphs produced by GraphBuilder (which emit 'name')
|
||||||
|
# export with an empty semantica:text on all RDF paths. See #1097.
|
||||||
|
data = self.serializer.convert_kg_to_rdf(data)
|
||||||
|
|
||||||
self.progress_tracker.update_tracking(
|
self.progress_tracker.update_tracking(
|
||||||
tracking_id, message="Validating RDF data..."
|
tracking_id, message="Validating RDF data..."
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from semantica.export import RDFExporter
|
from semantica.export import RDFExporter
|
||||||
|
|
||||||
|
|
||||||
RDF_DATA = {
|
RDF_DATA = {
|
||||||
"entities": [
|
"entities": [
|
||||||
{"id": "e1", "text": "Apple Inc.", "type": "ORG", "confidence": 0.95},
|
{"id": "e1", "text": "Apple Inc.", "type": "ORG", "confidence": 0.95},
|
||||||
@@ -89,3 +88,74 @@ def test_validate_rdf_returns_overall_valid_key(exporter):
|
|||||||
result = exporter.validate_rdf(RDF_DATA)
|
result = exporter.validate_rdf(RDF_DATA)
|
||||||
assert "overall_valid" in result
|
assert "overall_valid" in result
|
||||||
assert isinstance(result["overall_valid"], bool)
|
assert isinstance(result["overall_valid"], bool)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Regression tests for #1097 -------------------------------------------
|
||||||
|
# convert_kg_to_rdf() normalizes an entity's 'name' into 'label'/'text' but was
|
||||||
|
# never called from the export path, so GraphBuilder graphs (which emit 'name')
|
||||||
|
# exported with an empty semantica:text on every RDF format. These tests assert
|
||||||
|
# the human-readable label survives export on all four serializers.
|
||||||
|
|
||||||
|
NAME_ONLY_DATA = {
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"id": "https://example.org/acme",
|
||||||
|
"name": "Acme Corp",
|
||||||
|
"type": "https://example.org/Org",
|
||||||
|
"confidence": 0.91,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"relationships": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("fmt", ["turtle", "ntriples", "rdfxml", "jsonld"])
|
||||||
|
def test_name_only_entity_exports_nonempty_label(exporter, fmt):
|
||||||
|
"""A GraphBuilder-style 'name'-only entity must export a non-empty label.
|
||||||
|
|
||||||
|
Regression for #1097: previously every RDF path dropped the label because
|
||||||
|
convert_kg_to_rdf() was never invoked from export_to_rdf().
|
||||||
|
"""
|
||||||
|
result = exporter.export_to_rdf(NAME_ONLY_DATA, format=fmt)
|
||||||
|
assert "Acme Corp" in result
|
||||||
|
# The empty-text pattern that the bug produced must not appear.
|
||||||
|
assert 'semantica:text ""' not in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_name_only_export_to_file_contains_label(exporter, tmp_path):
|
||||||
|
"""The file-writing entry point must also normalize name -> label (#1097)."""
|
||||||
|
out = tmp_path / "acme.ttl"
|
||||||
|
exporter.export(NAME_ONLY_DATA, str(out), format="turtle")
|
||||||
|
content = out.read_text()
|
||||||
|
assert "Acme Corp" in content
|
||||||
|
assert 'semantica:text ""' not in content
|
||||||
|
|
||||||
|
|
||||||
|
def test_existing_text_not_overwritten_by_name(exporter):
|
||||||
|
"""An entity that already has 'text' must keep it, not be clobbered by 'name' (#1097)."""
|
||||||
|
data = {
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"id": "https://example.org/acme",
|
||||||
|
"name": "Acme Corp",
|
||||||
|
"text": "Explicit Text",
|
||||||
|
"type": "https://example.org/Org",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"relationships": [],
|
||||||
|
}
|
||||||
|
result = exporter.export_to_rdf(data, format="turtle")
|
||||||
|
assert "Explicit Text" in result
|
||||||
|
assert "Acme Corp" not in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_id_fallback_label_when_no_name(exporter):
|
||||||
|
"""With neither 'name' nor 'text', the id tail is used as the label (#1097)."""
|
||||||
|
data = {
|
||||||
|
"entities": [
|
||||||
|
{"id": "https://example.org/acme", "type": "https://example.org/Org"}
|
||||||
|
],
|
||||||
|
"relationships": [],
|
||||||
|
}
|
||||||
|
result = exporter.export_to_rdf(data, format="turtle")
|
||||||
|
assert 'semantica:text ""' not in result
|
||||||
|
|||||||
Reference in New Issue
Block a user