Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
f29329c
Adaptive learning: Add per-course auto-orchestration configuration
jaylann Jun 29, 2026
3689e5e
Merge branch 'develop' into feature/atlas/per-course-auto-orchestration
jaylann Jun 29, 2026
bafd347
Fix ci and reviews
jaylann Jun 29, 2026
820fe6b
fix review
jaylann Jun 29, 2026
6f3ad35
fix review
jaylann Jun 29, 2026
d37d6e7
fix tests
jaylann Jul 1, 2026
b72cffa
fix tests
jaylann Jul 1, 2026
c765276
Merge branch 'develop'
jaylann Jul 6, 2026
1062e51
fix: admin only access
jaylann Jul 6, 2026
512275e
fix: address review
jaylann Jul 6, 2026
977acc2
Merge branch 'develop' into feature/atlas/per-course-auto-orchestration
jaylann Jul 6, 2026
0afc929
merge
jaylann Jul 23, 2026
e7223be
Merge branch 'develop' into feature/atlas/per-course-auto-orchestration
jaylann Jul 23, 2026
35682f7
merge and fix review
jaylann Aug 3, 2026
ac084d4
merge and fix review
jaylann Aug 3, 2026
0cb318c
fix review
jaylann Aug 3, 2026
890f3e3
update to newest standard
jaylann Aug 4, 2026
f27cee1
merge
jaylann Aug 5, 2026
1ed42da
fix lint
jaylann Aug 5, 2026
1ec39d4
fix review
jaylann Aug 5, 2026
420d259
Merge branch 'develop' into feature/atlas/per-course-auto-orchestration
jaylann Aug 7, 2026
d75325d
Merge branch 'develop' into feature/atlas/per-course-auto-orchestration
jaylann Aug 10, 2026
acdad63
Merge branch 'develop' into feature/atlas/per-course-auto-orchestration
jaylann Aug 11, 2026
bd70540
Merge branch 'develop' into feature/atlas/per-course-auto-orchestration
jaylann Aug 11, 2026
0700fa8
Merge branch 'develop' into feature/atlas/per-course-auto-orchestration
jaylann Aug 12, 2026
9766589
Merge
jaylann Aug 14, 2026
db764ee
address review
jaylann Aug 14, 2026
7e0010f
address review
jaylann Aug 14, 2026
9832392
Development: Fix server code style
jaylann Aug 15, 2026
a2d013c
Merge remote-tracking branch 'origin/develop' into feature/atlas/per-…
jaylann Aug 24, 2026
70aeea3
Merge remote-tracking branch 'origin/develop' into feature/atlas/per-…
jaylann Aug 25, 2026
e34dc70
Merge branch 'develop' into feature/atlas/per-course-auto-orchestration
MaximilianAnzinger Aug 25, 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package de.tum.cit.aet.artemis.atlas.api;

import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Controller;

import de.tum.cit.aet.artemis.atlas.config.AtlasEnabled;
import de.tum.cit.aet.artemis.atlas.service.ContentChangeAccumulatorService;

/**
* API facade for a course's auto-orchestration configuration. Keeps the {@code course} module's only
* {@code atlas} touchpoint behind {@code *.api}: the course update flow flushes buffered content changes
* when auto-orchestration is disabled.
* <p>
* Reading and writing the configuration itself deliberately does <em>not</em> go through this facade. This
* bean is conditional on the Atlas module, whereas the settings must be preserved across course updates
* regardless of whether Atlas is active. They therefore live on the (unconditional) {@code CourseConfiguration}
* and are read and written by the course update flow directly.
*/
@Controller
@Conditional(AtlasEnabled.class)
@Lazy
public class CourseAutoOrchestrationApi extends AbstractAtlasApi {

private final ContentChangeAccumulatorService contentChangeAccumulatorService;

public CourseAutoOrchestrationApi(ContentChangeAccumulatorService contentChangeAccumulatorService) {
this.contentChangeAccumulatorService = contentChangeAccumulatorService;
}

/**
* Drops a course's buffered content changes. Called when a course disables auto-orchestration so a
* stale batch buffered while it was enabled cannot fire (e.g. on re-enable within the debounce
* window or a scheduler tick before the change propagates).
*
* @param courseId the course whose buffered content changes should be dropped
*/
public void flushBufferedContentChanges(long courseId) {
contentChangeAccumulatorService.flush(courseId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package de.tum.cit.aet.artemis.atlas.dto;

import org.jspecify.annotations.Nullable;

import com.fasterxml.jackson.annotation.JsonInclude;

/**
* Lightweight projection of a course's Atlas auto-orchestration configuration. Read on the
* accumulator hot path ({@code record} / {@code claimDueBatch} / {@code listDueCourseIds}) so the
* pipeline can resolve the per-course kill switch and the debounce / daily-cap overrides without
* loading the full configuration entity. The config stays authoritative in the database and is
* never duplicated into the distributed accumulator state.
*
* @param autoOrchestratorEnabled hard per-course kill switch for the auto-orchestration pipeline
* @param debounceWindowSecondsOverride per-course debounce window override in seconds, or {@code null} to use the global default
* @param maxDailyOrchestrationOverride per-course daily run cap override, or {@code null} to use the global default
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record CourseAutoOrchestrationConfigDTO(boolean autoOrchestratorEnabled, @Nullable Integer debounceWindowSecondsOverride, @Nullable Integer maxDailyOrchestrationOverride) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package de.tum.cit.aet.artemis.atlas.dto;

import com.fasterxml.jackson.annotation.JsonInclude;

/**
* Global default values for the per-course Atlas auto-orchestration overrides, surfaced to the
* course-settings form so it can show the instructor what an empty (unset) override resolves to.
* Sourced from {@code AtlasOrchestratorProperties} (server-side YAML); the per-course overrides on
* {@link de.tum.cit.aet.artemis.course.domain.CourseConfiguration} fall back to these defaults when
* not set.
*
* @param debounceWindowSeconds global default debounce window in seconds
* @param maxDailyOrchestrations global default daily run cap
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record OrchestratorDefaultsDTO(int debounceWindowSeconds, int maxDailyOrchestrations) {
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package de.tum.cit.aet.artemis.atlas.service;

import java.util.Collections;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Conditional;
Expand All @@ -9,12 +12,15 @@
import org.springframework.stereotype.Component;

import de.tum.cit.aet.artemis.atlas.config.AtlasEnabled;
import de.tum.cit.aet.artemis.atlas.dto.CourseAutoOrchestrationConfigDTO;
import de.tum.cit.aet.artemis.core.security.SecurityUtils;
import de.tum.cit.aet.artemis.core.service.feature.Feature;
import de.tum.cit.aet.artemis.core.service.feature.FeatureToggleService;
import de.tum.cit.aet.artemis.course.domain.Course;
import de.tum.cit.aet.artemis.course.repository.CourseConfigurationRepository;
import de.tum.cit.aet.artemis.exercise.domain.Exercise;
import de.tum.cit.aet.artemis.exercise.domain.event.ExerciseVersionCreatedEvent;
import de.tum.cit.aet.artemis.exercise.service.ExerciseVersionService;

/**
* Feeds the automatic competency pipeline whenever an exercise version is created. Hooks into the
Expand All @@ -23,8 +29,16 @@
* resource.
* <p>
* Everything is gated behind the {@link Feature#AtlasAgent} toggle, which is
* disabled by default — instructors can opt in per-instance via the feature-toggle admin UI. Exam
* exercises are skipped because competency management is scoped to course content only.
* disabled by default — instructors can opt in per-instance via the feature-toggle admin UI — and,
* additionally, behind the per-course {@code autoOrchestratorEnabled} kill switch: a course only
* participates in the pipeline when the instructor has explicitly enabled it. Exam exercises are
* skipped because competency management is scoped to course content only.
* <p>
* The recording is further filtered by the changed-field set carried on the event
* ({@link ExerciseVersionCreatedEvent#changedFields()}): only versions that touched a
* {@link ExerciseVersionService#COMPETENCY_RELEVANT_FIELDS content-bearing field} record into the
* accumulator, so purely administrative edits (dates, points, grading config, …) never burn the
* per-course daily cap on an orchestration that could not change competency mapping.
*/
@Conditional(AtlasEnabled.class)
@Lazy
Expand All @@ -37,19 +51,28 @@ public class AutonomousCompetencyExerciseEventListener {

private final FeatureToggleService featureToggleService;

public AutonomousCompetencyExerciseEventListener(ContentChangeAccumulatorService accumulator, FeatureToggleService featureToggleService) {
private final CourseConfigurationRepository courseConfigurationRepository;

public AutonomousCompetencyExerciseEventListener(ContentChangeAccumulatorService accumulator, FeatureToggleService featureToggleService,
CourseConfigurationRepository courseConfigurationRepository) {
this.accumulator = accumulator;
this.featureToggleService = featureToggleService;
this.courseConfigurationRepository = courseConfigurationRepository;
}

/**
* Fires on every {@link ExerciseVersionCreatedEvent} — publishers live in the exercise module,
* so one listener covers every authoring path (programming / text / modeling / quiz / file
* upload). The method is a no-op when the toggle is off, when the exercise is an exam exercise,
* or when any null guard trips; in the success path it merges the exercise id into the
* per-course accumulator for the scheduler to pick up.
* upload). The method is a no-op when the global toggle is off, when the exercise is an exam
* exercise, when any null guard trips, or when the change touched no content-bearing field; in
* the success path it merges the exercise id into the per-course accumulator for the scheduler to
* pick up.
* <p>
* When the owning course has auto-orchestration disabled the method flushes the course's
* accumulator bucket (dropping any ids buffered while it was enabled) and returns without
* recording, so disabling acts as an immediate per-course kill switch.
*
* @param event the just-published event carrying the newly versioned exercise
* @param event the just-published event carrying the newly versioned exercise and its changed fields
*/
@EventListener
@Async
Expand All @@ -66,7 +89,25 @@ public void onExerciseVersionCreated(ExerciseVersionCreatedEvent event) {
if (course == null || course.getId() == null) {
return;
}
log.debug("atlas.automatic recorded exercise change courseId={} exerciseId={}", course.getId(), exercise.getId());
accumulator.record(course.getId(), exercise.getId());
long courseId = course.getId();
boolean autoOrchestratorEnabled = courseConfigurationRepository.findAutoOrchestrationConfigByCourseId(courseId)
.map(CourseAutoOrchestrationConfigDTO::autoOrchestratorEnabled).orElse(false);
if (!autoOrchestratorEnabled) {
// Per-course kill switch is off: drop anything buffered while it was on so a later
// re-enable or scheduler tick cannot resurrect stale changes for a disabled course.
accumulator.flush(courseId);
return;
}
// Filter on the changed-field set: only record when the version touched a content-bearing
// field that could affect competency mapping. An empty set (e.g. legacy events) is treated
// as not relevant.
Set<String> changedFields = event.changedFields() == null ? Collections.emptySet() : event.changedFields();
if (Collections.disjoint(changedFields, ExerciseVersionService.COMPETENCY_RELEVANT_FIELDS)) {
log.debug("atlas.automatic skipping exercise change courseId={} exerciseId={}: no competency-relevant field changed (changed={})", courseId, exercise.getId(),
changedFields);
return;
}
log.debug("atlas.automatic recorded exercise change courseId={} exerciseId={}", courseId, exercise.getId());
accumulator.record(courseId, exercise.getId());
}
}
Loading
Loading