naas-abi-core 1.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- assets/favicon.ico +0 -0
- assets/logo.png +0 -0
- naas_abi_core/__init__.py +1 -0
- naas_abi_core/apps/api/api.py +245 -0
- naas_abi_core/apps/api/api_test.py +281 -0
- naas_abi_core/apps/api/openapi_doc.py +144 -0
- naas_abi_core/apps/mcp/Dockerfile.mcp +35 -0
- naas_abi_core/apps/mcp/mcp_server.py +243 -0
- naas_abi_core/apps/mcp/mcp_server_test.py +163 -0
- naas_abi_core/apps/terminal_agent/main.py +555 -0
- naas_abi_core/apps/terminal_agent/terminal_style.py +175 -0
- naas_abi_core/engine/Engine.py +87 -0
- naas_abi_core/engine/EngineProxy.py +109 -0
- naas_abi_core/engine/Engine_test.py +6 -0
- naas_abi_core/engine/IEngine.py +91 -0
- naas_abi_core/engine/conftest.py +45 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration.py +216 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_Deploy.py +7 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_GenericLoader.py +49 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_ObjectStorageService.py +159 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_ObjectStorageService_test.py +26 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_SecretService.py +138 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_SecretService_test.py +74 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_TripleStoreService.py +224 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_TripleStoreService_test.py +109 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_VectorStoreService.py +76 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_VectorStoreService_test.py +33 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_test.py +9 -0
- naas_abi_core/engine/engine_configuration/utils/PydanticModelValidator.py +15 -0
- naas_abi_core/engine/engine_loaders/EngineModuleLoader.py +302 -0
- naas_abi_core/engine/engine_loaders/EngineOntologyLoader.py +16 -0
- naas_abi_core/engine/engine_loaders/EngineServiceLoader.py +47 -0
- naas_abi_core/integration/__init__.py +7 -0
- naas_abi_core/integration/integration.py +28 -0
- naas_abi_core/models/Model.py +198 -0
- naas_abi_core/models/OpenRouter.py +18 -0
- naas_abi_core/models/OpenRouter_test.py +36 -0
- naas_abi_core/module/Module.py +252 -0
- naas_abi_core/module/ModuleAgentLoader.py +50 -0
- naas_abi_core/module/ModuleUtils.py +20 -0
- naas_abi_core/modules/templatablesparqlquery/README.md +196 -0
- naas_abi_core/modules/templatablesparqlquery/__init__.py +39 -0
- naas_abi_core/modules/templatablesparqlquery/ontologies/TemplatableSparqlQueryOntology.ttl +116 -0
- naas_abi_core/modules/templatablesparqlquery/workflows/GenericWorkflow.py +48 -0
- naas_abi_core/modules/templatablesparqlquery/workflows/TemplatableSparqlQueryLoader.py +192 -0
- naas_abi_core/pipeline/__init__.py +6 -0
- naas_abi_core/pipeline/pipeline.py +70 -0
- naas_abi_core/services/__init__.py +0 -0
- naas_abi_core/services/agent/Agent.py +1619 -0
- naas_abi_core/services/agent/AgentMemory_test.py +28 -0
- naas_abi_core/services/agent/Agent_test.py +214 -0
- naas_abi_core/services/agent/IntentAgent.py +1179 -0
- naas_abi_core/services/agent/IntentAgent_test.py +139 -0
- naas_abi_core/services/agent/beta/Embeddings.py +181 -0
- naas_abi_core/services/agent/beta/IntentMapper.py +120 -0
- naas_abi_core/services/agent/beta/LocalModel.py +88 -0
- naas_abi_core/services/agent/beta/VectorStore.py +89 -0
- naas_abi_core/services/agent/test_agent_memory.py +278 -0
- naas_abi_core/services/agent/test_postgres_integration.py +145 -0
- naas_abi_core/services/cache/CacheFactory.py +31 -0
- naas_abi_core/services/cache/CachePort.py +63 -0
- naas_abi_core/services/cache/CacheService.py +246 -0
- naas_abi_core/services/cache/CacheService_test.py +85 -0
- naas_abi_core/services/cache/adapters/secondary/CacheFSAdapter.py +39 -0
- naas_abi_core/services/object_storage/ObjectStorageFactory.py +57 -0
- naas_abi_core/services/object_storage/ObjectStoragePort.py +47 -0
- naas_abi_core/services/object_storage/ObjectStorageService.py +41 -0
- naas_abi_core/services/object_storage/adapters/secondary/ObjectStorageSecondaryAdapterFS.py +52 -0
- naas_abi_core/services/object_storage/adapters/secondary/ObjectStorageSecondaryAdapterNaas.py +131 -0
- naas_abi_core/services/object_storage/adapters/secondary/ObjectStorageSecondaryAdapterS3.py +171 -0
- naas_abi_core/services/ontology/OntologyPorts.py +36 -0
- naas_abi_core/services/ontology/OntologyService.py +17 -0
- naas_abi_core/services/ontology/adaptors/secondary/OntologyService_SecondaryAdaptor_NERPort.py +37 -0
- naas_abi_core/services/secret/Secret.py +138 -0
- naas_abi_core/services/secret/SecretPorts.py +45 -0
- naas_abi_core/services/secret/Secret_test.py +65 -0
- naas_abi_core/services/secret/adaptors/secondary/Base64Secret.py +57 -0
- naas_abi_core/services/secret/adaptors/secondary/Base64Secret_test.py +39 -0
- naas_abi_core/services/secret/adaptors/secondary/NaasSecret.py +88 -0
- naas_abi_core/services/secret/adaptors/secondary/NaasSecret_test.py +25 -0
- naas_abi_core/services/secret/adaptors/secondary/dotenv_secret_secondaryadaptor.py +29 -0
- naas_abi_core/services/triple_store/TripleStoreFactory.py +116 -0
- naas_abi_core/services/triple_store/TripleStorePorts.py +223 -0
- naas_abi_core/services/triple_store/TripleStoreService.py +419 -0
- naas_abi_core/services/triple_store/adaptors/secondary/AWSNeptune.py +1300 -0
- naas_abi_core/services/triple_store/adaptors/secondary/AWSNeptune_test.py +284 -0
- naas_abi_core/services/triple_store/adaptors/secondary/Oxigraph.py +597 -0
- naas_abi_core/services/triple_store/adaptors/secondary/Oxigraph_test.py +1474 -0
- naas_abi_core/services/triple_store/adaptors/secondary/TripleStoreService__SecondaryAdaptor__Filesystem.py +223 -0
- naas_abi_core/services/triple_store/adaptors/secondary/TripleStoreService__SecondaryAdaptor__ObjectStorage.py +234 -0
- naas_abi_core/services/triple_store/adaptors/secondary/base/TripleStoreService__SecondaryAdaptor__FileBase.py +18 -0
- naas_abi_core/services/vector_store/IVectorStorePort.py +101 -0
- naas_abi_core/services/vector_store/IVectorStorePort_test.py +189 -0
- naas_abi_core/services/vector_store/VectorStoreFactory.py +47 -0
- naas_abi_core/services/vector_store/VectorStoreService.py +171 -0
- naas_abi_core/services/vector_store/VectorStoreService_test.py +185 -0
- naas_abi_core/services/vector_store/__init__.py +13 -0
- naas_abi_core/services/vector_store/adapters/QdrantAdapter.py +251 -0
- naas_abi_core/services/vector_store/adapters/QdrantAdapter_test.py +57 -0
- naas_abi_core/tests/test_services_imports.py +69 -0
- naas_abi_core/utils/Expose.py +55 -0
- naas_abi_core/utils/Graph.py +182 -0
- naas_abi_core/utils/JSON.py +49 -0
- naas_abi_core/utils/LazyLoader.py +44 -0
- naas_abi_core/utils/Logger.py +12 -0
- naas_abi_core/utils/OntologyReasoner.py +141 -0
- naas_abi_core/utils/OntologyYaml.py +681 -0
- naas_abi_core/utils/SPARQL.py +256 -0
- naas_abi_core/utils/Storage.py +33 -0
- naas_abi_core/utils/StorageUtils.py +398 -0
- naas_abi_core/utils/String.py +52 -0
- naas_abi_core/utils/Workers.py +114 -0
- naas_abi_core/utils/__init__.py +0 -0
- naas_abi_core/utils/onto2py/README.md +0 -0
- naas_abi_core/utils/onto2py/__init__.py +10 -0
- naas_abi_core/utils/onto2py/__main__.py +29 -0
- naas_abi_core/utils/onto2py/onto2py.py +611 -0
- naas_abi_core/utils/onto2py/tests/ttl2py_test.py +271 -0
- naas_abi_core/workflow/__init__.py +5 -0
- naas_abi_core/workflow/workflow.py +48 -0
- naas_abi_core-1.4.1.dist-info/METADATA +630 -0
- naas_abi_core-1.4.1.dist-info/RECORD +124 -0
- naas_abi_core-1.4.1.dist-info/WHEEL +4 -0
- naas_abi_core-1.4.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
import rdflib
|
|
2
|
+
import io
|
|
3
|
+
from typing import Dict, Set, List, Optional
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class PropertyInfo:
|
|
9
|
+
"""Information about a property (data or object property)"""
|
|
10
|
+
name: str
|
|
11
|
+
property_type: str # 'data' or 'object'
|
|
12
|
+
range_class: Optional[str] = None
|
|
13
|
+
datatype: Optional[str] = None
|
|
14
|
+
cardinality: Optional[str] = None # 'single', 'multiple', 'exactly_one', etc.
|
|
15
|
+
required: bool = False
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ClassInfo:
|
|
19
|
+
"""Information about an RDF class"""
|
|
20
|
+
name: str
|
|
21
|
+
uri: str
|
|
22
|
+
parent_classes: List[str]
|
|
23
|
+
properties: List[PropertyInfo]
|
|
24
|
+
description: Optional[str] = None
|
|
25
|
+
property_uris: Dict[str, str] = field(default_factory=dict) # Maps property name to URI
|
|
26
|
+
|
|
27
|
+
def onto2py(ttl_file: str | io.TextIOBase) -> str:
|
|
28
|
+
"""
|
|
29
|
+
Convert TTL file to Python classes
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
ttl_file: Path to TTL file or file-like object
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Generated Python code as string
|
|
36
|
+
"""
|
|
37
|
+
if isinstance(ttl_file, str):
|
|
38
|
+
with open(ttl_file, "r") as f:
|
|
39
|
+
content = f.read()
|
|
40
|
+
g = rdflib.Graph()
|
|
41
|
+
g.parse(data=content, format="turtle")
|
|
42
|
+
else:
|
|
43
|
+
content = ttl_file.read()
|
|
44
|
+
g = rdflib.Graph()
|
|
45
|
+
g.parse(data=content, format="turtle")
|
|
46
|
+
|
|
47
|
+
# Define common RDF/OWL/SHACL namespaces
|
|
48
|
+
RDF = rdflib.Namespace("http://www.w3.org/1999/02/22-rdf-syntax-ns#")
|
|
49
|
+
RDFS = rdflib.Namespace("http://www.w3.org/2000/01/rdf-schema#")
|
|
50
|
+
OWL = rdflib.Namespace("http://www.w3.org/2002/07/owl#")
|
|
51
|
+
SHACL = rdflib.Namespace("http://www.w3.org/ns/shacl#")
|
|
52
|
+
|
|
53
|
+
# Extract classes and their information
|
|
54
|
+
classes: Dict[str, ClassInfo] = {}
|
|
55
|
+
|
|
56
|
+
# Find all OWL classes
|
|
57
|
+
for cls in g.subjects(RDF.type, OWL.Class):
|
|
58
|
+
class_name = extract_class_name(cls)
|
|
59
|
+
if class_name:
|
|
60
|
+
classes[str(cls)] = ClassInfo(
|
|
61
|
+
name=class_name,
|
|
62
|
+
uri=str(cls),
|
|
63
|
+
parent_classes=[],
|
|
64
|
+
properties=[],
|
|
65
|
+
description=get_description(g, cls)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Find all RDFS classes (if not already OWL classes)
|
|
69
|
+
for cls in g.subjects(RDF.type, RDFS.Class):
|
|
70
|
+
if str(cls) not in classes:
|
|
71
|
+
class_name = extract_class_name(cls)
|
|
72
|
+
if class_name:
|
|
73
|
+
classes[str(cls)] = ClassInfo(
|
|
74
|
+
name=class_name,
|
|
75
|
+
uri=str(cls),
|
|
76
|
+
parent_classes=[],
|
|
77
|
+
properties=[],
|
|
78
|
+
description=get_description(g, cls)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Extract inheritance relationships
|
|
82
|
+
for cls_uri, class_info in classes.items():
|
|
83
|
+
for parent in g.objects(rdflib.URIRef(cls_uri), RDFS.subClassOf):
|
|
84
|
+
if str(parent) in classes:
|
|
85
|
+
parent_name = classes[str(parent)].name
|
|
86
|
+
class_info.parent_classes.append(parent_name)
|
|
87
|
+
|
|
88
|
+
# Extract properties
|
|
89
|
+
properties: Dict[str, PropertyInfo] = {}
|
|
90
|
+
|
|
91
|
+
# Object properties
|
|
92
|
+
for prop in g.subjects(RDF.type, OWL.ObjectProperty):
|
|
93
|
+
prop_name = extract_property_name(prop)
|
|
94
|
+
if prop_name:
|
|
95
|
+
properties[str(prop)] = PropertyInfo(
|
|
96
|
+
name=prop_name,
|
|
97
|
+
property_type='object',
|
|
98
|
+
range_class=get_property_range(g, prop, classes)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Data properties
|
|
102
|
+
for prop in g.subjects(RDF.type, OWL.DatatypeProperty):
|
|
103
|
+
prop_name = extract_property_name(prop)
|
|
104
|
+
if prop_name:
|
|
105
|
+
properties[str(prop)] = PropertyInfo(
|
|
106
|
+
name=prop_name,
|
|
107
|
+
property_type='data',
|
|
108
|
+
datatype=get_datatype_range(g, prop)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Extract SHACL shapes and constraints
|
|
112
|
+
extract_shacl_constraints(g, classes, properties, SHACL)
|
|
113
|
+
|
|
114
|
+
# Associate properties with classes based on domain
|
|
115
|
+
for prop_uri, prop_info in properties.items():
|
|
116
|
+
for domain in g.objects(rdflib.URIRef(prop_uri), RDFS.domain):
|
|
117
|
+
if str(domain) in classes:
|
|
118
|
+
class_info = classes[str(domain)]
|
|
119
|
+
# Avoid emitting duplicate property declarations when a property
|
|
120
|
+
# specifies the same domain multiple times in the ontology.
|
|
121
|
+
existing_props = {prop.name: prop for prop in class_info.properties}
|
|
122
|
+
|
|
123
|
+
if prop_info.name in existing_props:
|
|
124
|
+
existing_prop = existing_props[prop_info.name]
|
|
125
|
+
# Merge stronger constraints if the duplicate carries them.
|
|
126
|
+
if prop_info.required and not existing_prop.required:
|
|
127
|
+
existing_prop.required = True
|
|
128
|
+
if prop_info.cardinality and not existing_prop.cardinality:
|
|
129
|
+
existing_prop.cardinality = prop_info.cardinality
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
class_info.properties.append(prop_info)
|
|
133
|
+
class_info.property_uris[prop_info.name] = prop_uri
|
|
134
|
+
|
|
135
|
+
# Inherit properties from parent classes
|
|
136
|
+
inherit_parent_properties(classes)
|
|
137
|
+
|
|
138
|
+
# Generate Python code
|
|
139
|
+
return generate_python_code(classes, properties)
|
|
140
|
+
|
|
141
|
+
def extract_class_name(uri) -> Optional[str]:
|
|
142
|
+
"""Extract a clean class name from a URI"""
|
|
143
|
+
uri_str = str(uri)
|
|
144
|
+
if '#' in uri_str:
|
|
145
|
+
name = uri_str.split('#')[-1]
|
|
146
|
+
elif '/' in uri_str:
|
|
147
|
+
name = uri_str.split('/')[-1]
|
|
148
|
+
else:
|
|
149
|
+
name = uri_str
|
|
150
|
+
|
|
151
|
+
# Clean up the name to be a valid Python class name
|
|
152
|
+
name = re.sub(r'[^a-zA-Z0-9_]', '', name)
|
|
153
|
+
if name and name[0].islower():
|
|
154
|
+
name = name[0].upper() + name[1:]
|
|
155
|
+
|
|
156
|
+
return name if name and name.isidentifier() else None
|
|
157
|
+
|
|
158
|
+
def extract_property_name(uri) -> Optional[str]:
|
|
159
|
+
"""Extract a clean property name from a URI"""
|
|
160
|
+
uri_str = str(uri)
|
|
161
|
+
if '#' in uri_str:
|
|
162
|
+
name = uri_str.split('#')[-1]
|
|
163
|
+
elif '/' in uri_str:
|
|
164
|
+
name = uri_str.split('/')[-1]
|
|
165
|
+
else:
|
|
166
|
+
name = uri_str
|
|
167
|
+
|
|
168
|
+
# Clean up the name to be a valid Python property name
|
|
169
|
+
name = re.sub(r'[^a-zA-Z0-9_]', '', name)
|
|
170
|
+
if name and name[0].isupper():
|
|
171
|
+
name = name[0].lower() + name[1:]
|
|
172
|
+
|
|
173
|
+
return name if name and name.isidentifier() else None
|
|
174
|
+
|
|
175
|
+
def get_description(g: rdflib.Graph, resource) -> Optional[str]:
|
|
176
|
+
"""Get description/comment for a resource"""
|
|
177
|
+
RDFS = rdflib.Namespace("http://www.w3.org/2000/01/rdf-schema#")
|
|
178
|
+
|
|
179
|
+
for comment in g.objects(resource, RDFS.comment):
|
|
180
|
+
return str(comment)
|
|
181
|
+
|
|
182
|
+
for label in g.objects(resource, RDFS.label):
|
|
183
|
+
return str(label)
|
|
184
|
+
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
def get_property_range(g: rdflib.Graph, prop, classes: Dict[str, ClassInfo]) -> Optional[str]:
|
|
188
|
+
"""Get the range class for an object property"""
|
|
189
|
+
RDFS = rdflib.Namespace("http://www.w3.org/2000/01/rdf-schema#")
|
|
190
|
+
|
|
191
|
+
for range_cls in g.objects(prop, RDFS.range):
|
|
192
|
+
if str(range_cls) in classes:
|
|
193
|
+
return classes[str(range_cls)].name
|
|
194
|
+
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
def get_datatype_range(g: rdflib.Graph, prop) -> Optional[str]:
|
|
198
|
+
"""Get the datatype range for a data property"""
|
|
199
|
+
RDFS = rdflib.Namespace("http://www.w3.org/2000/01/rdf-schema#")
|
|
200
|
+
XSD = rdflib.Namespace("http://www.w3.org/2001/XMLSchema#")
|
|
201
|
+
|
|
202
|
+
datatype_mapping = {
|
|
203
|
+
str(XSD.string): 'str',
|
|
204
|
+
str(XSD.integer): 'int',
|
|
205
|
+
str(XSD.int): 'int',
|
|
206
|
+
str(XSD.float): 'float',
|
|
207
|
+
str(XSD.double): 'float',
|
|
208
|
+
str(XSD.boolean): 'bool',
|
|
209
|
+
str(XSD.date): 'datetime.date',
|
|
210
|
+
str(XSD.dateTime): 'datetime.datetime',
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for range_type in g.objects(prop, RDFS.range):
|
|
214
|
+
return datatype_mapping.get(str(range_type), 'Any')
|
|
215
|
+
|
|
216
|
+
return 'Any'
|
|
217
|
+
|
|
218
|
+
def extract_shacl_constraints(g: rdflib.Graph, classes: Dict[str, ClassInfo],
|
|
219
|
+
properties: Dict[str, PropertyInfo], SHACL):
|
|
220
|
+
"""Extract SHACL constraints and apply them to properties"""
|
|
221
|
+
|
|
222
|
+
# Find SHACL shapes
|
|
223
|
+
for shape in g.subjects(rdflib.RDF.type, SHACL.NodeShape):
|
|
224
|
+
# Get target class
|
|
225
|
+
for target_class in g.objects(shape, SHACL.targetClass):
|
|
226
|
+
if str(target_class) in classes:
|
|
227
|
+
# Process property shapes
|
|
228
|
+
for prop_shape in g.objects(shape, SHACL.property):
|
|
229
|
+
process_property_shape(g, prop_shape, classes[str(target_class)],
|
|
230
|
+
properties, SHACL)
|
|
231
|
+
|
|
232
|
+
def process_property_shape(g: rdflib.Graph, prop_shape, class_info: ClassInfo,
|
|
233
|
+
properties: Dict[str, PropertyInfo], SHACL):
|
|
234
|
+
"""Process a SHACL property shape"""
|
|
235
|
+
|
|
236
|
+
# Get the property path
|
|
237
|
+
for path in g.objects(prop_shape, SHACL.path):
|
|
238
|
+
if str(path) in properties:
|
|
239
|
+
prop_info = properties[str(path)]
|
|
240
|
+
|
|
241
|
+
# Check cardinality constraints
|
|
242
|
+
for min_count in g.objects(prop_shape, SHACL.minCount):
|
|
243
|
+
if int(str(min_count)) > 0:
|
|
244
|
+
prop_info.required = True
|
|
245
|
+
|
|
246
|
+
for max_count in g.objects(prop_shape, SHACL.maxCount):
|
|
247
|
+
if int(str(max_count)) == 1:
|
|
248
|
+
prop_info.cardinality = 'single'
|
|
249
|
+
else:
|
|
250
|
+
prop_info.cardinality = 'multiple'
|
|
251
|
+
|
|
252
|
+
def inherit_parent_properties(classes: Dict[str, ClassInfo]):
|
|
253
|
+
"""
|
|
254
|
+
Inherit properties from parent classes to child classes.
|
|
255
|
+
Properties inherited from parents are made optional to represent capability links.
|
|
256
|
+
"""
|
|
257
|
+
# Create a mapping from class name to class info for easier lookup
|
|
258
|
+
name_to_class = {class_info.name: class_info for class_info in classes.values()}
|
|
259
|
+
|
|
260
|
+
def collect_inherited_properties(class_info: ClassInfo, visited: Optional[Set[str]] = None) -> List[PropertyInfo]:
|
|
261
|
+
"""Recursively collect properties from parent classes"""
|
|
262
|
+
if visited is None:
|
|
263
|
+
visited = set()
|
|
264
|
+
|
|
265
|
+
# Avoid infinite recursion in case of circular inheritance
|
|
266
|
+
if class_info.name in visited:
|
|
267
|
+
return []
|
|
268
|
+
|
|
269
|
+
visited.add(class_info.name)
|
|
270
|
+
inherited_props = []
|
|
271
|
+
|
|
272
|
+
for parent_name in class_info.parent_classes:
|
|
273
|
+
if parent_name in name_to_class:
|
|
274
|
+
parent_class = name_to_class[parent_name]
|
|
275
|
+
|
|
276
|
+
# Add direct properties from parent (preserve their required status)
|
|
277
|
+
for prop in parent_class.properties:
|
|
278
|
+
# Create a copy of the property preserving the original required status
|
|
279
|
+
inherited_prop = PropertyInfo(
|
|
280
|
+
name=prop.name,
|
|
281
|
+
property_type=prop.property_type,
|
|
282
|
+
range_class=prop.range_class,
|
|
283
|
+
datatype=prop.datatype,
|
|
284
|
+
cardinality=prop.cardinality,
|
|
285
|
+
required=prop.required # Preserve original required status
|
|
286
|
+
)
|
|
287
|
+
inherited_props.append(inherited_prop)
|
|
288
|
+
|
|
289
|
+
# Recursively collect from grandparents
|
|
290
|
+
inherited_props.extend(collect_inherited_properties(parent_class, visited.copy()))
|
|
291
|
+
|
|
292
|
+
return inherited_props
|
|
293
|
+
|
|
294
|
+
# Apply inheritance to each class
|
|
295
|
+
for class_info in classes.values():
|
|
296
|
+
inherited_props = collect_inherited_properties(class_info)
|
|
297
|
+
|
|
298
|
+
# Add inherited properties that don't already exist
|
|
299
|
+
existing_prop_names = {prop.name for prop in class_info.properties}
|
|
300
|
+
|
|
301
|
+
for inherited_prop in inherited_props:
|
|
302
|
+
if inherited_prop.name not in existing_prop_names:
|
|
303
|
+
class_info.properties.append(inherited_prop)
|
|
304
|
+
# Find the property URI from the inheritance chain
|
|
305
|
+
def find_property_uri(prop_name: str, current_class: ClassInfo, search_visited: Optional[Set[str]] = None) -> Optional[str]:
|
|
306
|
+
if search_visited is None:
|
|
307
|
+
search_visited = set()
|
|
308
|
+
|
|
309
|
+
if current_class.name in search_visited:
|
|
310
|
+
return None
|
|
311
|
+
search_visited.add(current_class.name)
|
|
312
|
+
|
|
313
|
+
# Check if current class has the property URI
|
|
314
|
+
if prop_name in current_class.property_uris:
|
|
315
|
+
return current_class.property_uris[prop_name]
|
|
316
|
+
|
|
317
|
+
# Search in parent classes
|
|
318
|
+
for parent_name in current_class.parent_classes:
|
|
319
|
+
if parent_name in name_to_class:
|
|
320
|
+
parent_class = name_to_class[parent_name]
|
|
321
|
+
uri = find_property_uri(prop_name, parent_class, search_visited.copy())
|
|
322
|
+
if uri:
|
|
323
|
+
return uri
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
prop_uri = find_property_uri(inherited_prop.name, class_info)
|
|
327
|
+
if prop_uri:
|
|
328
|
+
class_info.property_uris[inherited_prop.name] = prop_uri
|
|
329
|
+
existing_prop_names.add(inherited_prop.name)
|
|
330
|
+
|
|
331
|
+
def generate_python_code(classes: Dict[str, ClassInfo],
|
|
332
|
+
properties: Dict[str, PropertyInfo]) -> str:
|
|
333
|
+
"""Generate Python code from extracted class and property information"""
|
|
334
|
+
|
|
335
|
+
code_lines = [
|
|
336
|
+
"from __future__ import annotations",
|
|
337
|
+
"from typing import Optional, List, Any, Union, ClassVar",
|
|
338
|
+
"from pydantic import BaseModel, Field, PrivateAttr",
|
|
339
|
+
"import datetime",
|
|
340
|
+
"import uuid",
|
|
341
|
+
"import rdflib",
|
|
342
|
+
"from rdflib import Graph, URIRef, Literal, Namespace",
|
|
343
|
+
"from rdflib.namespace import RDF, RDFS, OWL, XSD",
|
|
344
|
+
"",
|
|
345
|
+
"# Generated classes from TTL file",
|
|
346
|
+
"",
|
|
347
|
+
"# Base class for all RDF entities",
|
|
348
|
+
"class RDFEntity(BaseModel):",
|
|
349
|
+
" \"\"\"Base class for all RDF entities with URI and namespace management\"\"\"",
|
|
350
|
+
" _namespace: ClassVar[str] = \"http://example.org/instance/\"",
|
|
351
|
+
" _uri: str = \"\"",
|
|
352
|
+
" ",
|
|
353
|
+
" model_config = {",
|
|
354
|
+
" 'arbitrary_types_allowed': True,",
|
|
355
|
+
" 'extra': 'forbid'",
|
|
356
|
+
" }",
|
|
357
|
+
" ",
|
|
358
|
+
" def __init__(self, **kwargs):",
|
|
359
|
+
" uri = kwargs.pop('_uri', None)",
|
|
360
|
+
" super().__init__(**kwargs)",
|
|
361
|
+
" if uri is not None:",
|
|
362
|
+
" self._uri = uri",
|
|
363
|
+
" elif not self._uri:",
|
|
364
|
+
" self._uri = f\"{self._namespace}{uuid.uuid4()}\"",
|
|
365
|
+
" ",
|
|
366
|
+
" @classmethod",
|
|
367
|
+
" def set_namespace(cls, namespace: str):",
|
|
368
|
+
" \"\"\"Set the namespace for generating URIs\"\"\"",
|
|
369
|
+
" cls._namespace = namespace",
|
|
370
|
+
" ",
|
|
371
|
+
" def rdf(self, subject_uri: str | None = None) -> Graph:",
|
|
372
|
+
" \"\"\"Generate RDF triples for this instance\"\"\"",
|
|
373
|
+
" g = Graph()",
|
|
374
|
+
" ",
|
|
375
|
+
" # Use stored URI or provided subject_uri",
|
|
376
|
+
" if subject_uri is None:",
|
|
377
|
+
" subject_uri = self._uri",
|
|
378
|
+
" subject = URIRef(subject_uri)",
|
|
379
|
+
" ",
|
|
380
|
+
" # Add class type",
|
|
381
|
+
" if hasattr(self, '_class_uri'):",
|
|
382
|
+
" g.add((subject, RDF.type, URIRef(self._class_uri)))",
|
|
383
|
+
" ",
|
|
384
|
+
" # Add properties",
|
|
385
|
+
" if hasattr(self, '_property_uris'):",
|
|
386
|
+
" for prop_name, prop_uri in self._property_uris.items():",
|
|
387
|
+
" prop_value = getattr(self, prop_name, None)",
|
|
388
|
+
" if prop_value is not None:",
|
|
389
|
+
" if isinstance(prop_value, list):",
|
|
390
|
+
" for item in prop_value:",
|
|
391
|
+
" if hasattr(item, 'rdf'):",
|
|
392
|
+
" # Add triples from related object",
|
|
393
|
+
" g += item.rdf()",
|
|
394
|
+
" g.add((subject, URIRef(prop_uri), URIRef(item._uri)))",
|
|
395
|
+
" else:",
|
|
396
|
+
" g.add((subject, URIRef(prop_uri), Literal(item)))",
|
|
397
|
+
" elif hasattr(prop_value, 'rdf'):",
|
|
398
|
+
" # Add triples from related object",
|
|
399
|
+
" g += prop_value.rdf()",
|
|
400
|
+
" g.add((subject, URIRef(prop_uri), URIRef(prop_value._uri)))",
|
|
401
|
+
" else:",
|
|
402
|
+
" g.add((subject, URIRef(prop_uri), Literal(prop_value)))",
|
|
403
|
+
" ",
|
|
404
|
+
" return g",
|
|
405
|
+
"",
|
|
406
|
+
""
|
|
407
|
+
]
|
|
408
|
+
|
|
409
|
+
# Sort classes to handle inheritance properly
|
|
410
|
+
sorted_classes = topological_sort_classes(classes)
|
|
411
|
+
|
|
412
|
+
for class_info in sorted_classes:
|
|
413
|
+
code_lines.extend(generate_class_code(class_info))
|
|
414
|
+
code_lines.append("")
|
|
415
|
+
|
|
416
|
+
# Add model_rebuild() calls for forward references
|
|
417
|
+
code_lines.append("# Rebuild models to resolve forward references")
|
|
418
|
+
for class_info in sorted_classes:
|
|
419
|
+
code_lines.append(f"{class_info.name}.model_rebuild()")
|
|
420
|
+
code_lines.append("")
|
|
421
|
+
|
|
422
|
+
return "\n".join(code_lines)
|
|
423
|
+
|
|
424
|
+
def topological_sort_classes(classes: Dict[str, ClassInfo]) -> List[ClassInfo]:
|
|
425
|
+
"""Sort classes so that dependencies come before classes that use them"""
|
|
426
|
+
|
|
427
|
+
# More aggressive topological sort that prioritizes inheritance dependencies
|
|
428
|
+
# and handles circular dependencies better
|
|
429
|
+
sorted_classes = []
|
|
430
|
+
visited = set()
|
|
431
|
+
|
|
432
|
+
def get_inheritance_depth(class_info: ClassInfo, depth=0, visited_in_chain=None):
|
|
433
|
+
"""Calculate inheritance depth, handling cycles"""
|
|
434
|
+
if visited_in_chain is None:
|
|
435
|
+
visited_in_chain = set()
|
|
436
|
+
|
|
437
|
+
if class_info.name in visited_in_chain:
|
|
438
|
+
return depth # Cycle detected, return current depth
|
|
439
|
+
|
|
440
|
+
if not class_info.parent_classes:
|
|
441
|
+
return depth
|
|
442
|
+
|
|
443
|
+
visited_in_chain.add(class_info.name)
|
|
444
|
+
max_parent_depth = depth
|
|
445
|
+
|
|
446
|
+
for parent_name in class_info.parent_classes:
|
|
447
|
+
for parent_class in classes.values():
|
|
448
|
+
if parent_class.name == parent_name:
|
|
449
|
+
parent_depth = get_inheritance_depth(parent_class, depth + 1, visited_in_chain.copy())
|
|
450
|
+
max_parent_depth = max(max_parent_depth, parent_depth)
|
|
451
|
+
break
|
|
452
|
+
|
|
453
|
+
return max_parent_depth
|
|
454
|
+
|
|
455
|
+
# Sort by inheritance depth first (deepest inheritance last)
|
|
456
|
+
classes_by_depth = [(get_inheritance_depth(class_info), class_info) for class_info in classes.values()]
|
|
457
|
+
classes_by_depth.sort(key=lambda x: x[0])
|
|
458
|
+
|
|
459
|
+
# Then do standard topological sort within each depth level
|
|
460
|
+
def visit(class_info: ClassInfo, temp_visited=None):
|
|
461
|
+
if temp_visited is None:
|
|
462
|
+
temp_visited = set()
|
|
463
|
+
|
|
464
|
+
if class_info.name in visited:
|
|
465
|
+
return
|
|
466
|
+
if class_info.name in temp_visited:
|
|
467
|
+
# Circular dependency - add the class anyway to avoid infinite loops
|
|
468
|
+
if class_info not in sorted_classes:
|
|
469
|
+
sorted_classes.append(class_info)
|
|
470
|
+
visited.add(class_info.name)
|
|
471
|
+
return
|
|
472
|
+
|
|
473
|
+
temp_visited.add(class_info.name)
|
|
474
|
+
|
|
475
|
+
# Visit parent classes first (inheritance dependencies)
|
|
476
|
+
for parent_name in class_info.parent_classes:
|
|
477
|
+
for parent_class in classes.values():
|
|
478
|
+
if parent_class.name == parent_name:
|
|
479
|
+
visit(parent_class, temp_visited.copy())
|
|
480
|
+
break
|
|
481
|
+
|
|
482
|
+
visited.add(class_info.name)
|
|
483
|
+
sorted_classes.append(class_info)
|
|
484
|
+
|
|
485
|
+
# Process classes in order of inheritance depth
|
|
486
|
+
for depth, class_info in classes_by_depth:
|
|
487
|
+
visit(class_info)
|
|
488
|
+
|
|
489
|
+
return sorted_classes
|
|
490
|
+
|
|
491
|
+
def generate_class_code(class_info: ClassInfo) -> List[str]:
|
|
492
|
+
"""Generate Python code for a single class"""
|
|
493
|
+
|
|
494
|
+
lines = []
|
|
495
|
+
|
|
496
|
+
# Deduplicate properties by name while merging stricter constraints.
|
|
497
|
+
unique_props: Dict[str, PropertyInfo] = {}
|
|
498
|
+
for prop in class_info.properties:
|
|
499
|
+
if prop.name in unique_props:
|
|
500
|
+
existing = unique_props[prop.name]
|
|
501
|
+
if prop.required and not existing.required:
|
|
502
|
+
existing.required = True
|
|
503
|
+
if prop.cardinality and not existing.cardinality:
|
|
504
|
+
existing.cardinality = prop.cardinality
|
|
505
|
+
continue
|
|
506
|
+
unique_props[prop.name] = prop
|
|
507
|
+
|
|
508
|
+
properties_list = list(unique_props.values())
|
|
509
|
+
|
|
510
|
+
# Determine class bases
|
|
511
|
+
if class_info.parent_classes:
|
|
512
|
+
parents = list(class_info.parent_classes)
|
|
513
|
+
if "RDFEntity" not in parents:
|
|
514
|
+
parents.append("RDFEntity")
|
|
515
|
+
lines.append(f"class {class_info.name}({', '.join(parents)}):")
|
|
516
|
+
else:
|
|
517
|
+
lines.append(f"class {class_info.name}(RDFEntity):")
|
|
518
|
+
|
|
519
|
+
# Add class docstring if description exists
|
|
520
|
+
if class_info.description:
|
|
521
|
+
lines.append(' """')
|
|
522
|
+
for line in class_info.description.splitlines():
|
|
523
|
+
lines.append(f" {line}")
|
|
524
|
+
lines.append(' """')
|
|
525
|
+
|
|
526
|
+
if class_info.description:
|
|
527
|
+
lines.append("")
|
|
528
|
+
|
|
529
|
+
# Add class-specific metadata
|
|
530
|
+
lines.append(f" _class_uri: ClassVar[str] = '{class_info.uri}'")
|
|
531
|
+
|
|
532
|
+
# Add property URI mapping
|
|
533
|
+
if class_info.property_uris:
|
|
534
|
+
prop_uris_dict = ", ".join(
|
|
535
|
+
[
|
|
536
|
+
f"'{prop_name}': '{prop_uri}'"
|
|
537
|
+
for prop_name, prop_uri in sorted(class_info.property_uris.items())
|
|
538
|
+
]
|
|
539
|
+
)
|
|
540
|
+
lines.append(f" _property_uris: ClassVar[dict] = {{{prop_uris_dict}}}")
|
|
541
|
+
else:
|
|
542
|
+
lines.append(" _property_uris: ClassVar[dict] = {}")
|
|
543
|
+
|
|
544
|
+
if class_info.property_uris:
|
|
545
|
+
lines.append("")
|
|
546
|
+
|
|
547
|
+
# Add properties grouped by type for readability
|
|
548
|
+
data_properties = sorted(
|
|
549
|
+
(prop for prop in properties_list if prop.property_type == 'data'),
|
|
550
|
+
key=lambda prop: prop.name,
|
|
551
|
+
)
|
|
552
|
+
object_properties = sorted(
|
|
553
|
+
(prop for prop in properties_list if prop.property_type == 'object'),
|
|
554
|
+
key=lambda prop: prop.name,
|
|
555
|
+
)
|
|
556
|
+
other_properties = sorted(
|
|
557
|
+
(
|
|
558
|
+
prop
|
|
559
|
+
for prop in properties_list
|
|
560
|
+
if prop.property_type not in {'data', 'object'}
|
|
561
|
+
),
|
|
562
|
+
key=lambda prop: prop.name,
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
property_groups = [
|
|
566
|
+
("Data properties", data_properties),
|
|
567
|
+
("Object properties", object_properties),
|
|
568
|
+
("Other properties", other_properties),
|
|
569
|
+
]
|
|
570
|
+
|
|
571
|
+
emitted_property_group = False
|
|
572
|
+
for group_label, props in property_groups:
|
|
573
|
+
if not props:
|
|
574
|
+
continue
|
|
575
|
+
if emitted_property_group:
|
|
576
|
+
lines.append("")
|
|
577
|
+
lines.append(f" # {group_label}")
|
|
578
|
+
for prop in props:
|
|
579
|
+
lines.append(f" {generate_property_code(prop)}")
|
|
580
|
+
emitted_property_group = True
|
|
581
|
+
|
|
582
|
+
if not emitted_property_group:
|
|
583
|
+
lines.append(" pass")
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
return lines
|
|
588
|
+
|
|
589
|
+
def generate_property_code(prop: PropertyInfo) -> str:
|
|
590
|
+
"""Generate code for a single property"""
|
|
591
|
+
|
|
592
|
+
# Determine type annotation and Pydantic Field
|
|
593
|
+
if prop.property_type == 'object' and prop.range_class:
|
|
594
|
+
if prop.cardinality == 'multiple':
|
|
595
|
+
type_annotation = f"List[{prop.range_class}]"
|
|
596
|
+
default_value = "Field(default_factory=list)"
|
|
597
|
+
else:
|
|
598
|
+
type_annotation = f"Optional[{prop.range_class}]" if not prop.required else prop.range_class
|
|
599
|
+
default_value = "Field(default=None)" if not prop.required else "Field(...)"
|
|
600
|
+
elif prop.property_type == 'data' and prop.datatype:
|
|
601
|
+
if prop.cardinality == 'multiple':
|
|
602
|
+
type_annotation = f"List[{prop.datatype}]"
|
|
603
|
+
default_value = "Field(default_factory=list)"
|
|
604
|
+
else:
|
|
605
|
+
type_annotation = f"Optional[{prop.datatype}]" if not prop.required else prop.datatype
|
|
606
|
+
default_value = "Field(default=None)" if not prop.required else "Field(...)"
|
|
607
|
+
else:
|
|
608
|
+
type_annotation = "Optional[Any]"
|
|
609
|
+
default_value = "Field(default=None)"
|
|
610
|
+
|
|
611
|
+
return f"{prop.name}: {type_annotation} = {default_value}"
|