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

Iris: Improve code quality #9494

Draft
wants to merge 3 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
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 @@ -9,13 +9,30 @@
import de.tum.cit.aet.artemis.atlas.domain.competency.CompetencyJol;

/**
* A DTO for the CompetencyJol entity.
* Pyris DTO mapping for a {@code CompetencyJol}.
*/
// @formatter:off
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record CompetencyJolDTO(long id, long competencyId, short jolValue, ZonedDateTime judgementTime, double competencyProgress, double competencyConfidence) {
public record CompetencyJolDTO(
long id,
long competencyId,
short jolValue,
ZonedDateTime judgementTime,
double competencyProgress,
double competencyConfidence
// @formatter:on
) {

public static CompetencyJolDTO of(@NotNull CompetencyJol competencyJol) {
return new CompetencyJolDTO(competencyJol.getId(), competencyJol.getCompetency().getId(), competencyJol.getValue(), competencyJol.getJudgementTime(),
competencyJol.getCompetencyProgress(), competencyJol.getCompetencyConfidence());
public static CompetencyJolDTO from(@NotNull CompetencyJol competencyJol) {
// @formatter:off
return new CompetencyJolDTO(
competencyJol.getId(),
competencyJol.getCompetency().getId(),
competencyJol.getValue(),
competencyJol.getJudgementTime(),
competencyJol.getCompetencyProgress(),
competencyJol.getCompetencyConfidence()
);
// @formatter:on
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record CompetencyJolPairDTO(CompetencyJolDTO current, CompetencyJolDTO prior) {

public static CompetencyJolPairDTO of(CompetencyJol current, CompetencyJol prior) {
return new CompetencyJolPairDTO(current != null ? CompetencyJolDTO.of(current) : null, prior != null ? CompetencyJolDTO.of(prior) : null);
public static CompetencyJolPairDTO from(CompetencyJol current, CompetencyJol prior) {
return new CompetencyJolPairDTO(current != null ? CompetencyJolDTO.from(current) : null, prior != null ? CompetencyJolDTO.from(prior) : null);
Comment on lines +17 to +18
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Approved with a minor suggestion for improved readability.

The changes look good overall. The method name change from of to from improves consistency with CompetencyJolDTO, and the updated logic correctly uses CompetencyJolDTO.from(). The null checks are maintained, preserving the existing behavior.

Consider splitting the long line for better readability:

public static CompetencyJolPairDTO from(CompetencyJol current, CompetencyJol prior) {
    return new CompetencyJolPairDTO(
        current != null ? CompetencyJolDTO.from(current) : null,
        prior != null ? CompetencyJolDTO.from(prior) : null
    );
}

This formatting maintains the logic while improving code readability.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ public CompetencyJolPairDTO getLatestJudgementOfLearningPairForUserByCompetencyI
return new CompetencyJolPairDTO(null, null);
}
final var priorJol = competencyJolRepository.findLatestByCompetencyIdAndUserIdExceptJolId(competencyId, userId, currentJol.get().getId());
return CompetencyJolPairDTO.of(currentJol.get(), priorJol.orElse(null));
return CompetencyJolPairDTO.from(currentJol.get(), priorJol.orElse(null));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import de.tum.cit.aet.artemis.atlas.domain.competency.CompetencyTaxonomy;
import de.tum.cit.aet.artemis.core.domain.Course;
import de.tum.cit.aet.artemis.core.domain.User;
import de.tum.cit.aet.artemis.iris.service.pyris.PyrisJobService;
import de.tum.cit.aet.artemis.iris.service.pyris.PyrisPipelineService;
import de.tum.cit.aet.artemis.iris.service.pyris.dto.competency.PyrisCompetencyExtractionPipelineExecutionDTO;
import de.tum.cit.aet.artemis.iris.service.pyris.dto.competency.PyrisCompetencyRecommendationDTO;
Expand All @@ -27,12 +26,9 @@ public class IrisCompetencyGenerationService {

private final IrisWebsocketService websocketService;

private final PyrisJobService pyrisJobService;

public IrisCompetencyGenerationService(PyrisPipelineService pyrisPipelineService, IrisWebsocketService websocketService, PyrisJobService pyrisJobService) {
public IrisCompetencyGenerationService(PyrisPipelineService pyrisPipelineService, IrisWebsocketService websocketService) {
this.pyrisPipelineService = pyrisPipelineService;
this.websocketService = websocketService;
this.pyrisJobService = pyrisJobService;
}
MichaelOwenDyer marked this conversation as resolved.
Show resolved Hide resolved

/**
Expand All @@ -48,7 +44,7 @@ public void executeCompetencyExtractionPipeline(User user, Course course, String
pyrisPipelineService.executePipeline(
"competency-extraction",
"default",
pyrisJobService.createTokenForJob(token -> new CompetencyExtractionJob(token, course.getId(), user.getLogin())),
jobId -> new CompetencyExtractionJob(jobId, course.getId(), user.getLogin()),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

LGTM: Simplified job creation logic

The changes in the executeCompetencyExtractionPipeline method improve the code by:

  • Simplifying the job creation process
  • Enhancing readability with a concise lambda expression
  • Maintaining adherence to the single responsibility principle

These modifications contribute to a more streamlined and maintainable codebase.

Consider extracting the lambda expression to a separate method for improved readability:

private CompetencyExtractionJob createCompetencyExtractionJob(String jobId, Course course, User user) {
    return new CompetencyExtractionJob(jobId, course.getId(), user.getLogin());
}

Then use it in the executePipeline call:

jobId -> createCompetencyExtractionJob(jobId, course, user),

This change would further enhance the method's readability and maintainability.

executionDto -> new PyrisCompetencyExtractionPipelineExecutionDTO(executionDto, courseDescription, currentCompetencies, CompetencyTaxonomy.values(), 5),
stages -> websocketService.send(user.getLogin(), websocketTopic(course.getId()), new PyrisCompetencyStatusUpdateDTO(stages, null))
);
MichaelOwenDyer marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@

import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException;
import de.tum.cit.aet.artemis.core.exception.ConflictException;
import de.tum.cit.aet.artemis.iris.service.pyris.job.CourseChatJob;
import de.tum.cit.aet.artemis.iris.service.pyris.job.ExerciseChatJob;
import de.tum.cit.aet.artemis.iris.service.pyris.job.IngestionWebhookJob;
import de.tum.cit.aet.artemis.iris.service.pyris.job.PyrisJob;

Expand Down Expand Up @@ -69,27 +67,13 @@ public void init() {
* @param tokenToJobFunction the function to run with the token
* @return the generated token
*/
public String createTokenForJob(Function<String, PyrisJob> tokenToJobFunction) {
public String registerJob(Function<String, PyrisJob> tokenToJobFunction) {
var token = generateJobIdToken();
var job = tokenToJobFunction.apply(token);
jobMap.put(token, job);
return token;
Comment on lines +71 to 75
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Update method documentation to reflect the method's new purpose

The method registerJob was renamed from createTokenForJob, but the Javadoc still refers to creating a token. To maintain clarity, update the documentation to emphasize job registration.

Apply this diff to update the documentation:

 /**
- * Creates a token for an arbitrary job, runs the provided function with the token as an argument,
+ * Registers an arbitrary job by generating a token, running the provided function with the token as an argument,
  * and stores the job in the job map.
  *
  * @param tokenToJobFunction the function to run with the token
  * @return the generated token
  */
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public String registerJob(Function<String, PyrisJob> tokenToJobFunction) {
var token = generateJobIdToken();
var job = tokenToJobFunction.apply(token);
jobMap.put(token, job);
return token;
/**
* Registers an arbitrary job by generating a token, running the provided function with the token as an argument,
* and stores the job in the job map.
*
* @param tokenToJobFunction the function to run with the token
* @return the generated token
*/
public String registerJob(Function<String, PyrisJob> tokenToJobFunction) {
var token = generateJobIdToken();
var job = tokenToJobFunction.apply(token);
jobMap.put(token, job);
return token;

}

MichaelOwenDyer marked this conversation as resolved.
Show resolved Hide resolved
public String addExerciseChatJob(Long courseId, Long exerciseId, Long sessionId) {
var token = generateJobIdToken();
var job = new ExerciseChatJob(token, courseId, exerciseId, sessionId);
jobMap.put(token, job);
return token;
}

public String addCourseChatJob(Long courseId, Long sessionId) {
var token = generateJobIdToken();
var job = new CourseChatJob(token, courseId, sessionId);
jobMap.put(token, job);
return token;
}

/**
* Adds a new ingestion webhook job to the job map with a timeout.
*
Expand Down Expand Up @@ -129,7 +113,7 @@ public PyrisJob getJob(String token) {
* 2. Retrieves the PyrisJob object associated with the provided token.
* 3. Throws an AccessForbiddenException if the token is invalid or not provided.
* <p>
* The token was previously generated via {@link #createTokenForJob(Function)}
* The token was previously generated via {@link #registerJob(Function)}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Clarify JavaDoc comment regarding token generation

The JavaDoc comment states:

* The token was previously generated via {@link #registerJob(Function)}

The word "previously" might be misleading since it suggests that the method was formerly named something else. Consider rephrasing the comment for clarity.

Suggestion:

Update the comment to:

* The token is generated via {@link #registerJob(Function)}

*
* @param request the HttpServletRequest object representing the incoming request
* @param jobClass the class of the PyrisJob object to cast the retrieved job to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@

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

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;

Expand All @@ -17,27 +12,11 @@
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;

import de.tum.cit.aet.artemis.atlas.domain.competency.CompetencyJol;
import de.tum.cit.aet.artemis.atlas.dto.CompetencyJolDTO;
import de.tum.cit.aet.artemis.core.domain.Course;
import de.tum.cit.aet.artemis.core.repository.CourseRepository;
import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation;
import de.tum.cit.aet.artemis.exercise.repository.StudentParticipationRepository;
import de.tum.cit.aet.artemis.exercise.service.LearningMetricsService;
import de.tum.cit.aet.artemis.iris.domain.session.IrisCourseChatSession;
import de.tum.cit.aet.artemis.iris.domain.session.IrisExerciseChatSession;
import de.tum.cit.aet.artemis.iris.exception.IrisException;
import de.tum.cit.aet.artemis.iris.service.pyris.dto.PyrisPipelineExecutionDTO;
import de.tum.cit.aet.artemis.iris.service.pyris.dto.PyrisPipelineExecutionSettingsDTO;
import de.tum.cit.aet.artemis.iris.service.pyris.dto.chat.course.PyrisCourseChatPipelineExecutionDTO;
import de.tum.cit.aet.artemis.iris.service.pyris.dto.chat.exercise.PyrisExerciseChatPipelineExecutionDTO;
import de.tum.cit.aet.artemis.iris.service.pyris.dto.data.PyrisCourseDTO;
import de.tum.cit.aet.artemis.iris.service.pyris.dto.data.PyrisExtendedCourseDTO;
import de.tum.cit.aet.artemis.iris.service.pyris.dto.data.PyrisUserDTO;
import de.tum.cit.aet.artemis.iris.service.pyris.dto.status.PyrisStageDTO;
import de.tum.cit.aet.artemis.iris.service.websocket.IrisChatWebsocketService;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingSubmission;
import de.tum.cit.aet.artemis.iris.service.pyris.job.PyrisJob;

/**
* Service responsible for executing the various Pyris pipelines in a type-safe manner.
Expand All @@ -53,34 +32,18 @@ public class PyrisPipelineService {

private final PyrisJobService pyrisJobService;

private final PyrisDTOService pyrisDTOService;

private final IrisChatWebsocketService irisChatWebsocketService;

private final CourseRepository courseRepository;

private final StudentParticipationRepository studentParticipationRepository;

private final LearningMetricsService learningMetricsService;

@Value("${server.url}")
private String artemisBaseUrl;

public PyrisPipelineService(PyrisConnectorService pyrisConnectorService, PyrisJobService pyrisJobService, PyrisDTOService pyrisDTOService,
IrisChatWebsocketService irisChatWebsocketService, CourseRepository courseRepository, LearningMetricsService learningMetricsService,
StudentParticipationRepository studentParticipationRepository) {
public PyrisPipelineService(PyrisConnectorService pyrisConnectorService, PyrisJobService pyrisJobService) {
this.pyrisConnectorService = pyrisConnectorService;
this.pyrisJobService = pyrisJobService;
this.pyrisDTOService = pyrisDTOService;
this.irisChatWebsocketService = irisChatWebsocketService;
this.courseRepository = courseRepository;
this.learningMetricsService = learningMetricsService;
this.studentParticipationRepository = studentParticipationRepository;
}

/**
* Executes a pipeline on Pyris, identified by the given name and variant.
* The pipeline execution is tracked by a unique job token, which must be provided by the caller.
* The pipeline execution is tracked by a unique job token, which is created for each new job automatically.
* The caller must provide a mapper function to take this job token and produce a {@code PyrisJob} object to be registered.
MichaelOwenDyer marked this conversation as resolved.
Show resolved Hide resolved
Comment on lines +45 to +46
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Improve clarity in JavaDoc comments

Consider rephrasing the JavaDoc comments for better clarity:

  • Change "which is created for each new job automatically" to "which is automatically created for each new job."
  • Modify "must provide a mapper function to take this job token and produce" to "must provide a mapper function that takes this job token and produces."

Apply this diff to enhance the JavaDoc comments:

- * The pipeline execution is tracked by a unique job token, which is created for each new job automatically.
+ * The pipeline execution is tracked by a unique job token, which is automatically created for each new job.
- * The caller must provide a mapper function to take this job token and produce a {@code PyrisJob} object to be registered.
+ * The caller must provide a mapper function that takes this job token and produces a {@code PyrisJob} object to be registered.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* The pipeline execution is tracked by a unique job token, which is created for each new job automatically.
* The caller must provide a mapper function to take this job token and produce a {@code PyrisJob} object to be registered.
* The pipeline execution is tracked by a unique job token, which is automatically created for each new job.
* The caller must provide a mapper function that takes this job token and produces a {@code PyrisJob} object to be registered.

Comment on lines +45 to +46
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Clarify Parameter Description in Documentation

In the Javadoc, the description of the jobFunction parameter refers to a "job token." However, the jobFunction actually takes a job ID (String) as input to produce a PyrisJob. To avoid confusion, consider updating the documentation to accurately reflect the parameter.

Suggested change:

- * The caller must provide a mapper function to take this job token and produce a {@code PyrisJob} object to be registered.
+ * The caller must provide a mapper function that takes the generated job ID and produces a {@code PyrisJob} object to be registered.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* The pipeline execution is tracked by a unique job token, which is created for each new job automatically.
* The caller must provide a mapper function to take this job token and produce a {@code PyrisJob} object to be registered.
* The pipeline execution is tracked by a unique job token, which is created for each new job automatically.
* The caller must provide a mapper function that takes the generated job ID and produces a {@code PyrisJob} object to be registered.

* The caller must additionally provide a mapper function to create the concrete DTO type for this pipeline from the base DTO.
* The status of the pipeline execution is updated via a consumer that accepts a list of stages. This method will
* call the consumer with the initial stages of the pipeline execution. Later stages will be sent back from Pyris,
Expand All @@ -89,11 +52,12 @@ public PyrisPipelineService(PyrisConnectorService pyrisConnectorService, PyrisJo
*
* @param name the name of the pipeline to be executed
* @param variant the variant of the pipeline
* @param jobToken a unique job token for tracking the pipeline execution
* @param dtoMapper a function to create the concrete DTO type for this pipeline from the base DTO
* @param statusUpdater a consumer to update the status of the pipeline execution
* @param jobFunction a function from job ID to job. Creates a new {@code PyrisJob} which will be registered in Hazelcast
* @param dtoMapper a function to create the concrete DTO type for this pipeline from the base execution DTO
* @param statusUpdater a consumer of stages to send status updates while the pipeline is being prepared
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Enhance parameter descriptions in JavaDoc

To improve clarity and consistency in the parameter descriptions, consider the following revisions:

  • @param jobFunction: "A function that accepts a job ID and returns a PyrisJob to be registered in Hazelcast."
  • @param dtoMapper: "A function that transforms the base execution DTO into a concrete DTO type for this pipeline."
  • @param statusUpdater: "A consumer that accepts a list of stages to provide status updates during pipeline preparation."

Apply this diff to update the parameter descriptions:

- * @param jobFunction   a function from job ID to job. Creates a new {@code PyrisJob} which will be registered in Hazelcast
+ * @param jobFunction   a function that accepts a job ID and returns a {@code PyrisJob} to be registered in Hazelcast
- * @param dtoMapper     a function to create the concrete DTO type for this pipeline from the base execution DTO
+ * @param dtoMapper     a function that transforms the base execution DTO into a concrete DTO type for this pipeline
- * @param statusUpdater a consumer of stages to send status updates while the pipeline is being prepared
+ * @param statusUpdater a consumer that accepts a list of stages to provide status updates during pipeline preparation
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* @param jobFunction a function from job ID to job. Creates a new {@code PyrisJob} which will be registered in Hazelcast
* @param dtoMapper a function to create the concrete DTO type for this pipeline from the base execution DTO
* @param statusUpdater a consumer of stages to send status updates while the pipeline is being prepared
* @param jobFunction a function that accepts a job ID and returns a {@code PyrisJob} to be registered in Hazelcast
* @param dtoMapper a function that transforms the base execution DTO into a concrete DTO type for this pipeline
* @param statusUpdater a consumer that accepts a list of stages to provide status updates during pipeline preparation

*/
public void executePipeline(String name, String variant, String jobToken, Function<PyrisPipelineExecutionDTO, Object> dtoMapper, Consumer<List<PyrisStageDTO>> statusUpdater) {
public void executePipeline(String name, String variant, Function<String, PyrisJob> jobFunction, Function<PyrisPipelineExecutionDTO, Object> dtoMapper,
Consumer<List<PyrisStageDTO>> statusUpdater) {
Comment on lines +59 to +60
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider Simplifying Method Parameters

The executePipeline method currently accepts multiple parameters, which may affect readability and maintainability. Consider encapsulating related parameters into a configuration object or using a builder pattern to simplify the method signature and enhance clarity.

// Define the preparation stages of pipeline execution with their initial states
// There will be more stages added in Pyris later
var preparing = new PyrisStageDTO("Preparing", 10, null, null);
Expand All @@ -102,6 +66,8 @@ public void executePipeline(String name, String variant, String jobToken, Functi
// Send initial status update indicating that the preparation stage is in progress
statusUpdater.accept(List.of(preparing.inProgress(), executing.notStarted()));

String jobToken = pyrisJobService.registerJob(jobFunction);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add Error Handling for Job Registration

The call to pyrisJobService.registerJob(jobFunction) may throw an exception if job registration fails. To ensure robustness, consider adding error handling to manage potential exceptions and provide appropriate feedback.

Apply this diff to include error handling:

+        String jobToken;
+        try {
+            jobToken = pyrisJobService.registerJob(jobFunction);
+        } catch (Exception e) {
+            log.error("Failed to register job", e);
+            statusUpdater.accept(List.of(preparing.error("Failed to register job"), executing.notStarted()));
+            return;
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
String jobToken = pyrisJobService.registerJob(jobFunction);
String jobToken;
try {
jobToken = pyrisJobService.registerJob(jobFunction);
} catch (Exception e) {
log.error("Failed to register job", e);
statusUpdater.accept(List.of(preparing.error("Failed to register job"), executing.notStarted()));
return;
}


MichaelOwenDyer marked this conversation as resolved.
Show resolved Hide resolved
var baseDto = new PyrisPipelineExecutionDTO(new PyrisPipelineExecutionSettingsDTO(jobToken, List.of(), artemisBaseUrl), List.of(preparing.done()));
var pipelineDto = dtoMapper.apply(baseDto);

Expand All @@ -123,106 +89,4 @@ public void executePipeline(String name, String variant, String jobToken, Functi
statusUpdater.accept(List.of(preparing.error("An internal error occurred"), executing.notStarted()));
}
}

/**
* Execute the exercise chat pipeline for the given session.
* It provides specific data for the exercise chat pipeline, including:
* - The latest submission of the student
* - The programming exercise
* - The course the exercise is part of
* <p>
*
* @param variant the variant of the pipeline
* @param latestSubmission the latest submission of the student
* @param exercise the programming exercise
* @param session the chat session
* @see PyrisPipelineService#executePipeline for more details on the pipeline execution process.
*/
public void executeExerciseChatPipeline(String variant, Optional<ProgrammingSubmission> latestSubmission, ProgrammingExercise exercise, IrisExerciseChatSession session) {
// @formatter:off
executePipeline(
"tutor-chat", // TODO: Rename this to 'exercise-chat' with next breaking Pyris version
variant,
pyrisJobService.addExerciseChatJob(exercise.getCourseViaExerciseGroupOrCourseMember().getId(), exercise.getId(), session.getId()),
executionDto -> {
var course = exercise.getCourseViaExerciseGroupOrCourseMember();
return new PyrisExerciseChatPipelineExecutionDTO(
latestSubmission.map(pyrisDTOService::toPyrisSubmissionDTO).orElse(null),
pyrisDTOService.toPyrisProgrammingExerciseDTO(exercise),
new PyrisCourseDTO(course),
pyrisDTOService.toPyrisMessageDTOList(session.getMessages()),
new PyrisUserDTO(session.getUser()),
executionDto.settings(),
executionDto.initialStages()
);
},
stages -> irisChatWebsocketService.sendStatusUpdate(session, stages)
);
// @formatter:on
}

/**
* Execute the course chat pipeline for the given session.
* It provides specific data for the course chat pipeline, including:
* - The full course with the participation of the student
* - The metrics of the student in the course
* - The competency JoL if this is due to a JoL set event
* <p>
*
* @param variant the variant of the pipeline
* @param session the chat session
* @param competencyJol if this is due to a JoL set event, this must be the newly created competencyJoL
* @see PyrisPipelineService#executePipeline for more details on the pipeline execution process.
*/
public void executeCourseChatPipeline(String variant, IrisCourseChatSession session, CompetencyJol competencyJol) {
// @formatter:off
var courseId = session.getCourse().getId();
var studentId = session.getUser().getId();
executePipeline(
"course-chat",
variant,
pyrisJobService.addCourseChatJob(courseId, session.getId()),
executionDto -> {
var fullCourse = loadCourseWithParticipationOfStudent(courseId, studentId);
return new PyrisCourseChatPipelineExecutionDTO(
PyrisExtendedCourseDTO.of(fullCourse),
learningMetricsService.getStudentCourseMetrics(session.getUser().getId(), courseId),
competencyJol == null ? null : CompetencyJolDTO.of(competencyJol),
pyrisDTOService.toPyrisMessageDTOList(session.getMessages()),
new PyrisUserDTO(session.getUser()),
executionDto.settings(), // flatten the execution dto here
executionDto.initialStages()
);
},
stages -> irisChatWebsocketService.sendStatusUpdate(session, stages)
);
// @formatter:on
}

/**
* Load the course with the participation of the student and set the participations on the exercises.
* <p>
* Spring Boot 3 does not support conditional left joins, so we have to load the participations separately.
*
* @param courseId the id of the course
* @param studentId the id of the student
*/
private Course loadCourseWithParticipationOfStudent(long courseId, long studentId) {
Course course = courseRepository.findWithEagerExercisesAndLecturesAndAttachmentsAndLectureUnitsAndCompetenciesAndExamsById(courseId).orElseThrow();
List<StudentParticipation> participations = studentParticipationRepository.findByStudentIdAndIndividualExercisesWithEagerSubmissionsResultIgnoreTestRuns(studentId,
course.getExercises());

Map<Long, Set<StudentParticipation>> participationMap = new HashMap<>();
for (StudentParticipation participation : participations) {
Long exerciseId = participation.getExercise().getId();
participationMap.computeIfAbsent(exerciseId, k -> new HashSet<>()).add(participation);
}

course.getExercises().forEach(exercise -> {
Set<StudentParticipation> exerciseParticipations = participationMap.getOrDefault(exercise.getId(), Set.of());
exercise.setStudentParticipations(exerciseParticipations);
});

return course;
}
}
Loading
Loading