Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c732664
feat(causal): add parser and web endpoint for causal reasoner
odzhychko Mar 31, 2025
19cf4fb
feat: add command to get significant atoms for argumentation-based ca…
odzhychko Aug 14, 2025
ff18c7b
fix: invalid Javadoc in org-tweetyproject-causal
odzhychko Oct 27, 2025
eeb44d7
fix: add org-tweetyproject-causal as dependency of org-tweetyproject-web
odzhychko Oct 27, 2025
b1d9fb2
feat(causal): implement querying dialectic sequence explanations
odzhychko Oct 31, 2025
0e139d6
Merge branch 'TweetyProjectTeam:main' into dev
odzhychko Nov 21, 2025
23fbdb0
feat: implement web endpoint to get sequence explanations for argumen…
odzhychko Nov 22, 2025
919b003
test: add failing test case for DialecticalSequenceExplanationReasone…
odzhychko Nov 25, 2025
e6b7838
ci: add publish workflow
odzhychko Nov 25, 2025
9158812
ci: disable signing in publish workflow
odzhychko Nov 25, 2025
f9c0819
deps: raise to Tomcat dependencies to higher version with security fixes
odzhychko Nov 25, 2025
fd795d0
build: change configuration to create Docker image
odzhychko Nov 25, 2025
db74e7b
build: change default logback config for published container
odzhychko Nov 25, 2025
da3364e
test: setup Dung theory in DialecticalSequenceExplanationReasonerTest…
odzhychko Nov 26, 2025
5bc7fa5
Merge branch 'TweetyProjectTeam:main' into dev
odzhychko Nov 28, 2025
6e100dd
feat: add configuration for client side routing
odzhychko Dec 10, 2025
21b0873
build: modify configuration to publish to GitHub
odzhychko Dec 15, 2025
dfb01d4
build: publish versions with date and commit hash
odzhychko Dec 15, 2025
901c70d
build: publish maven version
odzhychko Dec 15, 2025
de955da
build: publish Fat Jar of web server in release
odzhychko Dec 15, 2025
d243538
build: change version schema to match semantic versioning
odzhychko Dec 20, 2025
8812829
build: remove date and commit hash from build version
odzhychko Dec 20, 2025
645e0f3
feat: implement command to get argumentation framework for causal model
odzhychko Feb 9, 2026
845534d
build: configure Docker to support v1.24 API
odzhychko Feb 16, 2026
35f8640
Merge branch 'TweetyProjectTeam:main' into dev
odzhychko May 4, 2026
734676b
Merge branch 'TweetyProjectTeam:main' into dev
odzhychko May 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
name: Publish
on:
workflow_dispatch:

env:
REGISTRY: ghcr.io

jobs:
build-and-publish-image:
runs-on: ubuntu-24.04

# Permissions required for publishing Docker image.
# See https://docs.github.com/en/actions/use-cases-and-examples/publishing-packages/publishing-docker-images#publishing-images-to-github-packages
# And permissions required for creating releases.
# See https://github.com/softprops/action-gh-release
permissions:
contents: write
packages: write

steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: maven

- name: Setup Docker
uses: docker/setup-docker-action@v4
with:
# `org.springframework.boot:spring-boot-maven-plugin:2.7.18:build-image` uses an older API version
# that is not supported by default.
daemon-config: |
{
"min-api-version": "1.24"
}
set-host: true

- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Get releases
id: get_releases
run: |
ALL_RELEASES=$(gh api "repos/${GITHUB_REPOSITORY}/releases" --paginate --jq '.[].tag_name')
{
echo 'all_releases<<EOF'
echo "$ALL_RELEASES"
echo EOF
} >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# Derive build versions from:
# - Maven version (e.g., 1.29-SNAPSHOT)
# - Existing preview releases (e.g., v1.29.0-preview)
#
# The build version is derived as `1.29.0-preview`.
# If a release already exists with a tag starting with `v1.29.0-preview`,
# the derived version is incremented with a numeric suffix:
# 1.29.0-preview.1, 1.29.0-preview.2, etc.
#
# This versioning follows Semantic Versioning 2.0.0.
# See https://semver.org/
- name: Set version
id: set_version
run: |
VERSION="$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)"
BASE_VERSION=${VERSION%-SNAPSHOT}
BUILD_VERSION="${BASE_VERSION}.0-preview"
PREVIEWS_COUNT=$(grep -c -F "v${BUILD_VERSION}" <<< "${{ steps.get_releases.outputs.all_releases }}" || true)
if (( PREVIEWS_COUNT != 0 )); then
BUILD_VERSION="${BUILD_VERSION}.${PREVIEWS_COUNT}"
fi
mvn --batch-mode versions:set -DnewVersion="$BUILD_VERSION" -DgenerateBackupPoms=false
echo "build_version=$BUILD_VERSION" >> $GITHUB_OUTPUT

- name: Build with Maven
run: |
LOGGING_CONFIG_PATH="org-tweetyproject-web/src/main/resources"
# Logging to stdout by default is more appropriate for a container deployment
mv "$LOGGING_CONFIG_PATH"/logback_stdout.xml "$LOGGING_CONFIG_PATH"/logback.xml
mvn --batch-mode --update-snapshots install -Dgpg.skip=true
mvn --batch-mode --update-snapshots spring-boot:build-image -pl org-tweetyproject-web -DskipTests -Dgpg.skip=true

- name: Publish Fat Jar
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.set_version.outputs.build_version }}
files: org-tweetyproject-web/target/web-*.jar
prerelease: true

- name: Build, tag and push Docker image
run: |
IMAGE_NAME="${{ env.REGISTRY }}/${GITHUB_REPOSITORY@L}/tweetyproject-web-server"
IMAGE_REF_WITH_VERSION="$IMAGE_NAME:${{ steps.set_version.outputs.build_version }}"
docker tag web:${{ steps.set_version.outputs.build_version }} "$IMAGE_REF_WITH_VERSION"
docker push "$IMAGE_REF_WITH_VERSION"
IMAGE_REF_LATEST="$IMAGE_NAME:latest"
docker tag web:${{ steps.set_version.outputs.build_version }} "$IMAGE_REF_LATEST"
docker push "$IMAGE_REF_LATEST"
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*
* Copyright 2025 The TweetyProject Team <http://tweetyproject.org/contact/>
*/

package org.tweetyproject.arg.explanations.reasoner.acceptance;

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.tweetyproject.arg.dung.syntax.Argument;
import org.tweetyproject.arg.dung.syntax.Attack;
import org.tweetyproject.arg.dung.syntax.DungTheory;

import static org.junit.jupiter.api.Assertions.fail;

/**
* @author Oleksandr Dzhychko
*/
class DialecticalSequenceExplanationReasonerTest {

DialecticalSequenceExplanationReasoner reasoner = new DialecticalSequenceExplanationReasoner();

@Disabled("Reasoner fails to provide explanations.")
@Test
public void circleOfFourArguments() {
var argumentA = new Argument("a");
var argumentB = new Argument("b");
var argumentC = new Argument("c");
var argumentD = new Argument("d");
var attackAB = new Attack(argumentA, argumentB);
var attackBC = new Attack(argumentB, argumentC);
var attackCD = new Attack(argumentC, argumentD);
var attackDA = new Attack(argumentD, argumentA);

var theory = new DungTheory();
theory.add(argumentA, argumentB, argumentC, argumentD);
theory.add(attackAB, attackBC, attackCD, attackDA);

// #getExplanations fails with `java.lang.IndexOutOfBoundsException: Index: 1, Size: 1`
var explanations = reasoner.getExplanations(theory, argumentA);

fail("Missing assertion.");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,83 @@
package org.tweetyproject.causal.parser;

import org.tweetyproject.causal.syntax.CausalKnowledgeBase;
import org.tweetyproject.causal.syntax.StructuralCausalModel;
import org.tweetyproject.commons.Parser;
import org.tweetyproject.commons.ParserException;
import org.tweetyproject.logics.pl.parser.PlParserFactory;
import org.tweetyproject.logics.pl.syntax.PlBeliefSet;
import org.tweetyproject.logics.pl.syntax.PlFormula;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Parser for {@link CausalKnowledgeBase} and observation as consumed by {@link org.tweetyproject.causal.reasoner.AbstractCausalReasoner}.
* A causal knowledge base can be parsed with {@link #parseBeliefBase(Reader)}.
* Observations can be parsed with {@link #parseListOfFormulae(String, String)}.
*
* @author Lars Bengel
* @author Oleksandr Dzhychko
*/
public class CausalParser extends Parser<CausalKnowledgeBase, PlFormula> {
static final Pattern ASSUMPTIONS_PATTERN = Pattern.compile("^\\s*\\{(.*)\\}\\s*$");
static final String SYMBOL_COMMA = ",";
static Parser<PlBeliefSet, PlFormula> 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:
* <br>equation ::= formula '&lt;=&gt;' formula
* <br>assumptions ::= '{' assumption (',' assumption)* '}'
* <br>assumption ::= formula
* <br>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<PlFormula> assumptions = new ArrayList<>();
List<PlFormula> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -44,12 +46,7 @@
public class ArgumentationBasedCausalReasoner extends AbstractArgumentationBasedCausalReasoner {
@Override
public DungTheory getInducedTheory(CausalKnowledgeBase cbase, Collection<PlFormula> observations, Map<Proposition,Boolean> 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<PlFormula> literals = new HashSet<>();
for (Proposition atom : base.getSignature()) {
Expand Down Expand Up @@ -111,6 +108,19 @@ public DungTheory getInducedTheory(CausalKnowledgeBase cbase, Collection<PlFormu
return theory;
}

private static PlBeliefSet createBeliefSetWithObservationsAndInterventions(
CausalKnowledgeBase cbase,
Collection<PlFormula> observations,
Map<Proposition, Boolean> 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
*
Expand All @@ -132,4 +142,92 @@ public boolean query(CausalKnowledgeBase cbase, CausalStatement statement) {
public boolean query(CausalKnowledgeBase cbase, InterventionalStatement statement) {
return query(cbase, statement.getObservations(), statement.getInterventions(), statement.getConclusion());
}

/**
* Computes, for each atom that appears in the knowledge base, the set of atoms that are
* significant for establishing that conclusion under the given observations and interventions.
* <p>
* This method:
* <ul>
* <li>Induces an argumentation theory from the given knowledge base, observations,
* and interventions.</li>
* <li>Groups arguments by the (single) atom occurring in their conclusion (positive
* or negated).</li>
* <li>Collects, per atom, all arguments concluding that atom (or its negation) and
* all their ancestors in the attack graph.</li>
* <li>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.</li>
* </ul>
*
* @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<Proposition, Collection<Proposition>> getSignificantAtoms(
CausalKnowledgeBase cbase,
Collection<PlFormula> observations,
Map<Proposition, Boolean> interventions,
Collection<Proposition> atomFilter) {
var theory = getInducedTheory(cbase, observations, interventions);
var perAtomArgumentsWithAtomInConclusion = getPerAtomArgumentsWithAtomInConclusion(theory, atomFilter);
var beliefSetWithoutAssumptions = createBeliefSetWithObservationsAndInterventions(cbase, observations, interventions);

var perAtomSignificantAtoms = new HashMap<Proposition, Collection<Proposition>>();

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<Proposition>();
for (var argument : significantArguments) {
var causalArgument = (CausalArgument) argument;
var beliefSetWithAssumptions = new PlBeliefSet(beliefSetWithoutAssumptions);
beliefSetWithAssumptions.addAll(causalArgument.getPremises());
var kernels = reasoner.getKernels(beliefSetWithAssumptions, causalArgument.getConclusion());
for (var kernel : kernels) {
for (var formula : kernel) {
significantAtoms.addAll(formula.getAtoms());
}
}
}
perAtomSignificantAtoms.put(atom, significantAtoms);
}

return perAtomSignificantAtoms;
}


/**
* Returns, for each atom, the set of arguments whose conclusion is the atom or its negation.
*
* @param theory the theory containing the arguments
* @param atomFilter atoms for which to get the significant atoms.
* If {@code null}, the filter is not applied.
* @return a map from atom to the set of matching arguments
*/
public Map<Proposition, Collection<CausalArgument>> getPerAtomArgumentsWithAtomInConclusion(DungTheory theory, Collection<Proposition> atomFilter) {
var perAtomArguments = new HashMap<Proposition, Collection<CausalArgument>>();

for (var argument: theory) {
var causalArgument = (CausalArgument) argument;
var signature = causalArgument.getConclusion().getAtoms();
if (signature.size() != 1) {
throw new IllegalStateException("Encountered invalid argument with more than one atom in the its conclusion: " + causalArgument);
}
var atom = signature.stream().findFirst().get();
if (atomFilter != null && !atomFilter.contains(atom)) continue;

var arguments = perAtomArguments.computeIfAbsent(atom, (_atom) -> new ArrayList<>());
arguments.add(causalArgument);
}
return perAtomArguments;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ public void clear() {
/**
* Thrown to indicate that the structural equations of a causal model contain a cyclic dependency
*/
public static class CyclicDependencyException extends Throwable {
public static class CyclicDependencyException extends Exception {
/**
* Constructs a CyclicDependencyException with the specified detail message
*
Expand Down
Loading
Loading