diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 000000000..a2336c3cc --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,106 @@ +name: Publish +on: + workflow_dispatch: + +env: + REGISTRY: ghcr.io + +jobs: + build-and-publish-image: + runs-on: ubuntu-24.04 + + # Permissions required for publishing Docker image. + # See https://docs.github.com/en/actions/use-cases-and-examples/publishing-packages/publishing-docker-images#publishing-images-to-github-packages + # And permissions required for creating releases. + # See https://github.com/softprops/action-gh-release + permissions: + contents: write + packages: write + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: maven + + - name: Setup Docker + uses: docker/setup-docker-action@v4 + with: + # `org.springframework.boot:spring-boot-maven-plugin:2.7.18:build-image` uses an older API version + # that is not supported by default. + daemon-config: | + { + "min-api-version": "1.24" + } + set-host: true + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Get releases + id: get_releases + run: | + ALL_RELEASES=$(gh api "repos/${GITHUB_REPOSITORY}/releases" --paginate --jq '.[].tag_name') + { + echo 'all_releases<> $GITHUB_OUTPUT + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Derive build versions from: + # - Maven version (e.g., 1.29-SNAPSHOT) + # - Existing preview releases (e.g., v1.29.0-preview) + # + # The build version is derived as `1.29.0-preview`. + # If a release already exists with a tag starting with `v1.29.0-preview`, + # the derived version is incremented with a numeric suffix: + # 1.29.0-preview.1, 1.29.0-preview.2, etc. + # + # This versioning follows Semantic Versioning 2.0.0. + # See https://semver.org/ + - name: Set version + id: set_version + run: | + VERSION="$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)" + BASE_VERSION=${VERSION%-SNAPSHOT} + BUILD_VERSION="${BASE_VERSION}.0-preview" + PREVIEWS_COUNT=$(grep -c -F "v${BUILD_VERSION}" <<< "${{ steps.get_releases.outputs.all_releases }}" || true) + if (( PREVIEWS_COUNT != 0 )); then + BUILD_VERSION="${BUILD_VERSION}.${PREVIEWS_COUNT}" + fi + mvn --batch-mode versions:set -DnewVersion="$BUILD_VERSION" -DgenerateBackupPoms=false + echo "build_version=$BUILD_VERSION" >> $GITHUB_OUTPUT + + - name: Build with Maven + run: | + LOGGING_CONFIG_PATH="org-tweetyproject-web/src/main/resources" + # Logging to stdout by default is more appropriate for a container deployment + mv "$LOGGING_CONFIG_PATH"/logback_stdout.xml "$LOGGING_CONFIG_PATH"/logback.xml + mvn --batch-mode --update-snapshots install -Dgpg.skip=true + mvn --batch-mode --update-snapshots spring-boot:build-image -pl org-tweetyproject-web -DskipTests -Dgpg.skip=true + + - name: Publish Fat Jar + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ steps.set_version.outputs.build_version }} + files: org-tweetyproject-web/target/web-*.jar + prerelease: true + + - name: Build, tag and push Docker image + run: | + IMAGE_NAME="${{ env.REGISTRY }}/${GITHUB_REPOSITORY@L}/tweetyproject-web-server" + IMAGE_REF_WITH_VERSION="$IMAGE_NAME:${{ steps.set_version.outputs.build_version }}" + docker tag web:${{ steps.set_version.outputs.build_version }} "$IMAGE_REF_WITH_VERSION" + docker push "$IMAGE_REF_WITH_VERSION" + IMAGE_REF_LATEST="$IMAGE_NAME:latest" + docker tag web:${{ steps.set_version.outputs.build_version }} "$IMAGE_REF_LATEST" + docker push "$IMAGE_REF_LATEST" diff --git a/org-tweetyproject-arg-explanations/src/test/java/org/tweetyproject/arg/explanations/reasoner/acceptance/DialecticalSequenceExplanationReasonerTest.java b/org-tweetyproject-arg-explanations/src/test/java/org/tweetyproject/arg/explanations/reasoner/acceptance/DialecticalSequenceExplanationReasonerTest.java new file mode 100644 index 000000000..18b123c1c --- /dev/null +++ b/org-tweetyproject-arg-explanations/src/test/java/org/tweetyproject/arg/explanations/reasoner/acceptance/DialecticalSequenceExplanationReasonerTest.java @@ -0,0 +1,59 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ + +package org.tweetyproject.arg.explanations.reasoner.acceptance; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.tweetyproject.arg.dung.syntax.Argument; +import org.tweetyproject.arg.dung.syntax.Attack; +import org.tweetyproject.arg.dung.syntax.DungTheory; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * @author Oleksandr Dzhychko + */ +class DialecticalSequenceExplanationReasonerTest { + + DialecticalSequenceExplanationReasoner reasoner = new DialecticalSequenceExplanationReasoner(); + + @Disabled("Reasoner fails to provide explanations.") + @Test + public void circleOfFourArguments() { + var argumentA = new Argument("a"); + var argumentB = new Argument("b"); + var argumentC = new Argument("c"); + var argumentD = new Argument("d"); + var attackAB = new Attack(argumentA, argumentB); + var attackBC = new Attack(argumentB, argumentC); + var attackCD = new Attack(argumentC, argumentD); + var attackDA = new Attack(argumentD, argumentA); + + var theory = new DungTheory(); + theory.add(argumentA, argumentB, argumentC, argumentD); + theory.add(attackAB, attackBC, attackCD, attackDA); + + // #getExplanations fails with `java.lang.IndexOutOfBoundsException: Index: 1, Size: 1` + var explanations = reasoner.getExplanations(theory, argumentA); + + fail("Missing assertion."); + } + +} \ No newline at end of file diff --git a/org-tweetyproject-causal/src/main/java/org/tweetyproject/causal/parser/CausalParser.java b/org-tweetyproject-causal/src/main/java/org/tweetyproject/causal/parser/CausalParser.java index 304e25c3e..7fff6ce5a 100644 --- a/org-tweetyproject-causal/src/main/java/org/tweetyproject/causal/parser/CausalParser.java +++ b/org-tweetyproject-causal/src/main/java/org/tweetyproject/causal/parser/CausalParser.java @@ -19,24 +19,83 @@ package org.tweetyproject.causal.parser; import org.tweetyproject.causal.syntax.CausalKnowledgeBase; +import org.tweetyproject.causal.syntax.StructuralCausalModel; import org.tweetyproject.commons.Parser; import org.tweetyproject.commons.ParserException; +import org.tweetyproject.logics.pl.parser.PlParserFactory; +import org.tweetyproject.logics.pl.syntax.PlBeliefSet; import org.tweetyproject.logics.pl.syntax.PlFormula; +import java.io.BufferedReader; import java.io.IOException; import java.io.Reader; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** + * Parser for {@link CausalKnowledgeBase} and observation as consumed by {@link org.tweetyproject.causal.reasoner.AbstractCausalReasoner}. + * A causal knowledge base can be parsed with {@link #parseBeliefBase(Reader)}. + * Observations can be parsed with {@link #parseListOfFormulae(String, String)}. * + * @author Lars Bengel + * @author Oleksandr Dzhychko */ public class CausalParser extends Parser { + static final Pattern ASSUMPTIONS_PATTERN = Pattern.compile("^\\s*\\{(.*)\\}\\s*$"); + static final String SYMBOL_COMMA = ","; + static Parser plParser = PlParserFactory.getParserForFormat(PlParserFactory.Format.TWEETY); + + /** + * Parses data from the reader into a {@link CausalKnowledgeBase}. + * Each line must contain either assumptions or are an equation. + * Assumptions and equations are defined as following: + *
equation ::= formula '<=>' formula + *
assumptions ::= '{' assumption (',' assumption)* '}' + *
assumption ::= formula + *
formula ::= a propositional formula as parsable by {@link org.tweetyproject.logics.pl.parser.PlParser#parseFormula(String)} + * + * @param reader a reader + * @return the parsed causal knowledge base + * @throws IOException if some IO issue occurred. + * @throws ParserException some parsing exceptions may be added here. + */ @Override public CausalKnowledgeBase parseBeliefBase(Reader reader) throws IOException, ParserException { - return null; + // Implementation similar to AbaParser.parseBeliefBase. + // But it simplified by not allowing empty lines or comments. + // If needed, it can be added later. + + List assumptions = new ArrayList<>(); + List equations = new ArrayList<>(); + BufferedReader br = new BufferedReader(reader); + while (true) { + String line = br.readLine(); + if (line == null) break; + Matcher matcher = ASSUMPTIONS_PATTERN.matcher(line); + if (matcher.matches()) { + String[] assumptionStrings = matcher.group(1).split(SYMBOL_COMMA); + for (String assumptionString : assumptionStrings) + if (!assumptionString.isBlank()) { + assumptions.add(parseFormula(assumptionString)); + } + } else { + equations.add(parseFormula(line)); + } + } + + StructuralCausalModel model; + try { + model = new StructuralCausalModel(equations); + } catch (IllegalArgumentException | StructuralCausalModel.CyclicDependencyException e) { + throw new ParserException(e); + } + return new CausalKnowledgeBase(model, assumptions); } @Override public PlFormula parseFormula(Reader reader) throws IOException, ParserException { - return null; + return plParser.parseFormula(reader); } } diff --git a/org-tweetyproject-causal/src/main/java/org/tweetyproject/causal/reasoner/ArgumentationBasedCausalReasoner.java b/org-tweetyproject-causal/src/main/java/org/tweetyproject/causal/reasoner/ArgumentationBasedCausalReasoner.java index 126c708a8..54d170e3c 100644 --- a/org-tweetyproject-causal/src/main/java/org/tweetyproject/causal/reasoner/ArgumentationBasedCausalReasoner.java +++ b/org-tweetyproject-causal/src/main/java/org/tweetyproject/causal/reasoner/ArgumentationBasedCausalReasoner.java @@ -28,7 +28,9 @@ import org.tweetyproject.commons.util.SetTools; import org.tweetyproject.logics.pl.syntax.*; +import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -44,12 +46,7 @@ public class ArgumentationBasedCausalReasoner extends AbstractArgumentationBasedCausalReasoner { @Override public DungTheory getInducedTheory(CausalKnowledgeBase cbase, Collection observations, Map interventions) { - StructuralCausalModel model = cbase.getCausalModel(); - for (Proposition atom : interventions.keySet()) { - model = model.intervene(atom, interventions.get(atom)); - } - PlBeliefSet base = new PlBeliefSet(model.getStructuralEquations()); - base.addAll(observations); + PlBeliefSet base = createBeliefSetWithObservationsAndInterventions(cbase, observations, interventions); Collection literals = new HashSet<>(); for (Proposition atom : base.getSignature()) { @@ -111,6 +108,19 @@ public DungTheory getInducedTheory(CausalKnowledgeBase cbase, Collection observations, + Map interventions) { + StructuralCausalModel model = cbase.getCausalModel(); + for (Proposition atom : interventions.keySet()) { + model = model.intervene(atom, interventions.get(atom)); + } + PlBeliefSet base = new PlBeliefSet(model.getStructuralEquations()); + base.addAll(observations); + return base; + } + /** * Determines whether the given causal statements holds under the causal knowledge base * @@ -132,4 +142,92 @@ public boolean query(CausalKnowledgeBase cbase, CausalStatement statement) { public boolean query(CausalKnowledgeBase cbase, InterventionalStatement statement) { return query(cbase, statement.getObservations(), statement.getInterventions(), statement.getConclusion()); } + + /** + * Computes, for each atom that appears in the knowledge base, the set of atoms that are + * significant for establishing that conclusion under the given observations and interventions. + *

+ * This method: + *

    + *
  • Induces an argumentation theory from the given knowledge base, observations, + * and interventions.
  • + *
  • Groups arguments by the (single) atom occurring in their conclusion (positive + * or negated).
  • + *
  • Collects, per atom, all arguments concluding that atom (or its negation) and + * all their ancestors in the attack graph.
  • + *
  • For each of these arguments, retrieves kernels for the argument’s conclusion + * under a belief set extended with the argument’s premises, and gathers all + * atoms that appear in those kernels.
  • + *
+ * + * @param cbase some causal knowledge base + * @param observations some logical formulae representing the observations of causal atoms + * @param interventions a set of interventions on causal atoms + * @param atomFilter atoms for which to get the significant atoms. + * If {@code null}, the filter is not applied. + * @return the argumentation framework induced from the causal knowledge base and the observations + */ + public Map> getSignificantAtoms( + CausalKnowledgeBase cbase, + Collection observations, + Map interventions, + Collection atomFilter) { + var theory = getInducedTheory(cbase, observations, interventions); + var perAtomArgumentsWithAtomInConclusion = getPerAtomArgumentsWithAtomInConclusion(theory, atomFilter); + var beliefSetWithoutAssumptions = createBeliefSetWithObservationsAndInterventions(cbase, observations, interventions); + + var perAtomSignificantAtoms = new HashMap>(); + + for (var entry : perAtomArgumentsWithAtomInConclusion.entrySet()) { + var atom = entry.getKey(); + var argumentsForAtom = entry.getValue(); + + var significantArguments = new HashSet<>(); + significantArguments.addAll(argumentsForAtom); + significantArguments.addAll(theory.getAncestors(argumentsForAtom)); + + var significantAtoms = new HashSet(); + for (var argument : significantArguments) { + var causalArgument = (CausalArgument) argument; + var beliefSetWithAssumptions = new PlBeliefSet(beliefSetWithoutAssumptions); + beliefSetWithAssumptions.addAll(causalArgument.getPremises()); + var kernels = reasoner.getKernels(beliefSetWithAssumptions, causalArgument.getConclusion()); + for (var kernel : kernels) { + for (var formula : kernel) { + significantAtoms.addAll(formula.getAtoms()); + } + } + } + perAtomSignificantAtoms.put(atom, significantAtoms); + } + + return perAtomSignificantAtoms; + } + + + /** + * Returns, for each atom, the set of arguments whose conclusion is the atom or its negation. + * + * @param theory the theory containing the arguments + * @param atomFilter atoms for which to get the significant atoms. + * If {@code null}, the filter is not applied. + * @return a map from atom to the set of matching arguments + */ + public Map> getPerAtomArgumentsWithAtomInConclusion(DungTheory theory, Collection atomFilter) { + var perAtomArguments = new HashMap>(); + + for (var argument: theory) { + var causalArgument = (CausalArgument) argument; + var signature = causalArgument.getConclusion().getAtoms(); + if (signature.size() != 1) { + throw new IllegalStateException("Encountered invalid argument with more than one atom in the its conclusion: " + causalArgument); + } + var atom = signature.stream().findFirst().get(); + if (atomFilter != null && !atomFilter.contains(atom)) continue; + + var arguments = perAtomArguments.computeIfAbsent(atom, (_atom) -> new ArrayList<>()); + arguments.add(causalArgument); + } + return perAtomArguments; + } } diff --git a/org-tweetyproject-causal/src/main/java/org/tweetyproject/causal/syntax/StructuralCausalModel.java b/org-tweetyproject-causal/src/main/java/org/tweetyproject/causal/syntax/StructuralCausalModel.java index 594e7a0be..a14233fa2 100644 --- a/org-tweetyproject-causal/src/main/java/org/tweetyproject/causal/syntax/StructuralCausalModel.java +++ b/org-tweetyproject-causal/src/main/java/org/tweetyproject/causal/syntax/StructuralCausalModel.java @@ -379,7 +379,7 @@ public void clear() { /** * Thrown to indicate that the structural equations of a causal model contain a cyclic dependency */ - public static class CyclicDependencyException extends Throwable { + public static class CyclicDependencyException extends Exception { /** * Constructs a CyclicDependencyException with the specified detail message * diff --git a/org-tweetyproject-causal/src/test/java/org/tweetyproject/causal/parser/CausalParserTest.java b/org-tweetyproject-causal/src/test/java/org/tweetyproject/causal/parser/CausalParserTest.java new file mode 100644 index 000000000..9b4740880 --- /dev/null +++ b/org-tweetyproject-causal/src/test/java/org/tweetyproject/causal/parser/CausalParserTest.java @@ -0,0 +1,134 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.causal.parser; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.tweetyproject.commons.ParserException; +import org.tweetyproject.logics.pl.syntax.Equivalence; +import org.tweetyproject.logics.pl.syntax.Negation; +import org.tweetyproject.logics.pl.syntax.Proposition; +import org.tweetyproject.logics.pl.syntax.Tautology; + +import java.io.IOException; +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; +import static org.tweetyproject.causal.syntax.StructuralCausalModel.*; + +/** + * @author Oleksandr Dzhychko + */ +public class CausalParserTest { + + CausalParser parser = new CausalParser(); + + @Test + public void parseBeliefBase() throws IOException { + var a = new Proposition("a"); + var b = new Proposition("b"); + var c = new Proposition("c"); + var d = new Proposition("d"); + var input = """ + a <=> b + c <=> d + { d, !b } + """; + + var knowledgeBase = parser.parseBeliefBase(input); + + assertEquals(Set.of(new Equivalence(a, b), new Equivalence(c, d)), knowledgeBase.getBeliefs()); + assertEquals(Set.of(new Negation(b), d), knowledgeBase.getAssumptions()); + } + + @Test + public void parseBeliefBaseWithMultipleAssumptionLines() throws IOException { + var a = new Proposition("a"); + var b = new Proposition("b"); + var c = new Proposition("c"); + var d = new Proposition("d"); + // Being able to break up assumptions to different lines + // allows to freely structure the knowledgeable. + var input = """ + { !b } + a <=> b + c <=> d + { d } + """; + + var knowledgeBase = parser.parseBeliefBase(input); + + assertEquals(Set.of(new Equivalence(a, b), new Equivalence(c, d)), knowledgeBase.getBeliefs()); + assertEquals(Set.of(new Negation(b), d), knowledgeBase.getAssumptions()); + } + + @Test + public void parseBeliefBaseWithEmptyAssumptions() throws IOException { + var a = new Proposition("a"); + var input = """ + a <=> + + {} + """; + + var knowledgeBase = parser.parseBeliefBase(input); + + assertEquals(Set.of(new Equivalence(a, new Tautology())), knowledgeBase.getBeliefs()); + } + + @Test + public void throwsParserExceptionForInvalidSyntax() { + var input = """ + (a + """; + + assertThrows(ParserException.class, () -> parser.parseBeliefBase(input)); + } + + @Test + public void throwsParserExceptionForFormulaThatIsNotAnEquivalence() { + var input = """ + a + """; + + assertThrows(ParserException.class, () -> parser.parseBeliefBase(input)); + } + + @Test + public void throwsParserExceptionForCyclicDependency() { + var input = """ + a <=> a + """; + + var exception = assertThrows(ParserException.class, () -> parser.parseBeliefBase(input)); + assertInstanceOf(CyclicDependencyException.class, exception.getCause()); + } + + + @Test + public void parseObservations() throws IOException { + var a = new Proposition("a"); + var b = new Proposition("b"); + var input = "a, !b"; + + var observations = parser.parseListOfFormulae(input, ","); + + assertEquals(List.of(a, new Negation(b)), observations); + } +} \ No newline at end of file diff --git a/org-tweetyproject-causal/src/test/java/org/tweetyproject/causal/reasoner/ArgumentationBasedCausalReasonerTest.java b/org-tweetyproject-causal/src/test/java/org/tweetyproject/causal/reasoner/ArgumentationBasedCausalReasonerTest.java index 8c31ae5bf..f0b974114 100644 --- a/org-tweetyproject-causal/src/test/java/org/tweetyproject/causal/reasoner/ArgumentationBasedCausalReasonerTest.java +++ b/org-tweetyproject-causal/src/test/java/org/tweetyproject/causal/reasoner/ArgumentationBasedCausalReasonerTest.java @@ -18,10 +18,64 @@ */ package org.tweetyproject.causal.reasoner; +import org.junit.jupiter.api.Test; +import org.tweetyproject.causal.syntax.CausalKnowledgeBase; +import org.tweetyproject.causal.syntax.StructuralCausalModel; +import org.tweetyproject.logics.pl.syntax.Equivalence; +import org.tweetyproject.logics.pl.syntax.Negation; +import org.tweetyproject.logics.pl.syntax.PlFormula; +import org.tweetyproject.logics.pl.syntax.Proposition; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; + public class ArgumentationBasedCausalReasonerTest extends AbstractCausalReasonerTestBase { @Override protected ArgumentationBasedCausalReasoner createReasoner() { return new ArgumentationBasedCausalReasoner(); } + + @Test + public void getSignificantAtoms() throws StructuralCausalModel.CyclicDependencyException { + Proposition corona = new Proposition("corona"); + Proposition influenza = new Proposition("influenza"); + Proposition atRisk = new Proposition("at-risk"); + Proposition covid = new Proposition("covid"); + Proposition flu = new Proposition("flu"); + Proposition shortOfBreath = new Proposition("short-of-breath"); + Proposition fever = new Proposition("fever"); + Proposition chills = new Proposition("chills"); + Collection modelEquations = List.of( + new Equivalence(covid, corona), + new Equivalence(flu, influenza), + new Equivalence(fever, covid.combineWithOr(flu)), + new Equivalence(chills, fever), + new Equivalence(shortOfBreath, covid.combineWithAnd(atRisk)) + ); + StructuralCausalModel model = new StructuralCausalModel(modelEquations); + List assumptions = List.of(atRisk, corona, new Negation(influenza)); + CausalKnowledgeBase knowledgeBase = new CausalKnowledgeBase(model, assumptions); + Collection observations = List.of(covid); + ArgumentationBasedCausalReasoner reasoner = createReasoner(); + + var perAtomInfluencingAtoms = reasoner.getSignificantAtoms(knowledgeBase, observations, Map.of(), null); + + Map> perConclusionExpectedInfluencingAtoms = Map.of( + atRisk, Set.of(atRisk), + corona, Set.of(corona, covid), + influenza, Set.of(influenza), + covid, Set.of(covid), + shortOfBreath, Set.of(atRisk, shortOfBreath, covid), + fever, Set.of(fever, flu, covid), + chills, Set.of(covid, fever, chills, flu), + flu, Set.of(influenza, flu) + ); + + assertEquals(perConclusionExpectedInfluencingAtoms, perAtomInfluencingAtoms); + } } \ No newline at end of file diff --git a/org-tweetyproject-commons/src/main/java/org/tweetyproject/commons/Parser.java b/org-tweetyproject-commons/src/main/java/org/tweetyproject/commons/Parser.java index 9cf16467c..edd2bfe47 100644 --- a/org-tweetyproject-commons/src/main/java/org/tweetyproject/commons/Parser.java +++ b/org-tweetyproject-commons/src/main/java/org/tweetyproject/commons/Parser.java @@ -151,8 +151,7 @@ public List parseListOfBeliefBases(String text) throws ParserException, IOExc * @throws ParserException some parsing exception */ public List parseListOfBeliefBases(String text, String delimiter) throws ParserException, IOException { - if (delimiter.matches(".*" + illegalDelimitors + ".*")) - throw new IllegalArgumentException("The given delimiter is similar to characters that are likely to appear in formulas. Try using a more unique delimiter."); + assertDelimiterIsLegal(delimiter); String[] kbs_string = text.split(delimiter); ArrayList kbs = new ArrayList(); for (String kb_string : kbs_string) { @@ -215,4 +214,30 @@ public static boolean isNumeric(String str) { return true; } + /** + * Parses the given text into a list of formulae of the given type. + * Formulae are separated by the given delimiter. + * + * @param text a string + * @param delimiter for separating formulae + * @return a list of formulae in the order in which they appear in the input + * string. + * @throws IOException if an IO error occurs + * @throws ParserException some parsing exception + */ + public List parseListOfFormulae(String text, String delimiter) throws IOException, ParserException { + assertDelimiterIsLegal(delimiter); + String[] formulaStrings = text.split(delimiter); + List formulae = new ArrayList<>(); + for (String formulaString : formulaStrings) { + if (!formulaString.isBlank()) + formulae.add(this.parseFormula(formulaString)); + } + return formulae; + } + + private void assertDelimiterIsLegal(String delimiter) { + if (delimiter.matches(".*" + illegalDelimitors + ".*")) + throw new IllegalArgumentException("The given delimiter is similar to characters that are likely to appear in formulas. Try using a more unique delimiter."); + } } diff --git a/org-tweetyproject-graphs/src/main/java/org/tweetyproject/graphs/Graph.java b/org-tweetyproject-graphs/src/main/java/org/tweetyproject/graphs/Graph.java index a707a56ee..56c3eb78a 100644 --- a/org-tweetyproject-graphs/src/main/java/org/tweetyproject/graphs/Graph.java +++ b/org-tweetyproject-graphs/src/main/java/org/tweetyproject/graphs/Graph.java @@ -1,27 +1,30 @@ -/* - * This file is part of "TweetyProject", a collection of Java libraries for - * logical aspects of artificial intelligence and knowledge representation. - * - * TweetyProject is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - * - * Copyright 2016 The TweetyProject Team - */ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2016 The TweetyProject Team + */ package org.tweetyproject.graphs; +import java.util.ArrayDeque; import java.util.Collection; +import java.util.HashSet; import java.util.Iterator; - -import org.tweetyproject.math.matrix.Matrix; +import java.util.List; + +import org.tweetyproject.math.matrix.Matrix; /** @@ -68,12 +71,12 @@ public interface Graph extends GeneralGraph{ * @return the number of nodes in this graph. */ public int getNumberOfNodes(); - - /** - * Returns the number of edges in this graph. - * @return the number of edges in this graph. - */ - public int getNumberOfEdges(); + + /** + * Returns the number of edges in this graph. + * @return the number of edges in this graph. + */ + public int getNumberOfEdges(); /** * Returns "true" iff the two nodes are connected by a directed edge @@ -130,7 +133,41 @@ public interface Graph extends GeneralGraph{ * @return the set of parents of the given node. */ public Collection getParents(Node node); - + + /** + * Returns the ancestors (nodes connected via an undirected or directed path + * where the given node is the descendant) of the given node. + * @param node some node (must be in the graph). + * @return the ancestors of the given node. + */ + public default Collection getAncestors(Node node) { + return getAncestors(List.of(node)); + } + + /** + * Returns the union of ancestors (nodes connected via an undirected or directed path + * where the given node is the descendant) of the given nodes. + * @param nodes some nodes (must be in the graph). + * @return union of ancestors of the given node. + */ + public default Collection getAncestors(Collection nodes) { + var ancestors = new HashSet(); + var visited = new HashSet(); + + var stack = new ArrayDeque(nodes); + while (!stack.isEmpty()) { + Node current = stack.pop(); + for (T parent : this.getParents(current)) { + if (!visited.contains(parent)) { + ancestors.add(parent); + stack.push(parent); + visited.add(parent); + } + } + } + return ancestors; + } + /** * Checks whether there is a (directed) path from node1 to node2. * @param node1 some node. diff --git a/org-tweetyproject-graphs/src/test/java/org/tweetyproject/graphs/GraphTest.java b/org-tweetyproject-graphs/src/test/java/org/tweetyproject/graphs/GraphTest.java new file mode 100644 index 000000000..01edde1de --- /dev/null +++ b/org-tweetyproject-graphs/src/test/java/org/tweetyproject/graphs/GraphTest.java @@ -0,0 +1,111 @@ +package org.tweetyproject.graphs; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.util.List; +import java.util.Set; + +import static org.junit.Assert.assertEquals; + +public class GraphTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void getAncestorsOfNode() { + var graph = new SimpleGraph<>(); + var nodeA = new SimpleNode("a"); + var nodeB = new SimpleNode("b"); + var nodeC = new SimpleNode("c"); + graph.addAll(List.of(nodeA, nodeB, nodeC)); + graph.addAllEdges(List.of( + new DirectedEdge<>(nodeA, nodeB), + new DirectedEdge<>(nodeB, nodeC)) + ); + + var ancestors = graph.getAncestors(nodeC); + + assertEquals(Set.of(nodeA, nodeB), ancestors); + } + + @Test + public void getAncestorsOfNodeWithCycle() { + var graph = new SimpleGraph<>(); + var nodeA = new SimpleNode("a"); + var nodeB = new SimpleNode("b"); + var nodeC = new SimpleNode("c"); + graph.addAll(List.of(nodeA, nodeB, nodeC)); + graph.addAllEdges(List.of( + new DirectedEdge<>(nodeA, nodeB), + new DirectedEdge<>(nodeB, nodeC), + new DirectedEdge<>(nodeC, nodeA)) + ); + + var ancestors = graph.getAncestors(nodeC); + + assertEquals(Set.of(nodeA, nodeB, nodeC), ancestors); + } + + @Test + public void getAncestorsOfNodeThrowWithNodeNotInGraph() { + var graph = new SimpleGraph<>(); + SimpleNode node = new SimpleNode("aNode"); + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("The node is not in this graph."); + graph.getAncestors(node); + } + + @Test + public void getAncestorsOfNodesInSeparateStronglyConnectedComponents() { + var graph = new SimpleGraph<>(); + var nodeA = new SimpleNode("a"); + var nodeB = new SimpleNode("b"); + var nodeC = new SimpleNode("c"); + graph.addAll(List.of(nodeA, nodeB, nodeC)); + graph.addAllEdges(List.of( + new DirectedEdge<>(nodeA, nodeB), + new DirectedEdge<>(nodeB, nodeC)) + ); + var nodeL = new SimpleNode("l"); + var nodeM = new SimpleNode("m"); + var nodeN = new SimpleNode("n"); + graph.addAll(List.of(nodeL, nodeM, nodeN)); + graph.addAllEdges(List.of( + new DirectedEdge<>(nodeL, nodeM), + new DirectedEdge<>(nodeM, nodeN)) + ); + + var ancestors = graph.getAncestors(List.of(nodeC, nodeN)); + + assertEquals(Set.of(nodeA, nodeB, nodeL, nodeM), ancestors); + } + + @Test + public void getAncestorsOfNodesInSameStronglyConnectedComponents() { + var graph = new SimpleGraph<>(); + var nodeA = new SimpleNode("a"); + var nodeB = new SimpleNode("b"); + var nodeC = new SimpleNode("c"); + var nodeD = new SimpleNode("d"); + var nodeE = new SimpleNode("e"); + var nodeF = new SimpleNode("f"); + graph.addAll(List.of(nodeA, nodeB, nodeC, nodeD, nodeE, nodeF)); + graph.addAllEdges(List.of( + new DirectedEdge<>(nodeA, nodeB), + new DirectedEdge<>(nodeB, nodeC), + new DirectedEdge<>(nodeB, nodeD), + new DirectedEdge<>(nodeB, nodeD), + new DirectedEdge<>(nodeD, nodeE), + new DirectedEdge<>(nodeE, nodeF) + )); + + var ancestors = graph.getAncestors(List.of(nodeB, nodeE)); + + assertEquals(3, ancestors.size()); + assertEquals(Set.of(nodeA, nodeB, nodeD), ancestors); + } +} \ No newline at end of file diff --git a/org-tweetyproject-web/pom.xml b/org-tweetyproject-web/pom.xml index 6a24c5e2f..640986d46 100644 --- a/org-tweetyproject-web/pom.xml +++ b/org-tweetyproject-web/pom.xml @@ -30,13 +30,51 @@ 2.7.18 pom import + + + + org.apache.tomcat.embed + tomcat-embed-core + + + org.apache.tomcat.embed + tomcat-embed-el + + + org.apache.tomcat.embed + tomcat-embed-websocket + + + org.apache.tomcat + tomcat-annotations-api + + - + + org.apache.tomcat.embed + tomcat-embed-core + 9.0.110 + + + org.apache.tomcat.embed + tomcat-embed-el + 9.0.110 + + + org.apache.tomcat.embed + tomcat-embed-websocket + 9.0.110 + + + org.apache.tomcat + tomcat-annotations-api + 9.0.110 + org.springframework.boot @@ -57,7 +95,12 @@ spring-boot-starter-web - + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot spring-boot-starter-test test @@ -101,6 +144,11 @@ 4.13.1 test + + org.tweetyproject + causal + 1.29-SNAPSHOT + org.tweetyproject.logics commons @@ -111,6 +159,11 @@ pl 1.30-SNAPSHOT + + org.tweetyproject.arg + explanations + 1.29-SNAPSHOT + org.tweetyproject.arg delp @@ -156,7 +209,7 @@ - 15 + 17 2.35 UTF-8 org.springframework.boot spring-boot-maven-plugin - 3.4.1 + 2.7.18 + + + + repackage + + + diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/RedirectControllerForClientSideRouting.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/RedirectControllerForClientSideRouting.java new file mode 100644 index 000000000..6f82560f8 --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/RedirectControllerForClientSideRouting.java @@ -0,0 +1,38 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * Configuration load index.html for routes that are handled by client side routing. + * + * @author Oleksandr Dzhychko + */ +@Controller +public class RedirectControllerForClientSideRouting { + + // Pattern from https://gedoplan.de/spring-boot-forward-fur-angular-spa/ + // NOTE This pattern is expect to break with Spring Boot 3.x + @RequestMapping(value = "/{path:[^\\.]*}") + public String forwardUnmatchedPaths() { + return "forward:/index.html"; + } +} \ No newline at end of file diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/RequestController.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/RequestController.java index bdebb812c..cb85e6502 100644 --- a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/RequestController.java +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/RequestController.java @@ -18,24 +18,34 @@ */ package org.tweetyproject.web.services; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import javafx.util.Pair; import org.json.JSONException; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.lang.Nullable; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; +import org.tweetyproject.arg.aba.parser.AbaParser; +import org.tweetyproject.arg.aba.reasoner.GeneralAbaReasoner; +import org.tweetyproject.arg.aba.semantics.AbaExtension; +import org.tweetyproject.arg.aba.syntax.AbaTheory; +import org.tweetyproject.arg.aba.syntax.Assumption; +import org.tweetyproject.arg.delp.parser.DelpParser; +import org.tweetyproject.arg.delp.reasoner.DelpReasoner; +import org.tweetyproject.arg.delp.semantics.ComparisonCriterion; +import org.tweetyproject.arg.delp.semantics.DelpAnswer; +import org.tweetyproject.arg.delp.semantics.EmptyCriterion; +import org.tweetyproject.arg.delp.semantics.GeneralizedSpecificity; +import org.tweetyproject.arg.delp.syntax.DefeasibleLogicProgram; +import org.tweetyproject.arg.dung.reasoner.AbstractExtensionReasoner; +import org.tweetyproject.arg.dung.semantics.Extension; +import org.tweetyproject.arg.dung.syntax.Argument; +import org.tweetyproject.arg.dung.syntax.Attack; import org.tweetyproject.arg.dung.syntax.DungTheory; +import org.tweetyproject.causal.parser.CausalParser; +import org.tweetyproject.causal.syntax.CausalKnowledgeBase; import org.tweetyproject.commons.BeliefSet; import org.tweetyproject.commons.Formula; import org.tweetyproject.commons.Parser; @@ -61,43 +71,33 @@ import org.tweetyproject.math.opt.solver.ApacheCommonsSimplex; import org.tweetyproject.math.opt.solver.GlpkSolver; import org.tweetyproject.math.opt.solver.Solver; -import org.tweetyproject.web.services.aba.AbaGetSemanticsResponse; -import org.tweetyproject.web.services.aba.AbaReasonerCalleeFactory; -import org.tweetyproject.web.services.aba.AbaReasonerPost; -import org.tweetyproject.web.services.aba.AbaReasonerResponse; -import org.tweetyproject.web.services.aba.GeneralAbaReasonerFactory; +import org.tweetyproject.web.services.aba.*; +import org.tweetyproject.web.services.causal.*; import org.tweetyproject.web.services.delp.DeLPCallee; import org.tweetyproject.web.services.delp.DeLPPost; import org.tweetyproject.web.services.delp.DeLPResponse; -import org.tweetyproject.web.services.dung.AbstractExtensionReasonerFactory; -import org.tweetyproject.web.services.dung.DungReasonerCalleeFactory; -import org.tweetyproject.web.services.dung.DungReasonerPost; -import org.tweetyproject.web.services.dung.DungReasonerResponse; -import org.tweetyproject.web.services.dung.DungServicesInfoResponse; +import org.tweetyproject.web.services.dung.*; import org.tweetyproject.web.services.dung.AbstractExtensionReasonerFactory.Semantics; import org.tweetyproject.web.services.dung.DungReasonerCalleeFactory.Command; import org.tweetyproject.web.services.incmes.InconsistencyGetMeasuresResponse; import org.tweetyproject.web.services.incmes.InconsistencyPost; import org.tweetyproject.web.services.incmes.InconsistencyValueResponse; -import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.bind.annotation.RequestBody; -import org.tweetyproject.arg.aba.parser.AbaParser; -import org.tweetyproject.arg.aba.reasoner.GeneralAbaReasoner; -import org.tweetyproject.arg.aba.semantics.AbaExtension; -import org.tweetyproject.arg.aba.syntax.AbaTheory; -import org.tweetyproject.arg.aba.syntax.Assumption; -import org.tweetyproject.arg.delp.parser.DelpParser; -import org.tweetyproject.arg.delp.reasoner.DelpReasoner; -import org.tweetyproject.arg.delp.semantics.ComparisonCriterion; -import org.tweetyproject.arg.delp.semantics.DelpAnswer; -import org.tweetyproject.arg.delp.semantics.EmptyCriterion; -import org.tweetyproject.arg.delp.semantics.GeneralizedSpecificity; -import org.tweetyproject.arg.delp.syntax.DefeasibleLogicProgram; -import org.tweetyproject.arg.dung.reasoner.AbstractExtensionReasoner; -import org.tweetyproject.arg.dung.semantics.Extension; +import org.tweetyproject.web.services.sequenceexplanation.*; +import org.tweetyproject.web.services.sequenceexplanation.SequenceExplanationPost.GetSequenceExplanationsCmd; +import org.tweetyproject.web.services.sequenceexplanation.SequenceExplanationPost.SequenceExplanationCmd; +import org.tweetyproject.web.services.sequenceexplanation.SequenceExplanationResponse.SequenceExplanationResult; +import org.tweetyproject.web.services.sequenceexplanation.SequenceExplanationResponse.GetSequenceExplanationsResult; -import javafx.util.Pair; +import javax.validation.Valid; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.*; import java.util.logging.Level; +import java.util.stream.Collectors; + +import static org.tweetyproject.web.services.causal.CausalReasonerResponse.Status.SUCCESS; +import static org.tweetyproject.web.services.causal.CausalReasonerResponse.Status.TIMEOUT; /** @@ -109,7 +109,22 @@ public class RequestController { private final int SERVICES_TIMEOUT_DUNG = 600; private final int SERVICES_TIMEOUT_DELP = 600; private final int SERVICES_TIMEOUT_INCMES = 300; + private final int SERVICES_TIMEOUT_CAUSAL = 300; + private final int SERVICES_TIMEOUT_SEQUENCE_EXPLANATION = 300; + + + private final ObjectMapper objectMapper; + private final CausalReasonerService causalReasonerService; + private final SequenceExplanationService sequenceExplanationService; + @Autowired + public RequestController(ObjectMapper objectMapper, + CausalReasonerService causalReasonerService, + SequenceExplanationService sequenceExplanationService) { + this.objectMapper = objectMapper; + this.causalReasonerService = causalReasonerService; + this.sequenceExplanationService = sequenceExplanationService; + } /** @@ -696,5 +711,236 @@ private AbaGetSemanticsResponse handleGetSemantics(AbaReasonerPost query) return response; } + /** + * Executes the causal reasoner as specified by the provided {@link CausalReasonerPost} + * + * @param request The request payload containing information for causal reasoning + * @return A Response object containing the result of the ABA reasoning operation. + */ + @PostMapping(value = "/causal", produces = "application/json") + @ResponseBody + @CrossOrigin + public CausalReasonerResponse handleRequest(@Valid @RequestBody CausalReasonerPost request) { + LoggerUtil.logger.info(String.format("Run causal reasoner command \"%s\" for user \"%s\" with timeout: %s %s", + request.getCmd(), + request.getEmail(), + request.getTimeout(), + request.getUnit_timeout())); + + TimeUnit timoutUnit = Utils.getTimoutUnit(request.getUnit_timeout()); + int timout = Utils.checkUserTimeout(request.getTimeout(), SERVICES_TIMEOUT_CAUSAL, timoutUnit); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + Pair resultAndExecutionTime; + try { + var future = executor.submit(() -> processCommand(request)); + resultAndExecutionTime = Utils.runServicesWithTimeout(future, timout, timoutUnit); + } catch (TimeoutException e) { + LoggerUtil.logger.info("Timeout while running causal reasoner."); + return new CausalReasonerResponse( + null, + request.getEmail(), + timout, + request.getUnit_timeout(), + TIMEOUT + ); + } catch (ExecutionException e) { + LoggerUtil.logger.warning(() -> "Error while running causal reasoner: " + e.getMessage()); + e.printStackTrace(); + throw new RuntimeException(e); + } catch (InterruptedException e) { + LoggerUtil.logger.warning(() -> "Interrupt while running causal reasoner: " + e.getMessage()); + e.printStackTrace(); + Thread.currentThread().interrupt(); + throw new RuntimeException("Thread was interrupted."); + } finally { + executor.shutdownNow(); + } + + long executionTime = resultAndExecutionTime.getValue(); + String result = resultAndExecutionTime.getKey(); + return new CausalReasonerResponse( + result, + request.getEmail(), + executionTime, + request.getUnit_timeout(), + SUCCESS + ); + } + + private String processCommand(CausalReasonerPost causalReasonerPost) { + return switch (causalReasonerPost.getCmd()) { + case GET_CONCLUSIONS -> processConclusionsCommand(causalReasonerPost); + case GET_SIGNIFICANT_ATOMS -> processSignificantAtomsCommand(causalReasonerPost); + case GET_ARGUMENTATION_FRAMEWORK -> processArgumentationFramework(causalReasonerPost); + case GET_SEQUENCE_EXPLANATIONS -> processSequenceExplanations(causalReasonerPost); + }; + } + + private String processConclusionsCommand(CausalReasonerPost causalReasonerPost) { + CausalKnowledgeBase causalKnowledgeBase = parseCausalKnowledgeBase(causalReasonerPost); + Collection observations = parseObservations(causalReasonerPost); + var conclusionFilter = parseConclusionFilter(causalReasonerPost); + + Collection conclusions = causalReasonerService.queryConclusions(causalKnowledgeBase, observations, conclusionFilter); + return conclusions.toString(); + } + + private String processSignificantAtomsCommand(CausalReasonerPost causalReasonerPost) { + CausalKnowledgeBase causalKnowledgeBase = parseCausalKnowledgeBase(causalReasonerPost); + Collection observations = parseObservations(causalReasonerPost); + var conclusionFilter = parseConclusionFilter(causalReasonerPost); + + var perAtomSignificantAtoms = causalReasonerService.queryPerAtomSignificantAtoms(causalKnowledgeBase, observations, conclusionFilter); + + Map> jsonData = new HashMap<>(); + for (Map.Entry> entry : perAtomSignificantAtoms.entrySet()) { + List list = new ArrayList<>(); + for (Proposition proposition : entry.getValue()) { + String string = proposition.toString(); + list.add(string); + } + jsonData.put(entry.getKey().toString(), list); + } + + try { + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonData); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + private String processSequenceExplanations(CausalReasonerPost causalReasonerPost) { + CausalKnowledgeBase causalKnowledgeBase = parseCausalKnowledgeBase(causalReasonerPost); + Collection observations = parseObservations(causalReasonerPost); + var conclusionFilter = parseConclusionFilter(causalReasonerPost); + + var result = causalReasonerService.querySequenceExplanations(causalKnowledgeBase, observations, conclusionFilter); + var reply = SequenceExplanationReply.from(result); + try { + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(reply); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + private String processArgumentationFramework(CausalReasonerPost causalReasonerPost) { + CausalKnowledgeBase causalKnowledgeBase = parseCausalKnowledgeBase(causalReasonerPost); + Collection observations = parseObservations(causalReasonerPost); + + var result = causalReasonerService.queryArgumentationFramework(causalKnowledgeBase, observations); + var reply = ArgumentationFrameworkReply.from(result); + try { + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(reply); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + private static Collection parseObservations(CausalReasonerPost causalReasonerPost) { + CausalParser causalParser = new CausalParser(); + Collection observations; + try { + observations = causalParser.parseListOfFormulae(causalReasonerPost.getObservations(), ","); + } catch (ParserException e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, null, e); + } catch (IOException e) { + throw new RuntimeException(e); + } + return observations; + } + + private static @Nullable Set parseConclusionFilter(CausalReasonerPost causalReasonerPost) { + return ConclusionsFilterSerialization.parse(causalReasonerPost.getConclusionsFilter()); + } + + private static CausalKnowledgeBase parseCausalKnowledgeBase(CausalReasonerPost causalReasonerPost) { + CausalParser causalParser = new CausalParser(); + CausalKnowledgeBase causalKnowledgeBase; + try { + causalKnowledgeBase = causalParser.parseBeliefBase(causalReasonerPost.getKb()); + } catch (ParserException e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, null, e); + } catch (IOException e) { + throw new RuntimeException(e); + } + return causalKnowledgeBase; + } + + @PostMapping(value = "/sequence-explanation", produces = "application/json") + @ResponseBody + @CrossOrigin + public SequenceExplanationResponse handleRequest(@Valid @RequestBody SequenceExplanationPost request) { + LoggerUtil.logger.info(String.format("Run sequence explanation command \"%s\" for user \"%s\" with timeout: %s %s", + request.getCmd().getClass().getSimpleName(), + request.getEmail(), + request.getTimeout(), + request.getUnit_timeout())); + + TimeUnit timeoutUnit = Utils.getTimoutUnit(request.getUnit_timeout()); + int timeout = Utils.checkUserTimeout(request.getTimeout(), SERVICES_TIMEOUT_SEQUENCE_EXPLANATION, timeoutUnit); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + Pair resultAndExecutionTime; + try { + var future = executor.submit(() -> processCommand(request.getCmd())); + resultAndExecutionTime = Utils.runServicesWithTimeout(future, timeout, timeoutUnit); + } catch (TimeoutException e) { + LoggerUtil.logger.info("Timeout while running sequence explanation."); + return new SequenceExplanationResponse( + null, + request.getEmail(), + timeout, + request.getUnit_timeout(), + SequenceExplanationResponse.Status.TIMEOUT + ); + } catch (ExecutionException e) { + LoggerUtil.logger.warning(() -> "Error while running sequence explanation reasoner: " + e.getMessage()); + e.printStackTrace(); + throw new RuntimeException(e); + } catch (InterruptedException e) { + LoggerUtil.logger.warning(() -> "Interrupt while running sequence explanation: " + e.getMessage()); + e.printStackTrace(); + Thread.currentThread().interrupt(); + throw new RuntimeException("Thread was interrupted."); + } finally { + executor.shutdownNow(); + } + + long executionTime = resultAndExecutionTime.getValue(); + var result = resultAndExecutionTime.getKey(); + return new SequenceExplanationResponse( + result, + request.getEmail(), + executionTime, + request.getUnit_timeout(), + SequenceExplanationResponse.Status.SUCCESS + ); + } + + private SequenceExplanationResult processCommand(SequenceExplanationCmd cmd) { + if (cmd instanceof GetSequenceExplanationsCmd) { + return processSequenceExplanationCmd((GetSequenceExplanationsCmd) cmd); + } else { + throw new IllegalStateException("Encountered invalid command:" + cmd.getClass().getSimpleName()); + } + } + + private GetSequenceExplanationsResult processSequenceExplanationCmd(GetSequenceExplanationsCmd cmd) { + var theory = new DungTheory(); + for (AttackDTO attackDTO: cmd.getAttacks()) { + var attacker = new Argument(attackDTO.getAttacker()); + var attacked = new Argument(attackDTO.getAttacked()); + theory.add(attacker); + theory.add(attacked); + var attack = new Attack(attacker, attacked); + theory.add(attack); + } + Set argumentFilter = ArgumentFilterSerialization.deserialize(cmd.getArgumentFilter()); + if (argumentFilter != null) { + theory.addAll(argumentFilter); + } + var sequenceExplanation = sequenceExplanationService.querySequenceExplanations(theory, argumentFilter); + return GetSequenceExplanationsResult.from(sequenceExplanation); + } } diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/ArgumentSerialization.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/ArgumentSerialization.java new file mode 100644 index 000000000..3038766d8 --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/ArgumentSerialization.java @@ -0,0 +1,47 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services.causal; + +import org.tweetyproject.arg.dung.syntax.Argument; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + + +/** + * @author Oleksandr Dzhychko + */ +public final class ArgumentSerialization { + public static String from(Argument argument) { + return argument.toString(); + } + + public static List fromCollection(Collection arguments) { + return arguments.stream() + .map(ArgumentSerialization::from) + .collect(Collectors.toUnmodifiableList()); + } + + public static List> fromCollectionOfCollections(List> collectionsOfArguments) { + return collectionsOfArguments.stream() + .map(ArgumentSerialization::fromCollection) + .collect(Collectors.toUnmodifiableList()); + } +} diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/ArgumentationFrameworkReply.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/ArgumentationFrameworkReply.java new file mode 100644 index 000000000..506744cd5 --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/ArgumentationFrameworkReply.java @@ -0,0 +1,56 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services.causal; + +import org.tweetyproject.arg.dung.syntax.Argument; +import org.tweetyproject.arg.dung.syntax.DungTheory; +import org.tweetyproject.web.services.sequenceexplanation.AttackDTO; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Oleksandr Dzhychko + */ +public final class ArgumentationFrameworkReply { + private final List arguments; + private final List attacks; + + public ArgumentationFrameworkReply(List arguments, List attacks) { + this.arguments = arguments; + this.attacks = attacks; + } + + public List getArguments() { + return arguments; + } + + public List getAttacks() { + return attacks; + } + + public static ArgumentationFrameworkReply from(DungTheory argumentationFramework) { + var arguments = argumentationFramework.stream() + .map(Argument::getName) + .collect(Collectors.toUnmodifiableList()); + var attacksConverted = AttackDTO.from(argumentationFramework.getAttacks()); + return new ArgumentationFrameworkReply(arguments, attacksConverted); + } +} + diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/CausalReasonerPost.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/CausalReasonerPost.java new file mode 100644 index 000000000..5a6d59ec4 --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/CausalReasonerPost.java @@ -0,0 +1,184 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services.causal; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.lang.Nullable; + +import javax.validation.constraints.NotNull; + +/** + * Request to execute a {@link CausalReasonerPost#cmd} with a {@link org.tweetyproject.causal.reasoner.AbstractCausalReasoner} + * + * @author Oleksandr Dzhychko + */ +public final class CausalReasonerPost { + + public CausalReasonerPost( + @JsonProperty(value = "cmd", required = true) + Cmd cmd, + @JsonProperty("email") + String email, + @JsonProperty(value = "kb", required = true) + String kb, + @JsonProperty(value = "observations", required = true) + String observations, + @JsonProperty(value = "conclusions_filter") + String conclusionsFilter, + @JsonProperty(value = "timeout", required = true) + int timeout, + @JsonProperty(value = "unit_timeout", required = true) + String unit_timeout + ) { + this.cmd = cmd; + this.email = email; + this.kb = kb; + this.observations = observations; + this.conclusionsFilter = conclusionsFilter; + this.timeout = timeout; + this.unit_timeout = unit_timeout; + } + + /** + * Describes which command should be executed by the causal reasoner + */ + public enum Cmd { + /** + * Instructs the reasoner to calculate the conclusions + * + * @see org.tweetyproject.causal.reasoner.AbstractCausalReasoner#getConclusions + */ + @JsonProperty("get_conclusions") GET_CONCLUSIONS, + + /** + * Instructs the reasoner to calculate per atom the atoms which are significant for its conclusion. + */ + @JsonProperty("get_significant_atoms") GET_SIGNIFICANT_ATOMS, + + /** + * Instructs the reasoner to calculate the corresponding argumentation framework. + */ + @JsonProperty("get_argumentation_framework") GET_ARGUMENTATION_FRAMEWORK, + + /** + * Instructs the reasoner to calculate the sequence of explanations for all consequences. + */ + @JsonProperty("get_sequence_explanations") GET_SEQUENCE_EXPLANATIONS; + } + + /** + * The command type for the reasoner request + */ + @NotNull + private Cmd cmd; + + /** + * The email associated with the request + */ + @Nullable + private String email; + + /** + * The knowledge base (KB) for the reasoner request + * The format of the knowledge base must be as described in {@link org.tweetyproject.causal.parser.CausalParser#parseBeliefBase(java.io.Reader)} + */ + @NotNull + private String kb; + + /** + * The observations for the reasoner request + * The format of the knowledge base must be as used by {@link org.tweetyproject.causal.parser.CausalParser#parseListOfFormulae} with "," (comma) as delimiter. + */ + @NotNull + private String observations; + + /** + * Atoms for which the conclusions should be queried and returned. + * The format of the knowledge base must be as described in {@link ConclusionsFilterSerialization#parse(String)} + */ + @Nullable + private String conclusionsFilter; + + /** + * The timeout in seconds for the reasoner request + */ + private int timeout; + + /** + * The unit timeout for the reasoner request + */ + @NotNull + private String unit_timeout; + + public Cmd getCmd() { + return this.cmd; + } + + public void setCmd(Cmd cmd) { + this.cmd = cmd; + } + + public String getEmail() { + return this.email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getKb() { + return this.kb; + } + + public void setKb(String kb) { + this.kb = kb; + } + + public String getObservations() { + return this.observations; + } + + public void setObservations(String observations) { + this.observations = observations; + } + + public String getConclusionsFilter() { + return this.conclusionsFilter; + } + + public void setConclusionsFilter(String conclusionsFilter) { + this.conclusionsFilter = conclusionsFilter; + } + + public int getTimeout() { + return this.timeout; + } + + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + public String getUnit_timeout() { + return this.unit_timeout; + } + + public void setUnit_timeout(String unit_timeout) { + this.unit_timeout = unit_timeout; + } +} \ No newline at end of file diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/CausalReasonerResponse.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/CausalReasonerResponse.java new file mode 100644 index 000000000..8f27e59fd --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/CausalReasonerResponse.java @@ -0,0 +1,86 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services.causal; + +import org.springframework.lang.NonNull; + +/** + * Response to {@link CausalReasonerPost} + * + * @author Oleksandr Dzhychko + */ +public final class CausalReasonerResponse { + + public enum Status { + SUCCESS, + TIMEOUT, + } + + /** + * Result of execution {@link CausalReasonerPost#getCmd()} if {@link CausalReasonerResponse#status} is {@link Status#SUCCESS}. + * Else {@code null}. + */ + private final String reply; + /** + * E-Mail (or other identifier) as provided by {@link CausalReasonerPost#getEmail()} + */ + private final String email; + /** + * Time it took execute the command + */ + private final double time; + /** + * The time unit of {@link CausalReasonerResponse#time} + */ + @NonNull + private final String unit_timeout; + /** + * Whether the execution executed successfully or timed out. + */ + private final Status status; + + public CausalReasonerResponse(String reply, String email, double time, @NonNull String unit_timeout, Status status) { + this.reply = reply; + this.email = email; + this.time = time; + this.status = status; + this.unit_timeout = unit_timeout; + } + + public String getReply() { + return reply; + } + + public String getEmail() { + return email; + } + + public double getTime() { + return time; + } + + @NonNull + public String getUnit_timeout() { + return unit_timeout; + } + + public Status getStatus() { + return status; + } +} diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/CausalReasonerService.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/CausalReasonerService.java new file mode 100644 index 000000000..43e2f3265 --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/CausalReasonerService.java @@ -0,0 +1,111 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services.causal; + +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Service; +import org.tweetyproject.arg.dung.syntax.Attack; +import org.tweetyproject.arg.dung.syntax.DungTheory; +import org.tweetyproject.arg.explanations.reasoner.acceptance.DialecticalSequenceExplanationReasoner; +import org.tweetyproject.arg.explanations.semantics.DialectialSequenceExplanation; +import org.tweetyproject.causal.reasoner.ArgumentationBasedCausalReasoner; +import org.tweetyproject.causal.syntax.CausalKnowledgeBase; +import org.tweetyproject.logics.pl.syntax.PlFormula; +import org.tweetyproject.logics.pl.syntax.Proposition; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author Oleksandr Dzhychko + */ +@Service +public final class CausalReasonerService { + private final ArgumentationBasedCausalReasoner causalReasoner = new ArgumentationBasedCausalReasoner(); + private final DialecticalSequenceExplanationReasoner explanationReasoner = new DialecticalSequenceExplanationReasoner(); + + public Collection queryConclusions(CausalKnowledgeBase causalKnowledgeBase, Collection observations, Set conclusionFilter) { + Collection conclusions = causalReasoner.getConclusions(causalKnowledgeBase, observations); + conclusions = filterConclusions(conclusions, conclusionFilter); + return conclusions; + } + + private static Collection filterConclusions(Collection conclusions, @Nullable Set conclusionFilter) { + if (conclusionFilter == null) { + return conclusions; + } + return conclusions.stream() + .filter(formula -> isConclusionInFilter(formula, conclusionFilter)) + .collect(Collectors.toUnmodifiableList()); + } + + public static boolean isConclusionInFilter(PlFormula conclusion, @NonNull Set conclusionFilter) { + return conclusionFilter.stream() + .anyMatch(proposition -> conclusion.getAtoms().contains(proposition)); + } + + + public Map> queryPerAtomSignificantAtoms(CausalKnowledgeBase causalKnowledgeBase, Collection observations, Set conclusionFilter) { + return causalReasoner.getSignificantAtoms(causalKnowledgeBase, observations, Map.of(), conclusionFilter); + } + + public SequenceExplanations querySequenceExplanations(CausalKnowledgeBase causalKnowledgeBase, Collection observations, Set conclusionFilter) { + var theory = causalReasoner.getInducedTheory(causalKnowledgeBase, observations, Map.of()); + var perAtomArgumentsWithAtomInConclusion = causalReasoner.getPerAtomArgumentsWithAtomInConclusion(theory, conclusionFilter); + + var perAtomPerSequenceExplanations = new LinkedHashMap>(); + for (var atomWithArgumentsAtomInConclusion : perAtomArgumentsWithAtomInConclusion.entrySet()) { + var atom = atomWithArgumentsAtomInConclusion.getKey(); + var allSequenceExplanations = new ArrayList(); + perAtomPerSequenceExplanations.put(atom, allSequenceExplanations); + for (var argument : atomWithArgumentsAtomInConclusion.getValue().stream().findFirst().stream().collect(Collectors.toUnmodifiableList())) { + var explanations = explanationReasoner.getExplanations(theory, argument); + var sequenceExplanations = explanations.stream() + .map(explanation -> (DialectialSequenceExplanation) explanation) + .collect(Collectors.toUnmodifiableList()); + allSequenceExplanations.addAll(sequenceExplanations); + } + } + return new SequenceExplanations(theory.getAttacks(), perAtomPerSequenceExplanations); + } + + public static final class SequenceExplanations { + private final Set attacks; + private final Map> perAtomSequenceExplanations; + + public SequenceExplanations(Set attacks, + Map> perAtomSequenceExplanations) { + this.attacks = attacks; + this.perAtomSequenceExplanations = perAtomSequenceExplanations; + } + + public Set getAttacks() { + return attacks; + } + + public Map> getPerAtomSequenceExplanations() { + return perAtomSequenceExplanations; + } + } + + public DungTheory queryArgumentationFramework(CausalKnowledgeBase causalKnowledgeBase, Collection observations) { + return causalReasoner.getInducedTheory(causalKnowledgeBase, observations, Map.of()); + } +} diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/ConclusionsFilterSerialization.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/ConclusionsFilterSerialization.java new file mode 100644 index 000000000..1584fa3d0 --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/ConclusionsFilterSerialization.java @@ -0,0 +1,75 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services.causal; + +import org.springframework.http.HttpStatus; +import org.springframework.lang.Nullable; +import org.springframework.web.server.ResponseStatusException; +import org.tweetyproject.causal.parser.CausalParser; +import org.tweetyproject.logics.pl.syntax.PlFormula; +import org.tweetyproject.logics.pl.syntax.Proposition; + +import java.io.IOException; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author Oleksandr Dzhychko + */ +public class ConclusionsFilterSerialization { + + private static final String ATOM_DELIMITER = ","; + private static final CausalParser CAUSAL_PARSER = new CausalParser(); + + /** + * Parse the filter string for conclusions. + * + * @param conclusionsFilterString {@link Proposition}s as parsable by {@link CausalParser#parseFormula(String)} seperated by {@link #ATOM_DELIMITER} + * @return Set of {@link Proposition}s or {@code null} if the input is {@code null} or empty. + */ + public static @Nullable Set parse(@Nullable String conclusionsFilterString) { + if (conclusionsFilterString == null) { + return null; + } + List formulae; + try { + formulae = CAUSAL_PARSER.parseListOfFormulae(conclusionsFilterString, ATOM_DELIMITER); + } catch (RuntimeException e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, null, e); + } catch (IOException e) { + throw new IllegalStateException(e); + } + + var propositions = formulae.stream() + .map(formula -> { + if (formula instanceof Proposition) { + return (Proposition) formula; + } + String msg = String.format("Formula `%s` is not a proposition,", formula); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, msg); + }) + .collect(Collectors.toUnmodifiableSet()); + + if (propositions.isEmpty()) { + return null; + } + return propositions; + }; +} diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/SequenceExplanationReply.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/SequenceExplanationReply.java new file mode 100644 index 000000000..7f869248a --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/SequenceExplanationReply.java @@ -0,0 +1,67 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services.causal; + +import org.tweetyproject.arg.explanations.semantics.DialectialSequenceExplanation; +import org.tweetyproject.logics.pl.syntax.Proposition; +import org.tweetyproject.web.services.sequenceexplanation.AttackDTO; +import org.tweetyproject.web.services.sequenceexplanation.DialectialSequenceExplanationDTO; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author Oleksandr Dzhychko + */ +public final class SequenceExplanationReply { + private final List attacks; + private final Map> perAtomSequenceExplanations; + + public SequenceExplanationReply(List attacks, Map> perAtomSequenceExplanations) { + this.attacks = attacks; + this.perAtomSequenceExplanations = perAtomSequenceExplanations; + } + + + public List getAttacks() { + return attacks; + } + + public Map> getPerAtomSequenceExplanations() { + return perAtomSequenceExplanations; + } + + public static SequenceExplanationReply from(CausalReasonerService.SequenceExplanations sequenceExplanations) { + var attacksConverted = AttackDTO.from(sequenceExplanations.getAttacks()); + var perAtomSequenceExplanationsConverted = from(sequenceExplanations.getPerAtomSequenceExplanations()); + return new SequenceExplanationReply(attacksConverted, perAtomSequenceExplanationsConverted); + } + + private static Map> from(Map> perAtomSequenceExplanations) { + return perAtomSequenceExplanations.entrySet().stream() + .collect(Collectors.toMap( + entry -> entry.getKey().toString(), + entry -> DialectialSequenceExplanationDTO.from(entry.getValue()), + (sequences1, sequences2) -> { throw new IllegalStateException("Encountered duplicate serialization of proposition."); }, + LinkedHashMap::new)); + } +} + diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/ArgumentFilterSerialization.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/ArgumentFilterSerialization.java new file mode 100644 index 000000000..90017bfd8 --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/ArgumentFilterSerialization.java @@ -0,0 +1,45 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services.sequenceexplanation; + +import org.springframework.lang.Nullable; +import org.tweetyproject.arg.dung.syntax.Argument; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author Oleksandr Dzhychko + */ +public class ArgumentFilterSerialization { + + /** + * Deserialize the filter for arguments. + * + * @param argumentFilter List of {@link Argument} names or {@code null} + * @return Set of {@link Argument}s or {@code null} if the input is {@code null} or empty. + */ + public static @Nullable Set deserialize(@Nullable List argumentFilter) { + if (argumentFilter == null || argumentFilter.isEmpty()) { + return null; + } + return argumentFilter.stream().map(Argument::new).collect(Collectors.toUnmodifiableSet()); + } +} diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/AttackDTO.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/AttackDTO.java new file mode 100644 index 000000000..a4114377c --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/AttackDTO.java @@ -0,0 +1,62 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services.sequenceexplanation; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.lang.NonNull; +import org.tweetyproject.arg.dung.syntax.Attack; + +import javax.validation.constraints.NotNull; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Oleksandr Dzhychko + */ +public final class AttackDTO { + private final @NonNull @NotNull String attacker; + private final @NonNull @NotNull String attacked; + + public AttackDTO( + @JsonProperty(value="attacker", required = true) @NonNull @NotNull String attacker, + @JsonProperty(value="attacked", required = true) @NonNull @NotNull String attacked) { + this.attacker = attacker; + this.attacked = attacked; + } + + public static List from(Collection attacks) { + return attacks.stream() + .map(AttackDTO::from) + .collect(Collectors.toUnmodifiableList()); + } + + public static AttackDTO from(Attack attack) { + return new AttackDTO(attack.getAttacker().toString(), attack.getAttacked().toString()); + } + + + public String getAttacker() { + return attacker; + } + + public String getAttacked() { + return attacked; + } +} diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/DialectialSequenceExplanationDTO.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/DialectialSequenceExplanationDTO.java new file mode 100644 index 000000000..4a07559c9 --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/DialectialSequenceExplanationDTO.java @@ -0,0 +1,65 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services.sequenceexplanation; + +import org.tweetyproject.arg.explanations.semantics.DialectialSequenceExplanation; +import org.tweetyproject.web.services.causal.ArgumentSerialization; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Oleksandr Dzhychko + */ +public final class DialectialSequenceExplanationDTO { + private final String argument; + private final List> supporters; + private final List> defeated; + + public DialectialSequenceExplanationDTO(String argument, List> supporters, List> defeated) { + this.argument = argument; + this.supporters = supporters; + this.defeated = defeated; + } + + public String getArgument() { + return argument; + } + + public List> getSupporters() { + return supporters; + } + + public List> getDefeated() { + return defeated; + } + + public static DialectialSequenceExplanationDTO from(DialectialSequenceExplanation explanation) { + return new DialectialSequenceExplanationDTO( + explanation.getArgument().toString(), + ArgumentSerialization.fromCollectionOfCollections(explanation.getSupporters()), + ArgumentSerialization.fromCollectionOfCollections(explanation.getDefeated()) + ); + } + + public static List from(List explanations) { + return explanations.stream().map(DialectialSequenceExplanationDTO::from) + .collect(Collectors.toUnmodifiableList()); + } +} diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/SequenceExplanationPost.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/SequenceExplanationPost.java new file mode 100644 index 000000000..4a058588a --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/SequenceExplanationPost.java @@ -0,0 +1,103 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services.sequenceexplanation; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.springframework.lang.Nullable; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * @author Oleksandr Dzhychko + */ +public class SequenceExplanationPost { + + public SequenceExplanationPost( + @JsonProperty("email") + String email, + @JsonProperty(value = "timeout", required = true) + int timeout, + @JsonProperty(value = "unit_timeout", required = true) + String unit_timeout, + @JsonProperty(value = "cmd", required = true) + SequenceExplanationCmd cmd + ) { + this.email = email; + this.timeout = timeout; + this.unit_timeout = unit_timeout; + this.cmd = cmd; + } + + private final @Nullable String email; + private final int timeout; + private final @NotNull String unit_timeout; + private final @Valid @NotNull SequenceExplanationCmd cmd; + + public String getEmail() { + return email; + } + + public int getTimeout() { + return timeout; + } + + public String getUnit_timeout() { + return unit_timeout; + } + + public SequenceExplanationCmd getCmd() { + return cmd; + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "type") + @JsonSubTypes({ + @JsonSubTypes.Type(value = GetSequenceExplanationsCmd.class, name = "get_sequence_explanations"), + }) + public interface SequenceExplanationCmd { + } + + public static class GetSequenceExplanationsCmd implements SequenceExplanationCmd { + /** The attack relations represented as a list of lists of integers.*/ + private final @Valid @NotNull List<@NotNull AttackDTO> attacks; + private final @Valid @Nullable List<@NotNull String> argumentFilter; + + public GetSequenceExplanationsCmd( + @JsonProperty(value="attacks", required = true) + List attacks, + @JsonProperty(value = "argument_filter") + List argumentFilter) { + this.attacks = attacks; + this.argumentFilter = argumentFilter; + } + + public List getAttacks() { + return attacks; + } + + @Nullable + public List getArgumentFilter() { + return argumentFilter; + } + } + +} diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/SequenceExplanationResponse.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/SequenceExplanationResponse.java new file mode 100644 index 000000000..c026b2200 --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/SequenceExplanationResponse.java @@ -0,0 +1,109 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services.sequenceexplanation; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.springframework.lang.NonNull; +import org.tweetyproject.web.services.sequenceexplanation.SequenceExplanationService.SequenceExplanations; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Response to {@link SequenceExplanationPost} + * + * @author Oleksandr Dzhychko + */ +public final class SequenceExplanationResponse { + + public enum Status { + SUCCESS, + TIMEOUT, + } + + private final SequenceExplanationResult reply; + private final String email; + private final double time; + private final String unit_timeout; + private final Status status; + + public SequenceExplanationResponse(SequenceExplanationResult reply, String email, double time, @NonNull String unit_timeout, Status status) { + this.reply = reply; + this.email = email; + this.time = time; + this.status = status; + this.unit_timeout = unit_timeout; + } + + public SequenceExplanationResult getReply() { + return reply; + } + + public String getEmail() { + return email; + } + + public double getTime() { + return time; + } + + @NonNull + public String getUnit_timeout() { + return unit_timeout; + } + + public Status getStatus() { + return status; + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "type") + @JsonSubTypes({ + @JsonSubTypes.Type(value = GetSequenceExplanationsResult.class, name = "get_sequence_explanations"), + }) + public interface SequenceExplanationResult { + } + + public static class GetSequenceExplanationsResult implements SequenceExplanationResult { + private final Map> perArgumentSequenceExplanations; + + public GetSequenceExplanationsResult(Map> perArgumentSequenceExplanations) { + this.perArgumentSequenceExplanations = perArgumentSequenceExplanations; + } + + public Map> getPerArgumentSequenceExplanations() { + return perArgumentSequenceExplanations; + } + + public static GetSequenceExplanationsResult from(SequenceExplanations sequenceExplanations) { + + var perArgumentSequenceExplanations = new LinkedHashMap>(); + for (var entry: sequenceExplanations.getPerArgumentSequenceExplanations().entrySet()) { + var argument = entry.getKey(); + var forArgumentSequenceExplanations = entry.getValue(); + var forArgumentSequenceExplanationDTOs = DialectialSequenceExplanationDTO.from(forArgumentSequenceExplanations); + perArgumentSequenceExplanations.put(argument.getName(), forArgumentSequenceExplanationDTOs); + } + + return new GetSequenceExplanationsResult(perArgumentSequenceExplanations); + } + } + +} diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/SequenceExplanationService.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/SequenceExplanationService.java new file mode 100644 index 000000000..0aa6f994f --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/SequenceExplanationService.java @@ -0,0 +1,65 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services.sequenceexplanation; + +import org.springframework.stereotype.Service; +import org.tweetyproject.arg.dung.syntax.Argument; +import org.tweetyproject.arg.dung.syntax.DungTheory; +import org.tweetyproject.arg.explanations.reasoner.acceptance.DialecticalSequenceExplanationReasoner; +import org.tweetyproject.arg.explanations.semantics.DialectialSequenceExplanation; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author Oleksandr Dzhychko + */ +@Service +public final class SequenceExplanationService { + private final DialecticalSequenceExplanationReasoner explanationReasoner = new DialecticalSequenceExplanationReasoner(); + + public SequenceExplanations querySequenceExplanations(DungTheory theory, Set argumentFilter) { + var perArgumentSequenceExplanations = new LinkedHashMap>(); + for (var argument: theory) { + if (argumentFilter != null && !argumentFilter.contains(argument)) { + continue; + } + var explanations = explanationReasoner.getExplanations(theory, argument); + var sequenceExplanations = explanations.stream() + .map(explanation -> (DialectialSequenceExplanation) explanation) + .collect(Collectors.toUnmodifiableList()); + var allSequenceExplanations = new ArrayList(); + perArgumentSequenceExplanations.put(argument, allSequenceExplanations); + allSequenceExplanations.addAll(sequenceExplanations); + } + return new SequenceExplanations(perArgumentSequenceExplanations); + } + + public static final class SequenceExplanations { + private final Map> perArgumentSequenceExplanations; + + public SequenceExplanations(Map> perAtomSequenceExplanations) { + this.perArgumentSequenceExplanations = perAtomSequenceExplanations; + } + + public Map> getPerArgumentSequenceExplanations() { + return perArgumentSequenceExplanations; + } + } +} diff --git a/org-tweetyproject-web/src/main/resources/logback_stdout.xml b/org-tweetyproject-web/src/main/resources/logback_stdout.xml new file mode 100644 index 000000000..cd1ee80a7 --- /dev/null +++ b/org-tweetyproject-web/src/main/resources/logback_stdout.xml @@ -0,0 +1,12 @@ + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + diff --git a/org-tweetyproject-web/src/test/java/org/tweetyproject/web/services/RequestControllerCausalTest.java b/org-tweetyproject-web/src/test/java/org/tweetyproject/web/services/RequestControllerCausalTest.java new file mode 100644 index 000000000..e26c2092a --- /dev/null +++ b/org-tweetyproject-web/src/test/java/org/tweetyproject/web/services/RequestControllerCausalTest.java @@ -0,0 +1,222 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + +/** + * @author Oleksandr Dzhychko + */ +@SpringBootTest +@AutoConfigureMockMvc +class RequestControllerCausalTest { + @Autowired + private MockMvc mvc; + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + public void causalReasonerWithInvalidKnowledgeBaseReturnsStatus400() throws Exception { + var post = post("/causal") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "email": "aId", + "cmd": "get_conclusions", + "kb": "a <=> (b\\nc <=> d\\n{ d, !b }", + "observations": "!a, !b", + "timeout": 10, + "unit_timeout": "s" + } + """); + + mvc.perform(post) + .andExpect(status().isBadRequest()); + } + + @Test + public void causalReasonerWithInvalidObservationsReturnsStatus400() throws Exception { + var post = post("/causal") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "email": "aId", + "cmd": "get_conclusions", + "kb": "a <=> b\\nc <=> d\\n{ d, !b }", + "observations": "!(a, !b", + "timeout": 10, + "unit_timeout": "s" + } + """); + + mvc.perform(post) + .andExpect(status().isBadRequest()); + } + + @Test + public void causalReasonerRepliesWithAllConclusions() throws Exception { + var post = post("/causal") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "email": "aId", + "cmd": "get_conclusions", + "kb": "a <=> b\\nc <=> d\\n{ d, !b }", + "observations": "!a, !b", + "timeout": 10, + "unit_timeout": "s" + } + """); + + mvc.perform(post) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { + "reply": "[!a, !b, c, d]", + "email": "aId", + "time": 0, + "unit_timeout": "s", + "status": "SUCCESS" + } + """, true)); + } + + @Test + public void causalReasonerRepliesWithFilteredConclusions() throws Exception { + var post = post("/causal") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "email": "aId", + "cmd": "get_conclusions", + "kb": "a <=> b\\nc <=> d\\n{ d, !b }", + "observations": "!a, !b", + "conclusionsFilter": "b, c, e", + "timeout": 10, + "unit_timeout": "s" + } + """); + + mvc.perform(post) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { + "reply": "[!b, c]", + "email": "aId", + "time": 0, + "unit_timeout": "s", + "status": "SUCCESS" + } + """, true)); + } + + @Test + public void causalReasonerCalculatesSignificantAtoms() throws Exception { + var post = post("/causal") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "email": "aId", + "cmd": "get_significant_atoms", + "kb": "a <=> b\\nc <=> d\\n{ d, !b }", + "observations": "!a, !b", + "conclusionsFilter": "a", + "timeout": 10, + "unit_timeout": "s" + } + """); + + mvc.perform(post) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { + "reply": "{\\n \\"a\\" : [ \\"a\\", \\"b\\" ]\\n}", + "email": "aId", + "time": 0, + "unit_timeout": "s", + "status": "SUCCESS" + } + """, true)); + } + + @Test + public void causalReasonerGetSequenceExplanations() throws Exception { + var post = post("/causal") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "email": "aId", + "cmd": "get_sequence_explanations", + "kb": "a <=> b\\n{ b, !b }", + "observations": "", + "conclusionsFilter": "a", + "timeout": 10, + "unit_timeout": "s" + } + """); + + + var expectedReplyJSON = """ + { + "attacks" : [ { + "attacker" : "([b] -> b)", + "attacked" : "([!b] -> !a)" + }, { + "attacker" : "([!b] -> !b)", + "attacked" : "([b] -> b)" + }, { + "attacker" : "([b] -> b)", + "attacked" : "([!b] -> !b)" + }, { + "attacker" : "([!b] -> !b)", + "attacked" : "([b] -> a)" + } ], + "perAtomSequenceExplanations" : { + "a" : [ { + "argument" : "([b] -> a)", + "supporters" : [ [ "([b] -> b)" ], [ "([b] -> a)" ] ], + "defeated" : [ [ "([!b] -> !b)" ], [ ] ] + } ] + } + }"""; + var expectedReplyJSONEscaped = objectMapper.writeValueAsString(expectedReplyJSON); + var expectedResponse = String.format(""" + { + "reply": %s, + "email": "aId", + "time": 0, + "unit_timeout": "s", + "status": "SUCCESS" + } + """, expectedReplyJSONEscaped); + mvc.perform(post) + .andExpect(status().isOk()) + .andExpect(content().json(expectedResponse, true)); + } +} \ No newline at end of file diff --git a/org-tweetyproject-web/src/test/java/org/tweetyproject/web/services/RequestControllerSequenceExplanationTest.java b/org-tweetyproject-web/src/test/java/org/tweetyproject/web/services/RequestControllerSequenceExplanationTest.java new file mode 100644 index 000000000..9b8c08630 --- /dev/null +++ b/org-tweetyproject-web/src/test/java/org/tweetyproject/web/services/RequestControllerSequenceExplanationTest.java @@ -0,0 +1,323 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.stream.Stream; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + +/** + * @author Oleksandr Dzhychko + */ +@SpringBootTest +@AutoConfigureMockMvc +class RequestControllerSequenceExplanationTest { + @Autowired + private MockMvc mvc; + + private static Stream badRequestsBodies() { + return Stream.of(Arguments.of("unit_timeout null", + // language=JSON + """ + { + "email": null, + "timeout": 10, + "unit_timeout": null, + "cmd": { + "type": "get_sequence_explanations", + "attacks": [] + } + } + """), Arguments.of("cmd null", + // language=JSON + """ + { + "email": null, + "timeout": 10, + "unit_timeout": "s", + "cmd": null + } + """), Arguments.of("attacks null", + // language=JSON + """ + { + "email": null, + "timeout": 10, + "unit_timeout": "s", + "cmd": { + "type": "get_sequence_explanations", + "attacks": null + } + } + """), Arguments.of("attack null", + // language=JSON + """ + { + "email": null, + "timeout": 10, + "unit_timeout": "s", + "cmd": { + "type": "get_sequence_explanations", + "attacks": [null] + } + } + """), Arguments.of("attacker null", + // language=JSON + """ + { + "email": null, + "timeout": 10, + "unit_timeout": "s", + "cmd": { + "type": "get_sequence_explanations", + "attacks": [{ + "attacker": null, + "attacked": "a" + }] + } + } + """), Arguments.of("attacked null", + // language=JSON + """ + { + "email": null, + "timeout": 10, + "unit_timeout": "s", + "cmd": { + "type": "get_sequence_explanations", + "attacks": [{ + "attacker": "a", + "attacked": null + }] + } + } + """), Arguments.of("argument null", + // language=JSON + """ + { + "email": null, + "timeout": 10, + "unit_timeout": "s", + "cmd": { + "type": "get_sequence_explanations", + "attacks": [], + "argument_filter": [null] + } + } + """)); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("badRequestsBodies") + public void sequenceExplanationBadRequest(String name, String requestBody) throws Exception { + var post = post("/sequence-explanation").contentType(MediaType.APPLICATION_JSON).content(requestBody); + + mvc.perform(post).andExpect(status().isBadRequest()); + } + + @Test + public void sequenceExplanationsForAllArguments() throws Exception { + var post = post("/sequence-explanation").contentType(MediaType.APPLICATION_JSON) + // language=JSON + .content(""" + { + "email": "aId", + "timeout": 10, + "unit_timeout": "s", + "cmd": { + "type": "get_sequence_explanations", + "attacks": [ + { + "attacker": "a", + "attacked": "b" + }, + { + "attacker": "b", + "attacked": "a" + } + ] + } + } + """); + + mvc.perform(post).andExpect(status().isOk()) + // language=JSON + .andExpect(content().json(""" + { + "reply": { + "type": "get_sequence_explanations", + "perArgumentSequenceExplanations": { + "a": [ + { + "argument": "a", + "supporters": [ + [ + "a" + ] + ], + "defeated": [ + [ + "b" + ] + ] + } + ], + "b": [ + { + "argument": "b", + "supporters": [ + [ + "b" + ] + ], + "defeated": [ + [ + "a" + ] + ] + } + ] + } + }, + "email": "aId", + "time": 0, + "unit_timeout": "s", + "status": "SUCCESS" + } + """, true)); + } + + @Test + public void sequenceExplanationsForSelectedArguments() throws Exception { + var post = post("/sequence-explanation").contentType(MediaType.APPLICATION_JSON) + // language=JSON + .content(""" + { + "email": "aId", + "timeout": 10, + "unit_timeout": "s", + "cmd": { + "type": "get_sequence_explanations", + "attacks": [ + { + "attacker": "a", + "attacked": "b" + }, + { + "attacker": "b", + "attacked": "a" + } + ], + "argument_filter": ["b"] + } + } + """); + + mvc.perform(post).andExpect(status().isOk()) + // language=JSON + .andExpect(content().json(""" + { + "reply": { + "type": "get_sequence_explanations", + "perArgumentSequenceExplanations": { + "b": [ + { + "argument": "b", + "supporters": [ + [ + "b" + ] + ], + "defeated": [ + [ + "a" + ] + ] + } + ] + } + }, + "email": "aId", + "time": 0, + "unit_timeout": "s", + "status": "SUCCESS" + } + """, true)); + } + + @Test + public void sequenceExplanationsForSelectedArgumentThatIsNotInAttackes() throws Exception { + var post = post("/sequence-explanation").contentType(MediaType.APPLICATION_JSON) + // language=JSON + .content(""" + { + "email": "aId", + "timeout": 10, + "unit_timeout": "s", + "cmd": { + "type": "get_sequence_explanations", + "attacks": [], + "argument_filter": ["a"] + } + } + """); + + mvc.perform(post).andExpect(status().isOk()) + // language=JSON + .andExpect(content().json(""" + { + "reply": { + "type": "get_sequence_explanations", + "perArgumentSequenceExplanations": { + "a": [ + { + "argument": "a", + "supporters": [ + [ + "a" + ] + ], + "defeated": [ + [] + ] + } + ] + } + }, + "email": "aId", + "time": 0, + "unit_timeout": "s", + "status": "SUCCESS" + } + """, true)); + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 1d5102da3..695403bdc 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,19 @@ >scm:git:https://github.com/TweetyProjectTeam/TweetyProject.git + + + github + GitHub Packages + ${github.repo.package.url} + + + github + GitHub Packages + ${github.repo.package.url} + + + mthimm @@ -39,8 +52,14 @@ - + + + central + Maven Central Repository + https://repo.maven.apache.org/maven2 + + tweety-mvn TweetyProject MVN Repository @@ -58,8 +77,9 @@ UTF-8 - 15 - 15 + 17 + 17 + https://maven.pkg.github.com/odzhychko/TweetyProject @@ -82,8 +102,8 @@ maven-compiler-plugin 2.3.2 - 15 - 15 + 17 + 17 @@ -127,11 +147,6 @@ org.apache.maven.plugins maven-jar-plugin 2.3.2 - - ${project.groupId}.${project.artifactId}-${project.version} - ./testBuild - - - org.sonatype.central - central-publishing-maven-plugin - 0.8.0 - true - - central - true - - + + + + + + + + + + org.apache.maven.plugins maven-gpg-plugin