Skip to content

Commit 810d7fb

Browse files
committed
feat/added_expected_outputs
- Added a mechanism to automatically get the expected output from the ontology and add the expected response schema. - Added to the example the updates required to test the new feature of response schema.
1 parent d561073 commit 810d7fb

8 files changed

Lines changed: 127 additions & 12 deletions

File tree

CHANGELOG.txt

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
Change Log
22
===============
3-
VERSION="0.0.20"
3+
VERSION="0.0.21"
44
LAST_UPDATE="11/02/2026"
55
------------------
6-
- Added to the Ontological Framework in the Business Model ontology a
7-
concept to link a business model to an output metadata: this is aligned
8-
with the integration goals with user interfaces and other systems by providing
9-
a data schema of the output.
10-
- Added Wiki at https://github.com/JCGCosta/OntologyToAPI/wiki
6+
- Added a mechanism to automatically get the expected output
7+
from the ontology and add the expected response schema.
8+
- Added to the example the updates required to test the new
9+
feature of response schema.
1110
------------------

OntologyToAPI/core/APIGenerator.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from Settings import auto_config as cfg
88
from datetime import date
99
from types import SimpleNamespace
10+
from pydantic import create_model, ConfigDict
1011

1112
from OntologyToAPI.core.Utility import *
1213
from OntologyToAPI.core.Ontology import Ontology
@@ -89,12 +90,14 @@ def generate_api_routes(self) -> FastAPI:
8990
name = bm_name.split(":")
9091
ec_func = import_function_from_file(filepath=bm_dt.externalCode.pythonFile, function_name=bm_dt.externalCode.function)
9192
handler = create_business_model_handler(bm_dt.requiresMetadata, bm_dt.requiresParameters, ec_func)
93+
response_model = bm_dt.hasOutputMetadata
9294
self.app.add_api_route(
9395
path=f"/{name[0]}/{name[1]}/run",
9496
endpoint=handler,
9597
methods=["POST"],
9698
name=bm_dt.desc,
9799
tags=[f"Business Model ({name[0]})"],
100+
response_model=response_model,
98101
)
99102
logging.info(f'Added {bm_name} running API route...')
100103
return self.app

OntologyToAPI/core/DTO/BusinessModel.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
class BusinessModel(BaseModel):
88
name: str
99
desc: str
10+
hasOutputMetadata: Optional[object]
1011
requiresMetadata: Optional[List[Metadata]]
1112
requiresParameters: Optional[dict]
1213
externalCode: Optional[ExternalCode]

OntologyToAPI/core/Ontology.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from OntologyToAPI.core.DTO.BusinessModel import *
1111

1212
from OntologyToAPI.core.Connectors.IndentifyConnector import identifyConnector
13-
from OntologyToAPI.core.Utility import ensure_package_installed
13+
from OntologyToAPI.core.Utility import ensure_package_installed, build_nested_model
1414

1515
class Ontology:
1616
def __init__(self):
@@ -67,6 +67,9 @@ def serialize_business_models(self):
6767
bm_name = self.g.qname(ec[0])
6868
required_metadata = self._verify_metadata(GET_REQUIRED_MD_FOR_BM_QUERY + URIRef(ec[0]) + ">)}")
6969
required_parameters = self.g.query(GET_REQUIRED_PARAMETERS_FOR_BM_QUERY + URIRef(ec[0]) + ">)}")
70+
required_output_parameters = self.g.query(GET_REQUIRED_OUTPUT_METADATA_FOR_BM_QUERY + URIRef(ec[0]) + ">)}")
71+
response_schema = {str(self.g.qname(out).split(":")[-1]) : (out_t, None) for _, out, out_t in required_output_parameters}
72+
response_schema = build_nested_model("OutputSchema", response_schema) if response_schema else None
7073
for module in str(ec[3]).split(","): ensure_package_installed(module)
7174
func_path = Path(str(ec[2])).expanduser()
7275
if not func_path.is_absolute(): func_path = (Path.cwd() / func_path).resolve(strict=False)
@@ -75,6 +78,7 @@ def serialize_business_models(self):
7578
raise FileNotFoundError(f'The external code file for the {bm_name} business model could not be found at "{func_path}"')
7679
BMs[bm_name] = BusinessModel(name=bm_name.split(":")[-1],
7780
desc=str(ec[6]) if ec[6] is not None else f"Business Model for the {bm_name} business model",
81+
hasOutputMetadata=response_schema,
7882
requiresMetadata=required_metadata,
7983
requiresParameters={str(l): t for _, l, t in required_parameters},
8084
externalCode=ExternalCode(

OntologyToAPI/core/Queries.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,11 @@
5151
?bmt rdfs:comment ?desc .
5252
FILTER(?bmt != owl:NamedIndividual)
5353
}
54-
"""
54+
"""
55+
56+
GET_REQUIRED_OUTPUT_METADATA_FOR_BM_QUERY = """
57+
SELECT ?bm ?out ?out_type
58+
WHERE {
59+
?bm <http://www.cedri.com/OntologyToAPI-BusinessModel#hasOutputMetadata> ?out .
60+
?out <http://www.cedri.com/OntologyToAPI-Metadata#hasType> ?out_type .
61+
FILTER (?bm = <"""

OntologyToAPI/core/Utility.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,21 @@
44
import os
55
import logging
66
from pathlib import Path
7+
from pydantic import BaseModel, create_model, ConfigDict
8+
from typing import Any, List, Dict, Union
9+
import rdflib
710
from Settings import auto_config as cfg
811

12+
TYPE_MAP = {
13+
'float': float,
14+
'str': str,
15+
'int': int,
16+
'bool': bool,
17+
'object': dict,
18+
'list': list,
19+
'object_list': list
20+
}
21+
922
def handle_upload_file(uploaded_file):
1023
UPLOAD_DIR = Path(cfg.UPLOAD_DIR)
1124
if not os.path.exists(UPLOAD_DIR):
@@ -61,4 +74,35 @@ def ensure_package_installed(package_name):
6174
if importlib.util.find_spec(package_name) is None:
6275
install_package(package_name)
6376
else:
64-
logging.info(f"Package '{package_name}' is already installed.")
77+
logging.info(f"Package '{package_name}' is already installed.")
78+
79+
80+
def build_nested_model(name: str, flat_data: Dict[str, Any]) -> type[BaseModel]:
81+
tree = {}
82+
for key, (rdf_type, _) in flat_data.items():
83+
parts = key.split('.')
84+
current = tree
85+
for i, part in enumerate(parts):
86+
if part not in current:
87+
current[part] = {"_children": {}}
88+
if i == len(parts) - 1:
89+
current[part]["_type"] = str(rdf_type)
90+
current = current[part]["_children"]
91+
92+
def generate_pydantic(model_name: str, node_dict: Dict) -> type[BaseModel]:
93+
fields = {}
94+
for field_name, metadata in node_dict.items():
95+
field_type_str = metadata.get("_type", "object")
96+
children = metadata.get("_children")
97+
if children:
98+
sub_model = generate_pydantic(f"{field_name}Model", children)
99+
if field_type_str == 'object_list':
100+
fields[field_name] = (List[sub_model], ...)
101+
else:
102+
fields[field_name] = (sub_model, ...)
103+
else:
104+
python_type = TYPE_MAP.get(field_type_str, Any)
105+
fields[field_name] = (python_type, ...)
106+
return create_model(model_name, **fields, __config__=ConfigDict(extra='allow'))
107+
108+
return generate_pydantic(name, tree)

example/bm.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,32 @@ async def convert_temperature_c_to_f(data):
44
if data.params["temperature_c"] is not None:
55
return {
66
"original": data.params["temperature_c"],
7-
"converted": data.params["temperature_c"] * 9 / 5 + 32
7+
"converted": data.params["temperature_c"] * 9 / 5 + 32,
8+
"test": {
9+
"inner": "lorem ipsum",
10+
},
11+
"test2": [
12+
{
13+
"inner1": "lorem ipsum 1",
14+
},
15+
{
16+
"inner1": "lorem ipsum 2",
17+
}
18+
]
819
}
920
else:
1021
return {
1122
"original": [t["temperature_c"] for t in data.metadata["Temperature_C_MD"]],
12-
"converted": [t["temperature_c"] * 9 / 5 + 32 for t in data.metadata["Temperature_C_MD"]]
23+
"converted": [t["temperature_c"] * 9 / 5 + 32 for t in data.metadata["Temperature_C_MD"]],
24+
"test": {
25+
"inner": "lorem ipsum",
26+
},
27+
"test2": [
28+
{
29+
"inner1": "lorem ipsum 1",
30+
},
31+
{
32+
"inner1": "lorem ipsum 2",
33+
}
34+
]
1335
}

example/bm_example.ttl

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,54 @@ ex:CelsiusToFahrenheitBM rdf:type owl:Class ;
1717

1818
# Individuals for Business Models
1919

20+
# Business Model External Code (Data Transformation)
2021
ex:CelsiusToFahrenheit.py rdf:type owl:NamedIndividual ,
2122
excode:ExternalCode ;
2223
excode:hasFunction "convert_temperature_c_to_f" ;
2324
excode:hasPythonFile "bm.py" ;
2425
excode:requiresLib "requests" .
2526

27+
# Endpoint Parameters (Expected Inputs)
2628
ex:Temperature_P rdf:type owl:NamedIndividual ,
2729
bm:Parameter ;
2830
bm:hasParameterLabel "temperature_c" ;
2931
bm:hasParameterType "float" .
3032

33+
# Endpoint Output Schema (Expected Output)
34+
ex:original rdf:type owl:NamedIndividual ,
35+
md:Metadata ;
36+
md:hasType "float" .
37+
38+
ex:converted rdf:type owl:NamedIndividual ,
39+
md:Metadata ;
40+
md:hasType "float" .
41+
42+
ex:test rdf:type owl:NamedIndividual ,
43+
md:Metadata ;
44+
md:hasType "object" .
45+
46+
ex:test.inner rdf:type owl:NamedIndividual ,
47+
md:Metadata ;
48+
md:hasType "str" .
49+
50+
ex:test2 rdf:type owl:NamedIndividual ,
51+
md:Metadata ;
52+
md:hasType "object_list" .
53+
54+
ex:test2.inner1 rdf:type owl:NamedIndividual ,
55+
md:Metadata ;
56+
md:hasType "str" .
57+
58+
59+
# Business Model concepts gathering
3160
ex:CelsiusToFahrenheit rdf:type owl:NamedIndividual ,
3261
ex:CelsiusToFahrenheitBM ;
3362
bm:requiresMetadata ex:Temperature_C_MD;
3463
bm:hasExternalCode ex:CelsiusToFahrenheit.py ;
35-
bm:hasParameter ex:Temperature_P .
64+
bm:hasParameter ex:Temperature_P ;
65+
bm:hasOutputMetadata ex:original ,
66+
ex:converted ,
67+
ex:test ,
68+
ex:test.inner ,
69+
ex:test2 ,
70+
ex:test2.inner1 .

0 commit comments

Comments
 (0)