diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisProactivitySubSettings.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisProactivitySubSettings.java index f7a99867a14d..60eed305b88a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisProactivitySubSettings.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisProactivitySubSettings.java @@ -4,27 +4,43 @@ import java.util.Set; import jakarta.persistence.CascadeType; -import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; import com.fasterxml.jackson.annotation.JsonInclude; +import de.tum.cit.aet.artemis.core.domain.DomainObject; import de.tum.cit.aet.artemis.iris.domain.settings.event.IrisEventSettings; /** - * Represents the specific ingestion sub-settings of lectures for Iris. - * This class extends {@link IrisSubSettings} to provide settings required for lecture data ingestion. + * Represents the proactivity sub settings for Iris. */ @Entity -@DiscriminatorValue("PROACTIVITY") +@Table(name = "iris_proactivity_settings") +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) @JsonInclude(JsonInclude.Include.NON_EMPTY) -public class IrisProactivitySubSettings extends IrisSubSettings { +public class IrisProactivitySubSettings extends DomainObject implements IrisToggleableSetting { + + @Column(name = "enabled") + private boolean enabled = false; @OneToMany(mappedBy = "proactivitySubSettings", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) private Set eventSettings = new HashSet<>(); + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + public Set getEventSettings() { return eventSettings; } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSubSettings.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSubSettings.java index 2cf38ce2fc80..f64746f783f8 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSubSettings.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSubSettings.java @@ -41,12 +41,11 @@ @JsonSubTypes.Type(value = IrisChatSubSettings.class, name = "chat"), @JsonSubTypes.Type(value = IrisTextExerciseChatSubSettings.class, name = "text-exercise-chat"), @JsonSubTypes.Type(value = IrisLectureIngestionSubSettings.class, name = "lecture-ingestion"), - @JsonSubTypes.Type(value = IrisCompetencyGenerationSubSettings.class, name = "competency-generation"), - @JsonSubTypes.Type(value = IrisProactivitySubSettings.class, name = "proactivity") + @JsonSubTypes.Type(value = IrisCompetencyGenerationSubSettings.class, name = "competency-generation") }) // @formatter:on @JsonInclude(JsonInclude.Include.NON_EMPTY) -public abstract class IrisSubSettings extends DomainObject { +public abstract class IrisSubSettings extends DomainObject implements IrisToggleableSetting { @Column(name = "enabled") private boolean enabled = false; diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisToggleableSetting.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisToggleableSetting.java new file mode 100644 index 000000000000..23623282e00c --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisToggleableSetting.java @@ -0,0 +1,11 @@ +package de.tum.cit.aet.artemis.iris.domain.settings; + +/** + * Represents an Iris setting that can be toggled on or off. + */ +public interface IrisToggleableSetting { + + boolean isEnabled(); + + void setEnabled(boolean enabled); +} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/event/IrisBuildFailedEventSettings.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/event/IrisBuildFailedEventSettings.java index 5c9766c573f1..5b4c5804b45c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/event/IrisBuildFailedEventSettings.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/event/IrisBuildFailedEventSettings.java @@ -14,12 +14,12 @@ public class IrisBuildFailedEventSettings extends IrisEventSettings { @Override - public IrisEventTarget getDefaultLevel() { - return IrisEventTarget.EXERCISE; + public IrisEventSessionType getDefaultSessionType() { + return IrisEventSessionType.EXERCISE; } @Override - public String getDefaultPipelineVariant() { + public String getDefaultSelectedEventVariant() { return "build_failed"; } } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/event/IrisEventTarget.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/event/IrisEventSessionType.java similarity index 84% rename from src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/event/IrisEventTarget.java rename to src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/event/IrisEventSessionType.java index be8f94cf9e45..390826937422 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/event/IrisEventTarget.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/event/IrisEventSessionType.java @@ -3,6 +3,6 @@ /** * The target session type of Iris event. Currently, only EXERCISE and COURSE are supported. */ -public enum IrisEventTarget { +public enum IrisEventSessionType { EXERCISE, COURSE } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/event/IrisEventSettings.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/event/IrisEventSettings.java index 4d8e6fc864f7..ba87520384bd 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/event/IrisEventSettings.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/event/IrisEventSettings.java @@ -1,6 +1,10 @@ package de.tum.cit.aet.artemis.iris.domain.settings.event; +import java.util.SortedSet; +import java.util.TreeSet; + import jakarta.persistence.Column; +import jakarta.persistence.Convert; import jakarta.persistence.DiscriminatorColumn; import jakarta.persistence.DiscriminatorType; import jakarta.persistence.Entity; @@ -25,8 +29,18 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import de.tum.cit.aet.artemis.core.domain.DomainObject; +import de.tum.cit.aet.artemis.iris.domain.settings.IrisListConverter; import de.tum.cit.aet.artemis.iris.domain.settings.IrisProactivitySubSettings; +/** + * IrisEventSettings is an abstract super class for the specific sub event settings types. + * Sub Event Settings are settings for a proactive event of Iris. + * {@link IrisProgressStalledEventSettings} are used to specify settings for the progress stalled event. + * {@link IrisBuildFailedEventSettings} are used to specify settings for the build failed event. + * {@link IrisJolEventSettings} are used to specify settings for the JOL event. + *

+ * Also see {@link de.tum.cit.aet.artemis.iris.service.settings.IrisSettingsService} for more information. + */ @Entity @Table(name = "iris_event_settings") @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @@ -41,19 +55,22 @@ }) @JsonInclude(JsonInclude.Include.NON_EMPTY) public abstract class IrisEventSettings extends DomainObject { - // Is event active - @Column(name = "is_active", nullable = false) - private boolean isActive; + @Column(name = "enabled", nullable = false) + private boolean enabled; + + @Column(name = "allowed_event_variants", nullable = false) + @Convert(converter = IrisListConverter.class) + private SortedSet allowedEventVariants = new TreeSet<>(); - // The variant of the pipeline the event is associated with - @Column(name = "pipeline_variant", nullable = false) - private String pipelineVariant; + // The selected event variant of the pipeline the event is associated with + @Column(name = "selected_event_variant", nullable = false) + private String selectedEventVariant; - // The level of the event which type of session the event will be triggered in + // The session type of the event which type of session the event will be triggered in @Nullable @Enumerated(EnumType.STRING) - @Column(name = "target") - private IrisEventTarget target; + @Column(name = "session_type", nullable = false) + private IrisEventSessionType sessionType; @JsonIgnore @ManyToOne @@ -63,11 +80,11 @@ public abstract class IrisEventSettings extends DomainObject { @PrePersist @PreUpdate protected void onCreate() { - if (target == null) { - target = getDefaultLevel(); + if (sessionType == null) { + sessionType = getDefaultSessionType(); } - if (pipelineVariant == null) { - pipelineVariant = getDefaultPipelineVariant(); + if (selectedEventVariant == null) { + selectedEventVariant = getDefaultSelectedEventVariant(); } } @@ -79,29 +96,38 @@ public void setProactivitySubSettings(IrisProactivitySubSettings proactivitySubS this.proactivitySubSettings = proactivitySubSettings; } - public boolean isActive() { - return isActive; + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean active) { + enabled = active; } - public void setActive(boolean active) { - isActive = active; + public IrisEventSessionType getSessionType() { + return sessionType; } - public String getPipelineVariant() { - return pipelineVariant; + public SortedSet getAllowedEventVariants() { + return allowedEventVariants; } - public void setPipelineVariant(String pipelineVariant) { - this.pipelineVariant = pipelineVariant; + public void setAllowedEventVariants(SortedSet allowedEventVariants) { + this.allowedEventVariants = allowedEventVariants; + } + + @Nullable + public String getSelectedEventVariant() { + return selectedEventVariant; } - public IrisEventTarget getTarget() { - return target; + public void setSelectedEventVariant(@Nullable String selectedVariant) { + this.selectedEventVariant = selectedVariant; } @JsonIgnore - protected abstract IrisEventTarget getDefaultLevel(); + protected abstract IrisEventSessionType getDefaultSessionType(); @JsonIgnore - protected abstract String getDefaultPipelineVariant(); + protected abstract String getDefaultSelectedEventVariant(); } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/event/IrisJolEventSettings.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/event/IrisJolEventSettings.java index 5e7f12df3e12..3e719a1465b8 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/event/IrisJolEventSettings.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/event/IrisJolEventSettings.java @@ -14,12 +14,12 @@ public class IrisJolEventSettings extends IrisEventSettings { @Override - public IrisEventTarget getDefaultLevel() { - return IrisEventTarget.COURSE; + public IrisEventSessionType getDefaultSessionType() { + return IrisEventSessionType.COURSE; } @Override - public String getDefaultPipelineVariant() { + public String getDefaultSelectedEventVariant() { return "jol"; } } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/event/IrisProgressStalledEventSettings.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/event/IrisProgressStalledEventSettings.java index 28884b91d9d0..3419ad630d57 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/event/IrisProgressStalledEventSettings.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/event/IrisProgressStalledEventSettings.java @@ -14,12 +14,12 @@ public class IrisProgressStalledEventSettings extends IrisEventSettings { @Override - public IrisEventTarget getDefaultLevel() { - return IrisEventTarget.EXERCISE; + public IrisEventSessionType getDefaultSessionType() { + return IrisEventSessionType.EXERCISE; } @Override - public String getDefaultPipelineVariant() { + public String getDefaultSelectedEventVariant() { return "progress_stalled"; } } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedEventSettingsDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedEventSettingsDTO.java index 922b07e2a8d6..83e3254b29fb 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedEventSettingsDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedEventSettingsDTO.java @@ -4,13 +4,13 @@ import com.fasterxml.jackson.annotation.JsonInclude; +import de.tum.cit.aet.artemis.iris.domain.settings.event.IrisEventSessionType; import de.tum.cit.aet.artemis.iris.domain.settings.event.IrisEventSettings; -import de.tum.cit.aet.artemis.iris.domain.settings.event.IrisEventTarget; @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record IrisCombinedEventSettingsDTO(boolean isActive, String pipelineVariant, @Nullable IrisEventTarget target) { +public record IrisCombinedEventSettingsDTO(boolean enabled, String selectedEventVariant, @Nullable IrisEventSessionType sessionType) { public static IrisCombinedEventSettingsDTO of(IrisEventSettings eventSettings) { - return new IrisCombinedEventSettingsDTO(eventSettings.isActive(), eventSettings.getPipelineVariant(), eventSettings.getTarget()); + return new IrisCombinedEventSettingsDTO(eventSettings.isEnabled(), eventSettings.getSelectedEventVariant(), eventSettings.getSessionType()); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/IrisCompetencyGenerationService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/IrisCompetencyGenerationService.java index 98182ae92b06..8dcf090546f3 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/IrisCompetencyGenerationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/IrisCompetencyGenerationService.java @@ -2,6 +2,8 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_IRIS; +import java.util.Optional; + import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -48,6 +50,7 @@ public void executeCompetencyExtractionPipeline(User user, Course course, String pyrisPipelineService.executePipeline( "competency-extraction", "default", + Optional.empty(), pyrisJobService.createTokenForJob(token -> new CompetencyExtractionJob(token, course.getId(), user.getLogin())), executionDto -> new PyrisCompetencyExtractionPipelineExecutionDTO(executionDto, courseDescription, currentCompetencies, CompetencyTaxonomy.values(), 5), stages -> websocketService.send(user.getLogin(), websocketTopic(course.getId()), new PyrisCompetencyStatusUpdateDTO(stages, null)) diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisConnectorService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisConnectorService.java index f41de6b6c97d..cf1f842b6036 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisConnectorService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisConnectorService.java @@ -4,6 +4,7 @@ import java.util.Arrays; import java.util.List; +import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -60,13 +61,13 @@ public List getOfferedVariants(IrisSubSettingsType feature) thr try { var response = restTemplate.getForEntity(pyrisUrl + "/api/v1/pipelines/" + feature.name() + "/variants", PyrisVariantDTO[].class); if (!response.getStatusCode().is2xxSuccessful() || !response.hasBody()) { - throw new PyrisConnectorException("Could not fetch offered models"); + throw new PyrisConnectorException("Could not fetch offered variants"); } return Arrays.asList(response.getBody()); } catch (HttpStatusCodeException e) { - log.error("Failed to fetch offered models from Pyris", e); - throw new PyrisConnectorException("Could not fetch offered models"); + log.error("Failed to fetch offered variants from Pyris", e); + throw new PyrisConnectorException("Could not fetch offered variants"); } } @@ -77,8 +78,10 @@ public List getOfferedVariants(IrisSubSettingsType feature) thr * @param variant The variant of the feature to execute * @param executionDTO The DTO sent as a body for the execution */ - public void executePipeline(String feature, String variant, Object executionDTO) { + public void executePipeline(String feature, String variant, Object executionDTO, Optional event) { var endpoint = "/api/v1/pipelines/" + feature + "/" + variant + "/run"; + // Add event query parameter if present + endpoint += event.map(e -> "?event=" + e).orElse(""); try { restTemplate.postForEntity(pyrisUrl + endpoint, objectMapper.valueToTree(executionDTO), Void.class); } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisPipelineService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisPipelineService.java index bf879d9a3e12..524a756b32c0 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisPipelineService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisPipelineService.java @@ -98,7 +98,8 @@ public PyrisPipelineService(PyrisConnectorService pyrisConnectorService, PyrisJo * @param dtoMapper a function to create the concrete DTO type for this pipeline from the base DTO * @param statusUpdater a consumer to update the status of the pipeline execution */ - public void executePipeline(String name, String variant, String jobToken, Function dtoMapper, Consumer> statusUpdater) { + public void executePipeline(String name, String variant, Optional event, String jobToken, Function dtoMapper, + Consumer> statusUpdater) { // Define the preparation stages of pipeline execution with their initial states // There will be more stages added in Pyris later var preparing = new PyrisStageDTO("Preparing", 10, null, null); @@ -116,7 +117,7 @@ public void executePipeline(String name, String variant, String jobToken, Functi try { // Execute the pipeline using the connector service - pyrisConnectorService.executePipeline(name, variant, pipelineDto); + pyrisConnectorService.executePipeline(name, variant, pipelineDto, event); } catch (PyrisConnectorException | IrisException e) { log.error("Failed to execute {} pipeline", name, e); @@ -143,11 +144,13 @@ public void executePipeline(String name, String variant, String jobToken, Functi * @param session the chat session * @see PyrisPipelineService#executePipeline for more details on the pipeline execution process. */ - public void executeExerciseChatPipeline(String variant, Optional latestSubmission, ProgrammingExercise exercise, IrisExerciseChatSession session) { + public void executeExerciseChatPipeline(String variant, Optional latestSubmission, ProgrammingExercise exercise, IrisExerciseChatSession session, + Optional eventVariant) { // @formatter:off executePipeline( "tutor-chat", // TODO: Rename this to 'exercise-chat' with next breaking Pyris version variant, + eventVariant, pyrisJobService.addExerciseChatJob(exercise.getCourseViaExerciseGroupOrCourseMember().getId(), exercise.getId(), session.getId()), executionDto -> { var course = exercise.getCourseViaExerciseGroupOrCourseMember(); @@ -180,10 +183,10 @@ public void executeExerciseChatPipeline(String variant, Optional the type of the object * @param the type of the DTO */ - private void executeCourseChatPipeline(String variant, IrisCourseChatSession session, T eventObject, Class eventDtoClass) { + private void executeCourseChatPipeline(String variant, IrisCourseChatSession session, T eventObject, Class eventDtoClass, Optional eventVariant) { var courseId = session.getCourse().getId(); var studentId = session.getUser().getId(); - executePipeline("course-chat", variant, pyrisJobService.addCourseChatJob(courseId, session.getId()), executionDto -> { + executePipeline("course-chat", variant, eventVariant, pyrisJobService.addCourseChatJob(courseId, session.getId()), executionDto -> { var fullCourse = loadCourseWithParticipationOfStudent(courseId, studentId); return new PyrisCourseChatPipelineExecutionDTO(PyrisExtendedCourseDTO.of(fullCourse), learningMetricsService.getStudentCourseMetrics(session.getUser().getId(), courseId), generateEventPayloadFromObjectType(eventDtoClass, eventObject), @@ -209,9 +212,9 @@ private void executeCourseChatPipeline(String variant, IrisCourseChatSess public void executeCourseChatPipeline(String variant, IrisCourseChatSession session, Object object) { log.debug("Executing course chat pipeline variant {} with object {}", variant, object); switch (object) { - case null -> executeCourseChatPipeline(variant, session, null, null); - case CompetencyJol competencyJol -> executeCourseChatPipeline(variant, session, competencyJol, CompetencyJolDTO.class); - case Exercise exercise -> executeCourseChatPipeline(variant, session, exercise, PyrisExerciseWithStudentSubmissionsDTO.class); + case null -> executeCourseChatPipeline(variant, session, null, null, Optional.empty()); + case CompetencyJol competencyJol -> executeCourseChatPipeline(variant, session, competencyJol, CompetencyJolDTO.class, Optional.of("jol")); + case Exercise exercise -> executeCourseChatPipeline(variant, session, exercise, PyrisExerciseWithStudentSubmissionsDTO.class, Optional.empty()); default -> throw new UnsupportedOperationException("Unsupported Pyris event payload type: " + object); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisCourseChatSessionService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisCourseChatSessionService.java index c2787fe1b56d..be73e37d2ba8 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisCourseChatSessionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisCourseChatSessionService.java @@ -4,8 +4,6 @@ import java.time.LocalDate; import java.time.ZoneId; -import java.util.HashSet; -import java.util.List; import java.util.Objects; import java.util.concurrent.CompletableFuture; @@ -17,16 +15,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; -import de.tum.cit.aet.artemis.assessment.domain.Result; import de.tum.cit.aet.artemis.atlas.domain.competency.CompetencyJol; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.domain.User; -import de.tum.cit.aet.artemis.core.exception.AccessForbiddenAlertException; import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException; -import de.tum.cit.aet.artemis.core.exception.ConflictException; import de.tum.cit.aet.artemis.core.security.Role; import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; -import de.tum.cit.aet.artemis.exercise.domain.Team; import de.tum.cit.aet.artemis.exercise.repository.StudentParticipationRepository; import de.tum.cit.aet.artemis.exercise.repository.SubmissionRepository; import de.tum.cit.aet.artemis.iris.domain.message.IrisMessage; @@ -44,8 +38,6 @@ import de.tum.cit.aet.artemis.iris.service.pyris.job.CourseChatJob; import de.tum.cit.aet.artemis.iris.service.settings.IrisSettingsService; import de.tum.cit.aet.artemis.iris.service.websocket.IrisChatWebsocketService; -import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; -import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation; /** * Service to handle the course chat subsystem of Iris. @@ -183,68 +175,7 @@ public void onJudgementOfLearningSet(CompetencyJol competencyJol) { var user = competencyJol.getUser(); user.hasAcceptedIrisElseThrow(); var session = getCurrentSessionOrCreateIfNotExistsInternal(course, user, false); - CompletableFuture.runAsync(() -> requestAndHandleResponse(session, "jol", competencyJol)); - } - - /** - * Triggers the course chat in response to a new submission for a non-exam exercise. - * Triggers the pipeline only the first time a user successfully submits an exercise. - * Subsequent successful submissions are ignored. - * - * @param result The submission event to trigger the course chat for - * @throws ConflictException If the exercise is an exam exercise - * @throws AccessForbiddenAlertException If the course chat is not enabled for the course - */ - public void onSubmissionSuccess(Result result) { - var participation = result.getParticipation(); - if (!(participation instanceof ProgrammingExerciseStudentParticipation studentParticipation)) { - return; - } - var exercise = (ProgrammingExercise) participation.getExercise(); - - if (exercise.isExamExercise()) { - throw new ConflictException("Iris is not supported for exam exercises", "Iris", "irisExamExercise"); - } - var course = exercise.getCourseViaExerciseGroupOrCourseMember(); - - irisSettingsService.isActivatedForElseThrow(IrisEventType.PROGRESS_STALLED, course); - - log.info("Submission was successful for user {}", studentParticipation.getParticipant().getName()); - // The submission was successful, so we inform Iris about the successful submission, - // but before we do that, we check if this is the first successful time out of all submissions out of all submissions for this exercise - var allSubmissions = submissionRepository.findAllWithResultsAndAssessorByParticipationId(studentParticipation.getId()); - var latestSubmission = allSubmissions.getLast(); - var allSuccessful = allSubmissions.stream().filter(submission -> submission.getLatestResult() != null && submission.getLatestResult().getScore() >= SUCCESS_THRESHOLD) - .count(); - if (allSuccessful == 1 && Objects.requireNonNull(latestSubmission.getLatestResult()).getScore() >= SUCCESS_THRESHOLD) { - log.info("First successful submission for user {}", studentParticipation.getParticipant().getName()); - var participant = studentParticipation.getParticipant(); - if (participant instanceof User user) { - setStudentParticipationsToExercise(user.getId(), exercise); - var session = getCurrentSessionOrCreateIfNotExistsInternal(course, user, false); - CompletableFuture.runAsync(() -> requestAndHandleResponse(session, "submission_successful", exercise)); - } - else { - var team = (Team) participant; - var teamMembers = team.getStudents(); - for (var user : teamMembers) { - setStudentParticipationsToExercise(user.getId(), exercise); - var session = getCurrentSessionOrCreateIfNotExistsInternal(course, user, false); - CompletableFuture.runAsync(() -> requestAndHandleResponse(session, "submission_successful", exercise)); - } - } - } - else { - log.info("User {} has already successfully submitted before, so we do not inform Iris about the successful submission", - studentParticipation.getParticipant().getName()); - } - } - - private void setStudentParticipationsToExercise(Long studentId, ProgrammingExercise exercise) { - // TODO: Write a repository function to pull student participations for a specific exercise instead of a list of exercises - var studentParticipation = new HashSet<>( - studentParticipationRepository.findByStudentIdAndIndividualExercisesWithEagerSubmissionsResultIgnoreTestRuns(studentId, List.of(exercise))); - exercise.setStudentParticipations(studentParticipation); + CompletableFuture.runAsync(() -> requestAndHandleResponse(session, "default", competencyJol)); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisExerciseChatSessionService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisExerciseChatSessionService.java index 6f9d505dc48a..55085e7380f4 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisExerciseChatSessionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisExerciseChatSessionService.java @@ -165,7 +165,7 @@ public void checkRateLimit(User user) { */ @Override public void requestAndHandleResponse(IrisExerciseChatSession session) { - requestAndHandleResponse(session, "default"); + requestAndHandleResponse(session, Optional.empty()); } /** @@ -173,9 +173,9 @@ public void requestAndHandleResponse(IrisExerciseChatSession session) { * and sending it to the student via the Websocket. * * @param session The chat session to send to the LLM - * @param variant The variant of the pipeline to use + * @param event The event to trigger on Pyris side */ - public void requestAndHandleResponse(IrisExerciseChatSession session, String variant) { + public void requestAndHandleResponse(IrisExerciseChatSession session, Optional event) { var chatSession = (IrisExerciseChatSession) irisSessionRepository.findByIdWithMessagesAndContents(session.getId()); if (chatSession.getExercise().isExamExercise()) { throw new ConflictException("Iris is not supported for exam exercises", "Iris", "irisExamExercise"); @@ -184,7 +184,7 @@ public void requestAndHandleResponse(IrisExerciseChatSession session, String var var latestSubmission = getLatestSubmissionIfExists(exercise, chatSession.getUser()); var variant = irisSettingsService.getCombinedIrisSettingsFor(session.getExercise(), false).irisChatSettings().selectedVariant(); - pyrisPipelineService.executeExerciseChatPipeline(variant, latestSubmission, exercise, chatSession); + pyrisPipelineService.executeExerciseChatPipeline(variant, latestSubmission, exercise, chatSession, event); } /** @@ -210,7 +210,7 @@ public void onBuildFailure(Result result) { if (participant instanceof User user) { var session = getCurrentSessionOrCreateIfNotExistsInternal(exercise, user, false); log.info("Build failed for user {}", user.getName()); - CompletableFuture.runAsync(() -> requestAndHandleResponse(session, "build_failed")); + CompletableFuture.runAsync(() -> requestAndHandleResponse(session, Optional.of("build_failed"))); } else { throw new ConflictException("Build failure event is not supported for team participations", "Iris", "irisTeamParticipation"); @@ -250,7 +250,7 @@ public void onNewResult(Result result) { var participant = ((ProgrammingExerciseStudentParticipation) participation).getParticipant(); if (participant instanceof User user) { var session = getCurrentSessionOrCreateIfNotExistsInternal(exercise, user, false); - CompletableFuture.runAsync(() -> requestAndHandleResponse(session, "progress_stalled")); + CompletableFuture.runAsync(() -> requestAndHandleResponse(session, Optional.of("progress_stalled"))); } else { throw new ConflictException("Progress stalled event is not supported for team participations", "Iris", "irisTeamParticipation"); diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisTextExerciseChatSessionService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisTextExerciseChatSessionService.java index 4520417aad48..6a2347c8ac1d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisTextExerciseChatSessionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisTextExerciseChatSessionService.java @@ -1,6 +1,7 @@ package de.tum.cit.aet.artemis.iris.service.session; import java.util.Comparator; +import java.util.Optional; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -103,6 +104,7 @@ public void requestAndHandleResponse(IrisTextExerciseChatSession irisSession) { pyrisPipelineService.executePipeline( "text-exercise-chat", "default", + Optional.empty(), pyrisJobService.createTokenForJob(token -> new TextExerciseChatJob(token, course.getId(), exercise.getId(), session.getId())), dto -> new PyrisTextExerciseChatPipelineExecutionDTO(dto, PyrisTextExerciseDTO.of(exercise), conversation, latestSubmissionText), stages -> irisChatWebsocketService.sendMessage(session, null, stages) diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSettingsService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSettingsService.java index 66a7eae407a4..f3d79ca31ec9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSettingsService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSettingsService.java @@ -133,7 +133,11 @@ private void initializeIrisLectureIngestionSettings(IrisGlobalSettings settings) private void initializeIrisProactiveSettings(IrisGlobalSettings settings) { var irisProactivitySettings = settings.getIrisProactivitySettings(); - irisProactivitySettings = initializeSettings(irisProactivitySettings, IrisProactivitySubSettings::new); + + if (irisProactivitySettings == null) { + irisProactivitySettings = new IrisProactivitySubSettings(); + irisProactivitySettings.setEnabled(false); + } initializeIrisEventSettings(irisProactivitySettings); settings.setIrisProactivitySettings(irisProactivitySettings); } @@ -142,15 +146,15 @@ private void initializeIrisEventSettings(IrisProactivitySubSettings settings) { HashSet eventSettings = new HashSet<>(); var jolEventSettings = new IrisJolEventSettings(); - jolEventSettings.setActive(false); + jolEventSettings.setEnabled(false); eventSettings.add(jolEventSettings); var submissionFailedEventSettings = new IrisBuildFailedEventSettings(); - submissionFailedEventSettings.setActive(false); + submissionFailedEventSettings.setEnabled(false); eventSettings.add(submissionFailedEventSettings); var submissionSuccessfulEventSettings = new IrisProgressStalledEventSettings(); - submissionSuccessfulEventSettings.setActive(false); + submissionSuccessfulEventSettings.setEnabled(false); eventSettings.add(submissionSuccessfulEventSettings); eventSettings.forEach(event -> event.setProactivitySubSettings(settings)); @@ -673,7 +677,7 @@ private boolean isFeatureEnabledInSettings(IrisCombinedSettingsDTO settings, Iri */ private boolean isEventEnabledInSettings(IrisCombinedEventSettingsDTO settings, IrisEventType type) { return switch (type) { - case JOL, PROGRESS_STALLED, BUILD_FAILED -> settings.isActive(); + case JOL, PROGRESS_STALLED, BUILD_FAILED -> settings.enabled(); }; } } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSubSettingsService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSubSettingsService.java index 0441f2a1192d..8c6a962d592f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSubSettingsService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSubSettingsService.java @@ -28,6 +28,7 @@ import de.tum.cit.aet.artemis.iris.domain.settings.IrisSettingsType; import de.tum.cit.aet.artemis.iris.domain.settings.IrisSubSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisTextExerciseChatSubSettings; +import de.tum.cit.aet.artemis.iris.domain.settings.IrisToggleableSetting; import de.tum.cit.aet.artemis.iris.domain.settings.event.IrisBuildFailedEventSettings; import de.tum.cit.aet.artemis.iris.domain.settings.event.IrisEventSettings; import de.tum.cit.aet.artemis.iris.domain.settings.event.IrisJolEventSettings; @@ -347,7 +348,7 @@ public IrisCombinedProactivitySubSettingsDTO combineProactivitySettings(ArrayLis * @param subSettingsFunction Function to get the sub settings from an IrisSettings object. * @return Combined enabled field. */ - private boolean getCombinedEnabled(List settingsList, Function subSettingsFunction) { + private boolean getCombinedEnabled(List settingsList, Function subSettingsFunction) { for (var irisSettings : settingsList) { if (irisSettings == null) { return false; diff --git a/src/main/resources/config/liquibase/changelog/20240714145600_changelog.xml b/src/main/resources/config/liquibase/changelog/20241021221900_changelog.xml similarity index 54% rename from src/main/resources/config/liquibase/changelog/20240714145600_changelog.xml rename to src/main/resources/config/liquibase/changelog/20241021221900_changelog.xml index 43d11fda41ea..703ab4db4fd0 100644 --- a/src/main/resources/config/liquibase/changelog/20240714145600_changelog.xml +++ b/src/main/resources/config/liquibase/changelog/20241021221900_changelog.xml @@ -1,17 +1,21 @@ - - - - - - + + + + + + + + + + + + @@ -23,27 +27,27 @@ - + - + + + - - - - + - + + referencedTableName="iris_proactivity_settings"/> + + referencedTableName="iris_proactivity_settings"/> diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index b993f9e6e9f0..a65fabe86825 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -18,7 +18,6 @@ - @@ -29,6 +28,7 @@ + diff --git a/src/main/webapp/app/entities/iris/settings/iris-event-settings.model.ts b/src/main/webapp/app/entities/iris/settings/iris-event-settings.model.ts index 8d18b562b927..f831ddc82070 100644 --- a/src/main/webapp/app/entities/iris/settings/iris-event-settings.model.ts +++ b/src/main/webapp/app/entities/iris/settings/iris-event-settings.model.ts @@ -1,6 +1,6 @@ import { BaseEntity } from 'app/shared/model/base-entity'; -export enum IrisEventLevel { +export enum IrisEventSessionType { COURSE = 'COURSE', EXERCISE = 'EXERCISE', } @@ -14,24 +14,24 @@ export enum IrisEventType { export class IrisEventSettings implements BaseEntity { id?: number; type: IrisEventType; - active = false; - pipelineVariant: string; - target: IrisEventLevel; + enabled = false; + selectedEventVariant: string; + sessionType: IrisEventSessionType; } export class JolEventSettings extends IrisEventSettings { - target = IrisEventLevel.COURSE; + sessionType = IrisEventSessionType.COURSE; type = IrisEventType.JOL; - pipelineVariant = 'jol'; + selectedEventVariant = 'jol'; } export class ProgressStalledEventSettings extends IrisEventSettings { - target = IrisEventLevel.COURSE; + sessionType = IrisEventSessionType.COURSE; type = IrisEventType.PROGRESS_STALLED; - pipelineVariant = 'progress_stalled'; + selectedEventVariant = 'progress_stalled'; } export class BuildFailedEventSettings extends IrisEventSettings { - target = IrisEventLevel.EXERCISE; + sessionType = IrisEventSessionType.EXERCISE; type = IrisEventType.BUILD_FAILED; - pipelineVariant = 'build_failed'; + selectedEventVariant = 'build_failed'; } diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-event-settings-update/iris-event-settings-update.component.html b/src/main/webapp/app/iris/settings/iris-settings-update/iris-event-settings-update/iris-event-settings-update.component.html index a4f36ddaeead..088c6a1f055f 100644 --- a/src/main/webapp/app/iris/settings/iris-settings-update/iris-event-settings-update/iris-event-settings-update.component.html +++ b/src/main/webapp/app/iris/settings/iris-settings-update/iris-event-settings-update/iris-event-settings-update.component.html @@ -1,17 +1,17 @@

{{ 'artemisApp.iris.settings.subSettings.proactivitySettings.eventSettings.active' | artemisTranslate }}
{{ 'artemisApp.iris.settings.subSettings.proactivitySettings.eventSettings.inactive' | artemisTranslate }}
diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-event-settings-update/iris-event-settings-update.component.ts b/src/main/webapp/app/iris/settings/iris-settings-update/iris-event-settings-update/iris-event-settings-update.component.ts index 97e84d2ba7ef..d5277cc5aacf 100644 --- a/src/main/webapp/app/iris/settings/iris-settings-update/iris-event-settings-update/iris-event-settings-update.component.ts +++ b/src/main/webapp/app/iris/settings/iris-settings-update/iris-event-settings-update/iris-event-settings-update.component.ts @@ -28,13 +28,13 @@ export class IrisEventSettingsUpdateComponent { private proactivityDisabledSignal = signal(false); inheritDisabled = computed(() => - this.parentEventSettings !== undefined ? this.proactivityDisabledSignal() || !this.parentEventSettings.active : this.proactivityDisabledSignal(), + this.parentEventSettings !== undefined ? this.proactivityDisabledSignal() || !this.parentEventSettings.enabled : this.proactivityDisabledSignal(), ); isSettingsSwitchDisabled = computed(() => (!this.isAdmin() && this.settingsType !== IrisSettingsType.EXERCISE) || this.inheritDisabled()); // Computed properties settings = computed(() => this.settingsSignal()); - active = computed(() => this.settings().active); + enabled = computed(() => this.settings().enabled); isAdmin = signal(false); // Constants diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-settings-update.component.html b/src/main/webapp/app/iris/settings/iris-settings-update/iris-settings-update.component.html index 1eecb5abec5c..985e9249030e 100644 --- a/src/main/webapp/app/iris/settings/iris-settings-update/iris-settings-update.component.html +++ b/src/main/webapp/app/iris/settings/iris-settings-update/iris-settings-update.component.html @@ -73,7 +73,6 @@

diff --git a/src/test/java/de/tum/cit/aet/artemis/iris/AbstractIrisIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/iris/AbstractIrisIntegrationTest.java index 1b62080cb587..92f18fb61433 100644 --- a/src/test/java/de/tum/cit/aet/artemis/iris/AbstractIrisIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/iris/AbstractIrisIntegrationTest.java @@ -19,6 +19,7 @@ import de.tum.cit.aet.artemis.core.connector.IrisRequestMockProvider; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.exercise.domain.Exercise; +import de.tum.cit.aet.artemis.iris.domain.settings.IrisProactivitySubSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisSubSettings; import de.tum.cit.aet.artemis.iris.domain.settings.event.IrisBuildFailedEventSettings; import de.tum.cit.aet.artemis.iris.domain.settings.event.IrisEventSettings; @@ -66,7 +67,7 @@ protected void activateIrisGlobally() { activateSubSettings(globalSettings.getIrisLectureIngestionSettings()); activateSubSettings(globalSettings.getIrisCompetencyGenerationSettings()); activateSubSettings(globalSettings.getIrisTextExerciseChatSettings()); - activateSubSettings(globalSettings.getIrisProactivitySettings()); + activateProactivitySettings(globalSettings.getIrisProactivitySettings()); // Active Iris events var eventSettings = globalSettings.getIrisProactivitySettings().getEventSettings(); @@ -89,6 +90,15 @@ private void activateSubSettings(IrisSubSettings settings) { settings.setAllowedVariants(new TreeSet<>(Set.of("default"))); } + /** + * Activates the proactivity settings. + * + * @param settings the proactivity settings to be activated + */ + private void activateProactivitySettings(IrisProactivitySubSettings settings) { + settings.setEnabled(true); + } + /** * Activates the given event settings for the given type of event. * @@ -97,7 +107,7 @@ private void activateSubSettings(IrisSubSettings settings) { */ private void activateEventSettingsFor(Class eventSettingsClass, Set settings) { settings.stream().filter(e -> e != null && e.getClass() == eventSettingsClass).forEach(e -> { - e.setActive(true); + e.setEnabled(true); }); } @@ -122,7 +132,7 @@ public void deactivateEventSettingsFor(Class ev * @param settings the settings to be deactivated */ private void deactivateEventSettingsFor(Class eventSettingsClass, Set settings) { - settings.stream().filter(e -> e != null && e.getClass() == eventSettingsClass).forEach(e -> e.setActive(false)); + settings.stream().filter(e -> e != null && e.getClass() == eventSettingsClass).forEach(e -> e.setEnabled(false)); } protected void activateIrisFor(Course course) { @@ -136,7 +146,7 @@ protected void activateIrisFor(Course course) { activateSubSettings(courseSettings.getIrisTextExerciseChatSettings()); - activateSubSettings(courseSettings.getIrisProactivitySettings()); + activateProactivitySettings(courseSettings.getIrisProactivitySettings()); // Active Iris events var eventSettings = courseSettings.getIrisProactivitySettings().getEventSettings(); diff --git a/src/test/java/de/tum/cit/aet/artemis/iris/PyrisConnectorServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/iris/PyrisConnectorServiceTest.java index de726729a035..b2d1d2121e43 100644 --- a/src/test/java/de/tum/cit/aet/artemis/iris/PyrisConnectorServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/iris/PyrisConnectorServiceTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.util.Optional; import java.util.stream.Stream; import org.junit.jupiter.params.ParameterizedTest; @@ -40,7 +41,7 @@ private static Stream irisExceptions() { void testExceptionV2(int httpStatus, Class exceptionClass) { irisRequestMockProvider.mockRunError(httpStatus); - assertThatThrownBy(() -> pyrisConnectorService.executePipeline("tutor-chat", "default", null)).isInstanceOf(exceptionClass); + assertThatThrownBy(() -> pyrisConnectorService.executePipeline("tutor-chat", "default", null, Optional.empty())).isInstanceOf(exceptionClass); } @ParameterizedTest