From c732664bb3c6baf9601688c09e9b6fd119312139 Mon Sep 17 00:00:00 2001 From: Oleksandr Dzhychko Date: Mon, 31 Mar 2025 11:46:36 +0200 Subject: [PATCH 01/22] feat(causal): add parser and web endpoint for causal reasoner --- .../causal/parser/CausalParser.java | 63 ++++++- .../causal/syntax/StructuralCausalModel.java | 2 +- .../causal/parser/CausalParserTest.java | 134 ++++++++++++++ .../org/tweetyproject/commons/Parser.java | 29 ++- .../web/services/RequestController.java | 112 ++++++++++-- .../services/causal/CausalReasonerPost.java | 171 ++++++++++++++++++ .../causal/CausalReasonerResponse.java | 86 +++++++++ .../web/services/RequestControllerTest.java | 90 +++++++++ 8 files changed, 671 insertions(+), 16 deletions(-) create mode 100644 org-tweetyproject-causal/src/test/java/org/tweetyproject/causal/parser/CausalParserTest.java create mode 100644 org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/CausalReasonerPost.java create mode 100644 org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/CausalReasonerResponse.java create mode 100644 org-tweetyproject-web/src/test/java/org/tweetyproject/web/services/RequestControllerTest.java 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..11f7f6f0c 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 this#parseBeliefBase(Reader)}. + * Observations can be parsed with {@link this#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/syntax/StructuralCausalModel.java b/org-tweetyproject-causal/src/main/java/org/tweetyproject/causal/syntax/StructuralCausalModel.java index a5d74a8e0..a3bb1630b 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 @@ -375,7 +375,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-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-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..0b7e5d742 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 @@ -25,17 +25,17 @@ 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 java.util.concurrent.*; import org.json.JSONException; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; import org.tweetyproject.arg.dung.syntax.DungTheory; +import org.tweetyproject.causal.parser.CausalParser; +import org.tweetyproject.causal.reasoner.AbstractCausalReasoner; +import org.tweetyproject.causal.reasoner.ArgumentationBasedCausalReasoner; +import org.tweetyproject.causal.syntax.CausalKnowledgeBase; import org.tweetyproject.commons.BeliefSet; import org.tweetyproject.commons.Formula; import org.tweetyproject.commons.Parser; @@ -66,6 +66,8 @@ 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.causal.CausalReasonerPost; +import org.tweetyproject.web.services.causal.CausalReasonerResponse; import org.tweetyproject.web.services.delp.DeLPCallee; import org.tweetyproject.web.services.delp.DeLPPost; import org.tweetyproject.web.services.delp.DeLPResponse; @@ -79,8 +81,6 @@ 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; @@ -97,8 +97,12 @@ import org.tweetyproject.arg.dung.semantics.Extension; import javafx.util.Pair; + import java.util.logging.Level; +import static org.tweetyproject.web.services.causal.CausalReasonerResponse.Status.SUCCESS; +import static org.tweetyproject.web.services.causal.CausalReasonerResponse.Status.TIMEOUT; + /** * andles HTTP POST requests at the provided endpoints @@ -109,7 +113,7 @@ 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; /** @@ -696,5 +700,91 @@ 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(@RequestBody CausalReasonerPost request) throws RuntimeException { + LoggerUtil.logger.info(String.format("Run causal reasoner command \"%s\" with timeout: %s %s", request.getCmd(),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 static String processCommand(CausalReasonerPost causalReasonerPost) { + switch (causalReasonerPost.getCmd()) { + case GET_CONCLUSIONS: + return processConclusionsCommand(causalReasonerPost); + default: + // `cmd` should never be null, because it is annotated with `@NonNull`. + throw new IllegalStateException("Command should be set."); + } + } + + private static String processConclusionsCommand(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); + } + + 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); + } + + AbstractCausalReasoner reasoner = new ArgumentationBasedCausalReasoner(); + Collection conclusions = reasoner.getConclusions(causalKnowledgeBase, observations); + return conclusions.toString(); + } } 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..b7dc62acc --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/CausalReasonerPost.java @@ -0,0 +1,171 @@ +/* + * 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.NonNull; + +import java.util.Objects; + +/** + * Request to execute a {@link CausalReasonerPost#cmd} with a {@link org.tweetyproject.causal.reasoner.AbstractCausalReasoner} + * + * @author Oleksandr Dzhychko + */ +public class CausalReasonerPost { + + public CausalReasonerPost( + @JsonProperty(value = "cmd", required = true) + @NonNull Cmd cmd, + @JsonProperty("email") + String email, + @JsonProperty(value = "kb", required = true) + @NonNull String kb, + @JsonProperty(value = "observations", required = true) + @NonNull String observations, + @JsonProperty(value = "timeout", required = true) + int timeout, @NonNull + @JsonProperty(value = "unit_timeout", required = true) + String unit_timeout + ) { + this.cmd = cmd; + this.email = email; + this.kb = kb; + this.observations = observations; + 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 + } + + /** + * The command type for the reasoner request + */ + @NonNull + private Cmd cmd; + + /** + * The email associated with the request + */ + private String email; + + /** + * The knowledge base (KB) for the reasoner request + * The format of the knowledge base must be as described be {@link org.tweetyproject.causal.parser.CausalParser}. + */ + @NonNull + private String kb; + + /** + * The observations for the reasoner request + * The format of the knowledge base must be as described be {@link org.tweetyproject.causal.parser.CausalParser}. + */ + @NonNull + private String observations; + + /** + * The timeout in seconds for the reasoner request + */ + private int timeout; + + /** + * The unit timeout for the reasoner request + */ + @NonNull + 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 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; + } + + @Override + public boolean equals(Object o) { + if (o == this) return true; + if (!(o instanceof CausalReasonerPost)) { + return false; + } + CausalReasonerPost CausalReasonerPost = (CausalReasonerPost) o; + return Objects.equals(cmd, CausalReasonerPost.cmd) && Objects.equals(email, CausalReasonerPost.email) && Objects.equals(kb, CausalReasonerPost.kb) && Objects.equals(observations, CausalReasonerPost.observations) && timeout == CausalReasonerPost.timeout && Objects.equals(unit_timeout, CausalReasonerPost.unit_timeout); + } + + @Override + public int hashCode() { + return Objects.hash(cmd, email, kb, observations, timeout, unit_timeout); + } + + @Override + public String toString() { + return "{" + " cmd='" + getCmd() + "'" + ", email='" + getEmail() + "'" + ", kb='" + getKb() + "'" + ", query_assumption='" + getObservations() + "'" + ", timeout='" + getTimeout() + "'" + ", unit_timeout='" + getUnit_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..28a795292 --- /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 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/test/java/org/tweetyproject/web/services/RequestControllerTest.java b/org-tweetyproject-web/src/test/java/org/tweetyproject/web/services/RequestControllerTest.java new file mode 100644 index 000000000..ff06261c9 --- /dev/null +++ b/org-tweetyproject-web/src/test/java/org/tweetyproject/web/services/RequestControllerTest.java @@ -0,0 +1,90 @@ +package org.tweetyproject.web.services; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit.jupiter.SpringExtension; +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 + */ +@ExtendWith(SpringExtension.class) +@WebMvcTest(RequestController.class) +class RequestControllerTest { + @Autowired + private MockMvc mvc; + + @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 causalReasonerIsCalledSuccessfully() 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" + } + """)); + } +} \ No newline at end of file From 19cf4fb8ad7ee1575d68e4a43b8abee7c7185259 Mon Sep 17 00:00:00 2001 From: Oleksandr Dzhychko Date: Thu, 14 Aug 2025 05:42:19 +0200 Subject: [PATCH 02/22] feat: add command to get significant atoms for argumentation-based causal reasoning # Conflicts: # org-tweetyproject-causal/src/test/java/org/tweetyproject/causal/reasoner/ArgumentationBasedCausalReasonerTest.java --- .../ArgumentationBasedCausalReasoner.java | 103 +++++++++++++++- .../ArgumentationBasedCausalReasonerTest.java | 54 +++++++++ .../java/org/tweetyproject/graphs/Graph.java | 91 +++++++++----- .../org/tweetyproject/graphs/GraphTest.java | 111 ++++++++++++++++++ .../web/services/RequestController.java | 105 ++++++++++++----- .../services/causal/CausalReasonerPost.java | 8 +- .../web/services/RequestControllerTest.java | 28 +++++ 7 files changed, 435 insertions(+), 65 deletions(-) create mode 100644 org-tweetyproject-graphs/src/test/java/org/tweetyproject/graphs/GraphTest.java 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..6714c746c 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,85 @@ 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. + * + * @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 + * @return the argumentation framework induced from the causal knowledge base and the observations + * @implNote 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.
  • + *
+ */ + public Map> getSignificantAtoms( + CausalKnowledgeBase cbase, + Collection observations, + Map interventions) { + var theory = getInducedTheory(cbase, observations, interventions); + var perAtomArgumentsWithAtomInConclusion = getPerAtomArgumentsWithAtomInConclusion(theory); + 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 + * @return a map from atom to the set of matching arguments + */ + private static Map> getPerAtomArgumentsWithAtomInConclusion(DungTheory theory) { + 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(); + + var arguments = perAtomArguments.computeIfAbsent(atom, (_atom) -> new ArrayList<>()); + arguments.add(causalArgument); + } + return perAtomArguments; + } } 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..84187ace4 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()); + + 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-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/src/main/java/org/tweetyproject/web/services/RequestController.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/RequestController.java index 0b7e5d742..b3816fc4e 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 @@ -25,9 +25,13 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.concurrent.*; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import org.json.JSONException; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ResponseStatusException; @@ -99,9 +103,9 @@ import javafx.util.Pair; 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; +import static org.tweetyproject.web.services.causal.CausalReasonerResponse.Status.*; /** @@ -116,6 +120,14 @@ public class RequestController { private final int SERVICES_TIMEOUT_CAUSAL = 300; + private final ObjectMapper objectMapper; + + @Autowired + public RequestController(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + /** * Handles HTTP POST requests at the endpoint "/aba". Parses and processes the incoming * JSON payload to perform various ABA reasoning commands. @@ -753,38 +765,69 @@ public CausalReasonerResponse handleRequest(@RequestBody CausalReasonerPost requ ); } - private static String processCommand(CausalReasonerPost causalReasonerPost) { - switch (causalReasonerPost.getCmd()) { - case GET_CONCLUSIONS: - return processConclusionsCommand(causalReasonerPost); - default: - // `cmd` should never be null, because it is annotated with `@NonNull`. - throw new IllegalStateException("Command should be set."); - } + private String processCommand(CausalReasonerPost causalReasonerPost) { + return switch (causalReasonerPost.getCmd()) { + case GET_CONCLUSIONS -> processConclusionsCommand(causalReasonerPost); + case GET_SIGNIFICANT_ATOMS -> processSignificantAtomsCommand(causalReasonerPost); + }; } private static String processConclusionsCommand(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); + CausalKnowledgeBase causalKnowledgeBase = parseCausalKnowledgeBase(causalReasonerPost); + Collection observations = parseObservations(causalReasonerPost); + + AbstractCausalReasoner reasoner = new ArgumentationBasedCausalReasoner(); + Collection conclusions = reasoner.getConclusions(causalKnowledgeBase, observations); + return conclusions.toString(); + } + + private String processSignificantAtomsCommand(CausalReasonerPost causalReasonerPost) { + CausalKnowledgeBase causalKnowledgeBase = parseCausalKnowledgeBase(causalReasonerPost); + Collection observations = parseObservations(causalReasonerPost); + + ArgumentationBasedCausalReasoner reasoner = new ArgumentationBasedCausalReasoner(); + var perAtomSignificantAtoms = reasoner.getSignificantAtoms(causalKnowledgeBase, observations, Map.of()); + + 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); } - 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); - } - - AbstractCausalReasoner reasoner = new ArgumentationBasedCausalReasoner(); - Collection conclusions = reasoner.getConclusions(causalKnowledgeBase, observations); - return conclusions.toString(); - } + try { + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonData); + } 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 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; + } } 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 index b7dc62acc..1694272a4 100644 --- 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 @@ -62,7 +62,13 @@ public enum Cmd { * @see org.tweetyproject.causal.reasoner.AbstractCausalReasoner#getConclusions */ @JsonProperty("get_conclusions") - 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; } /** diff --git a/org-tweetyproject-web/src/test/java/org/tweetyproject/web/services/RequestControllerTest.java b/org-tweetyproject-web/src/test/java/org/tweetyproject/web/services/RequestControllerTest.java index ff06261c9..e5eb25ee0 100644 --- a/org-tweetyproject-web/src/test/java/org/tweetyproject/web/services/RequestControllerTest.java +++ b/org-tweetyproject-web/src/test/java/org/tweetyproject/web/services/RequestControllerTest.java @@ -87,4 +87,32 @@ public void causalReasonerIsCalledSuccessfully() throws Exception { } """)); } + + @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", + "timeout": 10, + "unit_timeout": "s" + } + """); + + mvc.perform(post) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { + "reply": "{\\n \\"a\\" : [ \\"a\\", \\"b\\" ],\\n \\"b\\" : [ \\"a\\", \\"b\\" ],\\n \\"c\\" : [ \\"c\\", \\"d\\" ],\\n \\"d\\" : [ \\"d\\" ]\\n}", + "email": "aId", + "time": 0, + "unit_timeout": "s", + "status": "SUCCESS" + } + """)); + } } \ No newline at end of file From ff18c7b6bf7938aefd5ae48c526046ac38d6ef2a Mon Sep 17 00:00:00 2001 From: Oleksandr Dzhychko Date: Mon, 27 Oct 2025 15:42:40 +0100 Subject: [PATCH 03/22] fix: invalid Javadoc in org-tweetyproject-causal --- .../tweetyproject/causal/parser/CausalParser.java | 12 ++++++------ .../reasoner/ArgumentationBasedCausalReasoner.java | 13 +++++++------ 2 files changed, 13 insertions(+), 12 deletions(-) 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 11f7f6f0c..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 @@ -36,8 +36,8 @@ /** * Parser for {@link CausalKnowledgeBase} and observation as consumed by {@link org.tweetyproject.causal.reasoner.AbstractCausalReasoner}. - * A causal knowledge base can be parsed with {@link this#parseBeliefBase(Reader)}. - * Observations can be parsed with {@link this#parseListOfFormulae(String, String)}. + * 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 @@ -51,10 +51,10 @@ public class CausalParser extends Parser { * 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)} + *
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 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 6714c746c..4bfc7618a 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 @@ -146,12 +146,8 @@ public boolean query(CausalKnowledgeBase cbase, InterventionalStatement statemen /** * 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. - * - * @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 - * @return the argumentation framework induced from the causal knowledge base and the observations - * @implNote This method: + *

+ * This method: *

    *
  • Induces an argumentation theory from the given knowledge base, observations, * and interventions.
  • @@ -163,6 +159,11 @@ public boolean query(CausalKnowledgeBase cbase, InterventionalStatement statemen * 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 + * @return the argumentation framework induced from the causal knowledge base and the observations */ public Map> getSignificantAtoms( CausalKnowledgeBase cbase, From eeb44d714db97a563adef643d44c4599a0d866e5 Mon Sep 17 00:00:00 2001 From: Oleksandr Dzhychko Date: Mon, 27 Oct 2025 15:43:41 +0100 Subject: [PATCH 04/22] fix: add org-tweetyproject-causal as dependency of org-tweetyproject-web --- org-tweetyproject-web/pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/org-tweetyproject-web/pom.xml b/org-tweetyproject-web/pom.xml index caa97987f..2f26c3405 100644 --- a/org-tweetyproject-web/pom.xml +++ b/org-tweetyproject-web/pom.xml @@ -101,6 +101,11 @@ 4.13.1 test + + org.tweetyproject + causal + 1.29-SNAPSHOT + org.tweetyproject.logics commons From b1d9fb20ab75dbd9e552a96337f696760b8d371e Mon Sep 17 00:00:00 2001 From: Oleksandr Dzhychko Date: Fri, 31 Oct 2025 23:14:50 +0100 Subject: [PATCH 05/22] feat(causal): implement querying dialectic sequence explanations --- .../ArgumentationBasedCausalReasoner.java | 12 +- org-tweetyproject-web/logback_stdout.xml | 12 ++ org-tweetyproject-web/pom.xml | 5 + .../web/services/RequestController.java | 101 ++++++++------- .../causal/ArgumentSerialization.java | 47 +++++++ .../web/services/causal/AttackDTO.java | 57 +++++++++ .../services/causal/CausalReasonerPost.java | 58 ++++----- .../causal/CausalReasonerResponse.java | 2 +- .../causal/CausalReasonerService.java | 104 +++++++++++++++ .../ConclusionsFilterSerialization.java | 71 +++++++++++ .../DialectialSequenceExplanationDTO.java | 64 ++++++++++ .../causal/SequenceExplanationReply.java | 65 ++++++++++ .../web/services/RequestControllerTest.java | 118 ++++++++++++++++-- 13 files changed, 628 insertions(+), 88 deletions(-) create mode 100644 org-tweetyproject-web/logback_stdout.xml create mode 100644 org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/ArgumentSerialization.java create mode 100644 org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/AttackDTO.java create mode 100644 org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/CausalReasonerService.java create mode 100644 org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/ConclusionsFilterSerialization.java create mode 100644 org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/DialectialSequenceExplanationDTO.java create mode 100644 org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/SequenceExplanationReply.java 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 4bfc7618a..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 @@ -163,14 +163,17 @@ public boolean query(CausalKnowledgeBase cbase, InterventionalStatement statemen * @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) { + Map interventions, + Collection atomFilter) { var theory = getInducedTheory(cbase, observations, interventions); - var perAtomArgumentsWithAtomInConclusion = getPerAtomArgumentsWithAtomInConclusion(theory); + var perAtomArgumentsWithAtomInConclusion = getPerAtomArgumentsWithAtomInConclusion(theory, atomFilter); var beliefSetWithoutAssumptions = createBeliefSetWithObservationsAndInterventions(cbase, observations, interventions); var perAtomSignificantAtoms = new HashMap>(); @@ -206,9 +209,11 @@ public Map> getSignificantAtoms( * 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 */ - private static Map> getPerAtomArgumentsWithAtomInConclusion(DungTheory theory) { + public Map> getPerAtomArgumentsWithAtomInConclusion(DungTheory theory, Collection atomFilter) { var perAtomArguments = new HashMap>(); for (var argument: theory) { @@ -218,6 +223,7 @@ private static Map> getPerAtomArgumentsW 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); diff --git a/org-tweetyproject-web/logback_stdout.xml b/org-tweetyproject-web/logback_stdout.xml new file mode 100644 index 000000000..cd1ee80a7 --- /dev/null +++ b/org-tweetyproject-web/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/pom.xml b/org-tweetyproject-web/pom.xml index 2f26c3405..47c7c973e 100644 --- a/org-tweetyproject-web/pom.xml +++ b/org-tweetyproject-web/pom.xml @@ -116,6 +116,11 @@ pl 1.29-SNAPSHOT + + org.tweetyproject.arg + explanations + 1.29-SNAPSHOT + org.tweetyproject.arg delp 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 b3816fc4e..6987217b5 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,27 +18,31 @@ */ 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.Map; -import java.util.concurrent.*; - import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import javafx.util.Pair; import org.json.JSONException; 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.DungTheory; import org.tweetyproject.causal.parser.CausalParser; -import org.tweetyproject.causal.reasoner.AbstractCausalReasoner; -import org.tweetyproject.causal.reasoner.ArgumentationBasedCausalReasoner; import org.tweetyproject.causal.syntax.CausalKnowledgeBase; import org.tweetyproject.commons.BeliefSet; import org.tweetyproject.commons.Formula; @@ -65,47 +69,26 @@ 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.causal.CausalReasonerPost; -import org.tweetyproject.web.services.causal.CausalReasonerResponse; +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.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 javafx.util.Pair; +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.*; +import static org.tweetyproject.web.services.causal.CausalReasonerResponse.Status.SUCCESS; +import static org.tweetyproject.web.services.causal.CausalReasonerResponse.Status.TIMEOUT; /** @@ -121,10 +104,13 @@ public class RequestController { private final ObjectMapper objectMapper; + private final CausalReasonerService causalReasonerService; @Autowired - public RequestController(ObjectMapper objectMapper) { + public RequestController(ObjectMapper objectMapper, + CausalReasonerService causalReasonerService) { this.objectMapper = objectMapper; + this.causalReasonerService = causalReasonerService; } @@ -769,24 +755,25 @@ private String processCommand(CausalReasonerPost causalReasonerPost) { return switch (causalReasonerPost.getCmd()) { case GET_CONCLUSIONS -> processConclusionsCommand(causalReasonerPost); case GET_SIGNIFICANT_ATOMS -> processSignificantAtomsCommand(causalReasonerPost); + case GET_SEQUENCE_EXPLANATIONS -> processSequenceExplanations(causalReasonerPost); }; } - private static String processConclusionsCommand(CausalReasonerPost causalReasonerPost) { + private String processConclusionsCommand(CausalReasonerPost causalReasonerPost) { CausalKnowledgeBase causalKnowledgeBase = parseCausalKnowledgeBase(causalReasonerPost); Collection observations = parseObservations(causalReasonerPost); + var conclusionFilter = parseConclusionFilter(causalReasonerPost); - AbstractCausalReasoner reasoner = new ArgumentationBasedCausalReasoner(); - Collection conclusions = reasoner.getConclusions(causalKnowledgeBase, observations); + 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); - ArgumentationBasedCausalReasoner reasoner = new ArgumentationBasedCausalReasoner(); - var perAtomSignificantAtoms = reasoner.getSignificantAtoms(causalKnowledgeBase, observations, Map.of()); + var perAtomSignificantAtoms = causalReasonerService.queryPerAtomSignificantAtoms(causalKnowledgeBase, observations, conclusionFilter); Map> jsonData = new HashMap<>(); for (Map.Entry> entry : perAtomSignificantAtoms.entrySet()) { @@ -805,6 +792,20 @@ private String processSignificantAtomsCommand(CausalReasonerPost causalReasonerP } } + 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 static Collection parseObservations(CausalReasonerPost causalReasonerPost) { CausalParser causalParser = new CausalParser(); Collection observations; @@ -818,6 +819,10 @@ private static Collection parseObservations(CausalReasonerPost causal return observations; } + private static @Nullable List parseConclusionFilter(CausalReasonerPost causalReasonerPost) { + return ConclusionsFilterSerialization.parse(causalReasonerPost.getConclusionsFilter()); + } + private static CausalKnowledgeBase parseCausalKnowledgeBase(CausalReasonerPost causalReasonerPost) { CausalParser causalParser = new CausalParser(); CausalKnowledgeBase causalKnowledgeBase; 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/AttackDTO.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/AttackDTO.java new file mode 100644 index 000000000..573da5d2d --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/AttackDTO.java @@ -0,0 +1,57 @@ +/* + * 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.Attack; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Oleksandr Dzhychko + */ +public final class AttackDTO { + private final String attacker; + private final String attacked; + + public AttackDTO(String attacker, 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/causal/CausalReasonerPost.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/CausalReasonerPost.java index 1694272a4..f4a7296f5 100644 --- 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 @@ -20,15 +20,14 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.springframework.lang.NonNull; - -import java.util.Objects; +import org.springframework.lang.Nullable; /** * Request to execute a {@link CausalReasonerPost#cmd} with a {@link org.tweetyproject.causal.reasoner.AbstractCausalReasoner} * * @author Oleksandr Dzhychko */ -public class CausalReasonerPost { +public final class CausalReasonerPost { public CausalReasonerPost( @JsonProperty(value = "cmd", required = true) @@ -39,6 +38,8 @@ public CausalReasonerPost( @NonNull String kb, @JsonProperty(value = "observations", required = true) @NonNull String observations, + @JsonProperty(value = "conclusions_filter") + @Nullable String conclusionsFilter, @JsonProperty(value = "timeout", required = true) int timeout, @NonNull @JsonProperty(value = "unit_timeout", required = true) @@ -48,6 +49,7 @@ public CausalReasonerPost( this.email = email; this.kb = kb; this.observations = observations; + this.conclusionsFilter = conclusionsFilter; this.timeout = timeout; this.unit_timeout = unit_timeout; } @@ -61,14 +63,17 @@ public enum Cmd { * * @see org.tweetyproject.causal.reasoner.AbstractCausalReasoner#getConclusions */ - @JsonProperty("get_conclusions") - GET_CONCLUSIONS, + @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; + @JsonProperty("get_significant_atoms") GET_SIGNIFICANT_ATOMS, + + /** + * Instructs the reasoner to calculate the sequence of explanations for all consequences. + */ + @JsonProperty("get_sequence_explanations") GET_SEQUENCE_EXPLANATIONS; } /** @@ -84,18 +89,25 @@ public enum Cmd { /** * The knowledge base (KB) for the reasoner request - * The format of the knowledge base must be as described be {@link org.tweetyproject.causal.parser.CausalParser}. + * The format of the knowledge base must be as described in {@link org.tweetyproject.causal.parser.CausalParser#parseBeliefBase(java.io.Reader)} */ @NonNull private String kb; /** * The observations for the reasoner request - * The format of the knowledge base must be as described be {@link org.tweetyproject.causal.parser.CausalParser}. + * The format of the knowledge base must be as used by {@link org.tweetyproject.causal.parser.CausalParser#parseListOfFormulae} with "," (comma) as delimiter. */ @NonNull 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 */ @@ -139,6 +151,14 @@ 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; } @@ -154,24 +174,4 @@ public String getUnit_timeout() { public void setUnit_timeout(String unit_timeout) { this.unit_timeout = unit_timeout; } - - @Override - public boolean equals(Object o) { - if (o == this) return true; - if (!(o instanceof CausalReasonerPost)) { - return false; - } - CausalReasonerPost CausalReasonerPost = (CausalReasonerPost) o; - return Objects.equals(cmd, CausalReasonerPost.cmd) && Objects.equals(email, CausalReasonerPost.email) && Objects.equals(kb, CausalReasonerPost.kb) && Objects.equals(observations, CausalReasonerPost.observations) && timeout == CausalReasonerPost.timeout && Objects.equals(unit_timeout, CausalReasonerPost.unit_timeout); - } - - @Override - public int hashCode() { - return Objects.hash(cmd, email, kb, observations, timeout, unit_timeout); - } - - @Override - public String toString() { - return "{" + " cmd='" + getCmd() + "'" + ", email='" + getEmail() + "'" + ", kb='" + getKb() + "'" + ", query_assumption='" + getObservations() + "'" + ", timeout='" + getTimeout() + "'" + ", unit_timeout='" + getUnit_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 index 28a795292..8f27e59fd 100644 --- 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 @@ -25,7 +25,7 @@ * * @author Oleksandr Dzhychko */ -public class CausalReasonerResponse { +public final class CausalReasonerResponse { public enum Status { SUCCESS, 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..e0fad00a6 --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/CausalReasonerService.java @@ -0,0 +1,104 @@ +/* + * 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.beans.factory.annotation.Autowired; +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.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; + +@Service +public final class CausalReasonerService { + private final ArgumentationBasedCausalReasoner causalReasoner = new ArgumentationBasedCausalReasoner(); + private final DialecticalSequenceExplanationReasoner explanationReasoner = new DialecticalSequenceExplanationReasoner(); + + public Collection queryConclusions(CausalKnowledgeBase causalKnowledgeBase, Collection observations, List conclusionFilter) { + Collection conclusions = causalReasoner.getConclusions(causalKnowledgeBase, observations); + conclusions = filterConclusions(conclusions, conclusionFilter); + return conclusions; + } + + private static Collection filterConclusions(Collection conclusions, @Nullable List conclusionFilter) { + if (conclusionFilter == null) { + return conclusions; + } + return conclusions.stream() + .filter(formula -> isConclusionInFilter(formula, conclusionFilter)) + .collect(Collectors.toUnmodifiableList()); + } + + public static boolean isConclusionInFilter(PlFormula conclusion, @NonNull List conclusionFilter) { + return conclusionFilter.stream() + .anyMatch(proposition -> conclusion.getAtoms().contains(proposition)); + } + + + public Map> queryPerAtomSignificantAtoms(CausalKnowledgeBase causalKnowledgeBase, Collection observations, List conclusionFilter) { + return causalReasoner.getSignificantAtoms(causalKnowledgeBase, observations, Map.of(), conclusionFilter); + } + + public CausalReasonerService.SequenceExplanations querySequenceExplanations(CausalKnowledgeBase causalKnowledgeBase, Collection observations, List 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 CausalReasonerService.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; + } + } +} 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..4d1eaa1d4 --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/ConclusionsFilterSerialization.java @@ -0,0 +1,71 @@ +/* + * 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.stream.Collectors; + +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 List of {@link Proposition}s or {@code null} if the input is {@code null} or empty. + */ + public static @Nullable List 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.toUnmodifiableList()); + + if (propositions.isEmpty()) { + return null; + } + return propositions; + }; +} diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/DialectialSequenceExplanationDTO.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/DialectialSequenceExplanationDTO.java new file mode 100644 index 000000000..55cc8d023 --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/DialectialSequenceExplanationDTO.java @@ -0,0 +1,64 @@ +/* + * 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 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/causal/SequenceExplanationReply.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/SequenceExplanationReply.java new file mode 100644 index 000000000..a9950723d --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/SequenceExplanationReply.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.causal; + +import org.tweetyproject.arg.explanations.semantics.DialectialSequenceExplanation; +import org.tweetyproject.logics.pl.syntax.Proposition; + +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/test/java/org/tweetyproject/web/services/RequestControllerTest.java b/org-tweetyproject-web/src/test/java/org/tweetyproject/web/services/RequestControllerTest.java index e5eb25ee0..b5840455b 100644 --- a/org-tweetyproject-web/src/test/java/org/tweetyproject/web/services/RequestControllerTest.java +++ b/org-tweetyproject-web/src/test/java/org/tweetyproject/web/services/RequestControllerTest.java @@ -1,11 +1,29 @@ +/* + * 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.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +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.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -16,11 +34,12 @@ /** * @author Oleksandr Dzhychko */ -@ExtendWith(SpringExtension.class) -@WebMvcTest(RequestController.class) +@SpringBootTest +@AutoConfigureMockMvc class RequestControllerTest { @Autowired private MockMvc mvc; + private final ObjectMapper objectMapper = new ObjectMapper(); @Test public void causalReasonerWithInvalidKnowledgeBaseReturnsStatus400() throws Exception { @@ -61,7 +80,7 @@ public void causalReasonerWithInvalidObservationsReturnsStatus400() throws Excep } @Test - public void causalReasonerIsCalledSuccessfully() throws Exception { + public void causalReasonerRepliesWithAllConclusions() throws Exception { var post = post("/causal") .contentType(MediaType.APPLICATION_JSON) .content(""" @@ -88,6 +107,35 @@ public void causalReasonerIsCalledSuccessfully() throws Exception { """)); } + @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" + } + """)); + } + @Test public void causalReasonerCalculatesSignificantAtoms() throws Exception { var post = post("/causal") @@ -98,6 +146,7 @@ public void causalReasonerCalculatesSignificantAtoms() throws Exception { "cmd": "get_significant_atoms", "kb": "a <=> b\\nc <=> d\\n{ d, !b }", "observations": "!a, !b", + "conclusionsFilter": "a", "timeout": 10, "unit_timeout": "s" } @@ -107,7 +156,7 @@ public void causalReasonerCalculatesSignificantAtoms() throws Exception { .andExpect(status().isOk()) .andExpect(content().json(""" { - "reply": "{\\n \\"a\\" : [ \\"a\\", \\"b\\" ],\\n \\"b\\" : [ \\"a\\", \\"b\\" ],\\n \\"c\\" : [ \\"c\\", \\"d\\" ],\\n \\"d\\" : [ \\"d\\" ]\\n}", + "reply": "{\\n \\"a\\" : [ \\"a\\", \\"b\\" ]\\n}", "email": "aId", "time": 0, "unit_timeout": "s", @@ -115,4 +164,59 @@ public void causalReasonerCalculatesSignificantAtoms() throws Exception { } """)); } + + @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)); + } } \ No newline at end of file From 23fbdb093d279cbe79c4ac59d52a432a14692d51 Mon Sep 17 00:00:00 2001 From: Oleksandr Dzhychko Date: Sat, 22 Nov 2025 16:22:51 +0100 Subject: [PATCH 06/22] feat: implement web endpoint to get sequence explanations for argumentation framework --- .../ArgumentationBasedCausalReasonerTest.java | 2 +- org-tweetyproject-web/pom.xml | 7 +- .../web/services/RequestController.java | 104 +++++- .../services/causal/CausalReasonerPost.java | 22 +- .../causal/CausalReasonerService.java | 14 +- .../ConclusionsFilterSerialization.java | 10 +- .../causal/SequenceExplanationReply.java | 2 + .../ArgumentFilterSerialization.java | 45 +++ .../AttackDTO.java | 13 +- .../DialectialSequenceExplanationDTO.java | 3 +- .../SequenceExplanationPost.java | 103 ++++++ .../SequenceExplanationResponse.java | 109 ++++++ .../SequenceExplanationService.java | 65 ++++ ....java => RequestControllerCausalTest.java} | 10 +- ...uestControllerSequenceExplanationTest.java | 323 ++++++++++++++++++ 15 files changed, 796 insertions(+), 36 deletions(-) create mode 100644 org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/ArgumentFilterSerialization.java rename org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/{causal => sequenceexplanation}/AttackDTO.java (75%) rename org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/{causal => sequenceexplanation}/DialectialSequenceExplanationDTO.java (94%) create mode 100644 org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/SequenceExplanationPost.java create mode 100644 org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/SequenceExplanationResponse.java create mode 100644 org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/SequenceExplanationService.java rename org-tweetyproject-web/src/test/java/org/tweetyproject/web/services/{RequestControllerTest.java => RequestControllerCausalTest.java} (97%) create mode 100644 org-tweetyproject-web/src/test/java/org/tweetyproject/web/services/RequestControllerSequenceExplanationTest.java 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 84187ace4..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 @@ -63,7 +63,7 @@ public void getSignificantAtoms() throws StructuralCausalModel.CyclicDependencyE Collection observations = List.of(covid); ArgumentationBasedCausalReasoner reasoner = createReasoner(); - var perAtomInfluencingAtoms = reasoner.getSignificantAtoms(knowledgeBase, observations, Map.of()); + var perAtomInfluencingAtoms = reasoner.getSignificantAtoms(knowledgeBase, observations, Map.of(), null); Map> perConclusionExpectedInfluencingAtoms = Map.of( atRisk, Set.of(atRisk), diff --git a/org-tweetyproject-web/pom.xml b/org-tweetyproject-web/pom.xml index 47c7c973e..b007689dd 100644 --- a/org-tweetyproject-web/pom.xml +++ b/org-tweetyproject-web/pom.xml @@ -57,7 +57,12 @@ spring-boot-starter-web - + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot spring-boot-starter-test test 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 6987217b5..da70a2200 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 @@ -41,6 +41,8 @@ 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; @@ -80,12 +82,19 @@ import org.tweetyproject.web.services.incmes.InconsistencyGetMeasuresResponse; import org.tweetyproject.web.services.incmes.InconsistencyPost; import org.tweetyproject.web.services.incmes.InconsistencyValueResponse; +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 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; @@ -100,17 +109,21 @@ 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_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) { + CausalReasonerService causalReasonerService, + SequenceExplanationService sequenceExplanationService) { this.objectMapper = objectMapper; this.causalReasonerService = causalReasonerService; + this.sequenceExplanationService = sequenceExplanationService; } @@ -707,8 +720,12 @@ private AbaGetSemanticsResponse handleGetSemantics(AbaReasonerPost query) @PostMapping(value = "/causal", produces = "application/json") @ResponseBody @CrossOrigin - public CausalReasonerResponse handleRequest(@RequestBody CausalReasonerPost request) throws RuntimeException { - LoggerUtil.logger.info(String.format("Run causal reasoner command \"%s\" with timeout: %s %s", request.getCmd(),request.getTimeout(), request.getUnit_timeout())); + 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); @@ -819,7 +836,7 @@ private static Collection parseObservations(CausalReasonerPost causal return observations; } - private static @Nullable List parseConclusionFilter(CausalReasonerPost causalReasonerPost) { + private static @Nullable Set parseConclusionFilter(CausalReasonerPost causalReasonerPost) { return ConclusionsFilterSerialization.parse(causalReasonerPost.getConclusionsFilter()); } @@ -835,4 +852,81 @@ private static CausalKnowledgeBase parseCausalKnowledgeBase(CausalReasonerPost c } 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/CausalReasonerPost.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/CausalReasonerPost.java index f4a7296f5..f149a1cb5 100644 --- 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 @@ -19,9 +19,10 @@ package org.tweetyproject.web.services.causal; import com.fasterxml.jackson.annotation.JsonProperty; -import org.springframework.lang.NonNull; 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} * @@ -31,17 +32,17 @@ public final class CausalReasonerPost { public CausalReasonerPost( @JsonProperty(value = "cmd", required = true) - @NonNull Cmd cmd, + Cmd cmd, @JsonProperty("email") String email, @JsonProperty(value = "kb", required = true) - @NonNull String kb, + String kb, @JsonProperty(value = "observations", required = true) - @NonNull String observations, + String observations, @JsonProperty(value = "conclusions_filter") - @Nullable String conclusionsFilter, + String conclusionsFilter, @JsonProperty(value = "timeout", required = true) - int timeout, @NonNull + int timeout, @JsonProperty(value = "unit_timeout", required = true) String unit_timeout ) { @@ -79,26 +80,27 @@ public enum Cmd { /** * The command type for the reasoner request */ - @NonNull + @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)} */ - @NonNull + @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. */ - @NonNull + @NotNull private String observations; /** @@ -116,7 +118,7 @@ public enum Cmd { /** * The unit timeout for the reasoner request */ - @NonNull + @NotNull private String unit_timeout; public Cmd getCmd() { 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 index e0fad00a6..8a8cb56b1 100644 --- 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 @@ -18,7 +18,6 @@ */ package org.tweetyproject.web.services.causal; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.stereotype.Service; @@ -33,18 +32,21 @@ 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, List conclusionFilter) { + 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 List conclusionFilter) { + private static Collection filterConclusions(Collection conclusions, @Nullable Set conclusionFilter) { if (conclusionFilter == null) { return conclusions; } @@ -53,17 +55,17 @@ private static Collection filterConclusions(Collection con .collect(Collectors.toUnmodifiableList()); } - public static boolean isConclusionInFilter(PlFormula conclusion, @NonNull List conclusionFilter) { + 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, List conclusionFilter) { + public Map> queryPerAtomSignificantAtoms(CausalKnowledgeBase causalKnowledgeBase, Collection observations, Set conclusionFilter) { return causalReasoner.getSignificantAtoms(causalKnowledgeBase, observations, Map.of(), conclusionFilter); } - public CausalReasonerService.SequenceExplanations querySequenceExplanations(CausalKnowledgeBase causalKnowledgeBase, Collection observations, List conclusionFilter) { + public CausalReasonerService.SequenceExplanations querySequenceExplanations(CausalKnowledgeBase causalKnowledgeBase, Collection observations, Set conclusionFilter) { var theory = causalReasoner.getInducedTheory(causalKnowledgeBase, observations, Map.of()); var perAtomArgumentsWithAtomInConclusion = causalReasoner.getPerAtomArgumentsWithAtomInConclusion(theory, conclusionFilter); 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 index 4d1eaa1d4..1584fa3d0 100644 --- 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 @@ -27,8 +27,12 @@ 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 = ","; @@ -38,9 +42,9 @@ public class ConclusionsFilterSerialization { * Parse the filter string for conclusions. * * @param conclusionsFilterString {@link Proposition}s as parsable by {@link CausalParser#parseFormula(String)} seperated by {@link #ATOM_DELIMITER} - * @return List of {@link Proposition}s or {@code null} if the input is {@code null} or empty. + * @return Set of {@link Proposition}s or {@code null} if the input is {@code null} or empty. */ - public static @Nullable List parse(@Nullable String conclusionsFilterString) { + public static @Nullable Set parse(@Nullable String conclusionsFilterString) { if (conclusionsFilterString == null) { return null; } @@ -61,7 +65,7 @@ public class ConclusionsFilterSerialization { String msg = String.format("Formula `%s` is not a proposition,", formula); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, msg); }) - .collect(Collectors.toUnmodifiableList()); + .collect(Collectors.toUnmodifiableSet()); if (propositions.isEmpty()) { return null; 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 index a9950723d..7f869248a 100644 --- 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 @@ -20,6 +20,8 @@ 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; 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/causal/AttackDTO.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/AttackDTO.java similarity index 75% rename from org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/AttackDTO.java rename to org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/AttackDTO.java index 573da5d2d..a4114377c 100644 --- a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/AttackDTO.java +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/AttackDTO.java @@ -16,10 +16,13 @@ * * Copyright 2025 The TweetyProject Team */ -package org.tweetyproject.web.services.causal; +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; @@ -28,10 +31,12 @@ * @author Oleksandr Dzhychko */ public final class AttackDTO { - private final String attacker; - private final String attacked; + private final @NonNull @NotNull String attacker; + private final @NonNull @NotNull String attacked; - public AttackDTO(String attacker, 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; } diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/DialectialSequenceExplanationDTO.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/DialectialSequenceExplanationDTO.java similarity index 94% rename from org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/DialectialSequenceExplanationDTO.java rename to org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/DialectialSequenceExplanationDTO.java index 55cc8d023..4a07559c9 100644 --- a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/DialectialSequenceExplanationDTO.java +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/DialectialSequenceExplanationDTO.java @@ -16,9 +16,10 @@ * * Copyright 2025 The TweetyProject Team */ -package org.tweetyproject.web.services.causal; +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; 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/test/java/org/tweetyproject/web/services/RequestControllerTest.java b/org-tweetyproject-web/src/test/java/org/tweetyproject/web/services/RequestControllerCausalTest.java similarity index 97% rename from org-tweetyproject-web/src/test/java/org/tweetyproject/web/services/RequestControllerTest.java rename to org-tweetyproject-web/src/test/java/org/tweetyproject/web/services/RequestControllerCausalTest.java index b5840455b..e26c2092a 100644 --- a/org-tweetyproject-web/src/test/java/org/tweetyproject/web/services/RequestControllerTest.java +++ b/org-tweetyproject-web/src/test/java/org/tweetyproject/web/services/RequestControllerCausalTest.java @@ -36,7 +36,7 @@ */ @SpringBootTest @AutoConfigureMockMvc -class RequestControllerTest { +class RequestControllerCausalTest { @Autowired private MockMvc mvc; private final ObjectMapper objectMapper = new ObjectMapper(); @@ -104,7 +104,7 @@ public void causalReasonerRepliesWithAllConclusions() throws Exception { "unit_timeout": "s", "status": "SUCCESS" } - """)); + """, true)); } @Test @@ -133,7 +133,7 @@ public void causalReasonerRepliesWithFilteredConclusions() throws Exception { "unit_timeout": "s", "status": "SUCCESS" } - """)); + """, true)); } @Test @@ -162,7 +162,7 @@ public void causalReasonerCalculatesSignificantAtoms() throws Exception { "unit_timeout": "s", "status": "SUCCESS" } - """)); + """, true)); } @Test @@ -217,6 +217,6 @@ public void causalReasonerGetSequenceExplanations() throws Exception { """, expectedReplyJSONEscaped); mvc.perform(post) .andExpect(status().isOk()) - .andExpect(content().json(expectedResponse)); + .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 From 919b0035fa9655a28abbf26687caa60e30c4fd2b Mon Sep 17 00:00:00 2001 From: Oleksandr Dzhychko Date: Tue, 25 Nov 2025 10:53:59 +0100 Subject: [PATCH 07/22] test: add failing test case for DialecticalSequenceExplanationReasonerTest --- ...cticalSequenceExplanationReasonerTest.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 org-tweetyproject-arg-explanations/src/test/java/org/tweetyproject/arg/explanations/reasoner/acceptance/DialecticalSequenceExplanationReasonerTest.java 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..05eadd5cb --- /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); + 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 From e6b783801af1d6e634182a2ebacb87a95d933ffa Mon Sep 17 00:00:00 2001 From: Oleksandr Dzhychko Date: Tue, 25 Nov 2025 10:58:14 +0100 Subject: [PATCH 08/22] ci: add publish workflow --- .github/workflows/publish.yml | 51 +++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 000000000..989860aa9 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,51 @@ +name: Publish +on: + workflow_dispatch: + inputs: + tag: + description: 'Tag for OCI image to publish' + required: true +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 + permissions: + contents: read + 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: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build with Maven + run: | + mvn --batch-mode --update-snapshots install + mvn --batch-mode --update-snapshots spring-boot:build-image -pl org-tweetyproject-web + - name: Set MAVEN_PROJECT_VERSION environment variable + run: | + echo "MAVEN_PROJECT_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)" >> $GITHUB_ENV + - name: Provide image name variable + id: lowercase_repository_name + run: echo "::set-output name=val::${IMAGE_NAME,,}" + env: + IMAGE_NAME: ${{ github.repository }} + - name: Build, tag and push Docker image + run: | + docker tag web:${{ env.MAVEN_PROJECT_VERSION }} ${{ env.REGISTRY }}/${{ steps.lowercase_repository_name.outputs.val }}/tweetyproject-web-server:${{ inputs.tag }} + docker push ${{ env.REGISTRY }}/${{ steps.lowercase_repository_name.outputs.val }}/tweetyproject-web-server:${{ inputs.tag }} From 9158812eaf6bd916c87f9edb7ccab265718973be Mon Sep 17 00:00:00 2001 From: Oleksandr Dzhychko Date: Tue, 25 Nov 2025 11:06:22 +0100 Subject: [PATCH 09/22] ci: disable signing in publish workflow --- .github/workflows/publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 989860aa9..153229ea3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -35,8 +35,8 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build with Maven run: | - mvn --batch-mode --update-snapshots install - mvn --batch-mode --update-snapshots spring-boot:build-image -pl org-tweetyproject-web + mvn --batch-mode --update-snapshots install -Dgpg.skip=true + mvn --batch-mode --update-snapshots spring-boot:build-image -pl org-tweetyproject-web -Dgpg.skip=true - name: Set MAVEN_PROJECT_VERSION environment variable run: | echo "MAVEN_PROJECT_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)" >> $GITHUB_ENV From f9c0819ddd9d5b549c2a6054eb2116da4d51d946 Mon Sep 17 00:00:00 2001 From: Oleksandr Dzhychko Date: Tue, 25 Nov 2025 17:05:00 +0100 Subject: [PATCH 10/22] deps: raise to Tomcat dependencies to higher version with security fixes --- org-tweetyproject-web/pom.xml | 40 ++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/org-tweetyproject-web/pom.xml b/org-tweetyproject-web/pom.xml index b007689dd..a99bca744 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 From fd795d0a61824f28a4d6628b0722e7f682c8ad89 Mon Sep 17 00:00:00 2001 From: Oleksandr Dzhychko Date: Tue, 25 Nov 2025 18:05:37 +0100 Subject: [PATCH 11/22] build: change configuration to create Docker image --- org-tweetyproject-web/pom.xml | 11 +++++++++-- pom.xml | 21 +++++++++++---------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/org-tweetyproject-web/pom.xml b/org-tweetyproject-web/pom.xml index a99bca744..87c8d399c 100644 --- a/org-tweetyproject-web/pom.xml +++ b/org-tweetyproject-web/pom.xml @@ -204,7 +204,7 @@ - 15 + 17 2.35 UTF-8 org.springframework.boot spring-boot-maven-plugin - 3.4.1 + 2.7.18 + + + + repackage + + + diff --git a/pom.xml b/pom.xml index c8149c350..e5539f076 100644 --- a/pom.xml +++ b/pom.xml @@ -39,8 +39,14 @@ - + + + central + Maven Central Repository + https://repo.maven.apache.org/maven2 + + tweety-mvn TweetyProject MVN Repository @@ -58,8 +64,8 @@ UTF-8 - 15 - 15 + 17 + 17 @@ -82,8 +88,8 @@ maven-compiler-plugin 2.3.2 - 15 - 15 + 17 + 17 @@ -127,11 +133,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 From dfb01d439b93efa0eafd8f8bd72e147aeba1c918 Mon Sep 17 00:00:00 2001 From: Oleksandr Dzhychko Date: Mon, 15 Dec 2025 12:01:43 +0100 Subject: [PATCH 16/22] build: publish versions with date and commit hash --- .github/workflows/publish.yml | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 47e4eb3d5..d59ea5721 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,10 +1,7 @@ name: Publish on: workflow_dispatch: - inputs: - tag: - description: 'Tag for OCI image to publish' - required: true + env: REGISTRY: ghcr.io @@ -34,6 +31,15 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Set version + id: set_version + run: | + VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + BASE_VERSION=${VERSION%-SNAPSHOT} + BUILD_DATE=$(date +%Y%m%d) + BUILD_VERSION="${BASE_VERSION}-${BUILD_DATE}-${GITHUB_SHA::7}" + 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" @@ -41,15 +47,12 @@ jobs: 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 -Dgpg.skip=true - - name: Set MAVEN_PROJECT_VERSION environment variable - run: | - echo "MAVEN_PROJECT_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)" >> $GITHUB_ENV - - name: Provide image name variable - id: lowercase_repository_name - run: echo "::set-output name=val::${IMAGE_NAME,,}" - env: - IMAGE_NAME: ${{ github.repository }} - name: Build, tag and push Docker image run: | - docker tag web:${{ env.MAVEN_PROJECT_VERSION }} ${{ env.REGISTRY }}/${{ steps.lowercase_repository_name.outputs.val }}/tweetyproject-web-server:${{ inputs.tag }} - docker push ${{ env.REGISTRY }}/${{ steps.lowercase_repository_name.outputs.val }}/tweetyproject-web-server:${{ inputs.tag }} + 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" From 901c70dfbfdd26547e6f9ae4008cc9d521b739e2 Mon Sep 17 00:00:00 2001 From: Oleksandr Dzhychko Date: Mon, 15 Dec 2025 16:56:52 +0100 Subject: [PATCH 17/22] build: publish maven version --- .github/workflows/publish.yml | 5 +++++ pom.xml | 8 +++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d59ea5721..60d857693 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -47,6 +47,11 @@ jobs: 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 -Dgpg.skip=true + - name: Publish Maven package + run: | + mvn --batch-mode deploy -pl org-tweetyproject-web -Dgpg.skip=true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Build, tag and push Docker image run: | IMAGE_NAME="${{ env.REGISTRY }}/${GITHUB_REPOSITORY@L}/tweetyproject-web-server" diff --git a/pom.xml b/pom.xml index 2f0cfe689..f40aaad97 100644 --- a/pom.xml +++ b/pom.xml @@ -32,10 +32,15 @@ + + github + GitHub Packages + ${github.repo.package.url} + github GitHub Packages - https://maven.pkg.github.com/odzhychko/TweetyProject + ${github.repo.package.url} @@ -74,6 +79,7 @@ UTF-8 17 17 + https://maven.pkg.github.com/odzhychko/TweetyProject From de955da8f9f8eaf679d3104bd533a647d5389088 Mon Sep 17 00:00:00 2001 From: Oleksandr Dzhychko Date: Mon, 15 Dec 2025 20:11:03 +0100 Subject: [PATCH 18/22] build: publish Fat Jar of web server in release GitHub does not allow unauthorized read access to GitHub packages maven repository even if it is public. See https://github.com/orgs/community/discussions/26634 Therefore, it will be published as artifact. --- .github/workflows/publish.yml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 60d857693..c6f9edf01 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -11,8 +11,10 @@ jobs: # 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: read + contents: write packages: write steps: @@ -46,12 +48,12 @@ jobs: # 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 -Dgpg.skip=true - - name: Publish Maven package - run: | - mvn --batch-mode deploy -pl org-tweetyproject-web -Dgpg.skip=true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + 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: ${{ steps.set_version.outputs.build_version }} + files: org-tweetyproject-web/target/web-*.jar - name: Build, tag and push Docker image run: | IMAGE_NAME="${{ env.REGISTRY }}/${GITHUB_REPOSITORY@L}/tweetyproject-web-server" From d243538a258905426f3096d52d4fd7d8a40d6d22 Mon Sep 17 00:00:00 2001 From: Oleksandr Dzhychko Date: Sat, 20 Dec 2025 14:47:59 +0100 Subject: [PATCH 19/22] build: change version schema to match semantic versioning User can now recognize easier which version is newer. --- .github/workflows/publish.yml | 41 +++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c6f9edf01..7109e18a5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -33,15 +33,45 @@ jobs: 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) + # - Date (e.g., 2025-12-20) + # - Commit hash (e.g., a123456) + # - Existing releases (e.g., v1.29.0-20251220+a123456) + # + # The build version is derived as `1.29.0-20251220+`. + # If a release already exists with a tag starting with `v1.29.0-20251220`, + # the derived version is incremented with a numeric suffix: + # 1.29.0-20251220.1+, 1.29.0-20251220.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) + VERSION="$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)" BASE_VERSION=${VERSION%-SNAPSHOT} BUILD_DATE=$(date +%Y%m%d) - BUILD_VERSION="${BASE_VERSION}-${BUILD_DATE}-${GITHUB_SHA::7}" + BUILD_VERSION_WITH_PRERELEASE="${BASE_VERSION}.0-${BUILD_DATE}" + SAME_PRERELEASE_COUNT=$(grep -c -F "v${BUILD_VERSION_WITH_PRERELEASE}" <<< "${{ steps.get_releases.outputs.all_releases }}" || true) + if (( SAME_PRERELEASE_COUNT != 0 )); then + BUILD_VERSION_WITH_PRERELEASE="${BUILD_VERSION_WITH_PRERELEASE}.${SAME_PRERELEASE_COUNT}" + fi + BUILD_VERSION="${BUILD_VERSION_WITH_PRERELEASE}+${GITHUB_SHA::7}" 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" @@ -49,11 +79,14 @@ jobs: 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: ${{ steps.set_version.outputs.build_version }} + 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" From 8812829f491ce6b2760a9550cc240af872e3df0a Mon Sep 17 00:00:00 2001 From: Oleksandr Dzhychko Date: Sat, 20 Dec 2025 15:42:20 +0100 Subject: [PATCH 20/22] build: remove date and commit hash from build version --- .github/workflows/publish.yml | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7109e18a5..37066351e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -47,14 +47,12 @@ jobs: # Derive build versions from: # - Maven version (e.g., 1.29-SNAPSHOT) - # - Date (e.g., 2025-12-20) - # - Commit hash (e.g., a123456) - # - Existing releases (e.g., v1.29.0-20251220+a123456) + # - Existing preview releases (e.g., v1.29.0-preview) # - # The build version is derived as `1.29.0-20251220+`. - # If a release already exists with a tag starting with `v1.29.0-20251220`, + # 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-20251220.1+, 1.29.0-20251220.2+, etc. + # 1.29.0-preview.1, 1.29.0-preview.2, etc. # # This versioning follows Semantic Versioning 2.0.0. # See https://semver.org/ @@ -63,15 +61,14 @@ jobs: run: | VERSION="$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)" BASE_VERSION=${VERSION%-SNAPSHOT} - BUILD_DATE=$(date +%Y%m%d) - BUILD_VERSION_WITH_PRERELEASE="${BASE_VERSION}.0-${BUILD_DATE}" - SAME_PRERELEASE_COUNT=$(grep -c -F "v${BUILD_VERSION_WITH_PRERELEASE}" <<< "${{ steps.get_releases.outputs.all_releases }}" || true) - if (( SAME_PRERELEASE_COUNT != 0 )); then - BUILD_VERSION_WITH_PRERELEASE="${BUILD_VERSION_WITH_PRERELEASE}.${SAME_PRERELEASE_COUNT}" + 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 - BUILD_VERSION="${BUILD_VERSION_WITH_PRERELEASE}+${GITHUB_SHA::7}" 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" From 645e0f3e9bdfe58f1b4ed91eec55b6c2375356e4 Mon Sep 17 00:00:00 2001 From: Oleksandr Dzhychko Date: Mon, 9 Feb 2026 09:19:03 +0100 Subject: [PATCH 21/22] feat: implement command to get argumentation framework for causal model --- .../web/services/RequestController.java | 16 +++++- .../causal/ArgumentationFrameworkReply.java | 56 +++++++++++++++++++ .../services/causal/CausalReasonerPost.java | 5 ++ .../causal/CausalReasonerService.java | 9 ++- 4 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/ArgumentationFrameworkReply.java 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 da70a2200..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 @@ -772,7 +772,8 @@ private String processCommand(CausalReasonerPost causalReasonerPost) { return switch (causalReasonerPost.getCmd()) { case GET_CONCLUSIONS -> processConclusionsCommand(causalReasonerPost); case GET_SIGNIFICANT_ATOMS -> processSignificantAtomsCommand(causalReasonerPost); - case GET_SEQUENCE_EXPLANATIONS -> processSequenceExplanations(causalReasonerPost); + case GET_ARGUMENTATION_FRAMEWORK -> processArgumentationFramework(causalReasonerPost); + case GET_SEQUENCE_EXPLANATIONS -> processSequenceExplanations(causalReasonerPost); }; } @@ -823,6 +824,19 @@ private String processSequenceExplanations(CausalReasonerPost causalReasonerPost } } + 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; 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 index f149a1cb5..5a6d59ec4 100644 --- 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 @@ -71,6 +71,11 @@ public enum Cmd { */ @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. */ 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 index 8a8cb56b1..43e2f3265 100644 --- 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 @@ -22,6 +22,7 @@ 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; @@ -65,7 +66,7 @@ public Map> queryPerAtomSignificantAtoms(Ca return causalReasoner.getSignificantAtoms(causalKnowledgeBase, observations, Map.of(), conclusionFilter); } - public CausalReasonerService.SequenceExplanations querySequenceExplanations(CausalKnowledgeBase causalKnowledgeBase, Collection observations, Set conclusionFilter) { + public SequenceExplanations querySequenceExplanations(CausalKnowledgeBase causalKnowledgeBase, Collection observations, Set conclusionFilter) { var theory = causalReasoner.getInducedTheory(causalKnowledgeBase, observations, Map.of()); var perAtomArgumentsWithAtomInConclusion = causalReasoner.getPerAtomArgumentsWithAtomInConclusion(theory, conclusionFilter); @@ -82,7 +83,7 @@ public CausalReasonerService.SequenceExplanations querySequenceExplanations(Caus allSequenceExplanations.addAll(sequenceExplanations); } } - return new CausalReasonerService.SequenceExplanations(theory.getAttacks(), perAtomPerSequenceExplanations); + return new SequenceExplanations(theory.getAttacks(), perAtomPerSequenceExplanations); } public static final class SequenceExplanations { @@ -103,4 +104,8 @@ public Map> getPerAtomSequenceE return perAtomSequenceExplanations; } } + + public DungTheory queryArgumentationFramework(CausalKnowledgeBase causalKnowledgeBase, Collection observations) { + return causalReasoner.getInducedTheory(causalKnowledgeBase, observations, Map.of()); + } } From 845534d6a81fe05178acc06a0e27c8a7761159d1 Mon Sep 17 00:00:00 2001 From: Oleksandr Dzhychko Date: Mon, 16 Feb 2026 13:28:13 +0100 Subject: [PATCH 22/22] build: configure Docker to support v1.24 API --- .github/workflows/publish.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 37066351e..a2336c3cc 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -26,6 +26,17 @@ jobs: 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: