Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@ public void executePipeline(String name, String variant, Optional<String> event,
Consumer<List<PyrisStageDTO>> statusUpdater) {
// 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);
var executing = new PyrisStageDTO("Executing pipeline", 30, null, null);
var preparing = new PyrisStageDTO("Preparing", 10, null, null, false);
var executing = new PyrisStageDTO("Executing pipeline", 30, null, null, false);

// Send initial status update indicating that the preparation stage is in progress
statusUpdater.accept(List.of(preparing.inProgress(), executing.notStarted()));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
package de.tum.cit.aet.artemis.iris.service.pyris.dto.status;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;

@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record PyrisStageDTO(String name, int weight, PyrisStageState state, String message) {
public record PyrisStageDTO(String name, int weight, PyrisStageState state, String message, @JsonProperty(defaultValue = "false") boolean internal) {

public PyrisStageDTO notStarted() {
return new PyrisStageDTO(name, weight, PyrisStageState.NOT_STARTED, message);
return new PyrisStageDTO(name, weight, PyrisStageState.NOT_STARTED, message, internal);
}

public PyrisStageDTO inProgress() {
return new PyrisStageDTO(name, weight, PyrisStageState.IN_PROGRESS, message);
return new PyrisStageDTO(name, weight, PyrisStageState.IN_PROGRESS, message, internal);
}

public PyrisStageDTO error(String message) {
return new PyrisStageDTO(name, weight, PyrisStageState.ERROR, message);
return new PyrisStageDTO(name, weight, PyrisStageState.ERROR, message, internal);
}

public PyrisStageDTO done() {
return new PyrisStageDTO(name, weight, PyrisStageState.DONE, message);
return new PyrisStageDTO(name, weight, PyrisStageState.DONE, message, internal);
}

public PyrisStageDTO with(PyrisStageState state, String message) {
return new PyrisStageDTO(name, weight, state, message);
return new PyrisStageDTO(name, weight, state, message, internal);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,35 +23,35 @@ describe('ChatStatusBarComponent', () => {
});

it('should handle unfinished stages in ngOnChanges', () => {
component.stages = [{ name: 'Test Stage', state: IrisStageStateDTO.IN_PROGRESS, weight: 1, message: 'Test' }];
component.stages = [{ name: 'Test Stage', state: IrisStageStateDTO.IN_PROGRESS, weight: 1, message: 'Test', internal: false }];
component.ngOnChanges();
expect(component.open).toBeTrue();
expect(component.activeStage).toEqual(component.stages[0]);
expect(component.displayedText).toBe('Test Stage');
});

it('should handle all stages finished in ngOnChanges', () => {
component.stages = [{ name: 'Test Stage', state: IrisStageStateDTO.DONE, weight: 1, message: 'Test' }];
component.stages = [{ name: 'Test Stage', state: IrisStageStateDTO.DONE, weight: 1, message: 'Test', internal: false }];
component.ngOnChanges();
expect(component.open).toBeFalse();
expect(component.activeStage).toBeUndefined();
expect(component.displayedText).toBeUndefined();
});

it('should return true for finished stages in isStageFinished', () => {
const stage: IrisStageDTO = { name: 'Test Stage', state: IrisStageStateDTO.DONE, weight: 1, message: 'Test' };
const stage: IrisStageDTO = { name: 'Test Stage', state: IrisStageStateDTO.DONE, weight: 1, message: 'Test', internal: false };
expect(component.isStageFinished(stage)).toBeTrue();
stage.state = IrisStageStateDTO.SKIPPED;
expect(component.isStageFinished(stage)).toBeTrue();
});

it('should return false for unfinished stages in isStageFinished', () => {
const stage: IrisStageDTO = { name: 'Test Stage', state: IrisStageStateDTO.IN_PROGRESS, weight: 1, message: 'Test' };
const stage: IrisStageDTO = { name: 'Test Stage', state: IrisStageStateDTO.IN_PROGRESS, weight: 1, message: 'Test', internal: false };
expect(component.isStageFinished(stage)).toBeFalse();
});

it('should render progress bar when stages are present', () => {
component.stages = [{ name: 'Test Stage', state: IrisStageStateDTO.IN_PROGRESS, weight: 1, message: 'Test' }];
component.stages = [{ name: 'Test Stage', state: IrisStageStateDTO.IN_PROGRESS, weight: 1, message: 'Test', internal: false }];
fixture.detectChanges();
const progressBar = fixture.debugElement.query(By.css('.progress-bar'));
expect(progressBar).toBeTruthy();
Expand All @@ -65,7 +65,7 @@ describe('ChatStatusBarComponent', () => {
});

it('should render stage name when stages are present', () => {
component.stages = [{ name: 'Test Stage', state: IrisStageStateDTO.IN_PROGRESS, weight: 1, message: 'Test' }];
component.stages = [{ name: 'Test Stage', state: IrisStageStateDTO.IN_PROGRESS, weight: 1, message: 'Test', internal: false }];
component.ngOnChanges();
fixture.detectChanges();
const stageName = fixture.debugElement.query(By.css('.display')).nativeElement.textContent;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -406,18 +406,22 @@ export class IrisChatService implements OnDestroy {
this.replaceOrAddMessage(payload.message);
}
if (payload.stages) {
this.stages.next(payload.stages);
this.stages.next(this.filterStages(payload.stages));
}
break;
case IrisChatWebsocketPayloadType.STATUS:
this.stages.next(payload.stages || []);
this.stages.next(this.filterStages(payload.stages || []));
if (payload.suggestions) {
this.suggestions.next(payload.suggestions);
}
break;
}
}

private filterStages(stages: IrisStageDTO[]): IrisStageDTO[] {
return stages.filter((stage) => !stage.internal);
}

protected close(): void {
if (this.sessionId) {
this.ws.unsubscribeFromSession(this.sessionId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export class IrisStageDTO {
weight: number;
state: IrisStageStateDTO;
message: string;
// Internal stages are not shown in the UI and are hidden from the user
internal: boolean;

lowerCaseState?: string;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ void testTokenTrackingHandledExerciseChat() throws Exception {
IrisMessage messageToSend = IrisMessageFactory.createIrisMessageForSessionWithContent(irisSession);
var tokens = getMockLLMCosts("IRIS_CHAT_EXERCISE_MESSAGE");
List<PyrisStageDTO> doneStage = new ArrayList<>();
doneStage.add(new PyrisStageDTO("DoneTest", 10, PyrisStageState.DONE, "Done"));
doneStage.add(new PyrisStageDTO("DoneTest", 10, PyrisStageState.DONE, "Done", false));
irisRequestMockProvider.mockProgrammingExerciseChatResponse(dto -> {
assertThat(dto.settings().authenticationToken()).isNotNull();
assertThatNoException().isThrownBy(() -> sendStatus(dto.settings().authenticationToken(), "Hello World", doneStage, tokens));
Expand Down Expand Up @@ -181,7 +181,7 @@ void testTokenTrackingExerciseChatWithPipelineFail() throws Exception {
IrisMessage messageToSend = IrisMessageFactory.createIrisMessageForSessionWithContent(irisSession);
var tokens = getMockLLMCosts("IRIS_CHAT_EXERCISE_MESSAGE");
List<PyrisStageDTO> failedStages = new ArrayList<>();
failedStages.add(new PyrisStageDTO("TestTokenFail", 10, PyrisStageState.ERROR, "Failed running pipeline"));
failedStages.add(new PyrisStageDTO("TestTokenFail", 10, PyrisStageState.ERROR, "Failed running pipeline", false));
irisRequestMockProvider.mockProgrammingExerciseChatResponse(dto -> {
assertThat(dto.settings().authenticationToken()).isNotNull();
assertThatNoException().isThrownBy(() -> sendStatus(dto.settings().authenticationToken(), null, failedStages, tokens));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ void generateCompetencies_asEditor_shouldSucceed() throws Exception {

PyrisCompetencyRecommendationDTO expected = new PyrisCompetencyRecommendationDTO("test title", "test description", CompetencyTaxonomy.UNDERSTAND);
List<PyrisCompetencyRecommendationDTO> recommendations = List.of(expected, expected, expected);
List<PyrisStageDTO> stages = List.of(new PyrisStageDTO("Generating Competencies", 10, PyrisStageState.DONE, null));
List<PyrisStageDTO> stages = List.of(new PyrisStageDTO("Generating Competencies", 10, PyrisStageState.DONE, null, false));

// In the real system, this would be triggered by Pyris via a REST call to the Artemis server
String jobId = "testJobId";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ void testTutorSuggestionStatusUpdateShouldBeHandled() throws Exception {
var job = pyrisJobService.getAndAuthenticateJobFromHeaderElseThrow(mockRequest, de.tum.cit.aet.artemis.iris.service.pyris.job.TutorSuggestionJob.class);
assertThat(job).isNotNull();
assertThat(job.jobId()).isEqualTo(token);
List<PyrisStageDTO> stages = List.of(new PyrisStageDTO("Test stage", 0, PyrisStageState.DONE, "Done"));
List<PyrisStageDTO> stages = List.of(new PyrisStageDTO("Test stage", 0, PyrisStageState.DONE, "Done", false));
var statusUpdate = new TutorSuggestionStatusUpdateDTO("Test suggestion", "Test result", stages, null);
var mockRequestForStatusUpdate = new org.springframework.mock.web.MockHttpServletRequest();
mockRequestForStatusUpdate.addHeader(HttpHeaders.AUTHORIZATION, Constants.BEARER_PREFIX + token);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ void courseChat_IntermediateAccessedMemoriesThenCreatedMemories() throws Excepti

// Build non-terminal and terminal stage lists
var preparingDone = stagesRef.get().getFirst();
var executingInProgress = new PyrisStageDTO("Executing pipeline", 30, de.tum.cit.aet.artemis.iris.service.pyris.dto.status.PyrisStageState.IN_PROGRESS, null);
var executingDone = new PyrisStageDTO("Executing pipeline", 30, de.tum.cit.aet.artemis.iris.service.pyris.dto.status.PyrisStageState.DONE, null);
var executingInProgress = new PyrisStageDTO("Executing pipeline", 30, de.tum.cit.aet.artemis.iris.service.pyris.dto.status.PyrisStageState.IN_PROGRESS, null, false);
var executingDone = new PyrisStageDTO("Executing pipeline", 30, de.tum.cit.aet.artemis.iris.service.pyris.dto.status.PyrisStageState.DONE, null, false);

// Send intermediate status with accessed memories only (no result yet) and non-terminal stages
sendCourseStatus(jobIdRef.get(), null, List.of(preparingDone, executingInProgress), null,
Expand Down Expand Up @@ -190,7 +190,7 @@ void courseChat_CreatedMemoriesUpdateAssistantMessage() throws Exception {
await().until(() -> jobIdRef.get() != null && stagesRef.get() != null);

var preparingDone = stagesRef.get().getFirst();
var executingInProgress = new PyrisStageDTO("Executing pipeline", 30, de.tum.cit.aet.artemis.iris.service.pyris.dto.status.PyrisStageState.IN_PROGRESS, null);
var executingInProgress = new PyrisStageDTO("Executing pipeline", 30, de.tum.cit.aet.artemis.iris.service.pyris.dto.status.PyrisStageState.IN_PROGRESS, null, false);

// First: send assistant result to create assistant message and set assistantMessageId on the job (keep job running with non-terminal stages)
sendCourseStatus(jobIdRef.get(), "Initial Answer", List.of(preparingDone, executingInProgress), null, null, null);
Expand Down
Loading
Loading