diff --git a/admin-ui/src/main/java/eu/knowledge/engine/admin/MetadataKB.java b/admin-ui/src/main/java/eu/knowledge/engine/admin/MetadataKB.java index fcad2df0e..5a002d7bb 100644 --- a/admin-ui/src/main/java/eu/knowledge/engine/admin/MetadataKB.java +++ b/admin-ui/src/main/java/eu/knowledge/engine/admin/MetadataKB.java @@ -40,7 +40,23 @@ public class MetadataKB extends KnowledgeBaseImpl { private static final Logger LOG = LoggerFactory.getLogger(MetadataKB.class); - private static final String META_GRAPH_PATTERN_STR = "?kb rdf:type ke:KnowledgeBase . ?kb ke:hasName ?name . ?kb ke:hasDescription ?description . ?kb ke:hasKnowledgeInteraction ?ki . ?ki rdf:type ?kiType . ?ki ke:isMeta ?isMeta . ?ki ke:hasCommunicativeAct ?act . ?act rdf:type ke:CommunicativeAct . ?act ke:hasRequirement ?req . ?act ke:hasSatisfaction ?sat . ?req rdf:type ?reqType . ?sat rdf:type ?satType . ?ki ke:hasGraphPattern ?gp . ?gp rdf:type ?patternType . ?gp ke:hasPattern ?pattern ."; + private static final String META_GRAPH_PATTERN_STR = """ + ?kb rdf:type ke:KnowledgeBase . + ?kb ke:hasName ?name . + ?kb ke:hasDescription ?description . + ?kb ke:hasKnowledgeInteraction ?ki . + ?ki rdf:type ?kiType . + ?ki ke:isMeta ?isMeta . + ?ki ke:hasCommunicativeAct ?act . + ?act rdf:type ke:CommunicativeAct . + ?act ke:hasRequirement ?req . + ?act ke:hasSatisfaction ?sat . + ?req rdf:type ?reqType . + ?sat rdf:type ?satType . + ?ki ke:hasGraphPattern ?gp . + ?gp rdf:type ?patternType . + ?gp ke:hasPattern ?pattern . + """; private final PrefixMapping prefixes; diff --git a/examples/common/answering_kb/.python-version b/examples/common/answering_kb/.python-version deleted file mode 100644 index ec909d993..000000000 --- a/examples/common/answering_kb/.python-version +++ /dev/null @@ -1 +0,0 @@ -kes diff --git a/examples/common/asking_kb/.python-version b/examples/common/asking_kb/.python-version deleted file mode 100644 index ec909d993..000000000 --- a/examples/common/asking_kb/.python-version +++ /dev/null @@ -1 +0,0 @@ -kes diff --git a/examples/dcat/README.md b/examples/dcat/README.md new file mode 100644 index 000000000..29983f075 --- /dev/null +++ b/examples/dcat/README.md @@ -0,0 +1,51 @@ +# DCAT example +This example demonstrates how the Knowledge Bases (KBs) within a Knowledge Network can be exposed as a [DataService Catalog](https://www.w3.org/TR/vocab-dcat-3/) (DCAT). We reuse the docker compose file from the [multiple runtimes example](../multiple-runtimes/) and add a KB that gathers the metadata about the KBs using DCAT terminology. With specific domain knowledge the KE reasoner is able to automatically transform KE metadata into DCAT catalogue data. The following table shows how Knowledge Engine concepts are mapped to DCAT concepts. + +| KE | DCAT | +|----|------| +| ke:KnowledgeBase | dcat:DataService | +| ke:hasName | dcterms:title | +| ke:hasDescription | dcterms:description | + +## meta-kb +We use a modified [asking_kb](../common/asking_kb/asking_kb.py) to enable ASKing and printing for DCAT metadata. We use DCAT specific rules derived from the [Knowledge Engine Ontology]{https://github.com/TNO/knowledge-engine/blob/master/smart-connector/src/main/resources/knowledge-engine-ontology.ttl}. We could also use the following (more generic) RFDS rules, but these currently are too slow and require too much memory: + +``` +// DCAT facts +-> (ke:KnowledgeBase rdfs:subClassOf dcat:DataService ) . +-> (ke:hasName skos:exactMatch dcterms:title ) . +-> (ke:hasDescription skos:exactMatch dcterms:description ) . + +// RDFS rules +[exactMatch: (?i ?p1 ?v) (?p1 skos:exactMatch ?p2) -> (?i ?p2 ?v)] +[subClass: (?i rdf:type ?t1) (?t1 rdfs:subClassOf ?t2) -> (?i rdf:type ?t2)] +``` + +After building & starting the example with `docker compose build` and `docker compose up -d`, you can see the `meta-kb` in action by watching its log: `docker compose logs -f meta-kb` + +After a while you should see the following valid DCAT RDF appear in the log: + +``` +@prefix rdf: . +@prefix dcat: . +@prefix dcterms: . +@prefix ex: . + +ex:ke_catalog rdf:type dcat:Catalog . +ex:ke_catalog dcterms:title "Knowledge Engine DCAT Catalog" . + + rdf:type dcat:DataService . + dcterms:title "kb2" . + dcterms:description "kb2" . +ex:ke_catalog dcat:service . + + rdf:type dcat:DataService . + dcterms:title "kb1" . + dcterms:description "kb1" . +ex:ke_catalog dcat:service . + + rdf:type dcat:DataService . + dcterms:title "kb3" . + dcterms:description "kb3" . +ex:ke_catalog dcat:service . +``` diff --git a/examples/dcat/docker-compose.yml b/examples/dcat/docker-compose.yml new file mode 100644 index 000000000..65866af77 --- /dev/null +++ b/examples/dcat/docker-compose.yml @@ -0,0 +1,40 @@ +include: + - ../multiple-runtimes/docker-compose.yml + +services: + runtime-4: + image: ghcr.io/tno/knowledge-engine/smart-connector:1.4.1-SNAPSHOT + environment: + KE_RUNTIME_PORT: 8081 + KE_RUNTIME_EXPOSED_URL: http://runtime-4:8081 + KD_URL: http://knowledge-directory:8282 + KE_REASONER_LEVEL: 5 + JAVA_TOOL_OPTIONS: "-Dorg.slf4j.simpleLogger.log.eu.knowledge.engine=info" + meta-kb: + build: meta-kb + environment: + KE_URL: http://runtime-4:8280/rest + KB_ID: http://example.org/meta-kb + PREFIXES: | + { + "dcat": "http://www.w3.org/ns/dcat#", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "dcterms": "http://purl.org/dc/terms/" + } + GRAPH_PATTERN: | + ?ds rdf:type dcat:DataService . + ?ds dcterms:title ?title . + ?ds dcterms:description ?description . + DOMAIN_KNOWLEDGE: | + @prefix dcat: . + @prefix ke: . + @prefix rdfs: . + @prefix rdf: . + @prefix dcterms: . + @prefix skos: . + + // KE DCAT rules + [DCATDataService: (?i rdf:type ke:KnowledgeBase) -> (?i rdf:type dcat:DataService)] + [DCATTitle: (?i ke:hasName ?v) -> (?i dcterms:title ?v)] + [DCATDesc: (?i ke:hasDescription ?v) -> (?i dcterms:description ?v)] + diff --git a/examples/dcat/meta-kb/Dockerfile b/examples/dcat/meta-kb/Dockerfile new file mode 100644 index 000000000..53e00c675 --- /dev/null +++ b/examples/dcat/meta-kb/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.10.6-alpine + +# Create and enable venv +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +RUN pip install --upgrade pip + +WORKDIR /app/ + +COPY requirements.txt . +RUN pip install -r requirements.txt + +COPY ./asking_kb.py . + +ENTRYPOINT [ "python", "asking_kb.py" ] diff --git a/examples/dcat/meta-kb/asking_kb.py b/examples/dcat/meta-kb/asking_kb.py new file mode 100644 index 000000000..e664af9c5 --- /dev/null +++ b/examples/dcat/meta-kb/asking_kb.py @@ -0,0 +1,134 @@ +import os +import logging +import time +import json +import signal +import requests + +from knowledge_mapper.tke_client import TkeClient +from knowledge_mapper.knowledge_base import KnowledgeBaseRegistrationRequest +from knowledge_mapper.knowledge_interaction import ( + AskKnowledgeInteraction, + AskKnowledgeInteractionRegistrationRequest, +) + +KE_URL = os.getenv("KE_URL") +KB_ID = os.getenv("KB_ID") +KB_NAME = KB_ID.split("/")[-1] +if "PREFIXES" in os.environ: + PREFIXES = json.loads(os.getenv("PREFIXES")) +else: + PREFIXES = None +GRAPH_PATTERN = os.getenv("GRAPH_PATTERN") +DOMAIN_KNOWLEDGE= os.getenv("DOMAIN_KNOWLEDGE") + +log = logging.getLogger(KB_NAME) +log.setLevel(logging.INFO) + +""" +Make sure that this Python program handles SIGTERM by raising a +KeyboardInterrupt, to end the program. +""" +def handle_sigterm(*args): + raise KeyboardInterrupt() + +signal.signal(signal.SIGTERM, handle_sigterm) + +def register_domain_knowledge(domain_knowledge): + resp = requests.post(url = KE_URL + "/sc/knowledge", headers = {"Knowledge-Base-Id":KB_ID}, data = DOMAIN_KNOWLEDGE); + if resp.status_code != 200: + log.error(f"Our domain knowledge register should return 200 and not {resp.status_code} with message: " + resp.text); + +def register_ask_knowledge_interaction(graph_pattern, prefixes) -> dict: + resp = requests.post(url = KE_URL + "/sc/ki", headers = {"Knowledge-Base-Id":KB_ID}, json = { "knowledgeInteractionType": "AskKnowledgeInteraction", "graphPattern": graph_pattern, "prefixes": prefixes, "includeMetaKIs": "true"}); + if resp.status_code != 200: + log.error(f"Our ask KI register should return 200 and not {resp.status_code} with message: " + resp.text); + else: + log.info(f"Registering Ask succesfull!") + return resp.json(); + +def ask(kiId, bindingSet: dict ) -> dict: + resp = requests.post( + KE_URL + '/sc/ask', + json=bindingSet, + headers={ + 'Knowledge-Base-Id': KB_ID, + 'Knowledge-Interaction-Id': kiId, + } + ) + + if resp.status_code != 200: + log.error(f"Our ask should return 200 and not {resp.status_code} with message: " + resp.text); + else: + log.info(f"Ask activated succesfully!") + + return resp.json() + +def bindingset_to_dcatrdf(aGraphPattern, bindingSet: list[dict]): + someRDF = "" + + for binding in bindingSet: + partRDF = aGraphPattern + id = binding["ds"] + for aVar, aValue in binding.items(): + partRDF = partRDF.replace("?" + aVar, aValue) + + partRDF += f"ex:ke_catalog dcat:service {id} .\n" + someRDF += partRDF + "\n" + return someRDF; + +def kb_1(): + client = TkeClient(KE_URL) + client.connect() + log.info(f"registering KB...") + kb = client.register( + KnowledgeBaseRegistrationRequest( + id=f"{KB_ID}", + name=f"{KB_NAME}", + description=f"{KB_NAME}", + ) + ) + + # register domain knowledge + if DOMAIN_KNOWLEDGE is not None: + register_domain_knowledge(DOMAIN_KNOWLEDGE) + log.info("Domain knowledge registered!") + else: + log.debug(f"No domain knowledge found!") + + log.info(f"KB registered!") + log.info(f"registering ASK KI...") + + kiId = register_ask_knowledge_interaction(GRAPH_PATTERN, PREFIXES)["knowledgeInteractionId"] + + log.info(f"ASK KI ({kiId}) registered!") + result = [] + try: + while True: + log.info(f"asking...") + result = ask(kiId, [{}]) + + bs = result["bindingSet"] + + if len(bs) == 0: + log.info(f"asking gave no results; will sleep for 2s...") + else: + rdf = bindingset_to_dcatrdf(GRAPH_PATTERN, bs) + rdf = """@prefix rdf: . +@prefix dcat: . +@prefix dcterms: . +@prefix ex: . + +ex:ke_catalog rdf:type dcat:Catalog . +ex:ke_catalog dcterms:title \"Knowledge Engine DCAT Catalog\" . + +""" + rdf + log.info(f"Knowledge Network DCAT info: \n{rdf}") + time.sleep(2) + finally: + log.info(f"unregistering...") + kb.unregister() + + +if __name__ == "__main__": + kb_1() diff --git a/examples/dcat/meta-kb/requirements.txt b/examples/dcat/meta-kb/requirements.txt new file mode 100644 index 000000000..de0765136 --- /dev/null +++ b/examples/dcat/meta-kb/requirements.txt @@ -0,0 +1,9 @@ +knowledge-mapper==0.0.23 +# certifi==2022.9.24 # Installed as dependency for requests +# charset-normalizer==2.1.1 # Installed as dependency for requests +# idna==3.4 # Installed as dependency for requests +# json5==0.9.10 # Installed as dependency for knowledge-mapper +# mysql-connector-python==8.0.30 # Installed as dependency for knowledge-mapper +# protobuf==3.20.1 # Installed as dependency for mysql-connector-python +# requests==2.28.1 # Installed as dependency for knowledge-mapper +# urllib3==1.26.12 # Installed as dependency for requests diff --git a/reasoner/src/main/java/eu/knowledge/engine/reasoner/Rule.java b/reasoner/src/main/java/eu/knowledge/engine/reasoner/Rule.java index fa6dd1432..8cccad887 100644 --- a/reasoner/src/main/java/eu/knowledge/engine/reasoner/Rule.java +++ b/reasoner/src/main/java/eu/knowledge/engine/reasoner/Rule.java @@ -142,6 +142,11 @@ public CompletableFuture handle(BindingSet bs) { } } + public Rule(String aName, Set anAntecedent, Set aConsequent) { + this(anAntecedent, aConsequent); + this.setName(aName); + } + public Rule(Set anAntecedent, Set aConsequent, TransformBindingSetHandler aBindingSetHandler) { super(anAntecedent, aConsequent); diff --git a/reasoner/src/main/java/eu/knowledge/engine/reasoner/rulenode/BindingSetStore.java b/reasoner/src/main/java/eu/knowledge/engine/reasoner/rulenode/BindingSetStore.java index b03a6bf2b..72e00392d 100644 --- a/reasoner/src/main/java/eu/knowledge/engine/reasoner/rulenode/BindingSetStore.java +++ b/reasoner/src/main/java/eu/knowledge/engine/reasoner/rulenode/BindingSetStore.java @@ -171,18 +171,18 @@ public TripleVarBindingSet get() { if (this.combiMatches != null) this.cache = this.combineWithCombiMatches(this.graphPattern, this.combiMatches, this.neighborBindingSet); else { - LOG.trace("Ignoring combi matches for binding set construction."); + LOG.trace("Ignoring combi matches for binding set construction in {}."); TripleVarBindingSet combinedBS = new TripleVarBindingSet(graphPattern); for (TripleVarBindingSet bs : this.neighborBindingSet.values().stream().map(x -> x.values()) .flatMap(x -> x.stream()).collect(Collectors.toSet())) { - combinedBS = combinedBS.merge(bs); + combinedBS.addAll(bs.getBindings()); } // NOTE: we merge the bindings with themselves here (when the bindings // 'leave' the store), but it may be better to do it when they enter the // store, or when they get translated/matched. - this.cache = combinedBS.merge(combinedBS); + this.cache = combinedBS;// .merge(combinedBS); } return this.cache; @@ -197,10 +197,18 @@ private String getName(BaseRule r) { if (r.getName().isEmpty()) { return r.toString(); } else { - return r.getName(); + return "" + r.getName() + ""; } } + /** + * Prints a Markdown table to the console. Each triple pattern in the relevant + * graph pattern of this rule is respresented by a row in the table, while each + * neighbor's bindingset represents a column within the table. + * + * Use the following website to visualize the markdown table output: + * https://markdownlivepreview.com/ + */ public void printDebuggingTable() { StringBuilder table = new StringBuilder(); @@ -236,14 +244,18 @@ public void printDebuggingTable() { // content rows for (TriplePattern tp : this.graphPattern) { // triple pattern - table.append(" | ").append(tp.toString()).append(" | "); + table.append(" | ").append("").append(tp.toString()).append("") + .append(" | "); // bindings for (BaseRule neigh : allNeighbors) { + // TODO: we lose bindings with the following combine, maybe just generate + // multiple spans? TripleVarBindingSet tvbs = combine(this.neighborBindingSet.get(neigh)); if (tvbs != null) { + for (TripleVarBinding tvb : tvbs.getBindings()) { Set nodes = tvb.getTripleNodes(tp); if (!nodes.isEmpty()) { @@ -261,18 +273,22 @@ else if (tn.nodeIdx == 2) object = tvb.get(tn); } -// tp.getSubject().isVariable() ? - - table.append(subject != null ? subject : formatNode(tp.getSubject())).append(" "); - table.append(predicate != null ? predicate : formatNode(tp.getPredicate())).append(" "); - table.append(object != null ? object : formatNode(tp.getObject())).append(" "); + table.append(""); + table.append( + subject != null ? formatNode(subject, true) : formatNode(tp.getSubject(), false)) + .append(" "); + table.append(predicate != null ? formatNode(predicate, true) + : formatNode(tp.getPredicate(), false)).append(" "); + table.append(object != null ? formatNode(object, true) : formatNode(tp.getObject(), false)) + .append(" "); + table.append(""); } else { table.append(""); } - table.append(" | "); } + } else { table.append("|"); } @@ -282,12 +298,18 @@ else if (tn.nodeIdx == 2) System.out.println(table.toString()); } - private String formatNode(Node n) { + private String formatNode(Node n, boolean includeFormatting) { + + String color; + if (n.isVariable()) + color = "red"; + else + color = "green"; - String before = ""; + String before = ""; String after = ""; - return n.isVariable() ? before + TriplePattern.trunc(n) + after : TriplePattern.trunc(n); + return includeFormatting | n.isVariable() ? before + TriplePattern.trunc(n) + after : TriplePattern.trunc(n); } private TripleVarBindingSet combine(Map someBindingSets) { diff --git a/reasoner/src/main/java/eu/knowledge/engine/reasoner/util/JenaRules.java b/reasoner/src/main/java/eu/knowledge/engine/reasoner/util/JenaRules.java index 243fc2ca8..7726f0593 100644 --- a/reasoner/src/main/java/eu/knowledge/engine/reasoner/util/JenaRules.java +++ b/reasoner/src/main/java/eu/knowledge/engine/reasoner/util/JenaRules.java @@ -175,7 +175,10 @@ public static Set convertJenaToKeRules(String someRules) { throw new IllegalArgumentException("Rule '" + r.toShortString() + "' should have a consequent."); } else { // create normal rule - keRules.add(new Rule(antecedent, consequent)); + if (r.getName() != null) + keRules.add(new Rule(r.getName(), antecedent, consequent)); + else + keRules.add(new Rule(antecedent, consequent)); } } diff --git a/smart-connector/src/main/resources/knowledge-engine-ontology.ttl b/smart-connector/src/main/resources/knowledge-engine-ontology.ttl index 15b4e8db1..c18f7edd1 100644 --- a/smart-connector/src/main/resources/knowledge-engine-ontology.ttl +++ b/smart-connector/src/main/resources/knowledge-engine-ontology.ttl @@ -4,9 +4,11 @@ @prefix rdf: . @prefix xml: . @prefix xsd: . +@prefix dcat: . @prefix rdfs: . @prefix dcterms: . -@base . +@prefix skos: . +@base . rdf:type owl:Ontology ; dcterms:abstract "The Knowledge Engine ontology describes concepts and relations that are relevant for the Knowledge Engine. The most important concepts are Knowledge Bases and Knowledge Interactions." ; @@ -16,13 +18,13 @@ "Wilco Wijbrandi" ; dcterms:description "The Knowledge Engine ontology describes concepts and relations that are relevant for the Knowledge Engine. The most important concepts are Knowledge Bases and Knowledge Interactions."@en ; dcterms:license "https://www.apache.org/licenses/LICENSE-2.0" ; - dcterms:modified "2022-06-14T00:00:00"^^xsd:dateTime ; - dcterms:title "Knowledge Engine Ontology"^^xsd:string . + dcterms:modified "2026-04-24T16:16:00"^^xsd:dateTime ; + dcterms:title "Knowledge Engine Ontology" . ################################################################# -# Annotation properties +# Annotation properties ################################################################# - + ### http://purl.org/dc/terms/abstract dcterms:abstract rdf:type owl:AnnotationProperty . @@ -52,9 +54,9 @@ dcterms:title rdf:type owl:AnnotationProperty . ################################################################# -# Object Properties +# Object Properties ################################################################# - + ### https://w3id.org/knowledge-engine/consumerImplementsKnowledgeInteraction :consumerImplementsKnowledgeInteraction rdf:type owl:ObjectProperty . @@ -114,9 +116,9 @@ dcterms:title rdf:type owl:AnnotationProperty . ################################################################# -# Data properties +# Data properties ################################################################# - + ### https://w3id.org/knowledge-engine/hasData :hasData rdf:type owl:DatatypeProperty ; rdfs:comment """This property bridges the gap between metadata and data. @@ -126,7 +128,8 @@ It is not meant to be used in actual descriptions of knowledge bases. Instead, i ### https://w3id.org/knowledge-engine/hasDescription :hasDescription rdf:type owl:DatatypeProperty ; - rdfs:subPropertyOf owl:topDataProperty . + rdfs:subPropertyOf owl:topDataProperty ; + skos:exactMatch dcterms:description . ### https://w3id.org/knowledge-engine/hasEndpoint @@ -135,7 +138,8 @@ It is not meant to be used in actual descriptions of knowledge bases. Instead, i ### https://w3id.org/knowledge-engine/hasName :hasName rdf:type owl:DatatypeProperty ; - rdfs:subPropertyOf owl:topDataProperty . + rdfs:subPropertyOf owl:topDataProperty ; + skos:exactMatch dcterms:title . ### https://w3id.org/knowledge-engine/hasPattern @@ -151,9 +155,9 @@ It is not meant to be used in actual descriptions of knowledge bases. Instead, i ################################################################# -# Classes +# Classes ################################################################# - + ### https://w3id.org/knowledge-engine/ActuationPurpose :ActuationPurpose rdf:type owl:Class ; rdfs:subClassOf :Purpose ; @@ -241,7 +245,8 @@ If a Knowledge Base can enact the actuation it satisfies the purpose. E.g., when ### https://w3id.org/knowledge-engine/KnowledgeBase :KnowledgeBase rdf:type owl:Class ; - rdfs:subClassOf [ rdf:type owl:Restriction ; + rdfs:subClassOf dcat:DataService, + [ rdf:type owl:Restriction ; owl:onProperty :hasKnowledgeInteraction ; owl:someValuesFrom :KnowledgeInteraction ] , @@ -416,9 +421,9 @@ Examples include: ################################################################# -# General axioms +# General axioms ################################################################# - + [ rdf:type owl:AllDisjointClasses ; owl:members ( :AnswerKnowledgeInteraction :AskKnowledgeInteraction @@ -436,4 +441,4 @@ Examples include: ] . -### Generated by the OWL API (version 4.5.9.2019-02-01T07:24:44Z) https://github.com/owlcs/owlapi +### Generated by the OWL API (version 4.5.29.2024-05-13T12:11:03Z) https://github.com/owlcs/owlapi