diff --git a/src/main/java/de/tum/cit/aet/artemis/core/dto/CourseManagementDetailViewDTO.java b/src/main/java/de/tum/cit/aet/artemis/core/dto/CourseManagementDetailViewDTO.java index 253cbb648809..615584ee8b9c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/dto/CourseManagementDetailViewDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/dto/CourseManagementDetailViewDTO.java @@ -8,5 +8,5 @@ public record CourseManagementDetailViewDTO(Integer numberOfStudentsInCourse, Integer numberOfTeachingAssistantsInCourse, Integer numberOfEditorsInCourse, Integer numberOfInstructorsInCourse, Double currentPercentageAssessments, Long currentAbsoluteAssessments, Long currentMaxAssessments, Double currentPercentageComplaints, Long currentAbsoluteComplaints, Long currentMaxComplaints, Double currentPercentageMoreFeedbacks, Long currentAbsoluteMoreFeedbacks, Long currentMaxMoreFeedbacks, - Double currentPercentageAverageScore, Double currentAbsoluteAverageScore, Double currentMaxAverageScore, List activeStudents) { + Double currentPercentageAverageScore, Double currentAbsoluteAverageScore, Double currentMaxAverageScore, List activeStudents, Double currentTotalLlmCostInEur) { } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/repository/LLMTokenUsageTraceRepository.java b/src/main/java/de/tum/cit/aet/artemis/core/repository/LLMTokenUsageTraceRepository.java index cc1b0e588c4e..2516c59c4aa4 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/repository/LLMTokenUsageTraceRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/repository/LLMTokenUsageTraceRepository.java @@ -3,6 +3,8 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import org.springframework.context.annotation.Profile; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import de.tum.cit.aet.artemis.core.domain.LLMTokenUsageTrace; @@ -11,4 +13,13 @@ @Profile(PROFILE_CORE) @Repository public interface LLMTokenUsageTraceRepository extends ArtemisJpaRepository { + + @Query(""" + SELECT COALESCE(ROUND(SUM((req.numInputTokens * req.costPerMillionInputTokens / 1000000) + + (req.numOutputTokens * req.costPerMillionOutputTokens / 1000000)), 2), 0.0) + FROM LLMTokenUsageRequest req + JOIN req.trace trace + WHERE trace.courseId = :courseId + """) + Double calculateTotalLlmCostInEurForCourse(@Param("courseId") Long courseId); } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java index 350a165409b8..232e86070473 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java @@ -83,6 +83,7 @@ import de.tum.cit.aet.artemis.core.dto.TutorLeaderboardDTO; import de.tum.cit.aet.artemis.core.dto.pageablesearch.SearchTermPageableSearchDTO; import de.tum.cit.aet.artemis.core.repository.CourseRepository; +import de.tum.cit.aet.artemis.core.repository.LLMTokenUsageTraceRepository; import de.tum.cit.aet.artemis.core.repository.StatisticsRepository; import de.tum.cit.aet.artemis.core.repository.UserRepository; import de.tum.cit.aet.artemis.core.security.Role; @@ -214,6 +215,8 @@ public class CourseService { private final BuildJobRepository buildJobRepository; + private final LLMTokenUsageTraceRepository llmTokenUsageTraceRepository; + public CourseService(CourseRepository courseRepository, ExerciseService exerciseService, ExerciseDeletionService exerciseDeletionService, AuthorizationCheckService authCheckService, UserRepository userRepository, LectureService lectureService, GroupNotificationRepository groupNotificationRepository, ExerciseGroupRepository exerciseGroupRepository, AuditEventRepository auditEventRepository, UserService userService, ExamDeletionService examDeletionService, @@ -227,7 +230,7 @@ public CourseService(CourseRepository courseRepository, ExerciseService exercise LearningPathApi learningPathApi, Optional irisSettingsService, LectureRepository lectureRepository, TutorialGroupNotificationRepository tutorialGroupNotificationRepository, TutorialGroupChannelManagementService tutorialGroupChannelManagementService, PrerequisitesApi prerequisitesApi, CompetencyRelationApi competencyRelationApi, PostRepository postRepository, AnswerPostRepository answerPostRepository, - BuildJobRepository buildJobRepository, FaqRepository faqRepository) { + BuildJobRepository buildJobRepository, FaqRepository faqRepository, LLMTokenUsageTraceRepository llmTokenUsageTraceRepository) { this.courseRepository = courseRepository; this.exerciseService = exerciseService; this.exerciseDeletionService = exerciseDeletionService; @@ -271,6 +274,7 @@ public CourseService(CourseRepository courseRepository, ExerciseService exercise this.postRepository = postRepository; this.answerPostRepository = answerPostRepository; this.faqRepository = faqRepository; + this.llmTokenUsageTraceRepository = llmTokenUsageTraceRepository; } /** @@ -805,7 +809,7 @@ public CourseManagementDetailViewDTO getStatsForDetailView(Course course, Gradin Set exercises = exerciseRepository.findAllExercisesByCourseId(course.getId()); if (exercises == null || exercises.isEmpty()) { return new CourseManagementDetailViewDTO(numberOfStudentsInCourse, numberOfTeachingAssistantsInCourse, numberOfEditorsInCourse, numberOfInstructorsInCourse, 0.0, 0L, - 0L, 0.0, 0L, 0L, 0.0, 0L, 0L, 0.0, 0.0, 0.0, List.of()); + 0L, 0.0, 0L, 0L, 0.0, 0L, 0L, 0.0, 0.0, 0.0, List.of(), 0.0); } // For the average score we need to only consider scores which are included completely or as bonus Set includedExercises = exercises.stream().filter(Exercise::isCourseExercise) @@ -854,10 +858,12 @@ public CourseManagementDetailViewDTO getStatsForDetailView(Course course, Gradin var currentAbsoluteAverageScore = roundScoreSpecifiedByCourseSettings((averageScoreForCourse / 100.0) * currentMaxAverageScore, course); var currentPercentageAverageScore = currentMaxAverageScore > 0.0 ? roundScoreSpecifiedByCourseSettings(averageScoreForCourse, course) : 0.0; + double currentTotalLlmCostInEur = llmTokenUsageTraceRepository.calculateTotalLlmCostInEurForCourse(course.getId()); + return new CourseManagementDetailViewDTO(numberOfStudentsInCourse, numberOfTeachingAssistantsInCourse, numberOfEditorsInCourse, numberOfInstructorsInCourse, currentPercentageAssessments, numberOfAssessments, numberOfSubmissions, currentPercentageComplaints, currentAbsoluteComplaints, currentMaxComplaints, currentPercentageMoreFeedbacks, currentAbsoluteMoreFeedbacks, currentMaxMoreFeedbacks, currentPercentageAverageScore, currentAbsoluteAverageScore, - currentMaxAverageScore, activeStudents); + currentMaxAverageScore, activeStudents, currentTotalLlmCostInEur); } private double calculatePercentage(double positive, double total) { diff --git a/src/main/resources/config/liquibase/changelog/20241217150008_changelog.xml b/src/main/resources/config/liquibase/changelog/20241217150008_changelog.xml new file mode 100644 index 000000000000..c2189563b984 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241217150008_changelog.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index 1dbecdb3ea97..c25b722955e3 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -42,6 +42,7 @@ + diff --git a/src/main/webapp/app/course/manage/course-management-detail-view-dto.model.ts b/src/main/webapp/app/course/manage/course-management-detail-view-dto.model.ts index e14935d43ee0..e262f717a7de 100644 --- a/src/main/webapp/app/course/manage/course-management-detail-view-dto.model.ts +++ b/src/main/webapp/app/course/manage/course-management-detail-view-dto.model.ts @@ -26,5 +26,8 @@ export class CourseManagementDetailViewDto { activeStudents?: number[]; + // LLM Stats + currentTotalLlmCostInEur: number; + constructor() {} } diff --git a/src/main/webapp/app/course/manage/detail/course-detail-doughnut-chart.component.html b/src/main/webapp/app/course/manage/detail/course-detail-doughnut-chart.component.html index fa11b3dc4448..936bf068fa84 100644 --- a/src/main/webapp/app/course/manage/detail/course-detail-doughnut-chart.component.html +++ b/src/main/webapp/app/course/manage/detail/course-detail-doughnut-chart.component.html @@ -10,12 +10,17 @@
{{ 'artemisApp.course.detail.' + doughnutChartTitle + 'T
@if (receivedStats) { -
{{ currentPercentage }}%
- } - @if (receivedStats) { -
{{ currentAbsolute }} / {{ currentMax }}
- } - @if (!receivedStats) { + @if (currentPercentage != undefined) { +
{{ currentPercentage }}%
+ } @else if (showText) { +
{{ showText }}
+ } + @if (!showText) { +
{{ currentAbsolute }} / {{ currentMax }}
+ } @else if (currentPercentage != undefined) { +
{{ showText }}
+ } + } @else {
diff --git a/src/main/webapp/app/course/manage/detail/course-detail-doughnut-chart.component.ts b/src/main/webapp/app/course/manage/detail/course-detail-doughnut-chart.component.ts index e83532a70fbb..6e842fdd4309 100644 --- a/src/main/webapp/app/course/manage/detail/course-detail-doughnut-chart.component.ts +++ b/src/main/webapp/app/course/manage/detail/course-detail-doughnut-chart.component.ts @@ -17,15 +17,16 @@ const PIE_CHART_NA_FALLBACK_VALUE = [0, 0, 1]; }) export class CourseDetailDoughnutChartComponent implements OnChanges, OnInit { @Input() contentType: DoughnutChartType; - @Input() currentPercentage: number | undefined; - @Input() currentAbsolute: number | undefined; - @Input() currentMax: number | undefined; + @Input() currentPercentage?: number; + @Input() currentAbsolute?: number; + @Input() currentMax?: number; @Input() course: Course; + @Input() showText?: string; receivedStats = false; doughnutChartTitle: string; stats: number[]; - titleLink: string | undefined; + titleLink?: string; // Icons faSpinner = faSpinner; @@ -49,7 +50,7 @@ export class CourseDetailDoughnutChartComponent implements OnChanges, OnInit { ngOnChanges(): void { // [0, 0, 0] will lead to the chart not being displayed, // assigning [0, 0, 1] (PIE_CHART_NA_FALLBACK_VALUE) works around this issue and displays 0 %, 0 / 0 with a grey circle - if (this.currentAbsolute == undefined && !this.receivedStats) { + if (this.currentAbsolute == undefined && !this.receivedStats && !this.showText) { this.updatePieChartData(PIE_CHART_NA_FALLBACK_VALUE); } else { this.receivedStats = true; @@ -84,6 +85,10 @@ export class CourseDetailDoughnutChartComponent implements OnChanges, OnInit { } this.ngxData[0].name = 'Average score'; break; + case DoughnutChartType.CURRENT_LLM_COST: + this.doughnutChartTitle = 'currentTotalLLMCost'; + this.titleLink = undefined; + break; default: this.doughnutChartTitle = ''; this.titleLink = undefined; diff --git a/src/main/webapp/app/course/manage/detail/course-detail.component.html b/src/main/webapp/app/course/manage/detail/course-detail.component.html index edf03827380e..f34e808d1c5a 100644 --- a/src/main/webapp/app/course/manage/detail/course-detail.component.html +++ b/src/main/webapp/app/course/manage/detail/course-detail.component.html @@ -40,6 +40,15 @@ [currentAbsolute]="courseDTO?.currentAbsoluteAverageScore" [currentMax]="courseDTO?.currentMaxAverageScore" /> + @if (this.irisChatEnabled || this.isAthenaEnabled) { + + }
diff --git a/src/main/webapp/app/course/manage/detail/course-detail.component.ts b/src/main/webapp/app/course/manage/detail/course-detail.component.ts index d61213a17626..c6a3e11211ba 100644 --- a/src/main/webapp/app/course/manage/detail/course-detail.component.ts +++ b/src/main/webapp/app/course/manage/detail/course-detail.component.ts @@ -28,6 +28,7 @@ export enum DoughnutChartType { AVERAGE_EXERCISE_SCORE = 'AVERAGE_EXERCISE_SCORE', PARTICIPATIONS = 'PARTICIPATIONS', QUESTIONS = 'QUESTIONS', + CURRENT_LLM_COST = 'LLM_COST', } @Component({ diff --git a/src/main/webapp/i18n/de/course.json b/src/main/webapp/i18n/de/course.json index cac45d19e481..96f5cb90555c 100644 --- a/src/main/webapp/i18n/de/course.json +++ b/src/main/webapp/i18n/de/course.json @@ -259,7 +259,8 @@ "assessmentsTitle": "Insgesamte Bewertungen", "complaintsTitle": "Insgesamte Beschwerden", "moreFeedbackTitle": "Insgesamte Feedbackanfragen", - "averageStudentScoreTitle": "Durchschnittliche Punktzahl der Studierenden" + "averageStudentScoreTitle": "Durchschnittliche Punktzahl der Studierenden", + "currentTotalLLMCostTitle": "Insgesamte LLM Kosten" }, "activeStudents": "Aktive Studierende: {{ students }}", "notStartedYet": "Der Kurs hat noch nicht offiziell begonnen. Offizieller Kursstart:", diff --git a/src/main/webapp/i18n/en/course.json b/src/main/webapp/i18n/en/course.json index c7751e664577..44551fa61c4e 100644 --- a/src/main/webapp/i18n/en/course.json +++ b/src/main/webapp/i18n/en/course.json @@ -259,7 +259,8 @@ "assessmentsTitle": "Total Assessments", "complaintsTitle": "Total Complaints", "moreFeedbackTitle": "More Feedback Requests", - "averageStudentScoreTitle": "Average Student Score" + "averageStudentScoreTitle": "Average Student Score", + "currentTotalLLMCostTitle": "Total LLM Cost" }, "activeStudents": "Active students: {{ students }}", "notStartedYet": "The course has not officially started yet. Official course start:", diff --git a/src/test/javascript/spec/component/course/detail/course-detail-doughnut-chart.component.spec.ts b/src/test/javascript/spec/component/course/detail/course-detail-doughnut-chart.component.spec.ts index d73a030743dd..1aff55f90890 100644 --- a/src/test/javascript/spec/component/course/detail/course-detail-doughnut-chart.component.spec.ts +++ b/src/test/javascript/spec/component/course/detail/course-detail-doughnut-chart.component.spec.ts @@ -80,6 +80,13 @@ describe('CourseDetailDoughnutChartComponent', () => { expect(component.doughnutChartTitle).toBe('averageStudentScore'); expect(component.titleLink).toBe('scores'); + component.contentType = DoughnutChartType.CURRENT_LLM_COST; + + component.ngOnInit(); + + expect(component.doughnutChartTitle).toBe('currentTotalLLMCost'); + expect(component.titleLink).toBeUndefined(); + component.contentType = DoughnutChartType.AVERAGE_EXERCISE_SCORE; component.ngOnInit(); diff --git a/src/test/javascript/spec/component/course/detail/course-detail.component.spec.ts b/src/test/javascript/spec/component/course/detail/course-detail.component.spec.ts index d6e9b8defa07..9a29a31ed1e5 100644 --- a/src/test/javascript/spec/component/course/detail/course-detail.component.spec.ts +++ b/src/test/javascript/spec/component/course/detail/course-detail.component.spec.ts @@ -57,6 +57,8 @@ describe('Course Management Detail Component', () => { currentAbsoluteAverageScore: 90, currentMaxAverageScore: 100, activeStudents: [4, 10, 14, 35], + // LLM + currentTotalLlmCostInEur: 82.3, }; beforeEach(() => {