Merge pull request #449 from Hawksight-AI/ontology

fix(ontology): preserve user-facing schema fields in OWL generation\n…
This commit is contained in:
Mohd Kaif
2026-04-11 14:09:09 +05:30
committed by GitHub
3 changed files with 1266 additions and 97 deletions
+1116 -35
View File
File diff suppressed because it is too large Load Diff
+111 -62
View File
@@ -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