Skip to content

Commit

Permalink
General: Display total LLM cost in course detail view (#10019)
Browse files Browse the repository at this point in the history
  • Loading branch information
wasnertobias authored Dec 22, 2024
1 parent f30ec88 commit dd94f37
Show file tree
Hide file tree
Showing 14 changed files with 80 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<Integer> activeStudents) {
Double currentPercentageAverageScore, Double currentAbsoluteAverageScore, Double currentMaxAverageScore, List<Integer> activeStudents, Double currentTotalLlmCostInEur) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -11,4 +13,13 @@
@Profile(PROFILE_CORE)
@Repository
public interface LLMTokenUsageTraceRepository extends ArtemisJpaRepository<LLMTokenUsageTrace, Long> {

@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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -227,7 +230,7 @@ public CourseService(CourseRepository courseRepository, ExerciseService exercise
LearningPathApi learningPathApi, Optional<IrisSettingsService> 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;
Expand Down Expand Up @@ -271,6 +274,7 @@ public CourseService(CourseRepository courseRepository, ExerciseService exercise
this.postRepository = postRepository;
this.answerPostRepository = answerPostRepository;
this.faqRepository = faqRepository;
this.llmTokenUsageTraceRepository = llmTokenUsageTraceRepository;
}

/**
Expand Down Expand Up @@ -805,7 +809,7 @@ public CourseManagementDetailViewDTO getStatsForDetailView(Course course, Gradin
Set<Exercise> 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<Exercise> includedExercises = exercises.stream().filter(Exercise::isCourseExercise)
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>

<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet id="20241217150008" author="wasnertobias">
<createIndex indexName="idx_llm_token_usage_trace_course_id" tableName="llm_token_usage_trace">
<column name="course_id"/>
</createIndex>
</changeSet>
</databaseChangeLog>
1 change: 1 addition & 0 deletions src/main/resources/config/liquibase/master.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
<include file="classpath:config/liquibase/changelog/20241114122713_changelog.xml" relativeToChangelogFile="false"/>
<include file="classpath:config/liquibase/changelog/20241119191919_changelog.xml" relativeToChangelogFile="false"/>
<include file="classpath:config/liquibase/changelog/20241125000900_changelog.xml" relativeToChangelogFile="false"/>
<include file="classpath:config/liquibase/changelog/20241217150008_changelog.xml" relativeToChangelogFile="false"/>

<!-- NOTE: please use the format "YYYYMMDDhhmmss_changelog.xml", i.e. year month day hour minutes seconds and not something else! -->
<!-- we should also stay in a chronological order! -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,8 @@ export class CourseManagementDetailViewDto {

activeStudents?: number[];

// LLM Stats
currentTotalLlmCostInEur: number;

constructor() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@ <h5 class="text-center">{{ 'artemisApp.course.detail.' + doughnutChartTitle + 'T
<div class="doughnut-chart-container" [ngClass]="titleLink ? 'clickable' : ''" (click)="openCorrespondingPage()">
<div class="doughnut-chart-text">
@if (receivedStats) {
<div class="h4">{{ currentPercentage }}%</div>
}
@if (receivedStats) {
<div class="h6">{{ currentAbsolute }} / {{ currentMax }}</div>
}
@if (!receivedStats) {
@if (currentPercentage != undefined) {
<div class="h4">{{ currentPercentage }}%</div>
} @else if (showText) {
<div class="h4">{{ showText }}</div>
}
@if (!showText) {
<div class="h6">{{ currentAbsolute }} / {{ currentMax }}</div>
} @else if (currentPercentage != undefined) {
<div class="h6">{{ showText }}</div>
}
} @else {
<div class="h3">
<fa-icon [icon]="faSpinner" animation="spin" />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@
[currentAbsolute]="courseDTO?.currentAbsoluteAverageScore"
[currentMax]="courseDTO?.currentMaxAverageScore"
/>
@if (this.irisChatEnabled || this.isAthenaEnabled) {
<jhi-course-detail-doughnut-chart
class="doughnut-container col-sm-6 my-3"
[course]="course"
[contentType]="DoughnutChartType.CURRENT_LLM_COST"
[showText]="courseDTO?.currentTotalLlmCostInEur + ' €'"
[currentMax]="0"
/>
}
</div>
</div>
<jhi-course-detail-line-chart class="col-xxl-8" [course]="course" [numberOfStudentsInCourse]="course.numberOfStudents!" [initialStats]="activeStudents" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export enum DoughnutChartType {
AVERAGE_EXERCISE_SCORE = 'AVERAGE_EXERCISE_SCORE',
PARTICIPATIONS = 'PARTICIPATIONS',
QUESTIONS = 'QUESTIONS',
CURRENT_LLM_COST = 'LLM_COST',
}

@Component({
Expand Down
3 changes: 2 additions & 1 deletion src/main/webapp/i18n/de/course.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand Down
3 changes: 2 additions & 1 deletion src/main/webapp/i18n/en/course.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ describe('Course Management Detail Component', () => {
currentAbsoluteAverageScore: 90,
currentMaxAverageScore: 100,
activeStudents: [4, 10, 14, 35],
// LLM
currentTotalLlmCostInEur: 82.3,
};

beforeEach(() => {
Expand Down

0 comments on commit dd94f37

Please sign in to comment.