Skip to content

Commit

Permalink
Communication: Add Artemis intelligence rewrite action for FAQs (#10157)
Browse files Browse the repository at this point in the history
  • Loading branch information
cremertim authored Jan 30, 2025
1 parent 72ca6ae commit 32212f8
Show file tree
Hide file tree
Showing 29 changed files with 622 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package de.tum.cit.aet.artemis.iris.service;

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

import java.util.Optional;

import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;

import de.tum.cit.aet.artemis.core.domain.Course;
import de.tum.cit.aet.artemis.core.domain.LLMServiceType;
import de.tum.cit.aet.artemis.core.domain.User;
import de.tum.cit.aet.artemis.core.repository.CourseRepository;
import de.tum.cit.aet.artemis.core.repository.UserRepository;
import de.tum.cit.aet.artemis.core.service.LLMTokenUsageService;
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.rewriting.PyrisRewritingPipelineExecutionDTO;
import de.tum.cit.aet.artemis.iris.service.pyris.dto.rewriting.PyrisRewritingStatusUpdateDTO;
import de.tum.cit.aet.artemis.iris.service.pyris.dto.rewriting.RewritingVariant;
import de.tum.cit.aet.artemis.iris.service.pyris.job.RewritingJob;
import de.tum.cit.aet.artemis.iris.service.websocket.IrisWebsocketService;

/**
* Service to handle the rewriting subsystem of Iris.
*/
@Service
@Profile(PROFILE_IRIS)
public class IrisRewritingService {

private final PyrisPipelineService pyrisPipelineService;

private final LLMTokenUsageService llmTokenUsageService;

private final CourseRepository courseRepository;

private final IrisWebsocketService websocketService;

private final PyrisJobService pyrisJobService;

private final UserRepository userRepository;

public IrisRewritingService(PyrisPipelineService pyrisPipelineService, LLMTokenUsageService llmTokenUsageService, CourseRepository courseRepository,
IrisWebsocketService websocketService, PyrisJobService pyrisJobService, UserRepository userRepository) {
this.pyrisPipelineService = pyrisPipelineService;
this.llmTokenUsageService = llmTokenUsageService;
this.courseRepository = courseRepository;
this.websocketService = websocketService;
this.pyrisJobService = pyrisJobService;
this.userRepository = userRepository;
}

/**
* Executes the rewriting pipeline on Pyris
*
* @param user the user for which the pipeline should be executed
* @param course the course for which the pipeline should be executed
* @param variant the rewriting variant to be used
* @param toBeRewritten the text to be rewritten
*/
public void executeRewritingPipeline(User user, Course course, RewritingVariant variant, String toBeRewritten) {
// @formatter:off
pyrisPipelineService.executePipeline(
"rewriting",
variant.toString(),
Optional.empty(),
pyrisJobService.createTokenForJob(token -> new RewritingJob(token, course.getId(), user.getId())),
executionDto -> new PyrisRewritingPipelineExecutionDTO(executionDto, toBeRewritten),
stages -> websocketService.send(user.getLogin(), websocketTopic(course.getId()), new PyrisRewritingStatusUpdateDTO(stages, null, null))
);
// @formatter:on
}

/**
* Takes a status update from Pyris containing a new rewriting result and sends it to the client via websocket
*
* @param job Job related to the status update
* @param statusUpdate the status update containing text recommendations
* @return the same job that was passed in
*/
public RewritingJob handleStatusUpdate(RewritingJob job, PyrisRewritingStatusUpdateDTO statusUpdate) {
Course course = courseRepository.findByIdForUpdateElseThrow(job.courseId());
if (statusUpdate.tokens() != null && !statusUpdate.tokens().isEmpty()) {
llmTokenUsageService.saveLLMTokenUsage(statusUpdate.tokens(), LLMServiceType.IRIS, builder -> builder.withCourse(course.getId()).withUser(job.userId()));
}

var user = userRepository.findById(job.userId()).orElseThrow();
websocketService.send(user.getLogin(), websocketTopic(job.courseId()), statusUpdate);

return job;
}

private static String websocketTopic(long courseId) {
return "rewriting/" + courseId;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ public PyrisDTOService(GitService gitService, RepositoryService repositoryServic
public PyrisProgrammingExerciseDTO toPyrisProgrammingExerciseDTO(ProgrammingExercise exercise) {
var templateRepositoryContents = getFilteredRepositoryContents(exercise.getTemplateParticipation());
var solutionRepositoryContents = getFilteredRepositoryContents(exercise.getSolutionParticipation());

// var templateRepositoryContents = new HashMap<String, String>();
// var solutionRepositoryContents = new HashMap<String, String>();

Optional<Repository> testRepo = Optional.empty();
try {
testRepo = Optional.ofNullable(gitService.getOrCheckoutRepository(exercise.getVcsTestRepositoryUri(), true));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,20 @@
import org.springframework.stereotype.Service;

import de.tum.cit.aet.artemis.iris.service.IrisCompetencyGenerationService;
import de.tum.cit.aet.artemis.iris.service.IrisRewritingService;
import de.tum.cit.aet.artemis.iris.service.pyris.dto.chat.PyrisChatStatusUpdateDTO;
import de.tum.cit.aet.artemis.iris.service.pyris.dto.chat.textexercise.PyrisTextExerciseChatStatusUpdateDTO;
import de.tum.cit.aet.artemis.iris.service.pyris.dto.competency.PyrisCompetencyStatusUpdateDTO;
import de.tum.cit.aet.artemis.iris.service.pyris.dto.lectureingestionwebhook.PyrisLectureIngestionStatusUpdateDTO;
import de.tum.cit.aet.artemis.iris.service.pyris.dto.rewriting.PyrisRewritingStatusUpdateDTO;
import de.tum.cit.aet.artemis.iris.service.pyris.dto.status.PyrisStageDTO;
import de.tum.cit.aet.artemis.iris.service.pyris.dto.status.PyrisStageState;
import de.tum.cit.aet.artemis.iris.service.pyris.job.CompetencyExtractionJob;
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;
import de.tum.cit.aet.artemis.iris.service.pyris.job.RewritingJob;
import de.tum.cit.aet.artemis.iris.service.pyris.job.TextExerciseChatJob;
import de.tum.cit.aet.artemis.iris.service.pyris.job.TrackedSessionBasedPyrisJob;
import de.tum.cit.aet.artemis.iris.service.session.IrisCourseChatSessionService;
Expand All @@ -41,16 +44,19 @@ public class PyrisStatusUpdateService {

private final IrisCompetencyGenerationService competencyGenerationService;

private final IrisRewritingService rewritingService;

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

public PyrisStatusUpdateService(PyrisJobService pyrisJobService, IrisExerciseChatSessionService irisExerciseChatSessionService,
IrisTextExerciseChatSessionService irisTextExerciseChatSessionService, IrisCourseChatSessionService courseChatSessionService,
IrisCompetencyGenerationService competencyGenerationService) {
IrisCompetencyGenerationService competencyGenerationService, IrisRewritingService rewritingService) {
this.pyrisJobService = pyrisJobService;
this.irisExerciseChatSessionService = irisExerciseChatSessionService;
this.irisTextExerciseChatSessionService = irisTextExerciseChatSessionService;
this.courseChatSessionService = courseChatSessionService;
this.competencyGenerationService = competencyGenerationService;
this.rewritingService = rewritingService;
}

/**
Expand Down Expand Up @@ -105,6 +111,18 @@ public void handleStatusUpdate(CompetencyExtractionJob job, PyrisCompetencyStatu
removeJobIfTerminatedElseUpdate(statusUpdate.stages(), updatedJob);
}

/**
* Handles the status update of a rewriting job and forwards it to
* {@link IrisRewritingService#handleStatusUpdate(RewritingJob, PyrisRewritingStatusUpdateDTO)}
*
* @param job the job that is updated
* @param statusUpdate the status update
*/
public void handleStatusUpdate(RewritingJob job, PyrisRewritingStatusUpdateDTO statusUpdate) {
var updatedJob = rewritingService.handleStatusUpdate(job, statusUpdate);
removeJobIfTerminatedElseUpdate(statusUpdate.stages(), updatedJob);
}

/**
* Removes the job from the job service if the status update indicates that the job is terminated; updates it to distribute changes otherwise.
* A job is terminated if all stages are in a terminal state.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import de.tum.cit.aet.artemis.iris.service.pyris.dto.PyrisPipelineExecutionDTO;

/**
* DTO to execute the Iris competency extraction pipeline on Pyris
* DTO to execute the Iris rewriting pipeline on Pyris
*
* @param execution The pipeline execution details
* @param courseDescription The description of the course
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package de.tum.cit.aet.artemis.iris.service.pyris.dto.data;

import com.fasterxml.jackson.annotation.JsonInclude;

import de.tum.cit.aet.artemis.iris.service.pyris.dto.rewriting.RewritingVariant;

@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record PyrisRewriteTextRequestDTO(String toBeRewritten, RewritingVariant variant) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package de.tum.cit.aet.artemis.iris.service.pyris.dto.rewriting;

import com.fasterxml.jackson.annotation.JsonInclude;

import de.tum.cit.aet.artemis.iris.service.pyris.dto.PyrisPipelineExecutionDTO;

/**
* DTO to execute the Iris rewriting pipeline on Pyris
*
* @param execution The pipeline execution details
* @param toBeRewritten The text to be rewritten
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record PyrisRewritingPipelineExecutionDTO(PyrisPipelineExecutionDTO execution, String toBeRewritten) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package de.tum.cit.aet.artemis.iris.service.pyris.dto.rewriting;

import java.util.List;

import com.fasterxml.jackson.annotation.JsonInclude;

import de.tum.cit.aet.artemis.core.domain.LLMRequest;
import de.tum.cit.aet.artemis.iris.service.pyris.dto.status.PyrisStageDTO;

/**
* DTO for the Iris rewriting feature.
* Pyris sends callback updates back to Artemis during rewriting of the text. These updates contain the current status of the rewriting process,
* which are then forwarded to the user via Websockets.
*
* @param stages List of stages of the generation process
* @param result The result of the rewriting process so far
* @param tokens List of token usages send by Pyris for tracking the token usage and cost
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record PyrisRewritingStatusUpdateDTO(List<PyrisStageDTO> stages, String result, List<LLMRequest> tokens) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package de.tum.cit.aet.artemis.iris.service.pyris.dto.rewriting;

public enum RewritingVariant {
FAQ, PROBLEM_STATEMENT
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package de.tum.cit.aet.artemis.iris.service.pyris.job;

import com.fasterxml.jackson.annotation.JsonInclude;

import de.tum.cit.aet.artemis.core.domain.Course;

/**
* A Pyris job to rewrite a text.
*
* @param jobId the job id
* @param courseId the course in which the rewriting is being done
* @param userId the user who started the job
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record RewritingJob(String jobId, long courseId, long userId) implements PyrisJob {

@Override
public boolean canAccess(Course course) {
return course.getId().equals(courseId);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package de.tum.cit.aet.artemis.iris.web;

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

import java.util.Optional;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import de.tum.cit.aet.artemis.core.repository.CourseRepository;
import de.tum.cit.aet.artemis.core.repository.UserRepository;
import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInCourse.EnforceAtLeastTutorInCourse;
import de.tum.cit.aet.artemis.iris.service.IrisRewritingService;
import de.tum.cit.aet.artemis.iris.service.pyris.dto.data.PyrisRewriteTextRequestDTO;

/**
* REST controller for managing Markdown Rewritings.
*/
@Profile(PROFILE_IRIS)
@RestController
@RequestMapping("api/")
public class IrisRewritingResource {

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

private static final String ENTITY_NAME = "rewriting";

private final UserRepository userRepository;

private final CourseRepository courseRepository;

private final Optional<IrisRewritingService> irisRewritingService;

public IrisRewritingResource(UserRepository userRepository, CourseRepository courseRepository, Optional<IrisRewritingService> irisRewritingService) {
this.userRepository = userRepository;
this.courseRepository = courseRepository;
this.irisRewritingService = irisRewritingService;

}

/**
* POST /courses/{courseId}/rewrite-text : Rewrite a given text.
*
* @param request the request containing the text to be rewritten and the corresponding variant
* @param courseId the id of the course
* @return the ResponseEntity with status 200 (OK)
*/
@EnforceAtLeastTutorInCourse
@PostMapping("courses/{courseId}/rewrite-text")
public ResponseEntity<Void> rewriteText(@RequestBody PyrisRewriteTextRequestDTO request, @PathVariable Long courseId) {
var rewritingService = irisRewritingService.orElseThrow();
var user = userRepository.getUserWithGroupsAndAuthorities();
var course = courseRepository.findByIdElseThrow(courseId);
rewritingService.executeRewritingPipeline(user, course, request.variant(), request.toBeRewritten());
log.debug("REST request to rewrite text: {}", request.toBeRewritten());
return ResponseEntity.ok().build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@
import de.tum.cit.aet.artemis.iris.service.pyris.dto.chat.textexercise.PyrisTextExerciseChatStatusUpdateDTO;
import de.tum.cit.aet.artemis.iris.service.pyris.dto.competency.PyrisCompetencyStatusUpdateDTO;
import de.tum.cit.aet.artemis.iris.service.pyris.dto.lectureingestionwebhook.PyrisLectureIngestionStatusUpdateDTO;
import de.tum.cit.aet.artemis.iris.service.pyris.dto.rewriting.PyrisRewritingStatusUpdateDTO;
import de.tum.cit.aet.artemis.iris.service.pyris.job.CompetencyExtractionJob;
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;
import de.tum.cit.aet.artemis.iris.service.pyris.job.RewritingJob;
import de.tum.cit.aet.artemis.iris.service.pyris.job.TextExerciseChatJob;

/**
Expand Down Expand Up @@ -149,6 +151,31 @@ public ResponseEntity<Void> respondInTextExerciseChat(@PathVariable String runId
return ResponseEntity.ok().build();
}

/**
* POST public/pyris/pipelines/rewriting/runs/:runId/status : Send the rewritten text in a status update
* <p>
* Uses custom token based authentication.
*
* @param runId the ID of the job
* @param statusUpdateDTO the status update
* @param request the HTTP request
* @throws ConflictException if the run ID in the URL does not match the run ID in the request body
* @throws AccessForbiddenException if the token is invalid
* @return a {@link ResponseEntity} with status {@code 200 (OK)}
*/
@PostMapping("pipelines/rewriting/runs/{runId}/status")
@EnforceNothing
public ResponseEntity<Void> setRewritingJobStatus(@PathVariable String runId, @RequestBody PyrisRewritingStatusUpdateDTO statusUpdateDTO, HttpServletRequest request) {
var job = pyrisJobService.getAndAuthenticateJobFromHeaderElseThrow(request, RewritingJob.class);
if (!Objects.equals(job.jobId(), runId)) {
throw new ConflictException("Run ID in URL does not match run ID in request body", "Job", "runIdMismatch");
}

pyrisStatusUpdateService.handleStatusUpdate(job, statusUpdateDTO);

return ResponseEntity.ok().build();
}

/**
* {@code POST /api/public/pyris/webhooks/ingestion/runs/{runId}/status} : Set the status of an Ingestion job.
*
Expand Down
3 changes: 2 additions & 1 deletion src/main/webapp/app/faq/faq-update.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ <h2 id="jhi-faq-heading" jhiTranslate="artemisApp.faq.home.createOrEditLabel"></
id="field_description"
class="markdown-editor"
[domainActions]="domainActionsDescription"
[markdown]="faq.questionAnswer"
[artemisIntelligenceActions]="artemisIntelligenceActions()"
[markdown]="faq.questionAnswer || ''"
(markdownChange)="handleMarkdownChange($event)"
/>
</div>
Expand Down
Loading

0 comments on commit 32212f8

Please sign in to comment.