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

Text exercises: Add preliminary AI feedback requests for students on text exercises using Athena #9241

Merged
merged 77 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
f03e5e5
add UI options for AI Feedback and result history
Aug 4, 2024
1067ef1
Add Athena Feedback option for text exercises
Aug 16, 2024
80242a0
Add Athena AI Feedback button and history to Text Exercises
Aug 16, 2024
0245572
Add check for automatic Athena results in code editor so the student …
Aug 16, 2024
992a2bd
Update result components for Text Feedback
Aug 16, 2024
b49ca0f
Handle toggle of athena ai feedback request option
Aug 16, 2024
b79d325
Get the last submission for assessment for text exercises
Aug 21, 2024
c79b540
Preliminary Feedback Server
Aug 22, 2024
5405ae1
Fix Assessment tutor side issues
Aug 22, 2024
9ddc3d9
remove todo
Aug 22, 2024
01159dc
Merge branch 'feature/text-exercises/immediate-preliminary-feedback' …
Aug 22, 2024
79c4739
Merge branch 'develop' into feature/text-exercises/immediate-prelimin…
EneaGore Aug 22, 2024
a5b1c45
prettier
EneaGore Aug 22, 2024
da61ffa
Formattierung
Aug 22, 2024
95895b1
Merge branch 'feature/text-exercises/immediate-preliminary-feedback' …
Aug 22, 2024
f1cf351
Improve results for preliminary text AI Feedback
Aug 23, 2024
1c6ac3d
Fix result icon and color for Ai Feedback
Aug 23, 2024
52083fe
typo
Aug 23, 2024
6070cb6
ups
Aug 23, 2024
44f8cef
typo
Aug 23, 2024
ac360d4
improve result strings for text ai feedback
Aug 23, 2024
3de99f0
set ai feedback for text to graded
Aug 23, 2024
2f99541
add spinner for ai feedback
Aug 23, 2024
d1a30f7
small improvements
Aug 23, 2024
b192080
fix rate limit and editor sign when athena results
Aug 23, 2024
69a179e
clean up code
Aug 23, 2024
c8f5526
revert last change partially
Aug 23, 2024
f6e5aef
turn off automatic feedback requests if no module selected
Aug 23, 2024
753c050
update string for successful and failed athena feedback
Aug 27, 2024
a175663
refactor feedback request (Thanks Leon)
Aug 27, 2024
4c97671
show spinner, don't save or show empty or failed athena results
Aug 27, 2024
0c4f088
dont save emppty result, new method for new submission after athena f…
Aug 27, 2024
7269dcb
Improve error handling and better manage result templates
Aug 27, 2024
f73fad8
Add integration tests
Aug 28, 2024
464dfe8
no junit 4
Aug 28, 2024
560af55
no assert Throws
Aug 28, 2024
23dd234
save transient instance in test
Aug 28, 2024
db0e185
use text exercise id for text participation test
Aug 28, 2024
4067400
use text course
Aug 28, 2024
d60b372
separate programming and text athena tests
Aug 28, 2024
5dc5b6e
better tests
Aug 28, 2024
69ab55e
fix text case success
Aug 28, 2024
a9b2243
code clean up
Aug 28, 2024
a2c1ecd
more code clean up
Aug 28, 2024
fca28bc
turn athena created submission external and fix successful = false bugs
Aug 28, 2024
a684a48
address change Request (part 1)
Aug 28, 2024
ce056ff
optional chain
Aug 28, 2024
9de3a3f
disable feedback button when generating and address feedback (part 2)
Aug 28, 2024
d8f292d
bring back the spinner
Aug 28, 2024
8d5187d
better condition for spinner and some client tests
Aug 29, 2024
03c3bd1
fix result icon and color
Aug 29, 2024
4b72a4a
typo
Aug 29, 2024
b4dc9db
back to gray for Athena results
Aug 29, 2024
fc8c873
more result client tests
Aug 29, 2024
f59b2b3
result spec ts conflict
Aug 29, 2024
2099628
result spec ts conflict
Aug 29, 2024
c77e624
Merge branch 'develop' into feature/text-exercises/immediate-prelimin…
Aug 29, 2024
56982d0
add text athena result tests after conflict resolve
Aug 29, 2024
48d56c8
use notch instead of spinner
Aug 29, 2024
901eff2
blue notch, no endless spin in more results, error alert from client …
Aug 30, 2024
9010ae1
new submission only after student submits if athena results present, …
Aug 30, 2024
ce159f4
minor code improvements
Aug 30, 2024
4b0d29f
small bugfix
Aug 30, 2024
75f88ac
turn window alerts to alert warning and update tests
Aug 30, 2024
5b5554c
Add check if last submission already has results + test + getNonAthen…
Aug 31, 2024
fe3d235
Merge branch 'develop' into feature/text-exercises/immediate-prelimin…
FelixTJDietrich Sep 4, 2024
287e20c
after develop merge entities -> entities/text
EneaGore Sep 4, 2024
f23244d
Fix integration tests. Also send null results with non athena results
Sep 6, 2024
cf1b90a
Merge branch 'develop' into feature/text-exercises/immediate-prelimin…
EneaGore Sep 7, 2024
ac8cb0a
Merge branch 'develop' into feature/text-exercises/immediate-prelimin…
EneaGore Sep 8, 2024
ad97f5f
Revert Integration Tests
EneaGore Sep 9, 2024
1bd4f94
Try a signle import of AthenaRequestMockProvider
EneaGore Sep 9, 2024
3deaceb
Spy Bean -> Mock bean
EneaGore Sep 9, 2024
69bf43b
Add websocket spy bean declaration
EneaGore Sep 9, 2024
6684a2b
remvoe mock bean websocket from participipation integration test
EneaGore Sep 9, 2024
20d3811
java style
EneaGore Sep 9, 2024
9618c23
Merge branch 'develop' into feature/text-exercises/immediate-prelimin…
FelixTJDietrich Sep 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/main/java/de/tum/in/www1/artemis/domain/Exercise.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import com.fasterxml.jackson.annotation.JsonView;

import de.tum.in.www1.artemis.domain.competency.CourseCompetency;
import de.tum.in.www1.artemis.domain.enumeration.AssessmentType;
import de.tum.in.www1.artemis.domain.enumeration.ExerciseType;
import de.tum.in.www1.artemis.domain.enumeration.IncludedInOverallScore;
import de.tum.in.www1.artemis.domain.enumeration.InitializationState;
Expand Down Expand Up @@ -603,6 +604,10 @@ else if (resultDate1.isAfter(resultDate2)) {
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) {
return participation.getResults().stream().filter(result -> result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA).collect(Collectors.toSet());
EneaGore marked this conversation as resolved.
Show resolved Hide resolved
}
return Set.of();
}
return participation.getResults().stream().filter(result -> result.getCompletionDate() != null).collect(Collectors.toSet());
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/de/tum/in/www1/artemis/domain/Submission.java
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,16 @@ public List<Result> getManualResults() {
return results.stream().filter(result -> result != null && !result.isAutomatic() && !result.isAthenaAutomatic()).collect(Collectors.toCollection(ArrayList::new));
}

/**
* This method is necessary to ignore Athena results in the assessment view
*
* @return non athena automatic results including null results
*/
@JsonIgnore
public List<Result> getNonAthenaResults() {
return results.stream().filter(result -> result == null || !result.isAthenaAutomatic()).collect(Collectors.toCollection(ArrayList::new));
}

/**
* Get the manual result by id of the submission
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import de.tum.in.www1.artemis.domain.enumeration.InitializationState;
import de.tum.in.www1.artemis.domain.enumeration.SubmissionType;
import de.tum.in.www1.artemis.domain.participation.Participant;
import de.tum.in.www1.artemis.domain.participation.Participation;
import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation;
import de.tum.in.www1.artemis.domain.participation.StudentParticipation;
import de.tum.in.www1.artemis.domain.quiz.QuizExercise;
Expand Down Expand Up @@ -664,6 +665,21 @@ public Optional<StudentParticipation> findOneByExerciseAndStudentLoginWithEagerS
return studentParticipationRepository.findWithEagerLegalSubmissionsByExerciseIdAndStudentLogin(exercise.getId(), username);
}

/**
* Get one participation (in any state) by its student and exercise with eager submissions else throw exception.
*
* @param exercise the exercise for which to find a participation
* @param username the username of the student
* @return the participation of the given student and exercise with eager submissions in any state
*/
public StudentParticipation findOneByExerciseAndStudentLoginWithEagerSubmissionsAnyStateElseThrow(Exercise exercise, String username) {
Optional<StudentParticipation> optionalParticipation = findOneByExerciseAndStudentLoginWithEagerSubmissionsAnyState(exercise, username);
if (optionalParticipation.isEmpty()) {
throw new EntityNotFoundException("No participation found in exercise with id " + exercise.getId() + " for user " + username);
}
return optionalParticipation.get();
}

/**
* Get all exercise participations belonging to exercise and student.
*
Expand Down Expand Up @@ -693,6 +709,21 @@ public List<StudentParticipation> findByExerciseAndStudentIdWithEagerSubmissions
return studentParticipationRepository.findByExerciseIdAndStudentIdWithEagerLegalSubmissions(exercise.getId(), studentId);
}

/**
* Get the text exercise participation with the Latest Submissions and its results
*
* @param participationId the id of the participation
* @return the participation with latest submission and result
* @throws EntityNotFoundException
*/
EneaGore marked this conversation as resolved.
Show resolved Hide resolved
public StudentParticipation findTextExerciseParticipationWithLatestSubmissionAndResultElseThrow(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);
}
return studentParticipation;
}
EneaGore marked this conversation as resolved.
Show resolved Hide resolved

/**
* Get all programming exercise participations belonging to exercise and student with eager results and submissions.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package de.tum.in.www1.artemis.service;

import static de.tum.in.www1.artemis.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.in.www1.artemis.domain.Feedback;
import de.tum.in.www1.artemis.domain.Result;
import de.tum.in.www1.artemis.domain.TextExercise;
import de.tum.in.www1.artemis.domain.TextSubmission;
import de.tum.in.www1.artemis.domain.enumeration.AssessmentType;
import de.tum.in.www1.artemis.domain.enumeration.FeedbackType;
import de.tum.in.www1.artemis.domain.participation.Participation;
import de.tum.in.www1.artemis.domain.participation.StudentParticipation;
import de.tum.in.www1.artemis.repository.ResultRepository;
import de.tum.in.www1.artemis.service.connectors.athena.AthenaFeedbackSuggestionsService;
import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException;
import de.tum.in.www1.artemis.web.rest.errors.InternalServerErrorException;
import de.tum.in.www1.artemis.web.websocket.ResultWebsocketService;

@Profile(PROFILE_CORE)
@Service
public class TextExerciseFeedbackService {

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

public static final String NON_GRADED_FEEDBACK_SUGGESTION = "NonGradedFeedbackSuggestion:";

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 TextExerciseFeedbackService(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();

long countOfAthenaResults = athenaResults.size();

if (countOfAthenaResults >= 10) {
EneaGore marked this conversation as resolved.
Show resolved Hide resolved
throw new BadRequestAlertException("Maximum number of AI feedback requests reached.", "participation", "preconditions not met");
EneaGore marked this conversation as resolved.
Show resolved Hide resolved
}
}
EneaGore marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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) {
if (this.athenaFeedbackSuggestionsService.isPresent()) {
this.checkRateLimitOrThrow(participation);
CompletableFuture.runAsync(() -> this.generateAutomaticNonGradedFeedback(participation, textExercise));
}
return participation;
EneaGore marked this conversation as resolved.
Show resolved Hide resolved
EneaGore marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Generates automatic non-graded feedback for a text 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 textExercise the text exercise object.
*/
public void generateAutomaticNonGradedFeedback(StudentParticipation participation, TextExercise textExercise) {
log.debug("Using athena to generate (text exercise) feedback request: {}", textExercise.getId());

// athena takes over the control here
var submissionOptional = participationService.findTextExerciseParticipationWithLatestSubmissionAndResultElseThrow(participation.getId()).findLatestSubmission();

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

Result automaticResult = new Result();
automaticResult.setAssessmentType(AssessmentType.AUTOMATIC_ATHENA);
EneaGore marked this conversation as resolved.
Show resolved Hide resolved
automaticResult.setRated(true);
automaticResult.setScore(0.0);
automaticResult.setSuccessful(null);
automaticResult.setSubmission(submission);
automaticResult.setParticipation(participation);
try {
this.resultWebsocketService.broadcastNewResult((Participation) participation, automaticResult);

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

var athenaResponse = this.athenaFeedbackSuggestionsService.orElseThrow().getTextFeedbackSuggestions(textExercise, (TextSubmission) submission, false);

List<Feedback> feedbacks = athenaResponse.stream().filter(individualFeedbackItem -> individualFeedbackItem.description() != null).map(individualFeedbackItem -> {
var feedback = new Feedback();
feedback.setText(individualFeedbackItem.title());
feedback.setDetailText(individualFeedbackItem.description());
feedback.setHasLongFeedbackText(false);
feedback.setType(FeedbackType.AUTOMATIC);
feedback.setCredits(individualFeedbackItem.credits());
EneaGore marked this conversation as resolved.
Show resolved Hide resolved
return feedback;
}).toList();

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));

automaticResult = this.resultRepository.save(automaticResult);
resultService.storeFeedbackInResult(automaticResult, feedbacks, true);
submissionService.saveNewResult(submission, automaticResult);
this.resultWebsocketService.broadcastNewResult((Participation) participation, automaticResult);
}
catch (Exception e) {
log.error("Could not generate feedback", e);
throw new InternalServerErrorException("Something went wrong... AI Feedback could not be generated");
}
EneaGore marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ public TextSubmission handleTextSubmission(TextSubmission textSubmission, TextEx
if (exercise.isExamExercise() || exerciseDateService.isBeforeDueDate(participation)) {
textSubmission.setSubmitted(true);
}

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

textSubmission = save(textSubmission, participation, exercise, user);
return textSubmission;
}
Expand All @@ -104,7 +111,6 @@ private TextSubmission save(TextSubmission textSubmission, StudentParticipation
participation.setInitializationState(InitializationState.FINISHED);
studentParticipationRepository.save(participation);
}

// remove result from submission (in the unlikely case it is passed here), so that students cannot inject a result
textSubmission.setResults(new ArrayList<>());
textSubmission = textSubmissionRepository.save(textSubmission);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ protected ResponseEntity<List<Submission>> getAllSubmissions(Long exerciseId, bo
if (submission.getParticipation() != null && submission.getParticipation().getExercise() != null) {
submission.getParticipation().setExercise(null);
}
// Important for exercises with Athena results
if (assessedByTutor) {
submission.setResults(submission.getNonAthenaResults());
}
});

return ResponseEntity.ok().body(submissions);
Expand Down
Loading
Loading