-
Notifications
You must be signed in to change notification settings - Fork 387
Iris: Course Memory ingestion
#13002
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
base: develop
Are you sure you want to change the base?
Changes from 7 commits
49799cf
4e93924
5a3e9e6
1ca56f0
fb2d212
f67c260
9d73bd5
7c1d41b
6e8866c
4821ca3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -48,6 +48,7 @@ | |
| import de.tum.cit.aet.artemis.globalsearch.dto.searchableentity.PostSearchableEntityDTO; | ||
| import de.tum.cit.aet.artemis.globalsearch.service.SearchableEntityWeaviateService; | ||
| import de.tum.cit.aet.artemis.iris.api.AutonomousTutorApi; | ||
| import de.tum.cit.aet.artemis.iris.api.CourseMemoryIngestionApi; | ||
| import de.tum.cit.aet.artemis.notification.domain.course_notifications.NewAnswerNotification; | ||
| import de.tum.cit.aet.artemis.notification.domain.course_notifications.NewMentionNotification; | ||
| import de.tum.cit.aet.artemis.notification.service.CourseNotificationService; | ||
|
|
@@ -78,6 +79,8 @@ public class AnswerMessageService extends PostingService { | |
|
|
||
| private final Optional<AutonomousTutorApi> autonomousTutorApi; | ||
|
|
||
| private final Optional<CourseMemoryIngestionApi> courseMemoryIngestionApi; | ||
|
|
||
| private final TransactionTemplate transactionTemplate; | ||
|
|
||
| private final Optional<SearchableEntityWeaviateService> searchableEntityWeaviateService; | ||
|
|
@@ -88,7 +91,7 @@ public AnswerMessageService(SingleUserNotificationService singleUserNotification | |
| ConversationService conversationService, ExerciseRepository exerciseRepository, SavedPostRepository savedPostRepository, | ||
| WebsocketMessagingService websocketMessagingService, ConversationParticipantRepository conversationParticipantRepository, | ||
| ChannelAuthorizationService channelAuthorizationService, PostRepository postRepository, CourseNotificationService courseNotificationService, | ||
| Optional<AutonomousTutorApi> autonomousTutorApi, PlatformTransactionManager transactionManager, | ||
| Optional<AutonomousTutorApi> autonomousTutorApi, Optional<CourseMemoryIngestionApi> courseMemoryIngestionApi, PlatformTransactionManager transactionManager, | ||
| Optional<SearchableEntityWeaviateService> searchableEntityWeaviateService) { | ||
| super(courseRepository, userRepository, exerciseRepository, authorizationCheckService, websocketMessagingService, conversationParticipantRepository, savedPostRepository); | ||
| this.answerPostRepository = answerPostRepository; | ||
|
|
@@ -98,6 +101,7 @@ public AnswerMessageService(SingleUserNotificationService singleUserNotification | |
| this.singleUserNotificationService = singleUserNotificationService; | ||
| this.postRepository = postRepository; | ||
| this.courseNotificationService = courseNotificationService; | ||
| this.courseMemoryIngestionApi = courseMemoryIngestionApi; | ||
| this.autonomousTutorApi = autonomousTutorApi; | ||
| this.transactionTemplate = new TransactionTemplate(transactionManager); | ||
| this.searchableEntityWeaviateService = searchableEntityWeaviateService; | ||
|
|
@@ -229,14 +233,16 @@ public AnswerPost updateAnswerMessage(Long courseId, Long answerMessageId, Updat | |
| // only the content of the message can be updated | ||
| existingAnswerMessage.setContent(answerMessage.content()); | ||
|
|
||
| // determine if the update operation is to mark the answer message as resolving the original post | ||
| // determine if the update operation changes whether the answer message resolves the original post | ||
| boolean resolutionChanged = false; | ||
| if (existingAnswerMessage.doesResolvePost() != answerMessage.resolvesPost()) { | ||
| // check if requesting user is allowed to mark this answer message as resolving, i.e. if user is author or original message or at least tutor | ||
| mayMarkAnswerMessageAsResolvingElseThrow(existingAnswerMessage, user, course); | ||
| existingAnswerMessage.setResolvesPost(answerMessage.resolvesPost()); | ||
| // sets the message as resolved if there exists any resolving answer | ||
| existingAnswerMessage.getPost().setResolved(existingAnswerMessage.getPost().getAnswers().stream().anyMatch(AnswerPost::doesResolvePost)); | ||
| postRepository.save(existingAnswerMessage.getPost()); | ||
| resolutionChanged = true; | ||
| } | ||
| else { | ||
| // check if requesting user is allowed to update the content, i.e. if user is author of answer message or at least tutor | ||
|
|
@@ -251,6 +257,17 @@ public AnswerPost updateAnswerMessage(Long courseId, Long answerMessageId, Updat | |
| syncAnswerPostWithWeaviate(updatedAnswerMessage, conversation); | ||
|
|
||
| this.preparePostAndBroadcast(updatedAnswerMessage, course); | ||
|
|
||
| // Trigger B: the thread's resolution state changed. Fires in both directions so un-marking the | ||
| // last resolving answer retracts the Course Memory entry instead of leaving it served as verified. | ||
| if (resolutionChanged) { | ||
| try { | ||
| courseMemoryIngestionApi.ifPresent(api -> api.onThreadResolutionChanged(updatedAnswerMessage.getPost(), updatedAnswerMessage, user, course)); | ||
|
Comment on lines
+261
to
+268
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Refresh edited verified Iris answers through the verification path. If an authorized user edits a verified Iris answer that does not resolve the post, Add a dedicated update path for verified Iris answers that preserves the edited text and provenance. 🤖 Prompt for AI Agents |
||
| } | ||
| catch (Exception e) { | ||
| log.error("Failed to update course memory after resolution change on answer post {}", updatedAnswerMessage.getId(), e); | ||
| } | ||
| } | ||
| return updatedAnswerMessage; | ||
| } | ||
|
|
||
|
|
@@ -296,6 +313,11 @@ public void deleteAnswerMessageById(Long courseId, Long answerMessageId) { | |
| } | ||
| ensureConversationBelongsToCourseElseThrow(conversation, courseId); | ||
|
|
||
| // An answer that resolved the thread or that was a verified Iris answer is what the thread's | ||
| // Course Memory entry was built from, so its removal has to be reflected there too. | ||
| boolean contributedToCourseMemory = Boolean.TRUE.equals(answerMessage.doesResolvePost()) | ||
| || (answerMessage.getAuthor() != null && answerMessage.getAuthor().isBot() && answerMessage.isVerified()); | ||
|
|
||
| // we need to explicitly remove the answer post from the answers of the broadcast post to share up-to-date information | ||
| Post updatedMessage = answerMessage.getPost(); | ||
| updatedMessage.removeAnswerPost(answerMessage); | ||
|
|
@@ -314,6 +336,17 @@ public void deleteAnswerMessageById(Long courseId, Long answerMessageId) { | |
| savedPostRepository.deleteAll(savedPosts); | ||
|
|
||
| broadcastForPost(updatedMessage, MetisCrudAction.UPDATE, course.getId(), null); | ||
|
|
||
| // Re-ingest from whatever verified answer survives, or delete the entry if none does. Runs after | ||
| // the delete is committed so the re-fetched thread no longer contains the removed answer. | ||
| if (contributedToCourseMemory) { | ||
| try { | ||
| courseMemoryIngestionApi.ifPresent(api -> api.onThreadResolutionChanged(updatedMessage, null, user, course)); | ||
| } | ||
| catch (Exception e) { | ||
| log.error("Failed to update course memory after deletion of answer post {}", answerMessageId, e); | ||
| } | ||
| } | ||
|
Comment on lines
+354
to
+364
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Description: Inspect the eager thread query and any transaction annotations on the answer deletion path.
rg -n -C6 'findByPostIdsWithEagerRelationships' --type=java
rg -n -C3 'Transactional' src/main/java/de/tum/cit/aet/artemis/communication/service/AnswerMessageService.java src/main/java/de/tum/cit/aet/artemis/communication/web/ --type=javaRepository: ls1intum/Artemis Length of output: 154 🏁 Script executed: #!/bin/bash
set -u
echo "Repository files matching relevant names:"
fd -i 'AnswerMessageService|ConversationMessageRepository|CourseMemoryIngestionService|CourseMemoryIngestionApi' .
echo
echo "Find Java files by broad search if fd unavailable:"
git ls-files '*.java' | rg 'AnswerMessageService|ConversationMessageRepository|CourseMemoryIngestion|AnswerPost|ConversationMessage' | sed -n '1,200p'Repository: ls1intum/Artemis Length of output: 2617 🏁 Script executed: #!/bin/bash
set -u
echo "AnswerMessageService outline:"
ast-grep outline src/main/java/de/tum/cit/aet/artemis/communication/service/AnswerMessageService.java --view expanded | sed -n '1,220p' || true
echo
echo "ConversationMessageRepository outline:"
ast-grep outline src/main/java/de/tum/cit/aet/artemis/communication/repository/ConversationMessageRepository.java --view expanded | sed -n '1,220p' || true
echo
echo "CourseMemoryIngestionService outline:"
ast-grep outline src/main/java/de/tum/cit/aet/artemis/iris/service/CourseMemoryIngestionService.java --view expanded | sed -n '1,240p' || true
echo
echo "CourseMemoryIngestionApi outline:"
ast-grep outline src/main/java/de/tum/cit/aet/artemis/iris/api/CourseMemoryIngestionApi.java --view expanded | sed -n '1,180p' || trueRepository: ls1intum/Artemis Length of output: 4911 🏁 Script executed: #!/bin/bash
set -u
echo "AnswerMessageService relevant sections:"
sed -n '260,355p' src/main/java/de/tum/cit/aet/artemis/communication/service/AnswerMessageService.java | cat -n
echo
echo "AnswerMessageService imports"
sed -n '1,90p' src/main/java/de/tum/cit/aet/artemis/communication/service/AnswerMessageService.java | cat -n
echo
echo "ConversationMessageRepository relevant sections:"
sed -n '90,145p' src/main/java/de/tum/cit/aet/artemis/communication/repository/ConversationMessageRepository.java | cat -n
echo
echo "CourseMemoryIngestionService relevant sections:"
sed -n '120,350p' src/main/java/de/tum/cit/aet/artemis/iris/service/CourseMemoryIngestionService.java | cat -n
echo
echo "CourseMemoryIngestionApi relevant sections:"
sed -n '1,75p' src/main/java/de/tum/cit/aet/artemis/iris/api/CourseMemoryIngestionApi.java | cat -n
echo
echo "Search repository symbols"
rg -n -C4 'AnswerPostRepository|AnswerPost|AnswerMessage|conversationMessageRepository|deleteAnswerMessageById|onThreadResolutionChanged|fetchThread|selectedAnchor|selectAnchor|deleteThreadMemory|ingest' src/main/java/de/tum/cit/aet/artemis/communication/repository src/main/java/de/tum/cit/aet/artemis/communication/service src/main/java/de/tum/cit/aet/artemis/communication/web src/main/java/de/tum/cit/aet/artemis/iris/service src/main/java/de/tum/cit/aet/artemis/iris/api --type=javaRepository: ls1intum/Artemis Length of output: 50373 🏁 Script executed: #!/bin/bash
set -u
echo "CourseMemoryIngestionService.onThreadResolutionChanged/fetchThread/selectAnchor:"
sed -n '140,215p' src/main/java/de/tum/cit/aet/artemis/iris/service/CourseMemoryIngestionService.java | cat -n
echo
echo "CourseMemoryIngestionService.fetchThread implementation:"
sed -n '315,326p' src/main/java/de/tum/cit/aet/artemis/iris/service/CourseMemoryIngestionService.java | cat -n
echo
echo "CourseMemoryIngestionService.relevant sections (fetchThread plus helpers):"
sed -n '315,345p' src/main/java/de/tum/cit/aet/artemis/iris/service/CourseMemoryIngestionService.java | cat -n
echo
echo "CourseMemoryIngestionService imports"
sed -n '1,80p' src/main/java/de/tum/cit/aet/artemis/iris/service/CourseMemoryIngestionService.java | cat -n
echo
echo "Exact symbols across Java files:"
rg -n '\bfetchThread\b|\bselectAnchor\b|\bbuildThread\b|\bhandleResolutionChange\b|\bonThreadResolutionChanged\b|\bdeleteAnswerMessageById\b|\bcontributedToCourseMemory\b|\bcontainsAnswerPost\b|\bloade' src/main/java/de/tum/cit/aet/artemis --type=javaRepository: ls1intum/Artemis Length of output: 50372 🏁 Script executed: #!/bin/bash
set -u
echo "ConversationMessageRepository lines 99-130:"
sed -n '99,130p' src/main/java/de/tum/cit/aet/artemis/communication/repository/ConversationMessageRepository.java | cat -n
echo
echo "CourseMemoryIngestionService.handleResolutionChange/selectAnchor/buildThread:"
sed -n '140,180p' src/main/java/de/tum/cit/aet/artemis/iris/service/CourseMemoryIngestionService.java | cat -n
echo
echo "findAnswerMessageByIdElseThrow implementations:"
rg -n -C6 'findAnswerMessageByIdElseThrow|findAnswerMessage.*PessimisticWriteLock|findByIdIn' src/main/java/de/tum/cit/aet/artemis/communication/repository src/main/java/de/tum/cit/aet/artemis/communication/service --type=java
echo
echo "Delete path transaction annotations in focused files:"
rg -n '`@Transaction`|Transactional|runInTransaction|execute|`@Async`|`@Async`' src/main/java/de/tum/cit/aet/artemis/communication/repository src/main/java/de/tum/cit/aet/artemis/communication/service/AnswerMessageService.java src/main/java/de/tum/cit/aet/artemis/communication/web --type=javaRepository: ls1intum/Artemis Length of output: 23582 🏁 Script executed: #!/bin/bash
set -u
echo "AnswerMessageService imports and bean declarations:"
sed -n '1,110p' src/main/java/de/tum/cit/aet/artemis/communication/service/AnswerMessageService.java | cat -n
echo
echo "CourseMemoryIngestionService call path annotations:"
rg -n 'public |private |`@Transactional`|`@Async`|try \{|catch \(' src/main/java/de/tum/cit/aet/artemis/communication/service/AnswerMessageService.java:295 src/main/java/de/tum/cit/aet/artemis/communication/service/AnswerMessageService.java:340 src/main/java/de/tum/cit/aet/artemis/iris/service/CourseMemoryIngestionService.java:145 --type=java || true
echo
echo "AnswerPostRepository imports and first transactional declarations:"
sed -n '1,60p' src/main/java/de/tum/cit/aet/artemis/communication/repository/AnswerPostRepository.java | cat -n
echo
echo "ConversationMessageRepository transactional declarations:"
sed -n '1,115p' src/main/java/de/tum/cit/aet/artemis/communication/repository/ConversationMessageRepository.java | cat -nRepository: ls1intum/Artemis Length of output: 18162 🌐 Web query:
💡 Result: In Spring Data JPA, repository methods follow specific transactional behaviors depending on whether they are standard CRUD methods or custom query methods [1][2]. Default Transactional Behavior By default, Spring Data JPA repositories are transactional, governed by the SimpleJpaRepository implementation class [1][3]. - CRUD Methods: Standard methods inherited from CrudRepository (e.g., save, delete, findAll) are annotated with Citations:
🌐 Web query:
💡 Result: In Hibernate, when you remove an element from a collection that is part of a persistence context, the behavior depends on the collection mapping and whether the association is bidirectional or unidirectional [1][2]. Hibernate tracks changes to these collections by wrapping them in a PersistentCollection [3]. During the flush process, Hibernate synchronizes the in-memory state of these collections with the database [4]. For many-to-many or one-to-many associations, removing an item from the collection typically results in Hibernate executing a DELETE statement for the association row in the link table (for many-to-many) or updating the foreign key column to null (for one-to-many), depending on the specific mapping configuration [5][6][7]. If you remove all elements from a collection, Hibernate may clear the entire association by deleting all corresponding rows in the database [1][8]. Key considerations for managing this behavior include: 1. Bidirectional Associations: It is mandatory to synchronize both sides of a bidirectional relationship [9]. If you only remove an entity from the collection on the parent side, the database state may not update correctly if the inverse side (the child) still holds a reference to the parent [9]. Always provide helper methods (e.g., add/remove) on your entities to ensure both sides of the association are kept in sync [9]. 2. Performance: Modifying a collection—especially lists or bags—can sometimes be inefficient if Hibernate determines it must "recreate" the collection state by deleting and re-inserting elements, rather than issuing specific delete or update statements [1][8]. Using an Citations:
🌐 Web query:
💡 Result: In Hibernate, the first-level cache (the Persistence Context) acts as a transactional write-behind buffer that stores managed entities [1][2]. When you modify an entity—including its associations—within the Persistence Context, those changes are held in memory and are not immediately synchronized with the database [1][3]. When a flush operation occurs, Hibernate performs "dirty checking" to synchronize the in-memory state of managed entities with the underlying database [2]. During this process, Hibernate compares the current state of the managed entities in the first-level cache against the "loaded state" (a snapshot taken when the entity was originally fetched) [1]. If discrepancies are detected, Hibernate enqueues the necessary SQL statements (INSERT, UPDATE, DELETE) to align the database with the current state of the Persistence Context [4][3]. Regarding modified associations specifically: 1. Consistency and Synchronization: Hibernate does not automatically manage the synchronization of both sides of a bidirectional association [5][6]. It is the developer's responsibility to ensure that both sides of an association are updated in memory before the flush occurs [5]. Failure to manually synchronize both ends of a bidirectional relationship can lead to inconsistent state in the Persistence Context, which may result in data integrity issues or incorrect SQL being generated during the flush [5]. 2. Flush Behavior: The flush operation ensures that all pending state transitions—including association changes—are reflected in the database [3]. Because Hibernate uses a write-behind strategy, the actual SQL execution is deferred and optimized [2][3]. It is important to note that the flush order is specific (e.g., entity inserts generally precede collection updates or removals) [4]. 3. Data Visibility: Flushed changes are visible only within the current database transaction [3]. They remain pending in the database until the transaction is committed [3]. To ensure reliable behavior, always use helper methods to synchronize both sides of a bidirectional association immediately after modifying them in your application code [5][6]. Do not rely on Hibernate to reconcile asymmetric bidirectional associations during the flush process [5]. Citations:
Use a separate transaction context for the re-fetch.
🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| /** | ||
|
|
@@ -384,6 +417,15 @@ public AnswerPost verifyAnswerMessage(Long courseId, Long answerMessageId, Verif | |
|
|
||
| sendMentionNotificationForAnswerMessage(course, verificationResult.conversation(), verificationResult.answerMessage(), verificationResult.mentionedUsers()); | ||
| this.preparePostAndBroadcast(verificationResult.answerMessage(), course); | ||
|
|
||
| // Trigger A: a tutor approved (IRIS_AUTO) or edited (IRIS_CORRECTED) an Iris draft -> ingest into Course Memory | ||
| boolean edited = verifyDto != null && verifyDto.content() != null && !verifyDto.content().isBlank(); | ||
| try { | ||
| courseMemoryIngestionApi.ifPresent(api -> api.onAnswerVerified(verificationResult.answerMessage(), edited, user, course)); | ||
| } | ||
| catch (Exception e) { | ||
| log.error("Failed to ingest verified answer post {} into course memory", verificationResult.answerMessage().getId(), e); | ||
| } | ||
| return verificationResult.answerMessage(); | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| package de.tum.cit.aet.artemis.iris.api; | ||
|
|
||
| import org.jspecify.annotations.Nullable; | ||
| import org.springframework.context.annotation.Conditional; | ||
| import org.springframework.context.annotation.Lazy; | ||
| import org.springframework.stereotype.Controller; | ||
|
|
||
| import de.tum.cit.aet.artemis.account.domain.User; | ||
| import de.tum.cit.aet.artemis.communication.domain.AnswerPost; | ||
| import de.tum.cit.aet.artemis.communication.domain.Post; | ||
| import de.tum.cit.aet.artemis.course.domain.Course; | ||
| import de.tum.cit.aet.artemis.iris.config.IrisEnabled; | ||
| import de.tum.cit.aet.artemis.iris.service.CourseMemoryIngestionService; | ||
|
|
||
| /** | ||
| * Public facade for Course Memory ingestion, consumed by the communication module via an | ||
| * {@code Optional<CourseMemoryIngestionApi>} so it stays a no-op when Iris is disabled. | ||
| */ | ||
| @Conditional(IrisEnabled.class) | ||
| @Controller | ||
| @Lazy | ||
| public class CourseMemoryIngestionApi extends AbstractIrisApi { | ||
|
|
||
| private final CourseMemoryIngestionService courseMemoryIngestionService; | ||
|
|
||
| public CourseMemoryIngestionApi(CourseMemoryIngestionService courseMemoryIngestionService) { | ||
| this.courseMemoryIngestionService = courseMemoryIngestionService; | ||
| } | ||
|
|
||
| /** | ||
| * Trigger A: a tutor approved (optionally edited) an Iris-generated answer in the verification dashboard. | ||
| * | ||
| * @param verifiedAnswer the now-verified Iris answer post | ||
| * @param edited whether the tutor edited the draft before approving | ||
| * @param verifier the tutor who verified the answer | ||
| * @param course the course the answer belongs to | ||
| */ | ||
| public void onAnswerVerified(AnswerPost verifiedAnswer, boolean edited, User verifier, Course course) { | ||
| courseMemoryIngestionService.ingestVerifiedAnswer(verifiedAnswer, edited, verifier, course); | ||
| } | ||
|
|
||
| /** | ||
| * Trigger B: a thread's resolution state changed — an answer was marked resolving, un-marked, or | ||
| * deleted. Ingests the thread while it still holds an answer someone stands behind, and deletes | ||
| * its entry once none remains. | ||
| * | ||
| * @param post the thread's root post | ||
| * @param triggeringAnswer the answer whose flag changed, or {@code null} when it was deleted | ||
| * @param marker the user who changed the resolution state, if known | ||
| * @param course the course the thread belongs to | ||
| */ | ||
| public void onThreadResolutionChanged(Post post, @Nullable AnswerPost triggeringAnswer, @Nullable User marker, Course course) { | ||
| courseMemoryIngestionService.handleResolutionChange(post, triggeringAnswer, marker, course); | ||
| } | ||
|
|
||
| /** | ||
| * The whole thread was deleted, so its Course Memory entry is removed with it. | ||
| * | ||
| * @param post the thread's root post, before deletion | ||
| * @param actor the user who deleted the thread, notified about the removal | ||
| * @param course the course the thread belongs to | ||
| */ | ||
| public void onThreadDeleted(Post post, @Nullable User actor, Course course) { | ||
| courseMemoryIngestionService.handleThreadDeleted(post, actor, course); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package de.tum.cit.aet.artemis.iris.domain; | ||
|
|
||
| /** | ||
| * Which Course Memory operation a Pyris webhook run performs. Both share the same job type and | ||
| * status callback, so the job has to carry this to interpret a completion. | ||
| */ | ||
| public enum CourseMemoryOperation { | ||
| /** | ||
| * A thread's Q/A entry is being written or refreshed. | ||
| */ | ||
| INGEST, | ||
| /** | ||
| * A thread's entry is being removed because it stopped being resolved. | ||
| */ | ||
| DELETE | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package de.tum.cit.aet.artemis.iris.domain; | ||
|
|
||
| /** | ||
| * How far a Course Memory run has got. Sent to the acting user so the UI can report that ingestion | ||
| * actually started (as opposed to being skipped) and later that it finished. | ||
| */ | ||
| public enum CourseMemoryStage { | ||
| /** | ||
| * Artemis has dispatched the webhook to Pyris. | ||
| */ | ||
| TRIGGERED, | ||
| /** | ||
| * Pyris reported the run finished. | ||
| */ | ||
| COMPLETED, | ||
| /** | ||
| * Pyris reported the run failed. | ||
| */ | ||
| FAILED | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package de.tum.cit.aet.artemis.iris.dto; | ||
|
|
||
| import org.jspecify.annotations.Nullable; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonInclude; | ||
|
|
||
| import de.tum.cit.aet.artemis.iris.domain.CourseMemoryOperation; | ||
| import de.tum.cit.aet.artemis.iris.domain.CourseMemoryStage; | ||
|
|
||
| /** | ||
| * Progress of a Course Memory run, pushed over the websocket to the user who triggered it on | ||
| * {@code /topic/iris/course-memory/{courseId}}. | ||
| * <p> | ||
| * {@code TRIGGERED} is sent by Artemis at the moment it dispatches the webhook — not when the user | ||
| * clicks — because ingestion is conditional: non-public channels, Iris-disabled courses and | ||
| * bot-authored answers are skipped, and a retraction dispatches a deletion instead. A client-side | ||
| * toast fired on the HTTP response would claim ingestion started in all of those cases. | ||
| * {@code COMPLETED} / {@code FAILED} arrive later, from Pyris' status callback. | ||
| * | ||
| * @param operation whether the run writes or removes the entry | ||
| * @param stage how far the run has got | ||
| * @param courseId the course the entry is scoped to | ||
| * @param postId stringified id of the thread's root post, the entry's key | ||
| * @param errorMessage failure detail reported by Pyris, only set for {@link CourseMemoryStage#FAILED} | ||
| */ | ||
| @JsonInclude(JsonInclude.Include.NON_EMPTY) | ||
| public record IrisCourseMemoryStatusDTO(CourseMemoryOperation operation, CourseMemoryStage stage, long courseId, String postId, @Nullable String errorMessage) { | ||
|
|
||
| public static IrisCourseMemoryStatusDTO triggered(CourseMemoryOperation operation, long courseId, String postId) { | ||
| return new IrisCourseMemoryStatusDTO(operation, CourseMemoryStage.TRIGGERED, courseId, postId, null); | ||
| } | ||
|
|
||
| public static IrisCourseMemoryStatusDTO completed(CourseMemoryOperation operation, long courseId, String postId) { | ||
| return new IrisCourseMemoryStatusDTO(operation, CourseMemoryStage.COMPLETED, courseId, postId, null); | ||
| } | ||
|
|
||
| public static IrisCourseMemoryStatusDTO failed(CourseMemoryOperation operation, long courseId, String postId, @Nullable String errorMessage) { | ||
| return new IrisCourseMemoryStatusDTO(operation, CourseMemoryStage.FAILED, courseId, postId, errorMessage); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@toukhi [medium] This hook runs only when
resolvesPostchanges. If a tutor edits the content of an already-resolving answer while leaving that flag set, the update is persisted but no upsert is dispatched, so Iris continues serving the old answer indefinitely.🤖 Prompt for AI agents
In
src/main/java/de/tum/cit/aet/artemis/communication/service/AnswerMessageService.java, content-only edits to answers that already contribute to Course Memory are not re-ingested. Detect such content changes and dispatch a Course Memory refresh after persistence.