Skip to content

Commit

Permalink
Iris: Enable IRIS to answer with FAQs (#10093)
Browse files Browse the repository at this point in the history
  • Loading branch information
cremertim authored Jan 31, 2025
1 parent 59a9e64 commit c59f91c
Show file tree
Hide file tree
Showing 51 changed files with 1,245 additions and 59 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package de.tum.cit.aet.artemis.communication.service;

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

import java.util.Optional;

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

import de.tum.cit.aet.artemis.communication.domain.Faq;
import de.tum.cit.aet.artemis.communication.domain.FaqState;
import de.tum.cit.aet.artemis.communication.repository.FaqRepository;
import de.tum.cit.aet.artemis.core.service.ProfileService;
import de.tum.cit.aet.artemis.iris.service.pyris.PyrisWebhookService;

@Profile(PROFILE_CORE)
@Service
public class FaqService {

private final Optional<PyrisWebhookService> pyrisWebhookService;

private final FaqRepository faqRepository;

public FaqService(FaqRepository faqRepository, Optional<PyrisWebhookService> pyrisWebhookService, ProfileService profileService) {

this.pyrisWebhookService = pyrisWebhookService;
this.faqRepository = faqRepository;

}

/**
* Ingests FAQs into the Pyris system. If a specific FAQ ID is provided, the method will attempt to add
* that FAQ to Pyris. Otherwise, it will ingest all FAQs for the specified course that are in the "ACCEPTED" state.
* If the PyrisWebhookService is unavailable, the method does nothing.
*
* @param courseId the ID of the course for which FAQs will be ingested
* @param faqId an optional ID of a specific FAQ to ingest; if not provided, all accepted FAQs for the course are processed
* @throws IllegalArgumentException if a specific FAQ is provided but its state is not "ACCEPTED"
*/
public void ingestFaqsIntoPyris(Long courseId, Optional<Long> faqId) {
if (pyrisWebhookService.isEmpty()) {
return;
}

faqId.ifPresentOrElse(id -> {
Faq faq = faqRepository.findById(id).orElseThrow();
if (faq.getFaqState() != FaqState.ACCEPTED) {
throw new IllegalArgumentException("Faq is not in the state accepted, you cannot ingest this faq");
}
pyrisWebhookService.get().addFaq(faq);
}, () -> faqRepository.findAllByCourseIdAndFaqState(courseId, FaqState.ACCEPTED).forEach(faq -> pyrisWebhookService.get().addFaq(faq)));
}

/**
* Deletes an existing FAQ from the Pyris system. If the PyrisWebhookService is unavailable, the method does nothing.
*
* @param existingFaq the FAQ to be removed from Pyris
*/
public void deleteFaqInPyris(Faq existingFaq) {
if (pyrisWebhookService.isEmpty()) {
return;
}

pyrisWebhookService.get().deleteFaq(existingFaq);
}

/**
* Automatically updates or ingests a specific FAQ into the Pyris system for a given course.
* If the PyrisWebhookService is unavailable, the method does nothing.
*
* @param courseId the ID of the course to which the FAQ belongs
* @param faq the FAQ to be ingested or updated in Pyris
*/
public void autoIngestFaqsIntoPyris(Long courseId, Faq faq) {
if (pyrisWebhookService.isEmpty()) {
return;
}

if (faq.getFaqState() != FaqState.ACCEPTED) {
return;
}

pyrisWebhookService.get().autoUpdateFaqInPyris(courseId, faq);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package de.tum.cit.aet.artemis.communication.web;

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

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

Expand All @@ -20,12 +22,14 @@
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import de.tum.cit.aet.artemis.communication.domain.Faq;
import de.tum.cit.aet.artemis.communication.domain.FaqState;
import de.tum.cit.aet.artemis.communication.dto.FaqDTO;
import de.tum.cit.aet.artemis.communication.repository.FaqRepository;
import de.tum.cit.aet.artemis.communication.service.FaqService;
import de.tum.cit.aet.artemis.core.domain.Course;
import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException;
import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException;
Expand Down Expand Up @@ -58,10 +62,13 @@ public class FaqResource {

private final FaqRepository faqRepository;

public FaqResource(CourseRepository courseRepository, AuthorizationCheckService authCheckService, FaqRepository faqRepository) {
private final FaqService faqService;

public FaqResource(CourseRepository courseRepository, AuthorizationCheckService authCheckService, FaqRepository faqRepository, FaqService faqService) {
this.faqRepository = faqRepository;
this.courseRepository = courseRepository;
this.authCheckService = authCheckService;
this.faqService = faqService;
}

/**
Expand All @@ -86,6 +93,7 @@ public ResponseEntity<FaqDTO> createFaq(@RequestBody Faq faq, @PathVariable Long
}
Faq savedFaq = faqRepository.save(faq);
FaqDTO dto = new FaqDTO(savedFaq);
faqService.autoIngestFaqsIntoPyris(courseId, savedFaq);
return ResponseEntity.created(new URI("/api/courses/" + courseId + "/faqs/" + savedFaq.getId())).body(dto);
}

Expand All @@ -112,6 +120,7 @@ public ResponseEntity<FaqDTO> updateFaq(@RequestBody Faq faq, @PathVariable Long
throw new BadRequestAlertException("Course ID of the FAQ provided courseID must match", ENTITY_NAME, "idNull");
}
Faq updatedFaq = faqRepository.save(faq);
faqService.autoIngestFaqsIntoPyris(courseId, updatedFaq);
FaqDTO dto = new FaqDTO(updatedFaq);
return ResponseEntity.ok().body(dto);
}
Expand Down Expand Up @@ -152,6 +161,7 @@ public ResponseEntity<Void> deleteFaq(@PathVariable Long faqId, @PathVariable Lo
if (!Objects.equals(existingFaq.getCourse().getId(), courseId)) {
throw new BadRequestAlertException("Course ID of the FAQ provided courseID must match", ENTITY_NAME, "idNull");
}
faqService.deleteFaqInPyris(existingFaq);
faqRepository.deleteById(faqId);
return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, faqId.toString())).build();
}
Expand Down Expand Up @@ -212,6 +222,23 @@ public ResponseEntity<Set<String>> getFaqCategoriesForCourseByState(@PathVariabl
return ResponseEntity.ok().body(faqs);
}

/**
* POST /courses/{courseId}/ingest
* This endpoint is for starting the ingestion of all faqs or only one faq when triggered in Artemis.
*
* @param courseId the ID of the course for which all faqs should be ingested in pyris
* @param faqId If this id is present then only ingest this one faq of the respective course
* @return the ResponseEntity with status 200 (OK) and a message success or null if the operation failed
*/
@Profile(PROFILE_IRIS)
@PostMapping("courses/{courseId}/faqs/ingest")
@EnforceAtLeastInstructorInCourse
public ResponseEntity<Void> ingestFaqInIris(@PathVariable Long courseId, @RequestParam(required = false) Optional<Long> faqId) {
Course course = courseRepository.findByIdElseThrow(courseId);
faqService.ingestFaqsIntoPyris(courseId, faqId);
return ResponseEntity.ok().build();
}

/**
* @param courseId the id of the course the faq belongs to
* @param role the required role of the user
Expand All @@ -235,4 +262,8 @@ private void checkIsInstructorForAcceptedFaq(FaqState faqState, Long courseId) {
}
}

private boolean checkIfFaqIsAccepted(Faq faq) {
return faq.getFaqState() == FaqState.ACCEPTED;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,15 @@ public boolean isAeolusActive() {
return isProfileActive(Constants.PROFILE_AEOLUS);
}

/**
* Checks if the IRIS profile is active
*
* @return true if the aeolus profile is active, false otherwise
*/
public boolean isIrisActive() {
return isProfileActive(Constants.PROFILE_IRIS);
}

/**
* Checks if the lti profile is active
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ public class IrisCourseSettings extends IrisSettings {
@JoinColumn(name = "iris_lecture_ingestion_settings_id")
private IrisLectureIngestionSubSettings irisLectureIngestionSettings;

@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
@JoinColumn(name = "iris_faq_ingestion_settings_id")
private IrisFaqIngestionSubSettings irisFaqIngestionSettings;

@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER, optional = false)
@JoinColumn(name = "iris_competency_generation_settings_id")
private IrisCompetencyGenerationSubSettings irisCompetencyGenerationSettings;
Expand Down Expand Up @@ -101,4 +105,14 @@ public IrisCompetencyGenerationSubSettings getIrisCompetencyGenerationSettings()
public void setIrisCompetencyGenerationSettings(IrisCompetencyGenerationSubSettings irisCompetencyGenerationSubSettings) {
this.irisCompetencyGenerationSettings = irisCompetencyGenerationSubSettings;
}

@Override
public IrisFaqIngestionSubSettings getIrisFaqIngestionSettings() {
return irisFaqIngestionSettings;
}

@Override
public void setIrisFaqIngestionSettings(IrisFaqIngestionSubSettings irisFaqIngestionSubSettings) {
this.irisFaqIngestionSettings = irisFaqIngestionSubSettings;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,16 @@ public IrisCompetencyGenerationSubSettings getIrisCompetencyGenerationSettings()
public void setIrisCompetencyGenerationSettings(IrisCompetencyGenerationSubSettings irisCompetencyGenerationSubSettings) {

}

@Override
public IrisFaqIngestionSubSettings getIrisFaqIngestionSettings() {
// Empty because exercises don't have exercise faq settings
return null;
}

@Override
public void setIrisFaqIngestionSettings(IrisFaqIngestionSubSettings irisFaqIngestionSubSettings) {
// Empty because exercises don't have exercise faq settings

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

import jakarta.persistence.Column;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;

import com.fasterxml.jackson.annotation.JsonInclude;

/**
* Represents the specific ingestion sub-settings of faqs for Iris.
* This class extends {@link IrisSubSettings} to provide settings required for faq data ingestion.
*/
@Entity
@DiscriminatorValue("FAQ_INGESTION")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class IrisFaqIngestionSubSettings extends IrisSubSettings {

@Column(name = "auto_ingest_on_faq_creation")
private boolean autoIngestOnFaqCreation;

public boolean getAutoIngestOnFaqCreation() {
return autoIngestOnFaqCreation;
}

public void setAutoIngestOnFaqCreation(boolean autoIngestOnFaqCreation) {
this.autoIngestOnFaqCreation = autoIngestOnFaqCreation;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ public class IrisGlobalSettings extends IrisSettings {
@JoinColumn(name = "iris_lecture_ingestion_settings_id")
private IrisLectureIngestionSubSettings irisLectureIngestionSettings;

@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER, optional = false)
@JoinColumn(name = "iris_faq_ingestion_settings_id")
private IrisFaqIngestionSubSettings irisFaqIngestionSubSettings;

@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER, optional = false)
@JoinColumn(name = "iris_competency_generation_settings_id")
private IrisCompetencyGenerationSubSettings irisCompetencyGenerationSettings;
Expand Down Expand Up @@ -88,4 +92,15 @@ public IrisCompetencyGenerationSubSettings getIrisCompetencyGenerationSettings()
public void setIrisCompetencyGenerationSettings(IrisCompetencyGenerationSubSettings irisCompetencyGenerationSettings) {
this.irisCompetencyGenerationSettings = irisCompetencyGenerationSettings;
}

@Override
public IrisFaqIngestionSubSettings getIrisFaqIngestionSettings() {
return irisFaqIngestionSubSettings;
}

@Override
public void setIrisFaqIngestionSettings(IrisFaqIngestionSubSettings irisFaqIngestionSubSettings) {
this.irisFaqIngestionSubSettings = irisFaqIngestionSubSettings;

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,9 @@ public abstract class IrisSettings extends DomainObject {
public abstract IrisCompetencyGenerationSubSettings getIrisCompetencyGenerationSettings();

public abstract void setIrisCompetencyGenerationSettings(IrisCompetencyGenerationSubSettings irisCompetencyGenerationSubSettings);

public abstract IrisFaqIngestionSubSettings getIrisFaqIngestionSettings();

public abstract void setIrisFaqIngestionSettings(IrisFaqIngestionSubSettings irisFaqIngestionSubSettings);

}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
@JsonSubTypes.Type(value = IrisTextExerciseChatSubSettings.class, name = "text-exercise-chat"),
@JsonSubTypes.Type(value = IrisCourseChatSubSettings.class, name = "course-chat"),
@JsonSubTypes.Type(value = IrisLectureIngestionSubSettings.class, name = "lecture-ingestion"),
@JsonSubTypes.Type(value = IrisCompetencyGenerationSubSettings.class, name = "competency-generation")
@JsonSubTypes.Type(value = IrisCompetencyGenerationSubSettings.class, name = "competency-generation"),
@JsonSubTypes.Type(value = IrisFaqIngestionSubSettings.class, name = "faq-ingestion"),
})
// @formatter:on
@JsonInclude(JsonInclude.Include.NON_EMPTY)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

public enum IrisSubSettingsType {
CHAT, // TODO: Rename to PROGRAMMING_EXERCISE_CHAT
TEXT_EXERCISE_CHAT, COURSE_CHAT, COMPETENCY_GENERATION, LECTURE_INGESTION
TEXT_EXERCISE_CHAT, COURSE_CHAT, COMPETENCY_GENERATION, LECTURE_INGESTION, FAQ_INGESTION
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package de.tum.cit.aet.artemis.iris.dto;

import com.fasterxml.jackson.annotation.JsonInclude;

@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record IrisCombinedFaqIngestionSubSettingsDTO(boolean enabled) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public record IrisCombinedSettingsDTO(
IrisCombinedTextExerciseChatSubSettingsDTO irisTextExerciseChatSettings,
IrisCombinedCourseChatSubSettingsDTO irisCourseChatSettings,
IrisCombinedLectureIngestionSubSettingsDTO irisLectureIngestionSettings,
IrisCombinedCompetencyGenerationSubSettingsDTO irisCompetencyGenerationSettings
IrisCombinedCompetencyGenerationSubSettingsDTO irisCompetencyGenerationSettings,
IrisCombinedFaqIngestionSubSettingsDTO irisFaqIngestionSettings
) {}
// @formatter:on
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ default IrisGlobalSettings findGlobalSettingsElseThrow() {
LEFT JOIN FETCH irisSettings.irisTextExerciseChatSettings
LEFT JOIN FETCH irisSettings.irisLectureIngestionSettings
LEFT JOIN FETCH irisSettings.irisCompetencyGenerationSettings
WHERE irisSettings.course.id = :courseId
LEFT JOIN FETCH irisSettings.irisFaqIngestionSettings
WHERE irisSettings.course.id = :courseId
""")
Optional<IrisCourseSettings> findCourseSettings(@Param("courseId") long courseId);

Expand Down
Loading

0 comments on commit c59f91c

Please sign in to comment.