mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
Merge pull request #449 from Hawksight-AI/ontology
fix(ontology): preserve user-facing schema fields in OWL generation\n…
This commit is contained in:
+1116
-35
File diff suppressed because it is too large
Load Diff
@@ -156,11 +156,63 @@ class OWLGenerator:
|
||||
)
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def _as_list(value: Any) -> List[Any]:
|
||||
"""Normalize scalar-or-list ontology values to a list."""
|
||||
if value is None:
|
||||
return []
|
||||
if isinstance(value, list):
|
||||
return value
|
||||
return [value]
|
||||
|
||||
@staticmethod
|
||||
def _is_datatype_property(prop_type: Any) -> bool:
|
||||
"""Return True for supported datatype property aliases."""
|
||||
return str(prop_type or "").strip().lower() in {
|
||||
"datatype",
|
||||
"data",
|
||||
"datatypeproperty",
|
||||
}
|
||||
|
||||
def _get_generation_namespace_manager(self, ontology: Dict[str, Any]) -> NamespaceManager:
|
||||
"""Build a namespace manager anchored to the ontology base URI for this call."""
|
||||
base_uri = ontology.get("uri") or self.namespace_manager.get_base_uri()
|
||||
if isinstance(base_uri, str) and not base_uri.endswith(("/", "#")):
|
||||
base_uri = base_uri + "/"
|
||||
return NamespaceManager(
|
||||
base_uri=base_uri,
|
||||
version=self.namespace_manager.version,
|
||||
use_speaking_iris=self.namespace_manager.use_speaking_iris,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _is_http_uri(value: Any) -> bool:
|
||||
return isinstance(value, str) and value.startswith(("http://", "https://"))
|
||||
|
||||
def _resolve_class_uri(self, value: Any, ns_manager: NamespaceManager) -> str:
|
||||
if self._is_http_uri(value):
|
||||
return value
|
||||
return ns_manager.generate_class_iri(str(value))
|
||||
|
||||
def _resolve_property_identifier(self, prop: Dict[str, Any]) -> str:
|
||||
return prop.get("label") or prop.get("name")
|
||||
|
||||
def _resolve_class_identifier(self, cls: Dict[str, Any]) -> str:
|
||||
return cls.get("label") or cls.get("name")
|
||||
|
||||
def _resolve_datatype_range_uri(self, range_val: Any, ns_manager: NamespaceManager):
|
||||
if isinstance(range_val, str) and range_val.startswith("xsd:"):
|
||||
return XSD[range_val.replace("xsd:", "")]
|
||||
if self._is_http_uri(range_val):
|
||||
return URIRef(range_val)
|
||||
return URIRef(ns_manager.generate_class_iri(str(range_val)))
|
||||
|
||||
def _generate_with_rdflib(
|
||||
self, ontology: Dict[str, Any], format: str = "turtle", **options
|
||||
) -> Union[str, Graph]:
|
||||
"""Generate OWL using rdflib."""
|
||||
g = Graph()
|
||||
gen_ns_manager = self._get_generation_namespace_manager(ontology)
|
||||
|
||||
# Set up namespaces
|
||||
ns_manager = RDFNamespaceManager(g)
|
||||
@@ -173,6 +225,8 @@ class OWLGenerator:
|
||||
|
||||
# Register ontology namespace
|
||||
base_uri = ontology.get("uri") or self.namespace_manager.get_base_uri()
|
||||
if isinstance(base_uri, str) and not base_uri.endswith(("/", "#")):
|
||||
base_uri = base_uri + "/"
|
||||
ont_ns = Namespace(base_uri)
|
||||
g.bind("", ont_ns)
|
||||
|
||||
@@ -189,19 +243,23 @@ class OWLGenerator:
|
||||
# Add classes
|
||||
classes = ontology.get("classes", [])
|
||||
for cls in classes:
|
||||
class_name = self._resolve_class_identifier(cls)
|
||||
class_uri = URIRef(
|
||||
cls.get("uri") or self.namespace_manager.generate_class_iri(cls["name"])
|
||||
cls.get("uri")
|
||||
or gen_ns_manager.generate_class_iri(class_name)
|
||||
)
|
||||
g.add((class_uri, RDF.type, OWL.Class))
|
||||
|
||||
if cls.get("label"):
|
||||
g.add((class_uri, RDFS.label, Literal(cls["label"])))
|
||||
class_label = cls.get("label") or cls.get("name")
|
||||
if class_label:
|
||||
g.add((class_uri, RDFS.label, Literal(class_label)))
|
||||
if cls.get("comment"):
|
||||
g.add((class_uri, RDFS.comment, Literal(cls["comment"])))
|
||||
|
||||
# Add subclass relationships
|
||||
if cls.get("subClassOf"):
|
||||
parent_uri = URIRef(cls["subClassOf"])
|
||||
subclass_of = cls.get("subClassOf") or cls.get("subclassOf")
|
||||
if subclass_of:
|
||||
parent_uri = URIRef(self._resolve_class_uri(subclass_of, gen_ns_manager))
|
||||
g.add((class_uri, RDFS.subClassOf, parent_uri))
|
||||
|
||||
# Add object properties
|
||||
@@ -210,60 +268,52 @@ class OWLGenerator:
|
||||
if prop.get("type") == "object":
|
||||
prop_uri = URIRef(
|
||||
prop.get("uri")
|
||||
or self.namespace_manager.generate_property_iri(prop["name"])
|
||||
or gen_ns_manager.generate_property_iri(
|
||||
self._resolve_property_identifier(prop)
|
||||
)
|
||||
)
|
||||
g.add((prop_uri, RDF.type, OWL.ObjectProperty))
|
||||
|
||||
if prop.get("label"):
|
||||
g.add((prop_uri, RDFS.label, Literal(prop["label"])))
|
||||
prop_label = prop.get("label") or prop.get("name")
|
||||
if prop_label:
|
||||
g.add((prop_uri, RDFS.label, Literal(prop_label)))
|
||||
|
||||
# Add domain
|
||||
domains = prop.get("domain", [])
|
||||
domains = self._as_list(prop.get("domain", []))
|
||||
for domain in domains:
|
||||
domain_uri = URIRef(
|
||||
domain
|
||||
if domain.startswith("http")
|
||||
else self.namespace_manager.generate_class_iri(domain)
|
||||
)
|
||||
domain_uri = URIRef(self._resolve_class_uri(domain, gen_ns_manager))
|
||||
g.add((prop_uri, RDFS.domain, domain_uri))
|
||||
|
||||
# Add range
|
||||
ranges = prop.get("range", [])
|
||||
ranges = self._as_list(prop.get("range", []))
|
||||
for range_val in ranges:
|
||||
range_uri = URIRef(
|
||||
range_val
|
||||
if range_val.startswith("http")
|
||||
else self.namespace_manager.generate_class_iri(range_val)
|
||||
)
|
||||
range_uri = URIRef(self._resolve_class_uri(range_val, gen_ns_manager))
|
||||
g.add((prop_uri, RDFS.range, range_uri))
|
||||
|
||||
elif prop.get("type") == "data":
|
||||
elif self._is_datatype_property(prop.get("type")):
|
||||
prop_uri = URIRef(
|
||||
prop.get("uri")
|
||||
or self.namespace_manager.generate_property_iri(prop["name"])
|
||||
or gen_ns_manager.generate_property_iri(
|
||||
self._resolve_property_identifier(prop)
|
||||
)
|
||||
)
|
||||
g.add((prop_uri, RDF.type, OWL.DatatypeProperty))
|
||||
|
||||
if prop.get("label"):
|
||||
g.add((prop_uri, RDFS.label, Literal(prop["label"])))
|
||||
prop_label = prop.get("label") or prop.get("name")
|
||||
if prop_label:
|
||||
g.add((prop_uri, RDFS.label, Literal(prop_label)))
|
||||
|
||||
# Add domain
|
||||
domains = prop.get("domain", [])
|
||||
domains = self._as_list(prop.get("domain", []))
|
||||
for domain in domains:
|
||||
domain_uri = URIRef(
|
||||
domain
|
||||
if domain.startswith("http")
|
||||
else self.namespace_manager.generate_class_iri(domain)
|
||||
)
|
||||
domain_uri = URIRef(self._resolve_class_uri(domain, gen_ns_manager))
|
||||
g.add((prop_uri, RDFS.domain, domain_uri))
|
||||
|
||||
# Add range
|
||||
range_type = prop.get("range", "xsd:string")
|
||||
if range_type.startswith("xsd:"):
|
||||
range_uri = XSD[range_type.replace("xsd:", "")]
|
||||
else:
|
||||
range_uri = URIRef(range_type)
|
||||
g.add((prop_uri, RDFS.range, range_uri))
|
||||
range_values = self._as_list(prop.get("range", "xsd:string"))
|
||||
for range_val in range_values:
|
||||
range_uri = self._resolve_datatype_range_uri(range_val, gen_ns_manager)
|
||||
g.add((prop_uri, RDFS.range, range_uri))
|
||||
|
||||
# Serialize
|
||||
if format == "turtle":
|
||||
@@ -282,9 +332,12 @@ class OWLGenerator:
|
||||
) -> str:
|
||||
"""Generate OWL using basic string formatting (fallback)."""
|
||||
lines = []
|
||||
gen_ns_manager = self._get_generation_namespace_manager(ontology)
|
||||
|
||||
# Namespace declarations
|
||||
base_uri = ontology.get("uri") or self.namespace_manager.get_base_uri()
|
||||
if isinstance(base_uri, str) and not base_uri.endswith(("/", "#")):
|
||||
base_uri = base_uri + "/"
|
||||
lines.append(f"@prefix : <{base_uri}> .")
|
||||
lines.append("@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .")
|
||||
lines.append("@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .")
|
||||
@@ -303,16 +356,19 @@ class OWLGenerator:
|
||||
# Classes
|
||||
classes = ontology.get("classes", [])
|
||||
for cls in classes:
|
||||
class_uri = cls.get("uri") or self.namespace_manager.generate_class_iri(
|
||||
cls["name"]
|
||||
class_name = self._resolve_class_identifier(cls)
|
||||
class_uri = cls.get("uri") or gen_ns_manager.generate_class_iri(
|
||||
class_name
|
||||
)
|
||||
lines.append(f"<{class_uri}> a owl:Class ;")
|
||||
if cls.get("label"):
|
||||
lines.append(f' rdfs:label "{cls["label"]}" ;')
|
||||
class_label = cls.get("label") or cls.get("name")
|
||||
if class_label:
|
||||
lines.append(f' rdfs:label "{class_label}" ;')
|
||||
if cls.get("comment"):
|
||||
lines.append(f' rdfs:comment "{cls["comment"]}" ;')
|
||||
if cls.get("subClassOf"):
|
||||
parent_uri = cls["subClassOf"]
|
||||
subclass_of = cls.get("subClassOf") or cls.get("subclassOf")
|
||||
if subclass_of:
|
||||
parent_uri = self._resolve_class_uri(subclass_of, gen_ns_manager)
|
||||
lines.append(f" rdfs:subClassOf <{parent_uri}> .")
|
||||
else:
|
||||
lines[-1] = lines[-1].rstrip(" ;") + " ."
|
||||
@@ -321,8 +377,8 @@ class OWLGenerator:
|
||||
# Properties
|
||||
properties = ontology.get("properties", [])
|
||||
for prop in properties:
|
||||
prop_uri = prop.get("uri") or self.namespace_manager.generate_property_iri(
|
||||
prop["name"]
|
||||
prop_uri = prop.get("uri") or gen_ns_manager.generate_property_iri(
|
||||
self._resolve_property_identifier(prop)
|
||||
)
|
||||
prop_type = (
|
||||
"owl:ObjectProperty"
|
||||
@@ -330,32 +386,25 @@ class OWLGenerator:
|
||||
else "owl:DatatypeProperty"
|
||||
)
|
||||
lines.append(f"<{prop_uri}> a {prop_type} ;")
|
||||
if prop.get("label"):
|
||||
lines.append(f' rdfs:label "{prop["label"]}" ;')
|
||||
prop_label = prop.get("label") or prop.get("name")
|
||||
if prop_label:
|
||||
lines.append(f' rdfs:label "{prop_label}" ;')
|
||||
|
||||
# Domain
|
||||
domains = prop.get("domain", [])
|
||||
domains = self._as_list(prop.get("domain", []))
|
||||
for domain in domains:
|
||||
domain_uri = (
|
||||
domain
|
||||
if domain.startswith("http")
|
||||
else self.namespace_manager.generate_class_iri(domain)
|
||||
)
|
||||
domain_uri = self._resolve_class_uri(domain, gen_ns_manager)
|
||||
lines.append(f" rdfs:domain <{domain_uri}> ;")
|
||||
|
||||
# Range
|
||||
ranges = prop.get("range", [])
|
||||
ranges = self._as_list(prop.get("range", []))
|
||||
for range_val in ranges:
|
||||
if prop.get("type") == "data" and range_val.startswith("xsd:"):
|
||||
lines.append(
|
||||
f" rdfs:range {range_val.replace('xsd:', 'xsd:')} ;"
|
||||
)
|
||||
if self._is_datatype_property(prop.get("type")) and isinstance(
|
||||
range_val, str
|
||||
) and range_val.startswith("xsd:"):
|
||||
lines.append(f" rdfs:range {range_val} ;")
|
||||
else:
|
||||
range_uri = (
|
||||
range_val
|
||||
if range_val.startswith("http")
|
||||
else self.namespace_manager.generate_class_iri(range_val)
|
||||
)
|
||||
range_uri = self._resolve_class_uri(range_val, gen_ns_manager)
|
||||
lines.append(f" rdfs:range <{range_uri}> ;")
|
||||
|
||||
lines[-1] = lines[-1].rstrip(" ;") + " ."
|
||||
|
||||
@@ -165,6 +165,45 @@ class TestOntologyComprehensive(unittest.TestCase):
|
||||
self.assertIn("Person", owl_output)
|
||||
self.assertIn("hasName", owl_output)
|
||||
|
||||
def test_owl_generator_user_facing_schema_compatibility(self):
|
||||
try:
|
||||
from semantica.ontology.owl_generator import OWLGenerator
|
||||
except ImportError:
|
||||
self.skipTest("OWLGenerator not importable")
|
||||
|
||||
generator = OWLGenerator()
|
||||
ontology = {
|
||||
"name": "UserFacingOntology",
|
||||
"uri": "http://example.org/ontology/",
|
||||
"classes": [
|
||||
# label should be preferred over name for generated class IRI
|
||||
{"name": "Human", "label": "Person", "subclassOf": "Agent"},
|
||||
],
|
||||
"properties": [
|
||||
{
|
||||
# label should be preferred over name for generated property IRI
|
||||
"name": "birthDateInternal",
|
||||
"label": "birthDate",
|
||||
"type": "datatype",
|
||||
"domain": "Person",
|
||||
# datatype range may be list in user-facing schema
|
||||
"range": ["xsd:date", "http://example.org/ontology/CustomDateType"],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
owl_output = generator.generate_owl(ontology, format="turtle")
|
||||
|
||||
self.assertIn("owl:DatatypeProperty", owl_output)
|
||||
self.assertIn("@prefix : <http://example.org/ontology/> .", owl_output)
|
||||
self.assertIn("rdfs:subClassOf", owl_output)
|
||||
self.assertIn("birthDate", owl_output)
|
||||
self.assertIn("xsd:date", owl_output)
|
||||
self.assertIn("CustomDateType", owl_output)
|
||||
self.assertIn(":Person", owl_output)
|
||||
self.assertIn(":Agent", owl_output)
|
||||
self.assertNotIn("https://semantica.dev/ontology/", owl_output)
|
||||
|
||||
# --- OntologyValidator Tests ---
|
||||
# Removed as per request
|
||||
|
||||
|
||||
Reference in New Issue
Block a user