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

Modeling exercises: Inline AI feedback view #9799

Merged
merged 47 commits into from
Dec 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
5cb4a77
timeline
LeonWehrhahn Nov 14, 2024
ddfa893
Enhance result display in modeling submission component
LeonWehrhahn Nov 15, 2024
7c46295
Merge branch 'develop' into feature/modeling/inline-ai-feedback
LeonWehrhahn Nov 15, 2024
1419f8b
Enhance modeling submission component to display latest submission in…
LeonWehrhahn Nov 15, 2024
64cae96
Align reference extraction and feedback assignment
LeonWehrhahn Nov 15, 2024
17e99f2
Merge branch 'develop' into feature/modeling/inline-ai-feedback
LeonWehrhahn Nov 16, 2024
72540fa
Fix typos in modeling participation header and remove debug logs
LeonWehrhahn Nov 16, 2024
a332d1b
Merge branch 'feature/modeling/inline-ai-feedback' of https://github.…
LeonWehrhahn Nov 16, 2024
5b0d289
Align test with updated feedback reference type
LeonWehrhahn Nov 16, 2024
0126d9c
Refactor result collection to use toList()
LeonWehrhahn Nov 16, 2024
deacc2c
Align tests with new reference parsing
LeonWehrhahn Nov 17, 2024
39190c7
Align tests with new reference parsing
LeonWehrhahn Nov 17, 2024
812459f
Merge branch 'develop' into feature/modeling/inline-ai-feedback
LeonWehrhahn Nov 17, 2024
1aa614d
Add test for getLatestSubmissionForModelingEditor
LeonWehrhahn Nov 17, 2024
896594c
Merge branch 'feature/modeling/inline-ai-feedback' of https://github.…
LeonWehrhahn Nov 17, 2024
38ba6d5
Add test for getSubmissionsWithResultsForParticipation
LeonWehrhahn Nov 18, 2024
60ce6e9
Add test case for ModelingSubmissionComponent
LeonWehrhahn Nov 18, 2024
d7c1ea7
Merge branch 'develop' into feature/modeling/inline-ai-feedback
LeonWehrhahn Nov 18, 2024
110fb81
Add athena alerts for failures and successes
LeonWehrhahn Nov 20, 2024
9739563
Merge branch 'develop' into feature/modeling/inline-ai-feedback
LeonWehrhahn Nov 20, 2024
efb7b6e
Merge branch 'feature/modeling/inline-ai-feedback' of https://github.…
LeonWehrhahn Nov 20, 2024
76d02b5
Remove result header
LeonWehrhahn Nov 20, 2024
b6155ca
Remove result header
LeonWehrhahn Nov 20, 2024
2f97431
Fix result updates
LeonWehrhahn Nov 20, 2024
a7ba25a
Fix result updates
LeonWehrhahn Nov 20, 2024
9edd157
Merge branch 'develop' into feature/modeling/inline-ai-feedback
LeonWehrhahn Nov 20, 2024
af282cd
Adjust tests to remove resultString
LeonWehrhahn Nov 21, 2024
4dbc556
Merge remote-tracking branch 'origin/feature/modeling/inline-ai-feedb…
LeonWehrhahn Nov 21, 2024
d4702a9
Merge branch 'develop' into feature/modeling/inline-ai-feedback
LeonWehrhahn Nov 21, 2024
44a0200
Refactor result update handling in ModelingSubmissionComponent to sep…
LeonWehrhahn Nov 22, 2024
088714c
Merge branch 'feature/modeling/inline-ai-feedback' of https://github.…
LeonWehrhahn Nov 22, 2024
21aa1be
Adjust abd add tests
LeonWehrhahn Nov 22, 2024
afc503f
Adjust tests
LeonWehrhahn Nov 22, 2024
a9d44fd
Adjust tests
LeonWehrhahn Nov 22, 2024
cd8443c
Change alterService type
LeonWehrhahn Nov 23, 2024
96edbcb
Merge branch 'develop' into feature/modeling/inline-ai-feedback
LeonWehrhahn Nov 26, 2024
aab656e
Add sever check for existing Athena result
LeonWehrhahn Nov 26, 2024
3863e49
Merge remote-tracking branch 'origin/feature/modeling/inline-ai-feedb…
LeonWehrhahn Nov 26, 2024
067a596
Merge branch 'develop' into feature/modeling/inline-ai-feedback
LeonWehrhahn Nov 27, 2024
5c048be
Adjust test to align with alert type change
LeonWehrhahn Nov 29, 2024
2fb61bd
Merge branch 'feature/modeling/inline-ai-feedback' of https://github.…
LeonWehrhahn Nov 29, 2024
78e1a4a
Merge branch 'develop' into feature/modeling/inline-ai-feedback
LeonWehrhahn Nov 29, 2024
55e61c6
Adjust athena rate limit error message...
LeonWehrhahn Dec 4, 2024
cb61b84
Merge branch 'feature/modeling/inline-ai-feedback' of https://github.…
LeonWehrhahn Dec 4, 2024
60d87fb
Merge branch 'develop' into feature/modeling/inline-ai-feedback
LeonWehrhahn Dec 4, 2024
656c130
Merge branch 'develop' into feature/modeling/inline-ai-feedback
maximiliansoelch Dec 5, 2024
95d05a4
Merge branch 'develop' into feature/modeling/inline-ai-feedback
maximiliansoelch Dec 6, 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
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ default List<Result> findLatestAutomaticResultsWithEagerFeedbacksTestCasesForExe

Optional<Result> findFirstByParticipationIdOrderByCompletionDateDesc(long participationId);

Optional<Result> findFirstByParticipationIdAndAssessmentTypeOrderByCompletionDateDesc(long participationId, AssessmentType assessmentType);

@EntityGraph(type = LOAD, attributePaths = { "feedbacks", "feedbacks.testCase" })
Optional<Result> findResultWithFeedbacksAndTestCasesById(long resultId);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package de.tum.cit.aet.artemis.athena.dto;

import java.util.List;

import jakarta.validation.constraints.NotNull;

import com.fasterxml.jackson.annotation.JsonInclude;
Expand All @@ -13,7 +11,7 @@
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record ModelingFeedbackDTO(long id, long exerciseId, long submissionId, String title, String description, double credits, Long structuredGradingInstructionId,
List<String> elementIds) implements FeedbackBaseDTO {
String reference) implements FeedbackBaseDTO {

/**
* Creates a ModelingFeedbackDTO from a Feedback object
Expand All @@ -30,6 +28,6 @@ public static ModelingFeedbackDTO of(long exerciseId, long submissionId, @NotNul
}

return new ModelingFeedbackDTO(feedback.getId(), exerciseId, submissionId, feedback.getText(), feedback.getDetailText(), feedback.getCredits(), gradingInstructionId,
List.of(feedback.getReference()));
feedback.getReference());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,6 @@ public ModelingExerciseFeedbackService(Optional<AthenaFeedbackSuggestionsService
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.
Expand All @@ -79,6 +70,7 @@ private void checkRateLimitOrThrow(StudentParticipation participation) {
public StudentParticipation handleNonGradedFeedbackRequest(StudentParticipation participation, ModelingExercise modelingExercise) {
if (this.athenaFeedbackSuggestionsService.isPresent()) {
this.checkRateLimitOrThrow(participation);
this.checkLatestSubmissionHasAthenaResultOrThrow(participation);
CompletableFuture.runAsync(() -> this.generateAutomaticNonGradedFeedback(participation, modelingExercise));
}
return participation;
Expand Down Expand Up @@ -125,6 +117,10 @@ public void generateAutomaticNonGradedFeedback(StudentParticipation participatio
}
catch (Exception e) {
log.error("Could not generate feedback for exercise ID: {} and participation ID: {}", modelingExercise.getId(), participation.getId(), e);
automaticResult.setSuccessful(false);
automaticResult.setCompletionDate(null);
participation.addResult(automaticResult);
this.resultWebsocketService.broadcastNewResult(participation, automaticResult);
throw new InternalServerErrorException("Something went wrong... AI Feedback could not be generated");
}
}
Expand Down Expand Up @@ -173,6 +169,7 @@ private Feedback convertToFeedback(ModelingFeedbackDTO feedbackItem) {
feedback.setHasLongFeedbackText(false);
feedback.setType(FeedbackType.AUTOMATIC);
feedback.setCredits(feedbackItem.credits());
feedback.setReference(feedbackItem.reference());
return feedback;
}

Expand All @@ -193,4 +190,45 @@ private double calculateTotalFeedbackScore(List<Feedback> feedbacks, ModelingExe

return (totalCredits / maxPoints) * 100;
}

/**
* Checks if the number of Athena results for the given participation exceeds
* the allowed threshold and throws an exception if the limit is reached.
*
* @param participation the student participation to check
* @throws BadRequestAlertException if the maximum number of Athena feedback requests is exceeded
*/
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", "maxAthenaResultsReached", true);
}
}
krusche marked this conversation as resolved.
Show resolved Hide resolved

/**
* Ensures that the latest submission associated with the participation does not already
* have an Athena-generated result. Throws an exception if Athena result already exists.
*
* @param participation the student participation to validate
* @throws BadRequestAlertException if no legal submissions exist or if an Athena result is already present
*/
private void checkLatestSubmissionHasAthenaResultOrThrow(StudentParticipation participation) {
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 latestResult = submission.getLatestResult();

if (latestResult != null && latestResult.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA) {
log.debug("Submission ID: {} already has an Athena result. Skipping feedback generation.", submission.getId());
throw new BadRequestAlertException("Submission already has an Athena result", "submission", "submissionAlreadyHasAthenaResult", true);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
Expand Down Expand Up @@ -31,6 +32,8 @@
import de.tum.cit.aet.artemis.assessment.domain.GradingCriterion;
import de.tum.cit.aet.artemis.assessment.domain.Result;
import de.tum.cit.aet.artemis.assessment.repository.GradingCriterionRepository;
import de.tum.cit.aet.artemis.assessment.repository.ResultRepository;
import de.tum.cit.aet.artemis.assessment.service.ResultService;
import de.tum.cit.aet.artemis.core.domain.User;
import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException;
import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException;
Expand Down Expand Up @@ -67,6 +70,8 @@ public class ModelingSubmissionResource extends AbstractSubmissionResource {

private static final String ENTITY_NAME = "modelingSubmission";

private final ResultRepository resultRepository;

@Value("${jhipster.clientApp.name}")
private String applicationName;

Expand All @@ -82,17 +87,21 @@ public class ModelingSubmissionResource extends AbstractSubmissionResource {

private final PlagiarismService plagiarismService;

private final ResultService resultService;

public ModelingSubmissionResource(SubmissionRepository submissionRepository, ModelingSubmissionService modelingSubmissionService,
ModelingExerciseRepository modelingExerciseRepository, AuthorizationCheckService authCheckService, UserRepository userRepository, ExerciseRepository exerciseRepository,
GradingCriterionRepository gradingCriterionRepository, ExamSubmissionService examSubmissionService, StudentParticipationRepository studentParticipationRepository,
ModelingSubmissionRepository modelingSubmissionRepository, PlagiarismService plagiarismService) {
ModelingSubmissionRepository modelingSubmissionRepository, PlagiarismService plagiarismService, ResultService resultService, ResultRepository resultRepository) {
super(submissionRepository, authCheckService, userRepository, exerciseRepository, modelingSubmissionService, studentParticipationRepository);
this.modelingSubmissionService = modelingSubmissionService;
this.modelingExerciseRepository = modelingExerciseRepository;
this.gradingCriterionRepository = gradingCriterionRepository;
this.examSubmissionService = examSubmissionService;
this.modelingSubmissionRepository = modelingSubmissionRepository;
this.plagiarismService = plagiarismService;
this.resultService = resultService;
this.resultRepository = resultRepository;
}

/**
Expand Down Expand Up @@ -367,4 +376,81 @@ public ResponseEntity<ModelingSubmission> getLatestSubmissionForModelingEditor(@

return ResponseEntity.ok(modelingSubmission);
}

/**
* GET /participations/{participationId}/submissions-with-results : get submissions with results for a particular student participation.
* When the assessment period is not over yet, only submissions with Athena results are returned.
* When the assessment period is over, both Athena and normal results are returned.
*
* @param participationId the id of the participation for which to get the submissions with results
* @return the ResponseEntity with status 200 (OK) and with body the list of submissions with results and feedbacks, or with status 404 (Not Found) if the participation could
* not be found
*/
@GetMapping("participations/{participationId}/submissions-with-results")
@EnforceAtLeastStudent
public ResponseEntity<List<Submission>> getSubmissionsWithResultsForParticipation(@PathVariable long participationId) {
log.debug("REST request to get submissions with results for participation: {}", participationId);

// Retrieve and check the participation
StudentParticipation participation = studentParticipationRepository.findByIdWithLegalSubmissionsResultsFeedbackElseThrow(participationId);
User user = userRepository.getUserWithGroupsAndAuthorities();

if (participation.getExercise() == null) {
return ResponseEntity.badRequest()
.headers(HeaderUtil.createFailureAlert(applicationName, true, "modelingExercise", "exerciseEmpty", "The exercise belonging to the participation is null."))
.body(null);
}

if (!(participation.getExercise() instanceof ModelingExercise modelingExercise)) {
return ResponseEntity.badRequest().headers(
HeaderUtil.createFailureAlert(applicationName, true, "modelingExercise", "wrongExerciseType", "The exercise of the participation is not a modeling exercise."))
.body(null);
}

// Students can only see their own models (to prevent cheating). TAs, instructors and admins can see all models.
boolean isAtLeastTutor = authCheckService.isAtLeastTeachingAssistantForExercise(modelingExercise, user);
if (!(authCheckService.isOwnerOfParticipation(participation) || isAtLeastTutor)) {
throw new AccessForbiddenException();
}

// Exam exercises cannot be seen by students between the endDate and the publishResultDate
if (!authCheckService.isAllowedToGetExamResult(modelingExercise, participation, user)) {
throw new AccessForbiddenException();
}

boolean isStudent = !isAtLeastTutor;

// Get the submissions associated with the participation
Set<Submission> submissions = participation.getSubmissions();

// Filter submissions to only include those with relevant results
List<Submission> submissionsWithResults = submissions.stream().filter(submission -> {

submission.setParticipation(participation);

// Filter results within each submission based on assessment type and period
List<Result> filteredResults = submission.getResults().stream().filter(result -> {
if (isStudent) {
if (ExerciseDateService.isAfterAssessmentDueDate(modelingExercise)) {
return true; // Include all results if the assessment period is over
}
else {
return result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA; // Only include Athena results if the assessment period is not over
}
}
else {
return true; // Tutors and above can see all results
}
}).peek(Result::filterSensitiveInformation).sorted(Comparator.comparing(Result::getCompletionDate).reversed()).toList();

// Set filtered results back into the submission if any results remain after filtering
if (!filteredResults.isEmpty()) {
submission.setResults(filteredResults);
return true; // Include submission as it has relevant results
}
return false;
}).toList();
krusche marked this conversation as resolved.
Show resolved Hide resolved

return ResponseEntity.ok().body(submissionsWithResults);
}
}
27 changes: 7 additions & 20 deletions src/main/webapp/app/assessment/athena.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { TextBlockRef } from 'app/entities/text/text-block-ref.model';
import { TextSubmission } from 'app/entities/text/text-submission.model';
import { PROFILE_ATHENA } from 'app/app.constants';
import { ModelingSubmission } from 'app/entities/modeling-submission.model';
import { UMLModel, findElement } from '@ls1intum/apollon';

@Injectable({ providedIn: 'root' })
export class AthenaService {
Expand Down Expand Up @@ -169,45 +168,33 @@ export class AthenaService {
public getModelingFeedbackSuggestions(exercise: Exercise, submission: ModelingSubmission): Observable<Feedback[]> {
return this.getFeedbackSuggestions<ModelingFeedbackSuggestion>(exercise, submission.id!).pipe(
map((suggestions) => {
const referencedElementIDs = new Set();

const model: UMLModel | undefined = submission.model ? JSON.parse(submission.model) : undefined;

return suggestions.map((suggestion, index) => {
const feedback = new Feedback();
feedback.id = index;
feedback.credits = suggestion.credits;
feedback.positive = suggestion.credits >= 1;

// Even though Athena can reference multiple elements for the same feedback item, Apollon can only
// attach feedback to one element, so we select the first element ID mentioned. To ensure that not
// more than one feedback item is attached to the same element, we additionally ensure that the
// same element is only referenced once.
const referenceId: string | undefined = suggestion.elementIds.filter((id) => !referencedElementIDs.has(id))[0];
// Extract reference details if present
const reference = suggestion.reference?.split(':');
const [referenceType, referenceId] = reference || [];
krusche marked this conversation as resolved.
Show resolved Hide resolved

if (referenceId) {
feedback.type = FeedbackType.AUTOMATIC;
feedback.text = suggestion.description;

feedback.reference = suggestion.reference;
feedback.referenceId = referenceId;

referencedElementIDs.add(referenceId);

if (model && feedback.referenceId) {
const element = findElement(model, feedback.referenceId);
feedback.referenceType = element?.type;
feedback.reference = `${element?.type}:${referenceId}`;
}
feedback.referenceType = referenceType;
} else {
feedback.type = FeedbackType.MANUAL_UNREFERENCED;
feedback.text = `${FEEDBACK_SUGGESTION_IDENTIFIER}${suggestion.title}`;
feedback.detailText = suggestion.description;
}

// Load grading instruction from exercise, if available
// Attach grading instruction if available
if (suggestion.structuredGradingInstructionId) {
feedback.gradingInstruction = this.findGradingInstruction(exercise, suggestion.structuredGradingInstructionId);
}

return feedback;
});
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,6 @@ export class ModelingFeedbackSuggestion {
public description: string,
public credits: number,
public structuredGradingInstructionId: number | undefined,
public elementIds: string[],
public reference: string | undefined,
) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ArtemisFullscreenModule } from 'app/shared/fullscreen/fullscreen.module
import { RatingModule } from 'app/exercises/shared/rating/rating.module';
import { ArtemisMarkdownModule } from 'app/shared/markdown.module';
import { ArtemisTeamParticipeModule } from 'app/exercises/shared/team/team-participate/team-participate.module';
import { RequestFeedbackButtonComponent } from 'app/overview/exercise-details/request-feedback-button/request-feedback-button.component';

@NgModule({
imports: [
Expand All @@ -29,6 +30,7 @@ import { ArtemisTeamParticipeModule } from 'app/exercises/shared/team/team-parti
RatingModule,
ArtemisMarkdownModule,
ArtemisTeamParticipeModule,
RequestFeedbackButtonComponent,
LeonWehrhahn marked this conversation as resolved.
Show resolved Hide resolved
],
declarations: [ModelingSubmissionComponent],
exports: [ModelingSubmissionComponent],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ export const routes: Routes = [
canActivate: [UserRouteAccessService],
canDeactivate: [PendingChangesGuard],
},
{
path: 'participate/:participationId/submission/:submissionId',
component: ModelingSubmissionComponent,
data: {
authorities: [Authority.USER],
pageTitle: 'artemisApp.modelingExercise.home.title',
},
canActivate: [UserRouteAccessService],
canDeactivate: [PendingChangesGuard],
},
];

@NgModule({
Expand Down
Loading
Loading