Skip to content

Commit

Permalink
Modeling exercises: Add preliminary AI feedback requests for students (
Browse files Browse the repository at this point in the history
  • Loading branch information
LeonWehrhahn authored Sep 27, 2024
1 parent 00f38c0 commit 37a20f1
Show file tree
Hide file tree
Showing 21 changed files with 431 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,7 @@ public Set<Result> findResultsFilteredForStudents(Participation participation) {
boolean isAssessmentOver = getAssessmentDueDate() == null || getAssessmentDueDate().isBefore(ZonedDateTime.now());
if (!isAssessmentOver) {
// This allows the showing of preliminary feedback in case the assessment due date is set before its over.
if (this instanceof TextExercise) {
if (this instanceof TextExercise || this instanceof ModelingExercise) {
return participation.getResults().stream().filter(result -> result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA).collect(Collectors.toSet());
}
return Set.of();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,11 +220,11 @@ public List<Result> getManualResults() {
/**
* This method is necessary to ignore Athena results in the assessment view
*
* @return non athena automatic results including null results
* @return non athena automatic results excluding null results
*/
@JsonIgnore
public List<Result> getNonAthenaResults() {
return results.stream().filter(result -> result == null || !result.isAthenaAutomatic()).collect(Collectors.toCollection(ArrayList::new));
return results.stream().filter(result -> result != null && !result.isAthenaAutomatic()).collect(Collectors.toCollection(ArrayList::new));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -715,16 +715,16 @@ public List<StudentParticipation> findByExerciseAndStudentIdWithEagerSubmissions
}

/**
* Get the text exercise participation with the Latest Submissions and its results
* Retrieves a StudentParticipation with its latest Submission and associated Result.
*
* @param participationId the id of the participation
* @return the participation with latest submission and result
* @throws EntityNotFoundException
* @param participationId The unique identifier of the participation to retrieve.
* @return A StudentParticipation object containing the latest submission and result.
* @throws EntityNotFoundException If no StudentParticipation is found with the given ID.
*/
public StudentParticipation findTextExerciseParticipationWithLatestSubmissionAndResultElseThrow(Long participationId) throws EntityNotFoundException {
public StudentParticipation findExerciseParticipationWithLatestSubmissionAndResultElseThrow(Long participationId) throws EntityNotFoundException {
Optional<Participation> participation = participationRepository.findByIdWithLatestSubmissionAndResult(participationId);
if (participation.isEmpty() || !(participation.get() instanceof StudentParticipation studentParticipation)) {
throw new EntityNotFoundException("No text exercise participation found with id " + participationId);
throw new EntityNotFoundException("No exercise participation found with id " + participationId);
}
return studentParticipation;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,7 @@ private List<SubmissionWithComplaintDTO> getSubmissionsWithComplaintsFromComplai
// add each submission with its complaint to the DTO
submissions.stream().filter(submission -> submission.getResultWithComplaint() != null).forEach(submission -> {
// get the complaint which belongs to the submission
submission.setResults(submission.getNonAthenaResults());
Complaint complaintOfSubmission = complaintMap.get(submission.getResultWithComplaint().getId());
prepareComplaintAndSubmission(complaintOfSubmission, submission);
submissionWithComplaintDTOs.add(new SubmissionWithComplaintDTO(submission, complaintOfSubmission));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ protected ResponseEntity<List<Submission>> getAllSubmissions(Long exerciseId, bo
submission.getParticipation().setExercise(null);
}
// Important for exercises with Athena results
if (assessedByTutor) {
if (assessedByTutor && !examMode) {
submission.setResults(submission.getNonAthenaResults());
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@
import de.tum.cit.aet.artemis.exercise.service.ExerciseDateService;
import de.tum.cit.aet.artemis.exercise.service.ParticipationAuthorizationCheckService;
import de.tum.cit.aet.artemis.exercise.service.ParticipationService;
import de.tum.cit.aet.artemis.fileupload.domain.FileUploadExercise;
import de.tum.cit.aet.artemis.modeling.domain.ModelingExercise;
import de.tum.cit.aet.artemis.modeling.service.ModelingExerciseFeedbackService;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseParticipation;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation;
Expand Down Expand Up @@ -167,6 +169,8 @@ public class ParticipationResource {

private final TextExerciseFeedbackService textExerciseFeedbackService;

private final ModelingExerciseFeedbackService modelingExerciseFeedbackService;

public ParticipationResource(ParticipationService participationService, ProgrammingExerciseParticipationService programmingExerciseParticipationService,
CourseRepository courseRepository, QuizExerciseRepository quizExerciseRepository, ExerciseRepository exerciseRepository,
ProgrammingExerciseRepository programmingExerciseRepository, AuthorizationCheckService authCheckService,
Expand All @@ -176,7 +180,8 @@ public ParticipationResource(ParticipationService participationService, Programm
ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository, SubmissionRepository submissionRepository,
ResultRepository resultRepository, ExerciseDateService exerciseDateService, InstanceMessageSendService instanceMessageSendService, QuizBatchService quizBatchService,
SubmittedAnswerRepository submittedAnswerRepository, QuizSubmissionService quizSubmissionService, GradingScaleService gradingScaleService,
ProgrammingExerciseCodeReviewFeedbackService programmingExerciseCodeReviewFeedbackService, TextExerciseFeedbackService textExerciseFeedbackService) {
ProgrammingExerciseCodeReviewFeedbackService programmingExerciseCodeReviewFeedbackService, TextExerciseFeedbackService textExerciseFeedbackService,
ModelingExerciseFeedbackService modelingExerciseFeedbackService) {
this.participationService = participationService;
this.programmingExerciseParticipationService = programmingExerciseParticipationService;
this.quizExerciseRepository = quizExerciseRepository;
Expand All @@ -203,6 +208,7 @@ public ParticipationResource(ParticipationService participationService, Programm
this.gradingScaleService = gradingScaleService;
this.programmingExerciseCodeReviewFeedbackService = programmingExerciseCodeReviewFeedbackService;
this.textExerciseFeedbackService = textExerciseFeedbackService;
this.modelingExerciseFeedbackService = modelingExerciseFeedbackService;
}

/**
Expand Down Expand Up @@ -363,7 +369,7 @@ public ResponseEntity<StudentParticipation> requestFeedback(@PathVariable Long e

Exercise exercise = exerciseRepository.findByIdElseThrow(exerciseId);

if (!(exercise instanceof TextExercise) && !(exercise instanceof ProgrammingExercise)) {
if (exercise instanceof QuizExercise || exercise instanceof FileUploadExercise) {
throw new BadRequestAlertException("Unsupported exercise type", "participation", "unsupported type");
}

Expand Down Expand Up @@ -393,7 +399,7 @@ private ResponseEntity<StudentParticipation> handleExerciseFeedbackRequest(Exerc
participation = studentParticipationRepository.findByIdWithResultsElseThrow(participation.getId());

// Check submission requirements
if (exercise instanceof TextExercise) {
if (exercise instanceof TextExercise || exercise instanceof ModelingExercise) {
if (submissionRepository.findAllByParticipationId(participation.getId()).isEmpty()) {
throw new BadRequestAlertException("You need to submit at least once", "participation", "preconditions not met");
}
Expand All @@ -416,6 +422,9 @@ else if (exercise instanceof ProgrammingExercise) {
if (exercise instanceof TextExercise) {
updatedParticipation = textExerciseFeedbackService.handleNonGradedFeedbackRequest(exercise.getId(), participation, (TextExercise) exercise);
}
else if (exercise instanceof ModelingExercise) {
updatedParticipation = modelingExerciseFeedbackService.handleNonGradedFeedbackRequest(participation, (ModelingExercise) exercise);
}
else {
updatedParticipation = programmingExerciseCodeReviewFeedbackService.handleNonGradedFeedbackRequest(exercise.getId(),
(ProgrammingExerciseStudentParticipation) participation, (ProgrammingExercise) exercise);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package de.tum.cit.aet.artemis.modeling.service;

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

import java.time.ZonedDateTime;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;

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

import de.tum.cit.aet.artemis.assessment.domain.AssessmentType;
import de.tum.cit.aet.artemis.assessment.domain.Feedback;
import de.tum.cit.aet.artemis.assessment.domain.FeedbackType;
import de.tum.cit.aet.artemis.assessment.domain.Result;
import de.tum.cit.aet.artemis.assessment.repository.ResultRepository;
import de.tum.cit.aet.artemis.assessment.service.ResultService;
import de.tum.cit.aet.artemis.assessment.web.ResultWebsocketService;
import de.tum.cit.aet.artemis.athena.dto.ModelingFeedbackDTO;
import de.tum.cit.aet.artemis.athena.service.AthenaFeedbackSuggestionsService;
import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException;
import de.tum.cit.aet.artemis.core.exception.InternalServerErrorException;
import de.tum.cit.aet.artemis.core.exception.NetworkingException;
import de.tum.cit.aet.artemis.exercise.domain.Submission;
import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation;
import de.tum.cit.aet.artemis.exercise.service.ParticipationService;
import de.tum.cit.aet.artemis.exercise.service.SubmissionService;
import de.tum.cit.aet.artemis.modeling.domain.ModelingExercise;
import de.tum.cit.aet.artemis.modeling.domain.ModelingSubmission;

@Profile(PROFILE_CORE)
@Service
public class ModelingExerciseFeedbackService {

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

private final Optional<AthenaFeedbackSuggestionsService> athenaFeedbackSuggestionsService;

private final ResultWebsocketService resultWebsocketService;

private final SubmissionService submissionService;

private final ParticipationService participationService;

private final ResultService resultService;

private final ResultRepository resultRepository;

public ModelingExerciseFeedbackService(Optional<AthenaFeedbackSuggestionsService> athenaFeedbackSuggestionsService, SubmissionService submissionService,
ResultService resultService, ResultRepository resultRepository, ResultWebsocketService resultWebsocketService, ParticipationService participationService) {
this.athenaFeedbackSuggestionsService = athenaFeedbackSuggestionsService;
this.submissionService = submissionService;
this.resultService = resultService;
this.resultRepository = resultRepository;
this.resultWebsocketService = resultWebsocketService;
this.participationService = participationService;
}

private void checkRateLimitOrThrow(StudentParticipation participation) {

List<Result> athenaResults = participation.getResults().stream().filter(result -> result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA).toList();

if (athenaResults.size() >= 10) {
throw new BadRequestAlertException("Maximum number of AI feedback requests reached.", "participation", "preconditions not met");
}
}

/**
* Handles the request for generating feedback for a modeling exercise.
* Unlike programming exercises a tutor is not notified if Athena is not available.
*
* @param participation the student participation associated with the exercise.
* @param modelingExercise the modeling exercise object.
* @return StudentParticipation updated modeling exercise for an AI assessment
*/
public StudentParticipation handleNonGradedFeedbackRequest(StudentParticipation participation, ModelingExercise modelingExercise) {
if (this.athenaFeedbackSuggestionsService.isPresent()) {
this.checkRateLimitOrThrow(participation);
CompletableFuture.runAsync(() -> this.generateAutomaticNonGradedFeedback(participation, modelingExercise));
}
return participation;
}

/**
* Generates automatic non-graded feedback for a modeling exercise submission.
* This method leverages the Athena service to generate feedback based on the latest submission.
*
* @param participation the student participation associated with the exercise.
* @param modelingExercise the modeling exercise object.
*/
public void generateAutomaticNonGradedFeedback(StudentParticipation participation, ModelingExercise modelingExercise) {
log.debug("Using athena to generate (modeling exercise) feedback request: {}", modelingExercise.getId());

Optional<Submission> submissionOptional = participationService.findExerciseParticipationWithLatestSubmissionAndResultElseThrow(participation.getId())
.findLatestSubmission();

if (submissionOptional.isEmpty()) {
throw new BadRequestAlertException("No legal submissions found", "submission", "noSubmission");
}

Submission submission = submissionOptional.get();

Result automaticResult = createInitialResult(participation, submission);

try {
this.resultWebsocketService.broadcastNewResult(participation, automaticResult);

log.debug("Submission id: {}", submission.getId());

List<Feedback> feedbacks = getAthenaFeedback(modelingExercise, (ModelingSubmission) submission);

double totalFeedbackScore = calculateTotalFeedbackScore(feedbacks, modelingExercise);

automaticResult.setCompletionDate(ZonedDateTime.now());
automaticResult.setScore(Math.max(0, Math.min(totalFeedbackScore, 100)));
automaticResult.setSuccessful(true);

automaticResult = this.resultRepository.save(automaticResult);
resultService.storeFeedbackInResult(automaticResult, feedbacks, true);
submissionService.saveNewResult(submission, automaticResult);
this.resultWebsocketService.broadcastNewResult(participation, automaticResult);
}
catch (Exception e) {
log.error("Could not generate feedback for exercise ID: {} and participation ID: {}", modelingExercise.getId(), participation.getId(), e);
throw new InternalServerErrorException("Something went wrong... AI Feedback could not be generated");
}
}

/**
* Creates an initial Result object for the automatic feedback.
*
* @param participation the student participation
* @param submission the submission to which the result is associated
* @return the initial Result object
*/
private Result createInitialResult(StudentParticipation participation, Submission submission) {
Result result = new Result();
result.setAssessmentType(AssessmentType.AUTOMATIC_ATHENA);
result.setRated(true);
result.setScore(0.0);
result.setSuccessful(null);
result.setSubmission(submission);
result.setParticipation(participation);
return result;
}

/**
* Retrieves feedback from the Athena service.
*
* @param modelingExercise the modeling exercise
* @param submission the modeling submission
* @return a list of Feedback objects
* @throws NetworkingException if there's a problem communicating with Athena
*/
private List<Feedback> getAthenaFeedback(ModelingExercise modelingExercise, ModelingSubmission submission) throws NetworkingException {
return this.athenaFeedbackSuggestionsService.orElseThrow().getModelingFeedbackSuggestions(modelingExercise, submission, false).stream()
.filter(feedbackItem -> feedbackItem.description() != null).map(this::convertToFeedback).toList();
}

/**
* Converts an Athena feedback suggestion to a Feedback object.
*
* @param feedbackItem the Athena feedback suggestion
* @return the Feedback object
*/
private Feedback convertToFeedback(ModelingFeedbackDTO feedbackItem) {
Feedback feedback = new Feedback();
feedback.setText(feedbackItem.title());
feedback.setDetailText(feedbackItem.description());
feedback.setHasLongFeedbackText(false);
feedback.setType(FeedbackType.AUTOMATIC);
feedback.setCredits(feedbackItem.credits());
return feedback;
}

/**
* Calculates the total feedback score based on the list of feedbacks and the exercise's max points.
*
* @param feedbacks the list of feedbacks
* @param modelingExercise the modeling exercise
* @return the total feedback score
*/
private double calculateTotalFeedbackScore(List<Feedback> feedbacks, ModelingExercise modelingExercise) {
double totalCredits = feedbacks.stream().mapToDouble(Feedback::getCredits).sum();
Double maxPoints = modelingExercise.getMaxPoints();

if (maxPoints == null || maxPoints == 0) {
throw new IllegalArgumentException("Maximum points must be greater than zero.");
}

return (totalCredits / maxPoints) * 100;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,14 +124,19 @@ public ModelingSubmission handleModelingSubmission(ModelingSubmission modelingSu
throw new AccessForbiddenException();
}

// remove result from submission (in the unlikely case it is passed here), so that students cannot inject a result
modelingSubmission.setResults(new ArrayList<>());

// update submission properties
// NOTE: from now on we always set submitted to true to prevent problems here! Except for late submissions of course exercises to prevent issues in auto-save
if (exercise.isExamExercise() || exerciseDateService.isBeforeDueDate(participation)) {
modelingSubmission.setSubmitted(true);
}

// if athena results are present, then create a new submission on submit
if (modelingSubmission.getParticipation() != null && modelingSubmission.getParticipation().getResults() != null
&& !modelingSubmission.getParticipation().getResults().isEmpty()) {
log.debug("Creating a new submission due to Athena results for user: {}", user.getLogin());
modelingSubmission.setId(null);
}

modelingSubmission = save(modelingSubmission, exercise, user, participation);
return modelingSubmission;
}
Expand Down
Loading

0 comments on commit 37a20f1

Please sign in to comment.