Skip to content

Commit

Permalink
Merge branch 'develop' into feature/auxiliary-repositories/enable-in-…
Browse files Browse the repository at this point in the history
…edit-in-editor
  • Loading branch information
SimonEntholzer authored Oct 29, 2024
2 parents 3f66af5 + 7503f9c commit a4d86a8
Show file tree
Hide file tree
Showing 45 changed files with 706 additions and 370 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,9 @@ The following members of the project management team are responsible for specifi
| Exercises | Stephan Krusche ([@krusche](https://github.com/krusche)) |
| Programming exercises | Stephan Krusche ([@krusche](https://github.com/krusche)) |
| Build agents | Robert Jandow ([@robertjndw](https://github.com/robertjndw)) |
| Quiz exercises | Felix Dietrich ([@FelixTJDietrich](https://github.com/FelixTJDietrich)) |
| Quiz exercises | Timor Morrien ([@Hialus](https://github.com/Hialus)) |
| Modeling exercises | Felix Dietrich ([@FelixTJDietrich](https://github.com/FelixTJDietrich)) |
| Text exercises | Maximilian Sölch ([@maximiliansoelch](https://github.com/maximiliansoelch)) |
| Text exercises | Felix Dietrich ([@FelixTJDietrich](https://github.com/FelixTJDietrich)) |
| File upload exercises | Elisabeth Friesinger ([@easy-lisi](https://github.com/easy-lisi)) |
| Exam mode | Stephan Krusche ([@krusche](https://github.com/krusche)) |
| Assessment | Maximilian Sölch ([@maximiliansoelch](https://github.com/maximiliansoelch)) |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ else if (exercise instanceof ProgrammingExercise) {
// Process feedback request
StudentParticipation updatedParticipation;
if (exercise instanceof TextExercise) {
updatedParticipation = textExerciseFeedbackService.handleNonGradedFeedbackRequest(exercise.getId(), participation, (TextExercise) exercise);
updatedParticipation = textExerciseFeedbackService.handleNonGradedFeedbackRequest(participation, (TextExercise) exercise);
}
else if (exercise instanceof ModelingExercise) {
updatedParticipation = modelingExerciseFeedbackService.handleNonGradedFeedbackRequest(participation, (ModelingExercise) exercise);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE;

import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;

import org.slf4j.Logger;
Expand All @@ -21,11 +24,10 @@
import de.tum.cit.aet.artemis.assessment.web.ResultWebsocketService;
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.exercise.domain.participation.Participation;
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.text.domain.TextBlock;
import de.tum.cit.aet.artemis.text.domain.TextExercise;
import de.tum.cit.aet.artemis.text.domain.TextSubmission;

Expand All @@ -47,14 +49,18 @@ public class TextExerciseFeedbackService {

private final ResultRepository resultRepository;

private final TextBlockService textBlockService;

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

private void checkRateLimitOrThrow(StudentParticipation participation) {
Expand All @@ -64,20 +70,19 @@ private void checkRateLimitOrThrow(StudentParticipation participation) {
long countOfAthenaResults = athenaResults.size();

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

/**
* Handles the request for generating feedback for a text exercise.
* Unlike programming exercises a tutor is not notified if Athena is not available.
*
* @param exerciseId the id of the text exercise.
* @param participation the student participation associated with the exercise.
* @param textExercise the text exercise object.
* @return StudentParticipation updated text exercise for an AI assessment
*/
public StudentParticipation handleNonGradedFeedbackRequest(Long exerciseId, StudentParticipation participation, TextExercise textExercise) {
public StudentParticipation handleNonGradedFeedbackRequest(StudentParticipation participation, TextExercise textExercise) {
if (this.athenaFeedbackSuggestionsService.isPresent()) {
this.checkRateLimitOrThrow(participation);
CompletableFuture.runAsync(() -> this.generateAutomaticNonGradedFeedback(participation, textExercise));
Expand All @@ -101,50 +106,81 @@ public void generateAutomaticNonGradedFeedback(StudentParticipation participatio
if (submissionOptional.isEmpty()) {
throw new BadRequestAlertException("No legal submissions found", "submission", "noSubmission");
}
var submission = submissionOptional.get();
TextSubmission textSubmission = (TextSubmission) submissionOptional.get();

Result automaticResult = new Result();
automaticResult.setAssessmentType(AssessmentType.AUTOMATIC_ATHENA);
automaticResult.setRated(true);
automaticResult.setScore(0.0);
automaticResult.setSuccessful(null);
automaticResult.setSubmission(submission);
automaticResult.setSubmission(textSubmission);
automaticResult.setParticipation(participation);
try {
this.resultWebsocketService.broadcastNewResult((Participation) participation, automaticResult);
// This broadcast signals the client that feedback is being generated, does not save empty result
this.resultWebsocketService.broadcastNewResult(participation, automaticResult);

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

log.debug("Submission id: {}", submission.getId());
var athenaResponse = this.athenaFeedbackSuggestionsService.orElseThrow().getTextFeedbackSuggestions(textExercise, textSubmission, true);

var athenaResponse = this.athenaFeedbackSuggestionsService.orElseThrow().getTextFeedbackSuggestions(textExercise, (TextSubmission) submission, false);
Set<TextBlock> textBlocks = new HashSet<>();
List<Feedback> feedbacks = new ArrayList<>();

List<Feedback> feedbacks = athenaResponse.stream().filter(individualFeedbackItem -> individualFeedbackItem.description() != null).map(individualFeedbackItem -> {
athenaResponse.stream().filter(individualFeedbackItem -> individualFeedbackItem.description() != null).forEach(individualFeedbackItem -> {
var textBlock = new TextBlock();
var feedback = new Feedback();

feedback.setText(individualFeedbackItem.title());
feedback.setDetailText(individualFeedbackItem.description());
feedback.setHasLongFeedbackText(false);
feedback.setType(FeedbackType.AUTOMATIC);
feedback.setCredits(individualFeedbackItem.credits());
return feedback;
}).toList();

if (textSubmission.getText() != null && individualFeedbackItem.indexStart() != null && individualFeedbackItem.indexEnd() != null) {
textBlock.setStartIndex(individualFeedbackItem.indexStart());
textBlock.setEndIndex(individualFeedbackItem.indexEnd());
textBlock.setSubmission(textSubmission);
textBlock.setTextFromSubmission();
textBlock.automatic();
textBlock.computeId();
feedback.setReference(textBlock.getId());
textBlock.setFeedback(feedback);
log.debug(textBlock.toString());

textBlocks.add(textBlock);
}
feedbacks.add(feedback);
});

double totalFeedbacksScore = 0.0;
for (Feedback feedback : feedbacks) {
totalFeedbacksScore += feedback.getCredits();
}
totalFeedbacksScore = totalFeedbacksScore / textExercise.getMaxPoints() * 100;
automaticResult.setSuccessful(true);
automaticResult.setCompletionDate(ZonedDateTime.now());

automaticResult.setScore(Math.clamp(totalFeedbacksScore, 0, 100));

// For Athena automatic results successful = true will mean that the generation was successful
// undefined in progress and false it failed
automaticResult.setSuccessful(true);

automaticResult = this.resultRepository.save(automaticResult);
resultService.storeFeedbackInResult(automaticResult, feedbacks, true);
submissionService.saveNewResult(submission, automaticResult);
this.resultWebsocketService.broadcastNewResult((Participation) participation, automaticResult);
textBlockService.saveAll(textBlocks);
textSubmission.setBlocks(textBlocks);
submissionService.saveNewResult(textSubmission, automaticResult);
// This broadcast signals the client that feedback generation succeeded, result is saved in this case only
this.resultWebsocketService.broadcastNewResult(participation, automaticResult);
}
catch (Exception e) {
log.error("Could not generate feedback", e);
throw new InternalServerErrorException("Something went wrong... AI Feedback could not be generated");
// Broadcast the failed result but don't save, note that successful = false is normally used to indicate a score < 100
// but since we do not differentiate for athena feedback we use it to indicate a failed generation
automaticResult.setSuccessful(false);
automaticResult.setCompletionDate(null);
participation.addResult(automaticResult); // for proper change detection
// This broadcast signals the client that feedback generation failed, does not save empty result
this.resultWebsocketService.broadcastNewResult(participation, automaticResult);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -421,44 +422,49 @@ public ResponseEntity<StudentParticipation> getDataForTextEditor(@PathVariable L
participation.setResults(new HashSet<>(results));
}

Optional<Submission> optionalSubmission = participation.findLatestSubmission();
if (!ExerciseDateService.isAfterAssessmentDueDate(textExercise)) {
// We want to have the preliminary feedback before the assessment due date too
Set<Result> athenaResults = participation.getResults().stream().filter(result -> result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA)
.collect(Collectors.toSet());
participation.setResults(athenaResults);
}

Set<Submission> submissions = participation.getSubmissions();
participation.setSubmissions(new HashSet<>());

if (optionalSubmission.isPresent()) {
TextSubmission textSubmission = (TextSubmission) optionalSubmission.get();
for (Submission submission : submissions) {
if (submission != null) {
TextSubmission textSubmission = (TextSubmission) submission;

// set reference to participation to null, since we are already inside a participation
textSubmission.setParticipation(null);
// set reference to participation to null, since we are already inside a participation
textSubmission.setParticipation(null);

if (!ExerciseDateService.isAfterAssessmentDueDate(textExercise)) {
// We want to have the preliminary feedback before the assessment due date too
List<Result> athenaResults = participation.getResults().stream().filter(result -> result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA).toList();
textSubmission.setResults(athenaResults);
Set<Result> athenaResultsSet = new HashSet<Result>(athenaResults);
participation.setResults(athenaResultsSet);
}
if (!ExerciseDateService.isAfterAssessmentDueDate(textExercise)) {
// We want to have the preliminary feedback before the assessment due date too
List<Result> athenaResults = submission.getResults().stream().filter(result -> result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA).toList();
textSubmission.setResults(athenaResults);
}

Result result = textSubmission.getLatestResult();
if (result != null) {
// Load TextBlocks for the Submission. They are needed to display the Feedback in the client.
final var textBlocks = textBlockRepository.findAllBySubmissionId(textSubmission.getId());
textSubmission.setBlocks(textBlocks);
Result result = textSubmission.getLatestResult();
if (result != null) {
// Load TextBlocks for the Submission. They are needed to display the Feedback in the client.
final var textBlocks = textBlockRepository.findAllBySubmissionId(textSubmission.getId());
textSubmission.setBlocks(textBlocks);

if (textSubmission.isSubmitted() && result.getCompletionDate() != null) {
List<Feedback> assessments = feedbackRepository.findByResult(result);
result.setFeedbacks(assessments);
}
if (textSubmission.isSubmitted() && result.getCompletionDate() != null) {
List<Feedback> assessments = feedbackRepository.findByResult(result);
result.setFeedbacks(assessments);
}

if (!authCheckService.isAtLeastTeachingAssistantForExercise(textExercise, user)) {
result.filterSensitiveInformation();
}
if (!authCheckService.isAtLeastTeachingAssistantForExercise(textExercise, user)) {
result.filterSensitiveInformation();
}

// only send the one latest result to the client
textSubmission.setResults(List.of(result));
participation.setResults(Set.of(result));
// only send the one latest result to the client
textSubmission.setResults(List.of(result));
}
participation.addSubmission(textSubmission);
}

participation.addSubmission(textSubmission);
}

if (!(authCheckService.isAtLeastInstructorForExercise(textExercise, user) || participation.isOwnedBy(user))) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ export class HeaderParticipationPageComponent implements OnInit, OnChanges {
this.exerciseStatusBadge = hasExerciseDueDatePassed(this.exercise, this.participation) ? 'bg-danger' : 'bg-success';
this.exerciseCategories = this.exercise.categories || [];
this.dueDate = getExerciseDueDate(this.exercise, this.participation);
if (this.participation?.results?.[0]?.rated) {
if (this.participation?.results?.last()?.rated) {
this.achievedPoints = roundValueSpecifiedByCourseSettings(
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
(this.participation.results?.[0].score! * this.exercise.maxPoints!) / 100,
(this.participation.results?.last()?.score! * this.exercise.maxPoints!) / 100,
getCourseFromExercise(this.exercise),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,6 @@ export class ExerciseScoresComponent implements OnInit, OnDestroy {
// the result of the first correction round will be at index 0,
// the result of a complaints or the second correction at index 1.
participation.results?.sort((result1, result2) => (result1.id ?? 0) - (result2.id ?? 0));

const resultsWithoutAthena = participation.results?.filter((result) => result.assessmentType !== AssessmentType.AUTOMATIC_ATHENA);
if (resultsWithoutAthena?.length != 0) {
if (resultsWithoutAthena?.[0].submission) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
id="allowFeedbackRequests"
(change)="toggleFeedbackRequests($event)"
/>
<label class="form-control-label" for="allowFeedbackRequests" jhiTranslate="artemisApp.programmingExercise.timeline.allowFeedbackRequests"></label>
<jhi-help-icon placement="right auto" [text]="'artemisApp.programmingExercise.timeline.allowFeedbackRequestsTooltip'" />
<label class="form-control-label" for="allowFeedbackRequests" jhiTranslate="artemisApp.textExercise.allowPreliminaryAthenaFeedbackRequests"></label>
<jhi-help-icon placement="right auto" [text]="'artemisApp.textExercise.allowPreliminaryAthenaFeedbackRequestsTooltip'" />
</div>
}
@if (!!this.exercise.feedbackSuggestionModule) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ export class FeedbackComponent implements OnInit, OnChanges {
faXmark = faXmark;
faCircleNotch = faCircleNotch;
faExclamationTriangle = faExclamationTriangle;

private showTestDetails = false;
isLoading = false;
loadingFailed = false;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div [class.non-clickable]="disableRating">
<div [class.non-clickable]="disableRating" [id]="'rating'">
<b>
<span jhiTranslate="artemisApp.rating.label"></span>
</b>
Expand Down
Loading

0 comments on commit a4d86a8

Please sign in to comment.