From 0f7b766376251277e21fb2b3767644c32ea8bec7 Mon Sep 17 00:00:00 2001 From: Barry Nouwt Date: Tue, 28 Apr 2026 09:18:09 +0200 Subject: [PATCH 1/5] First version of dcat example (not working yet). Problem with the reasoner and it taking way too long to give any results. Not sure if I can fix these, or whether this example is just not viable. --- .../eu/knowledge/engine/admin/MetadataKB.java | 18 ++- examples/common/answering_kb/.python-version | 1 - examples/common/asking_kb/.python-version | 1 - examples/dcat/README.md | 3 + examples/dcat/docker-compose.yml | 43 +++++++ examples/dcat/meta-kb/Dockerfile | 16 +++ examples/dcat/meta-kb/asking_kb.py | 108 ++++++++++++++++++ examples/dcat/meta-kb/requirements.txt | 9 ++ .../resources/knowledge-engine-ontology.ttl | 39 ++++--- 9 files changed, 218 insertions(+), 20 deletions(-) delete mode 100644 examples/common/answering_kb/.python-version delete mode 100644 examples/common/asking_kb/.python-version create mode 100644 examples/dcat/README.md create mode 100644 examples/dcat/docker-compose.yml create mode 100644 examples/dcat/meta-kb/Dockerfile create mode 100644 examples/dcat/meta-kb/asking_kb.py create mode 100644 examples/dcat/meta-kb/requirements.txt 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..ec0754c69 --- /dev/null +++ b/examples/dcat/README.md @@ -0,0 +1,3 @@ +DCAT example +============ +This example demonstrates how the Knowledge Bases (KBs) within a Knowledge Network can be exposed as a [Dataset Catalog](https://www.w3.org/TR/vocab-dcat-3/) (DCAT). We reuse the docker compose file from the multiple runtimes example 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 catalog data. \ No newline at end of file diff --git a/examples/dcat/docker-compose.yml b/examples/dcat/docker-compose.yml new file mode 100644 index 000000000..0784de504 --- /dev/null +++ b/examples/dcat/docker-compose.yml @@ -0,0 +1,43 @@ +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: 2 + JAVA_TOOL_OPTIONS: "-Xmx6g -Dorg.slf4j.simpleLogger.log.eu.knowledge.engine.smartconnector.impl.ReasonerProcessor=trace" + kb4: + build: meta-kb + environment: + KE_URL: http://runtime-4:8280/rest + KB_ID: http://example.org/kb4 + PREFIXES: | + { + "dcat": "http://www.w3.org/ns/dcat#", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#" + } + GRAPH_PATTERN: | + ?ds rdf:type dcat:DataService . + ?ds dcat:title ?title . + ?ds dcat:description ?description . + DOMAIN_KNOWLEDGE: | + @prefix dcat: . + @prefix ke: . + @prefix rdfs: . + @prefix rdf: . + @prefix dcterms: . + @prefix skos: . + + #KE DCAT facts + -> (ke:KnowledgeBase rdfs:subClassOf dcat:DataService ) . + -> (ke:hasName skos:exactMatch dcterms:title ) . + -> (ke:hasDescription skos:exactMatch dcterms:description ) . + + #RDFS rules + (?i rdf:type ?t1) (?t1 rdfs:subClassOf ?t2) -> (?i rdf:type ?t2) . + (?i ?p1 ?v) (?p1 skos:exactMatch ?p2) -> (?i ?p2 ?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..85e8049a4 --- /dev/null +++ b/examples/dcat/meta-kb/asking_kb.py @@ -0,0 +1,108 @@ +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 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, [{}])["bindingSet"] + if len(result) == 0: + log.info(f"asking gave no results; will sleep for 2s...") + else: + log.info(f"got answer: {result}") + 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/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 From 9f775d1468c2fba9773adf93f306b28447e46b65 Mon Sep 17 00:00:00 2001 From: Barry Nouwt Date: Fri, 1 May 2026 19:55:13 +0200 Subject: [PATCH 2/5] Fixed slow bindingsetstore merge. Now the example works. Also: - Improved binding set store markdown visualization. - Fixed bug in example. - Added dcat unit test. --- examples/dcat/docker-compose.yml | 13 +- .../eu/knowledge/engine/reasoner/Rule.java | 5 + .../reasoner/rulenode/BindingSetStore.java | 50 +++++-- .../engine/reasoner/util/JenaRules.java | 5 +- .../smartconnector/api/TestDCATAskAnswer.java | 131 ++++++++++++++++++ 5 files changed, 183 insertions(+), 21 deletions(-) create mode 100644 smart-connector/src/test/java/eu/knowledge/engine/smartconnector/api/TestDCATAskAnswer.java diff --git a/examples/dcat/docker-compose.yml b/examples/dcat/docker-compose.yml index 0784de504..93dc73c71 100644 --- a/examples/dcat/docker-compose.yml +++ b/examples/dcat/docker-compose.yml @@ -8,8 +8,8 @@ services: KE_RUNTIME_PORT: 8081 KE_RUNTIME_EXPOSED_URL: http://runtime-4:8081 KD_URL: http://knowledge-directory:8282 - KE_REASONER_LEVEL: 2 - JAVA_TOOL_OPTIONS: "-Xmx6g -Dorg.slf4j.simpleLogger.log.eu.knowledge.engine.smartconnector.impl.ReasonerProcessor=trace" + KE_REASONER_LEVEL: 5 + JAVA_TOOL_OPTIONS: "-Dorg.slf4j.simpleLogger.log.eu.knowledge.engine.smartconnector.impl.ReasonerProcessor=info" kb4: build: meta-kb environment: @@ -18,12 +18,13 @@ services: PREFIXES: | { "dcat": "http://www.w3.org/ns/dcat#", - "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#" + "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 dcat:title ?title . - ?ds dcat:description ?description . + ?ds dcterms:title ?title . + ?ds dcterms:description ?description . DOMAIN_KNOWLEDGE: | @prefix dcat: . @prefix ke: . @@ -31,7 +32,7 @@ services: @prefix rdf: . @prefix dcterms: . @prefix skos: . - + #KE DCAT facts -> (ke:KnowledgeBase rdfs:subClassOf dcat:DataService ) . -> (ke:hasName skos:exactMatch dcterms:title ) . 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/test/java/eu/knowledge/engine/smartconnector/api/TestDCATAskAnswer.java b/smart-connector/src/test/java/eu/knowledge/engine/smartconnector/api/TestDCATAskAnswer.java new file mode 100644 index 000000000..67e4b30e1 --- /dev/null +++ b/smart-connector/src/test/java/eu/knowledge/engine/smartconnector/api/TestDCATAskAnswer.java @@ -0,0 +1,131 @@ +package eu.knowledge.engine.smartconnector.api; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ExecutionException; + +import org.apache.jena.sparql.graph.PrefixMappingMem; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import eu.knowledge.engine.reasoner.BaseRule; +import eu.knowledge.engine.reasoner.Rule; +import eu.knowledge.engine.reasoner.util.JenaRules; +import eu.knowledge.engine.smartconnector.util.KnowledgeBaseImpl; +import eu.knowledge.engine.smartconnector.util.KnowledgeNetwork; + +/** + * This test is similar to the dcat example, which gives out of memory + * exceptions due to enormous amounts of binding sets. With this test we hope to + * investigate and maybe even resolve some of the performance issues. + */ +public class TestDCATAskAnswer { + + private static final Logger LOG = LoggerFactory.getLogger(TestDCATAskAnswer.class); + + private KnowledgeBaseImpl dcatKb; + private AskKnowledgeInteraction askKI; + + private KnowledgeBaseImpl otherKb; + + private static KnowledgeNetwork kn; + + @Test + public void test() throws InterruptedException, ExecutionException { + + kn = new KnowledgeNetwork(); + + createDCATKb(); + createOtherKb(); + kn.addKB(this.dcatKb); + kn.addKB(this.otherKb); + + kn.sync(); + + AskResult result = dcatKb.ask(askKI, new BindingSet()).get(); + + LOG.info("Result: {}", result.getBindings()); + + assertFalse(result.getBindings().isEmpty()); + } + + public void createDCATKb() { + + this.dcatKb = new KnowledgeBaseImpl("dcat-kb"); + + var prefixes = new PrefixMappingMem(); + prefixes.setNsPrefix("dcat", "http://www.w3.org/ns/dcat#"); + prefixes.setNsPrefix("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"); + prefixes.setNsPrefix("dcterms", "http://purl.org/dc/terms/"); + + this.askKI = new AskKnowledgeInteraction(new CommunicativeAct(), new GraphPattern(prefixes, """ + ?ds rdf:type dcat:DataService . + ?ds dcterms:description ?description . + ?ds dcterms:title ?title . + """), "dcat-ask", false, true, false, MatchStrategy.ULTRA_LEVEL); + this.dcatKb.register(this.askKI); + + String jenaRules = """ + @prefix dcat: . + @prefix ke: . + @prefix rdfs: . + @prefix rdf: . + @prefix dcterms: . + @prefix skos: . + + // 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)] + """; + +// alternative less general (and probably faster) way of representing the same domain knowledge +// String jenaRules = """ +// @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)] +// """; + + var rules = JenaRules.convertJenaToKeRules(jenaRules); + + Set theRules = new HashSet<>(); + for (BaseRule r : rules) { + theRules.add((Rule) r); + } + + this.dcatKb.setDomainKnowledge(theRules); + + } + + public void createOtherKb() { + + this.otherKb = new KnowledgeBaseImpl("other-kb"); + + var postKI = new PostKnowledgeInteraction(new CommunicativeAct(), new GraphPattern("?s ?p ."), null); + + this.otherKb.register(postKI); + } + + @AfterAll + public static void cleanup() throws InterruptedException, ExecutionException { + LOG.info("Clean up: {}", TestAskAnswer5.class.getSimpleName()); + kn.stop().get(); + } + +} From 9dc1ff16b58da0eede2a9b366a7555d56484e24d Mon Sep 17 00:00:00 2001 From: Barry Nouwt Date: Tue, 28 Apr 2026 09:18:09 +0200 Subject: [PATCH 3/5] First version of dcat example (not working yet). Problem with the reasoner and it taking way too long to give any results. Not sure if I can fix these, or whether this example is just not viable. --- .../eu/knowledge/engine/admin/MetadataKB.java | 18 ++- examples/common/answering_kb/.python-version | 1 - examples/common/asking_kb/.python-version | 1 - examples/dcat/README.md | 3 + examples/dcat/docker-compose.yml | 43 +++++++ examples/dcat/meta-kb/Dockerfile | 16 +++ examples/dcat/meta-kb/asking_kb.py | 108 ++++++++++++++++++ examples/dcat/meta-kb/requirements.txt | 9 ++ .../resources/knowledge-engine-ontology.ttl | 39 ++++--- 9 files changed, 218 insertions(+), 20 deletions(-) delete mode 100644 examples/common/answering_kb/.python-version delete mode 100644 examples/common/asking_kb/.python-version create mode 100644 examples/dcat/README.md create mode 100644 examples/dcat/docker-compose.yml create mode 100644 examples/dcat/meta-kb/Dockerfile create mode 100644 examples/dcat/meta-kb/asking_kb.py create mode 100644 examples/dcat/meta-kb/requirements.txt 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..ec0754c69 --- /dev/null +++ b/examples/dcat/README.md @@ -0,0 +1,3 @@ +DCAT example +============ +This example demonstrates how the Knowledge Bases (KBs) within a Knowledge Network can be exposed as a [Dataset Catalog](https://www.w3.org/TR/vocab-dcat-3/) (DCAT). We reuse the docker compose file from the multiple runtimes example 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 catalog data. \ No newline at end of file diff --git a/examples/dcat/docker-compose.yml b/examples/dcat/docker-compose.yml new file mode 100644 index 000000000..0784de504 --- /dev/null +++ b/examples/dcat/docker-compose.yml @@ -0,0 +1,43 @@ +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: 2 + JAVA_TOOL_OPTIONS: "-Xmx6g -Dorg.slf4j.simpleLogger.log.eu.knowledge.engine.smartconnector.impl.ReasonerProcessor=trace" + kb4: + build: meta-kb + environment: + KE_URL: http://runtime-4:8280/rest + KB_ID: http://example.org/kb4 + PREFIXES: | + { + "dcat": "http://www.w3.org/ns/dcat#", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#" + } + GRAPH_PATTERN: | + ?ds rdf:type dcat:DataService . + ?ds dcat:title ?title . + ?ds dcat:description ?description . + DOMAIN_KNOWLEDGE: | + @prefix dcat: . + @prefix ke: . + @prefix rdfs: . + @prefix rdf: . + @prefix dcterms: . + @prefix skos: . + + #KE DCAT facts + -> (ke:KnowledgeBase rdfs:subClassOf dcat:DataService ) . + -> (ke:hasName skos:exactMatch dcterms:title ) . + -> (ke:hasDescription skos:exactMatch dcterms:description ) . + + #RDFS rules + (?i rdf:type ?t1) (?t1 rdfs:subClassOf ?t2) -> (?i rdf:type ?t2) . + (?i ?p1 ?v) (?p1 skos:exactMatch ?p2) -> (?i ?p2 ?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..85e8049a4 --- /dev/null +++ b/examples/dcat/meta-kb/asking_kb.py @@ -0,0 +1,108 @@ +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 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, [{}])["bindingSet"] + if len(result) == 0: + log.info(f"asking gave no results; will sleep for 2s...") + else: + log.info(f"got answer: {result}") + 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/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 From 360bbf8426edc2b1a53cbca838b47aaf18f8f4d6 Mon Sep 17 00:00:00 2001 From: Barry Nouwt Date: Fri, 1 May 2026 19:55:13 +0200 Subject: [PATCH 4/5] Fixed slow bindingsetstore merge. Now the example works. Also: - Improved binding set store markdown visualization. - Fixed bug in example. - Added dcat unit test. --- examples/dcat/docker-compose.yml | 13 +- .../eu/knowledge/engine/reasoner/Rule.java | 5 + .../reasoner/rulenode/BindingSetStore.java | 50 +++++-- .../engine/reasoner/util/JenaRules.java | 5 +- .../smartconnector/api/TestDCATAskAnswer.java | 131 ++++++++++++++++++ 5 files changed, 183 insertions(+), 21 deletions(-) create mode 100644 smart-connector/src/test/java/eu/knowledge/engine/smartconnector/api/TestDCATAskAnswer.java diff --git a/examples/dcat/docker-compose.yml b/examples/dcat/docker-compose.yml index 0784de504..93dc73c71 100644 --- a/examples/dcat/docker-compose.yml +++ b/examples/dcat/docker-compose.yml @@ -8,8 +8,8 @@ services: KE_RUNTIME_PORT: 8081 KE_RUNTIME_EXPOSED_URL: http://runtime-4:8081 KD_URL: http://knowledge-directory:8282 - KE_REASONER_LEVEL: 2 - JAVA_TOOL_OPTIONS: "-Xmx6g -Dorg.slf4j.simpleLogger.log.eu.knowledge.engine.smartconnector.impl.ReasonerProcessor=trace" + KE_REASONER_LEVEL: 5 + JAVA_TOOL_OPTIONS: "-Dorg.slf4j.simpleLogger.log.eu.knowledge.engine.smartconnector.impl.ReasonerProcessor=info" kb4: build: meta-kb environment: @@ -18,12 +18,13 @@ services: PREFIXES: | { "dcat": "http://www.w3.org/ns/dcat#", - "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#" + "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 dcat:title ?title . - ?ds dcat:description ?description . + ?ds dcterms:title ?title . + ?ds dcterms:description ?description . DOMAIN_KNOWLEDGE: | @prefix dcat: . @prefix ke: . @@ -31,7 +32,7 @@ services: @prefix rdf: . @prefix dcterms: . @prefix skos: . - + #KE DCAT facts -> (ke:KnowledgeBase rdfs:subClassOf dcat:DataService ) . -> (ke:hasName skos:exactMatch dcterms:title ) . 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/test/java/eu/knowledge/engine/smartconnector/api/TestDCATAskAnswer.java b/smart-connector/src/test/java/eu/knowledge/engine/smartconnector/api/TestDCATAskAnswer.java new file mode 100644 index 000000000..67e4b30e1 --- /dev/null +++ b/smart-connector/src/test/java/eu/knowledge/engine/smartconnector/api/TestDCATAskAnswer.java @@ -0,0 +1,131 @@ +package eu.knowledge.engine.smartconnector.api; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ExecutionException; + +import org.apache.jena.sparql.graph.PrefixMappingMem; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import eu.knowledge.engine.reasoner.BaseRule; +import eu.knowledge.engine.reasoner.Rule; +import eu.knowledge.engine.reasoner.util.JenaRules; +import eu.knowledge.engine.smartconnector.util.KnowledgeBaseImpl; +import eu.knowledge.engine.smartconnector.util.KnowledgeNetwork; + +/** + * This test is similar to the dcat example, which gives out of memory + * exceptions due to enormous amounts of binding sets. With this test we hope to + * investigate and maybe even resolve some of the performance issues. + */ +public class TestDCATAskAnswer { + + private static final Logger LOG = LoggerFactory.getLogger(TestDCATAskAnswer.class); + + private KnowledgeBaseImpl dcatKb; + private AskKnowledgeInteraction askKI; + + private KnowledgeBaseImpl otherKb; + + private static KnowledgeNetwork kn; + + @Test + public void test() throws InterruptedException, ExecutionException { + + kn = new KnowledgeNetwork(); + + createDCATKb(); + createOtherKb(); + kn.addKB(this.dcatKb); + kn.addKB(this.otherKb); + + kn.sync(); + + AskResult result = dcatKb.ask(askKI, new BindingSet()).get(); + + LOG.info("Result: {}", result.getBindings()); + + assertFalse(result.getBindings().isEmpty()); + } + + public void createDCATKb() { + + this.dcatKb = new KnowledgeBaseImpl("dcat-kb"); + + var prefixes = new PrefixMappingMem(); + prefixes.setNsPrefix("dcat", "http://www.w3.org/ns/dcat#"); + prefixes.setNsPrefix("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"); + prefixes.setNsPrefix("dcterms", "http://purl.org/dc/terms/"); + + this.askKI = new AskKnowledgeInteraction(new CommunicativeAct(), new GraphPattern(prefixes, """ + ?ds rdf:type dcat:DataService . + ?ds dcterms:description ?description . + ?ds dcterms:title ?title . + """), "dcat-ask", false, true, false, MatchStrategy.ULTRA_LEVEL); + this.dcatKb.register(this.askKI); + + String jenaRules = """ + @prefix dcat: . + @prefix ke: . + @prefix rdfs: . + @prefix rdf: . + @prefix dcterms: . + @prefix skos: . + + // 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)] + """; + +// alternative less general (and probably faster) way of representing the same domain knowledge +// String jenaRules = """ +// @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)] +// """; + + var rules = JenaRules.convertJenaToKeRules(jenaRules); + + Set theRules = new HashSet<>(); + for (BaseRule r : rules) { + theRules.add((Rule) r); + } + + this.dcatKb.setDomainKnowledge(theRules); + + } + + public void createOtherKb() { + + this.otherKb = new KnowledgeBaseImpl("other-kb"); + + var postKI = new PostKnowledgeInteraction(new CommunicativeAct(), new GraphPattern("?s ?p ."), null); + + this.otherKb.register(postKI); + } + + @AfterAll + public static void cleanup() throws InterruptedException, ExecutionException { + LOG.info("Clean up: {}", TestAskAnswer5.class.getSimpleName()); + kn.stop().get(); + } + +} From 51278afd99a03bfec787d79b885a0651de7403ea Mon Sep 17 00:00:00 2001 From: Barry Nouwt Date: Fri, 15 May 2026 15:24:13 +0200 Subject: [PATCH 5/5] Finished DCAT example. It is now working with more specific rules instead of generic RDFS rules. Also: - extended the README.md - Removed Unit test, as this was not really necessary --- examples/dcat/README.md | 54 +++++++- examples/dcat/docker-compose.yml | 18 +-- examples/dcat/meta-kb/asking_kb.py | 42 ++++-- .../smartconnector/api/TestDCATAskAnswer.java | 131 ------------------ 4 files changed, 92 insertions(+), 153 deletions(-) delete mode 100644 smart-connector/src/test/java/eu/knowledge/engine/smartconnector/api/TestDCATAskAnswer.java diff --git a/examples/dcat/README.md b/examples/dcat/README.md index ec0754c69..29983f075 100644 --- a/examples/dcat/README.md +++ b/examples/dcat/README.md @@ -1,3 +1,51 @@ -DCAT example -============ -This example demonstrates how the Knowledge Bases (KBs) within a Knowledge Network can be exposed as a [Dataset Catalog](https://www.w3.org/TR/vocab-dcat-3/) (DCAT). We reuse the docker compose file from the multiple runtimes example 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 catalog data. \ No newline at end of file +# 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 index 93dc73c71..65866af77 100644 --- a/examples/dcat/docker-compose.yml +++ b/examples/dcat/docker-compose.yml @@ -9,12 +9,12 @@ services: 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.smartconnector.impl.ReasonerProcessor=info" - kb4: + 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/kb4 + KB_ID: http://example.org/meta-kb PREFIXES: | { "dcat": "http://www.w3.org/ns/dcat#", @@ -33,12 +33,8 @@ services: @prefix dcterms: . @prefix skos: . - #KE DCAT facts - -> (ke:KnowledgeBase rdfs:subClassOf dcat:DataService ) . - -> (ke:hasName skos:exactMatch dcterms:title ) . - -> (ke:hasDescription skos:exactMatch dcterms:description ) . - - #RDFS rules - (?i rdf:type ?t1) (?t1 rdfs:subClassOf ?t2) -> (?i rdf:type ?t2) . - (?i ?p1 ?v) (?p1 skos:exactMatch ?p2) -> (?i ?p2 ?v) . + // 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/asking_kb.py b/examples/dcat/meta-kb/asking_kb.py index 85e8049a4..e664af9c5 100644 --- a/examples/dcat/meta-kb/asking_kb.py +++ b/examples/dcat/meta-kb/asking_kb.py @@ -64,6 +64,19 @@ def ask(kiId, bindingSet: dict ) -> dict: 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() @@ -91,14 +104,27 @@ def kb_1(): log.info(f"ASK KI ({kiId}) registered!") result = [] try: - while True: - log.info(f"asking...") - result = ask(kiId, [{}])["bindingSet"] - if len(result) == 0: - log.info(f"asking gave no results; will sleep for 2s...") - else: - log.info(f"got answer: {result}") - time.sleep(2) + 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() diff --git a/smart-connector/src/test/java/eu/knowledge/engine/smartconnector/api/TestDCATAskAnswer.java b/smart-connector/src/test/java/eu/knowledge/engine/smartconnector/api/TestDCATAskAnswer.java deleted file mode 100644 index 67e4b30e1..000000000 --- a/smart-connector/src/test/java/eu/knowledge/engine/smartconnector/api/TestDCATAskAnswer.java +++ /dev/null @@ -1,131 +0,0 @@ -package eu.knowledge.engine.smartconnector.api; - -import static org.junit.jupiter.api.Assertions.assertFalse; - -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.ExecutionException; - -import org.apache.jena.sparql.graph.PrefixMappingMem; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import eu.knowledge.engine.reasoner.BaseRule; -import eu.knowledge.engine.reasoner.Rule; -import eu.knowledge.engine.reasoner.util.JenaRules; -import eu.knowledge.engine.smartconnector.util.KnowledgeBaseImpl; -import eu.knowledge.engine.smartconnector.util.KnowledgeNetwork; - -/** - * This test is similar to the dcat example, which gives out of memory - * exceptions due to enormous amounts of binding sets. With this test we hope to - * investigate and maybe even resolve some of the performance issues. - */ -public class TestDCATAskAnswer { - - private static final Logger LOG = LoggerFactory.getLogger(TestDCATAskAnswer.class); - - private KnowledgeBaseImpl dcatKb; - private AskKnowledgeInteraction askKI; - - private KnowledgeBaseImpl otherKb; - - private static KnowledgeNetwork kn; - - @Test - public void test() throws InterruptedException, ExecutionException { - - kn = new KnowledgeNetwork(); - - createDCATKb(); - createOtherKb(); - kn.addKB(this.dcatKb); - kn.addKB(this.otherKb); - - kn.sync(); - - AskResult result = dcatKb.ask(askKI, new BindingSet()).get(); - - LOG.info("Result: {}", result.getBindings()); - - assertFalse(result.getBindings().isEmpty()); - } - - public void createDCATKb() { - - this.dcatKb = new KnowledgeBaseImpl("dcat-kb"); - - var prefixes = new PrefixMappingMem(); - prefixes.setNsPrefix("dcat", "http://www.w3.org/ns/dcat#"); - prefixes.setNsPrefix("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"); - prefixes.setNsPrefix("dcterms", "http://purl.org/dc/terms/"); - - this.askKI = new AskKnowledgeInteraction(new CommunicativeAct(), new GraphPattern(prefixes, """ - ?ds rdf:type dcat:DataService . - ?ds dcterms:description ?description . - ?ds dcterms:title ?title . - """), "dcat-ask", false, true, false, MatchStrategy.ULTRA_LEVEL); - this.dcatKb.register(this.askKI); - - String jenaRules = """ - @prefix dcat: . - @prefix ke: . - @prefix rdfs: . - @prefix rdf: . - @prefix dcterms: . - @prefix skos: . - - // 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)] - """; - -// alternative less general (and probably faster) way of representing the same domain knowledge -// String jenaRules = """ -// @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)] -// """; - - var rules = JenaRules.convertJenaToKeRules(jenaRules); - - Set theRules = new HashSet<>(); - for (BaseRule r : rules) { - theRules.add((Rule) r); - } - - this.dcatKb.setDomainKnowledge(theRules); - - } - - public void createOtherKb() { - - this.otherKb = new KnowledgeBaseImpl("other-kb"); - - var postKI = new PostKnowledgeInteraction(new CommunicativeAct(), new GraphPattern("?s ?p ."), null); - - this.otherKb.register(postKI); - } - - @AfterAll - public static void cleanup() throws InterruptedException, ExecutionException { - LOG.info("Clean up: {}", TestAskAnswer5.class.getSimpleName()); - kn.stop().get(); - } - -}