Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Iris: Improve event handling for predictable scenarios #10025

Open
wants to merge 12 commits into
base: develop
Choose a base branch
from
Open
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import de.tum.cit.aet.artemis.atlas.repository.CompetencyRepository;
import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException;
import de.tum.cit.aet.artemis.core.repository.UserRepository;
import de.tum.cit.aet.artemis.iris.service.pyris.PyrisEventService;
import de.tum.cit.aet.artemis.iris.service.pyris.PyrisEventPublisherService;
import de.tum.cit.aet.artemis.iris.service.pyris.event.CompetencyJolSetEvent;

/**
Expand All @@ -45,15 +45,15 @@ public class CompetencyJolService {

private final UserRepository userRepository;

private final Optional<PyrisEventService> pyrisEventService;
private final Optional<PyrisEventPublisherService> pyrisEventPublisher;

public CompetencyJolService(CompetencyJolRepository competencyJolRepository, CompetencyRepository competencyRepository,
CompetencyProgressRepository competencyProgressRepository, UserRepository userRepository, Optional<PyrisEventService> pyrisEventService) {
CompetencyProgressRepository competencyProgressRepository, UserRepository userRepository, Optional<PyrisEventPublisherService> pyrisEventPublisher) {
this.competencyJolRepository = competencyJolRepository;
this.competencyRepository = competencyRepository;
this.competencyProgressRepository = competencyProgressRepository;
this.userRepository = userRepository;
this.pyrisEventService = pyrisEventService;
this.pyrisEventPublisher = pyrisEventPublisher;
}

/**
Expand Down Expand Up @@ -84,10 +84,10 @@ public void setJudgementOfLearning(long competencyId, long userId, short jolValu
final var jol = createCompetencyJol(competencyId, userId, jolValue, ZonedDateTime.now(), competencyProgress);
competencyJolRepository.save(jol);

pyrisEventService.ifPresent(service -> {
// Inform Iris so it can send a message to the user
// Inform Iris so it can send a message to the user
pyrisEventPublisher.ifPresent(service -> {
try {
service.trigger(new CompetencyJolSetEvent(jol));
service.publishEvent(new CompetencyJolSetEvent(this, jol));
}
catch (Exception e) {
log.warn("Something went wrong while sending the judgement of learning to Iris", e);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package de.tum.cit.aet.artemis.iris.service.pyris;

import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_IRIS;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;

import de.tum.cit.aet.artemis.iris.domain.settings.event.IrisEventType;
import de.tum.cit.aet.artemis.iris.service.pyris.event.CompetencyJolSetEvent;
import de.tum.cit.aet.artemis.iris.service.pyris.event.NewResultEvent;
import de.tum.cit.aet.artemis.iris.service.pyris.event.PyrisEvent;
import de.tum.cit.aet.artemis.iris.service.settings.IrisSettingsService;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingSubmission;

/**
* Service for publishing Pyris events.
*/
@Service
@Profile(PROFILE_IRIS)
public class PyrisEventPublisherService {

private static final Logger log = LoggerFactory.getLogger(PyrisEventPublisherService.class);

private final ApplicationEventPublisher eventPublisher;

private final IrisSettingsService irisSettingsService;

public PyrisEventPublisherService(ApplicationEventPublisher eventPublisher, IrisSettingsService irisSettingsService) {
this.eventPublisher = eventPublisher;
this.irisSettingsService = irisSettingsService;
}

/**
* Publishes the given event.
*
* @param event the event to publish
*/
public void publishEvent(PyrisEvent event) {
if (!isEventEnabled(event)) {
log.debug("Skipping event publication as conditions are not met: {}", event.getClass().getSimpleName());
return;
}
try {
eventPublisher.publishEvent(event);
}
catch (Exception e) {
log.error("Failed to publish event: {}", event, e);
throw e;
}
}

/**
* Checks if the given event is enabled.
*
* @param event the event to check
* @return true if the event is enabled and conditions are met, false otherwise
*/
private boolean isEventEnabled(PyrisEvent event) {
return switch (event) {
case NewResultEvent newResultEvent -> {
var result = newResultEvent.getResult();
if (result == null) {
yield false;
}
var submission = result.getSubmission();
if (submission == null) {
yield false;
}
if (submission instanceof ProgrammingSubmission programmingSubmission) {
var participation = programmingSubmission.getParticipation();
if (participation == null) {
yield false;
}
var programmingExercise = participation.getExercise();
if (programmingExercise == null) {
yield false;
}
if (programmingSubmission.isBuildFailed()) {
yield irisSettingsService.isActivatedFor(IrisEventType.BUILD_FAILED, programmingExercise);
}
else {
yield irisSettingsService.isActivatedFor(IrisEventType.PROGRESS_STALLED, programmingExercise);
}
}
else {
yield false;
}
}
case CompetencyJolSetEvent competencyJolSetEvent -> {
var competencyJol = competencyJolSetEvent.getCompetencyJol();
if (competencyJol == null) {
yield false;
}
var competency = competencyJol.getCompetency();
if (competency == null) {
yield false;
}
var course = competency.getCourse();
if (course == null) {
yield false;
}
yield irisSettingsService.isActivatedFor(IrisEventType.JOL, course);
}
default -> throw new UnsupportedPyrisEventException("Unsupported Pyris event: " + event.getClass().getSimpleName());
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,17 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;

import de.tum.cit.aet.artemis.iris.domain.session.IrisChatSession;
import de.tum.cit.aet.artemis.iris.service.pyris.event.CompetencyJolSetEvent;
import de.tum.cit.aet.artemis.iris.service.pyris.event.NewResultEvent;
import de.tum.cit.aet.artemis.iris.service.pyris.event.PyrisEvent;
import de.tum.cit.aet.artemis.iris.service.session.AbstractIrisChatSessionService;
import de.tum.cit.aet.artemis.iris.service.session.IrisCourseChatSessionService;
import de.tum.cit.aet.artemis.iris.service.session.IrisExerciseChatSessionService;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingSubmission;

/**
* Service to handle Pyris events.
* Service for handling Pyris events.
*/
@Service
@Profile(PROFILE_IRIS)
Expand All @@ -34,32 +33,53 @@ public PyrisEventService(IrisCourseChatSessionService irisCourseChatSessionServi
}

/**
* Triggers a Pyris pipeline based on the received {@link PyrisEvent}.
* Handles a CompetencyJolSetEvent. The event is passed to the {@link de.tum.cit.aet.artemis.iris.service.session.IrisCourseChatSessionService} to handle the judgement of
* learning set.
*
* @param event The event object received to trigger the matching pipeline
* @throws UnsupportedPyrisEventException if the event is not supported
* @see IrisCourseChatSessionService#onJudgementOfLearningSet
* @param event the {@link CompetencyJolSetEvent} to handle
*/
@EventListener
public void handleCompetencyJolSetEvent(CompetencyJolSetEvent event) {
log.debug("Processing CompetencyJolSetEvent");
kaancayli marked this conversation as resolved.
Show resolved Hide resolved
try {
irisCourseChatSessionService.onJudgementOfLearningSet(event.getCompetencyJol());
log.debug("Successfully processed CompetencyJolSetEvent");
}
catch (Exception e) {
log.error("Failed to process CompetencyJolSetEvent: {}", event, e);
throw e;
}
}

/**
* Handles a NewResultEvent. A new result represents a new submission result.
* Depending on whether there was a build failure or not, the result is passed to the appropriate handler method inside the
* {@link de.tum.cit.aet.artemis.iris.service.session.IrisExerciseChatSessionService}.
*
* @see PyrisEvent
* @see IrisExerciseChatSessionService#onBuildFailure
* @see IrisExerciseChatSessionService#onNewResult
* @param event the {@link NewResultEvent} to handle
*/
public void trigger(PyrisEvent<? extends AbstractIrisChatSessionService<? extends IrisChatSession>, ?> event) {
log.debug("Starting to process event of type: {}", event.getClass().getSimpleName());
@EventListener
public void handleNewResultEvent(NewResultEvent event) {
log.debug("Processing NewResultEvent");
try {
switch (event) {
case CompetencyJolSetEvent competencyJolSetEvent -> {
log.info("Processing CompetencyJolSetEvent: {}", competencyJolSetEvent);
competencyJolSetEvent.handleEvent(irisCourseChatSessionService);
log.debug("Successfully processed CompetencyJolSetEvent");
var result = event.getResult();
var submission = result.getSubmission();

kaancayli marked this conversation as resolved.
Show resolved Hide resolved
if (submission instanceof ProgrammingSubmission programmingSubmission) {
if (programmingSubmission.isBuildFailed()) {
irisExerciseChatSessionService.onBuildFailure(result);
}
case NewResultEvent newResultEvent -> {
log.info("Processing NewResultEvent: {}", newResultEvent);
newResultEvent.handleEvent(irisExerciseChatSessionService);
log.debug("Successfully processed NewResultEvent");
else {
irisExerciseChatSessionService.onNewResult(result);
}
default -> throw new UnsupportedPyrisEventException("Unsupported event type: " + event.getClass().getSimpleName());
}
log.debug("Successfully processed NewResultEvent");
}
catch (Exception e) {
log.error("Failed to process event: {}", event, e);
log.error("Failed to process NewResultEvent: {}", event, e);
throw e;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
package de.tum.cit.aet.artemis.iris.service.pyris.event;

import java.util.Optional;

import de.tum.cit.aet.artemis.atlas.domain.competency.CompetencyJol;
import de.tum.cit.aet.artemis.iris.service.session.IrisCourseChatSessionService;
import de.tum.cit.aet.artemis.core.domain.User;

public class CompetencyJolSetEvent extends PyrisEvent<IrisCourseChatSessionService, CompetencyJol> {
public class CompetencyJolSetEvent extends PyrisEvent {

private final CompetencyJol eventObject;
private final CompetencyJol competencyJol;

public CompetencyJolSetEvent(CompetencyJol eventObject) {
if (eventObject == null) {
throw new IllegalArgumentException("Event object cannot be null");
public CompetencyJolSetEvent(Object source, CompetencyJol competencyJol) {
super(source);
if (competencyJol == null) {
throw new IllegalArgumentException("CompetencyJol cannot be null");
}
this.eventObject = eventObject;
this.competencyJol = competencyJol;
}

public CompetencyJol getCompetencyJol() {
return competencyJol;
}

@Override
public void handleEvent(IrisCourseChatSessionService service) {
service.onJudgementOfLearningSet(eventObject);
public Optional<User> getUser() {
return Optional.ofNullable(competencyJol.getUser());
}
}
Original file line number Diff line number Diff line change
@@ -1,34 +1,32 @@
package de.tum.cit.aet.artemis.iris.service.pyris.event;

import java.util.Optional;

import de.tum.cit.aet.artemis.assessment.domain.Result;
import de.tum.cit.aet.artemis.iris.service.session.IrisExerciseChatSessionService;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingSubmission;
import de.tum.cit.aet.artemis.core.domain.User;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation;

public class NewResultEvent extends PyrisEvent<IrisExerciseChatSessionService, Result> {
public class NewResultEvent extends PyrisEvent {

private final Result eventObject;
private final Result result;

public NewResultEvent(Result eventObject) {
if (eventObject == null) {
throw new IllegalArgumentException("Event object cannot be null");
public NewResultEvent(Object source, Result result) {
super(source);
if (result == null) {
throw new IllegalArgumentException("Result cannot be null");
}
this.eventObject = eventObject;
this.result = result;
}

public Result getResult() {
return result;
}

@Override
public void handleEvent(IrisExerciseChatSessionService service) {
if (service == null) {
throw new IllegalArgumentException("Service cannot be null");
}
var submission = eventObject.getSubmission();
// We only care about programming submissions
if (submission instanceof ProgrammingSubmission programmingSubmission) {
if (programmingSubmission.isBuildFailed()) {
service.onBuildFailure(eventObject);
}
else {
service.onNewResult(eventObject);
}
public Optional<User> getUser() {
if (result.getSubmission() != null && result.getSubmission().getParticipation() instanceof ProgrammingExerciseStudentParticipation studentParticipation) {
return studentParticipation.getStudent();
}
return Optional.empty();
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
package de.tum.cit.aet.artemis.iris.service.pyris.event;

import de.tum.cit.aet.artemis.iris.domain.session.IrisChatSession;
import de.tum.cit.aet.artemis.iris.service.session.AbstractIrisChatSessionService;
import java.util.Optional;

public abstract class PyrisEvent<S extends AbstractIrisChatSessionService<? extends IrisChatSession>, T> {
import org.springframework.context.ApplicationEvent;

import de.tum.cit.aet.artemis.core.domain.User;

/**
* Base class for Pyris events.
*/
public abstract class PyrisEvent extends ApplicationEvent {

public PyrisEvent(Object source) {
super(source);
}

/**
* Handles the event using the given service.
* Returns the user associated with this event.
*
* @param service The service to handle the event for
* @return the user
*/
public abstract void handleEvent(S service);
public abstract Optional<User> getUser();
}
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,6 @@ protected void setLLMTokenUsageParameters(LLMTokenUsageService.LLMTokenUsageBuil
*/
public void onJudgementOfLearningSet(CompetencyJol competencyJol) {
var course = competencyJol.getCompetency().getCourse();
if (!irisSettingsService.isEnabledFor(IrisSubSettingsType.COURSE_CHAT, course)) {
return;
}
var user = competencyJol.getUser();
user.hasAcceptedIrisElseThrow();
var session = getCurrentSessionOrCreateIfNotExistsInternal(course, user, false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,6 @@ public void onBuildFailure(Result result) {
}
var exercise = validateExercise(participation.getExercise());

irisSettingsService.isActivatedForElseThrow(IrisEventType.BUILD_FAILED, exercise);

var participant = studentParticipation.getParticipant();
if (participant instanceof User user) {
var session = getCurrentSessionOrCreateIfNotExistsInternal(exercise, user, false);
Expand All @@ -215,8 +213,6 @@ public void onNewResult(Result result) {

var exercise = validateExercise(participation.getExercise());

irisSettingsService.isActivatedForElseThrow(IrisEventType.PROGRESS_STALLED, exercise);

var recentSubmissions = submissionRepository.findAllWithResultsByParticipationIdOrderBySubmissionDateAsc(studentParticipation.getId());

double successThreshold = 100.0; // TODO: Retrieve configuration from Iris settings
Expand Down
Loading
Loading