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

Programming exercises: Allow to choose preliminary feedback model #10441

Open
wants to merge 38 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
7e6761a
differentiate between feedback suggestions and preliminary feedback m…
dmytropolityka Dec 3, 2024
107a669
Merge remote-tracking branch 'origin/develop' into feature/programmin…
dmytropolityka Dec 11, 2024
e494976
server side for differentiation between athena modules
dmytropolityka Dec 11, 2024
1d1fa2f
merge conflict
dmytropolityka Dec 11, 2024
da45bab
fix some bugs and update existing data
dmytropolityka Dec 23, 2024
f142fab
Merge branch 'develop' into feature/programming-exercises/choose-prel…
dmytropolityka Jan 1, 2025
87e748c
Change module type in Athena Resource
dmytropolityka Jan 1, 2025
c8bd16d
Merge remote-tracking branch 'origin/feature/programming-exercises/ch…
dmytropolityka Jan 1, 2025
475bec2
Adjust client tests
dmytropolityka Jan 1, 2025
5760a66
add checks for new component
dmytropolityka Jan 2, 2025
ea3a0ce
fix other tests
dmytropolityka Jan 2, 2025
61f9258
make options components standalone
dmytropolityka Jan 2, 2025
fc3968c
Adjust server tests
dmytropolityka Jan 2, 2025
8fe51e5
remove translate pipe from declarations
dmytropolityka Jan 2, 2025
d4eb493
formatting
dmytropolityka Jan 2, 2025
4552184
fine grained disabled control
dmytropolityka Jan 2, 2025
dc0955b
sync transmitted object
dmytropolityka Jan 2, 2025
97d8e59
improve log message
dmytropolityka Jan 2, 2025
4aa1c7e
replace modules in integration tests
dmytropolityka Jan 4, 2025
e2250d4
spotless
dmytropolityka Jan 4, 2025
0aeb2a0
Merge branch 'develop' into feature/programming-exercises/choose-prel…
dmytropolityka Jan 4, 2025
82d77bc
change module
dmytropolityka Jan 4, 2025
f18a1e8
change migration file
dmytropolityka Jan 5, 2025
f51bd3a
Merge remote-tracking branch 'origin/develop' into feature/programmin…
dmytropolityka Mar 4, 2025
104679f
resolve merge conflict issues
dmytropolityka Mar 4, 2025
33c0ea7
prettier
dmytropolityka Mar 4, 2025
9f505a2
add a test to ensure the request feedback button is visible
dmytropolityka Mar 4, 2025
df6188a
change export url
dmytropolityka Mar 5, 2025
4b43396
update tests, reiterate the rules to enable feedback suggestions drop…
dmytropolityka Mar 5, 2025
565d11d
eslint, prettier
dmytropolityka Mar 5, 2025
25d909c
fix typo
dmytropolityka Mar 5, 2025
c547b9b
implement AfterViewChecked interface
dmytropolityka Mar 5, 2025
7623dcc
increase test coverage and minor changes
dmytropolityka Mar 6, 2025
ca68a51
add logic for preliminary feedback modules in AthenaModuleService
dmytropolityka Mar 6, 2025
bce5757
rename parameter and update javadoc
dmytropolityka Mar 6, 2025
3672ca6
spotless
dmytropolityka Mar 6, 2025
3cca9a5
spotless
dmytropolityka Mar 6, 2025
b07288f
rename parameter
dmytropolityka Mar 6, 2025
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package de.tum.cit.aet.artemis.athena.domain;

public enum ModuleType {
FEEDBACK_SUGGESTIONS, PRELIMINARY_FEEDBACK
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,11 @@ public void sendFeedback(Exercise exercise, Submission submission, List<Feedback

try {
// Only send manual feedback from tutors to Athena
// Based on the current design, this applies only to feedback suggestions
final RequestDTO request = new RequestDTO(athenaDTOConverterService.ofExercise(exercise), athenaDTOConverterService.ofSubmission(exercise.getId(), submission),
feedbacks.stream().filter(Feedback::isManualFeedback).map((feedback) -> athenaDTOConverterService.ofFeedback(exercise, submission.getId(), feedback)).toList());
ResponseDTO response = connector.invokeWithRetry(athenaModuleService.getAthenaModuleUrl(exercise) + "/feedbacks", request, maxRetries);
ResponseDTO response = connector.invokeWithRetry(
athenaModuleService.getAthenaModuleUrl(exercise.getExerciseType(), exercise.getFeedbackSuggestionModule()) + "/feedbacks", request, maxRetries);
log.info("Athena responded to feedback: {}", response.data);
}
catch (NetworkingException networkingException) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,65 +102,77 @@ private record ResponseDTOModeling(List<ModelingFeedbackDTO> data, ResponseMetaD
/**
* Calls the remote Athena service to get feedback suggestions for a given submission.
*
* @param exercise the {@link TextExercise} the suggestions are fetched for
* @param submission the {@link TextSubmission} the suggestions are fetched for
* @param isGraded the {@link Boolean} should Athena generate grade suggestions or not
* @param exercise the {@link TextExercise} the suggestions are fetched for
* @param submission the {@link TextSubmission} the suggestions are fetched for
* @param isPreliminary the {@link Boolean} should Athena generate grade suggestions or not
* @return a list of feedback suggestions
*/
public List<TextFeedbackDTO> getTextFeedbackSuggestions(TextExercise exercise, TextSubmission submission, boolean isGraded) throws NetworkingException {
log.debug("Start Athena '{}' Feedback Suggestions Service for Exercise '{}' (#{}).", isGraded ? "Graded" : "Non Graded", exercise.getTitle(), exercise.getId());
public List<TextFeedbackDTO> getTextFeedbackSuggestions(TextExercise exercise, TextSubmission submission, boolean isPreliminary) throws NetworkingException {
log.debug("Start Athena {} Feedback Suggestions Service for Exercise '{}' (#{}).", isPreliminary ? "Non Graded" : "Graded", exercise.getTitle(), exercise.getId());

if (!Objects.equals(submission.getParticipation().getExercise().getId(), exercise.getId())) {
log.error("Exercise id {} does not match submission's exercise id {}", exercise.getId(), submission.getParticipation().getExercise().getId());
throw new ConflictException("Exercise id " + exercise.getId() + " does not match submission's exercise id " + submission.getParticipation().getExercise().getId(),
"Exercise", "exerciseIdDoesNotMatch");
}

final RequestDTO request = new RequestDTO(athenaDTOConverterService.ofExercise(exercise), athenaDTOConverterService.ofSubmission(exercise.getId(), submission), isGraded);
ResponseDTOText response = textAthenaConnector.invokeWithRetry(athenaModuleService.getAthenaModuleUrl(exercise) + "/feedback_suggestions", request, 0);
log.info("Athena responded to '{}' feedback suggestions request: {}", isGraded ? "Graded" : "Non Graded", response.data);
storeTokenUsage(exercise, submission, response.meta, !isGraded);
final RequestDTO request = new RequestDTO(athenaDTOConverterService.ofExercise(exercise), athenaDTOConverterService.ofSubmission(exercise.getId(), submission),
!isPreliminary);
ResponseDTOText response = textAthenaConnector.invokeWithRetry(
athenaModuleService.getAthenaModuleUrl(exercise.getExerciseType(), isPreliminary ? exercise.getPreliminaryFeedbackModule() : exercise.getFeedbackSuggestionModule())
+ "/feedback_suggestions",
request, 0);
log.info("Athena responded to '{}' feedback suggestions request: {}", isPreliminary ? "Non Graded" : "Graded", response.data);
storeTokenUsage(exercise, submission, response.meta, !isPreliminary);
return response.data.stream().toList();
}

/**
* Calls the remote Athena service to get feedback suggestions for a given programming submission.
*
* @param exercise the {@link ProgrammingExercise} the suggestions are fetched for
* @param submission the {@link ProgrammingSubmission} the suggestions are fetched for
* @param isGraded the {@link Boolean} should Athena generate grade suggestions or not
* @param exercise the {@link ProgrammingExercise} the suggestions are fetched for
* @param submission the {@link ProgrammingSubmission} the suggestions are fetched for
* @param isPreliminary the {@link Boolean} should Athena generate grade suggestions or not
* @return a list of feedback suggestions
*/
public List<ProgrammingFeedbackDTO> getProgrammingFeedbackSuggestions(ProgrammingExercise exercise, ProgrammingSubmission submission, boolean isGraded)
public List<ProgrammingFeedbackDTO> getProgrammingFeedbackSuggestions(ProgrammingExercise exercise, ProgrammingSubmission submission, boolean isPreliminary)
throws NetworkingException {
log.debug("Start Athena '{}' Feedback Suggestions Service for Exercise '{}' (#{}).", isGraded ? "Graded" : "Non Graded", exercise.getTitle(), exercise.getId());
final RequestDTO request = new RequestDTO(athenaDTOConverterService.ofExercise(exercise), athenaDTOConverterService.ofSubmission(exercise.getId(), submission), isGraded);
ResponseDTOProgramming response = programmingAthenaConnector.invokeWithRetry(athenaModuleService.getAthenaModuleUrl(exercise) + "/feedback_suggestions", request, 0);
log.info("Athena responded to '{}' feedback suggestions request: {}", isGraded ? "Graded" : "Non Graded", response.data);
storeTokenUsage(exercise, submission, response.meta, !isGraded);
log.debug("Start Athena {} Feedback Suggestions Service for Exercise '{}' (#{}).", isPreliminary ? "Non Graded" : "Graded", exercise.getTitle(), exercise.getId());
final RequestDTO request = new RequestDTO(athenaDTOConverterService.ofExercise(exercise), athenaDTOConverterService.ofSubmission(exercise.getId(), submission),
!isPreliminary);
ResponseDTOProgramming response = programmingAthenaConnector.invokeWithRetry(
athenaModuleService.getAthenaModuleUrl(exercise.getExerciseType(), isPreliminary ? exercise.getPreliminaryFeedbackModule() : exercise.getFeedbackSuggestionModule())
+ "/feedback_suggestions",
request, 0);
log.info("Athena responded to '{}' feedback suggestions request: {}", isPreliminary ? "Non-Graded" : "Graded", response.data);
storeTokenUsage(exercise, submission, response.meta, !isPreliminary);
return response.data.stream().toList();
}

/**
* Retrieve feedback suggestions for a given modeling exercise submission from Athena
*
* @param exercise the {@link ModelingExercise} the suggestions are fetched for
* @param submission the {@link ModelingSubmission} the suggestions are fetched for
* @param isGraded the {@link Boolean} should Athena generate grade suggestions or not
* @param exercise the {@link ModelingExercise} the suggestions are fetched for
* @param submission the {@link ModelingSubmission} the suggestions are fetched for
* @param isPreliminary the {@link Boolean} should Athena generate grade suggestions or not
* @return a list of feedback suggestions generated by Athena
*/
public List<ModelingFeedbackDTO> getModelingFeedbackSuggestions(ModelingExercise exercise, ModelingSubmission submission, boolean isGraded) throws NetworkingException {
log.debug("Start Athena '{}' Feedback Suggestions Service for Modeling Exercise '{}' (#{}).", isGraded ? "Graded" : "Non Graded", exercise.getTitle(), exercise.getId());
public List<ModelingFeedbackDTO> getModelingFeedbackSuggestions(ModelingExercise exercise, ModelingSubmission submission, boolean isPreliminary) throws NetworkingException {
log.debug("Start Athena {} Feedback Suggestions Service for Modeling Exercise '{}' (#{}).", isPreliminary ? "Non Graded" : "Graded", exercise.getTitle(), exercise.getId());

if (!Objects.equals(submission.getParticipation().getExercise().getId(), exercise.getId())) {
throw new ConflictException("Exercise id " + exercise.getId() + " does not match submission's exercise id " + submission.getParticipation().getExercise().getId(),
"Exercise", "exerciseIdDoesNotMatch");
}

final RequestDTO request = new RequestDTO(athenaDTOConverterService.ofExercise(exercise), athenaDTOConverterService.ofSubmission(exercise.getId(), submission), isGraded);
ResponseDTOModeling response = modelingAthenaConnector.invokeWithRetry(athenaModuleService.getAthenaModuleUrl(exercise) + "/feedback_suggestions", request, 0);
log.info("Athena responded to '{}' feedback suggestions request: {}", isGraded ? "Graded" : "Non Graded", response.data);
storeTokenUsage(exercise, submission, response.meta, !isGraded);
final RequestDTO request = new RequestDTO(athenaDTOConverterService.ofExercise(exercise), athenaDTOConverterService.ofSubmission(exercise.getId(), submission),
!isPreliminary);
ResponseDTOModeling response = modelingAthenaConnector.invokeWithRetry(
athenaModuleService.getAthenaModuleUrl(exercise.getExerciseType(), isPreliminary ? exercise.getPreliminaryFeedbackModule() : exercise.getFeedbackSuggestionModule())
+ "/feedback_suggestions",
request, 0);
log.info("Athena responded to '{}' feedback suggestions request: {}", isPreliminary ? "Non Graded" : "Graded", response.data);
storeTokenUsage(exercise, submission, response.meta, !isPreliminary);
return response.data;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import de.tum.cit.aet.artemis.athena.domain.ModuleType;
import de.tum.cit.aet.artemis.core.domain.Course;
import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException;
import de.tum.cit.aet.artemis.core.exception.NetworkingException;
Expand Down Expand Up @@ -107,21 +108,22 @@ public List<String> getAthenaModulesForCourse(Course course, ExerciseType exerci
/**
* Get the URL for an Athena module, depending on the type of exercise.
*
* @param exercise The exercise for which the URL to Athena should be returned
* @param exerciseType The exercise type for which the URL to Athena should be returned
* @param module The name of the Athena module to be consulted
* @return The URL prefix to access the Athena module. Example: <a href="http://athena.example.com/modules/text/module_text_cofee"></a>
*/
public String getAthenaModuleUrl(Exercise exercise) {
switch (exercise.getExerciseType()) {
public String getAthenaModuleUrl(ExerciseType exerciseType, String module) {
switch (exerciseType) {
case TEXT -> {
return athenaUrl + "/modules/text/" + exercise.getFeedbackSuggestionModule();
return athenaUrl + "/modules/text/" + module;
}
case PROGRAMMING -> {
return athenaUrl + "/modules/programming/" + exercise.getFeedbackSuggestionModule();
return athenaUrl + "/modules/programming/" + module;
}
case MODELING -> {
return athenaUrl + "/modules/modeling/" + exercise.getFeedbackSuggestionModule();
return athenaUrl + "/modules/modeling/" + module;
}
default -> throw new IllegalArgumentException("Exercise type not supported: " + exercise.getExerciseType());
default -> throw new IllegalArgumentException("Exercise type not supported: " + exerciseType);
}
}

Expand All @@ -130,22 +132,34 @@ public String getAthenaModuleUrl(Exercise exercise) {
*
* @param exercise The exercise for which the access should be checked
* @param course The course to which the exercise belongs to.
* @param moduleType The module type for which the access should be checked.
* @param entityName Name of the entity
* @throws BadRequestAlertException when the exercise has no access to the exercise's provided module.
*/
public void checkHasAccessToAthenaModule(Exercise exercise, Course course, String entityName) throws BadRequestAlertException {
if (exercise.isExamExercise() && exercise.getFeedbackSuggestionModule() != null) {
public void checkHasAccessToAthenaModule(Exercise exercise, Course course, ModuleType moduleType, String entityName) throws BadRequestAlertException {
String module = getModule(exercise, moduleType);
if (exercise.isExamExercise() && module != null) {
throw new BadRequestAlertException("The exam exercise has no access to Athena", entityName, "examExerciseNoAccessToAthena");
}
if (!course.getRestrictedAthenaModulesAccess() && restrictedModules.contains(exercise.getFeedbackSuggestionModule())) {
if (!course.getRestrictedAthenaModulesAccess() && restrictedModules.contains(module)) {
// Course does not have access to the restricted Athena modules
throw new BadRequestAlertException("The exercise has no access to the selected Athena module", entityName, "noAccessToAthenaModule");
throw new BadRequestAlertException("The exercise has no access to the selected Athena module of type " + moduleType, entityName, "noAccessToAthenaModule");
}
}

private static String getModule(Exercise exercise, ModuleType moduleType) {
String module = null;
switch (moduleType) {
case ModuleType.FEEDBACK_SUGGESTIONS -> module = exercise.getFeedbackSuggestionModule();
case ModuleType.PRELIMINARY_FEEDBACK -> module = exercise.getPreliminaryFeedbackModule();
}
return module;
}

/**
* Checks if a module change is valid or not. In case it is not allowed it throws an exception.
* Modules cannot be changed after the exercise due date has passed.
* Holds only for feedback suggestion modules.
*
* @param originalExercise The exercise before the update
* @param updatedExercise The exercise after the update
Expand All @@ -154,9 +168,13 @@ public void checkHasAccessToAthenaModule(Exercise exercise, Course course, Strin
*/
public void checkValidAthenaModuleChange(Exercise originalExercise, Exercise updatedExercise, String entityName) throws BadRequestAlertException {
var dueDate = originalExercise.getDueDate();
if (!Objects.equals(originalExercise.getFeedbackSuggestionModule(), updatedExercise.getFeedbackSuggestionModule()) && dueDate != null
&& dueDate.isBefore(ZonedDateTime.now())) {
throw new BadRequestAlertException("Athena module can't be changed after due date has passed", entityName, "athenaModuleChangeAfterDueDate");
checkValidityOfAnAthenaModuleBasedOnDueDate(originalExercise.getFeedbackSuggestionModule(), updatedExercise.getFeedbackSuggestionModule(), entityName, dueDate);
checkValidityOfAnAthenaModuleBasedOnDueDate(originalExercise.getPreliminaryFeedbackModule(), updatedExercise.getPreliminaryFeedbackModule(), entityName, dueDate);
}

private static void checkValidityOfAnAthenaModuleBasedOnDueDate(String originalExerciseModule, String updatedExerciseModule, String entityName, ZonedDateTime dueDate) {
if (!Objects.equals(originalExerciseModule, updatedExerciseModule) && dueDate != null && dueDate.isBefore(ZonedDateTime.now())) {
throw new BadRequestAlertException("Athena module can't be changed after due date has passed", entityName, " ");
}
}

Expand All @@ -165,7 +183,8 @@ public void checkValidAthenaModuleChange(Exercise originalExercise, Exercise upd
*
* @param course The course for which the access to restricted modules should be revoked
*/
public void revokeAccessToRestrictedFeedbackSuggestionModules(Course course) {
public void revokeAccessToRestrictedFeedbackModules(Course course) {
exerciseRepository.revokeAccessToRestrictedFeedbackSuggestionModulesByCourseId(course.getId(), restrictedModules);
exerciseRepository.revokeAccessToRestrictedPreliminaryFeedbackModulesByCourseId(course.getId(), restrictedModules);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public AthenaRepositoryExportService(ProgrammingExerciseRepository programmingEx
* @throws AccessForbiddenException if the feedback suggestions are not enabled for the given exercise
*/
private void checkFeedbackSuggestionsOrAutomaticFeedbackEnabledElseThrow(Exercise exercise) {
if (!(exercise.areFeedbackSuggestionsEnabled() || exercise.getAllowFeedbackRequests())) {
if (!(exercise.areFeedbackSuggestionsEnabled() || exercise.isPreliminaryFeedbackEnabled())) {
log.error("Feedback suggestions are not enabled for exercise {}", exercise.getId());
throw new ServiceUnavailableException("Feedback suggestions are not enabled for exercise");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ public Optional<Long> getProposedSubmissionId(Exercise exercise, List<Long> subm
try {
final RequestDTO request = new RequestDTO(athenaDTOConverterService.ofExercise(exercise), submissionIds);
// allow no retries because this should be fast and it's not too bad if it fails
ResponseDTO response = connector.invokeWithRetry(athenaModuleService.getAthenaModuleUrl(exercise) + "/select_submission", request, 0);
// applies only to feedback suggestions
ResponseDTO response = connector
.invokeWithRetry(athenaModuleService.getAthenaModuleUrl(exercise.getExerciseType(), exercise.getFeedbackSuggestionModule()) + "/select_submission", request, 0);
log.info("Athena to calculate next proposes submissions responded: {}", response.submissionId);
if (response.submissionId == -1) {
return Optional.empty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ public void sendSubmissions(Exercise exercise, Set<Submission> submissions, int
try {
final RequestDTO request = new RequestDTO(athenaDTOConverterService.ofExercise(exercise),
filteredSubmissions.stream().map((submission) -> athenaDTOConverterService.ofSubmission(exercise.getId(), submission)).toList());
ResponseDTO response = connector.invokeWithRetry(athenaModuleService.getAthenaModuleUrl(exercise) + "/submissions", request, maxRetries);
// applies only to feedback suggestions
ResponseDTO response = connector.invokeWithRetry(
athenaModuleService.getAthenaModuleUrl(exercise.getExerciseType(), exercise.getFeedbackSuggestionModule()) + "/submissions", request, maxRetries);
log.info("Athena (calculating automatic feedback) responded: {}", response.data);
}
catch (NetworkingException error) {
Expand Down
Loading
Loading