Skip to content
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

General: Display total LLM cost in course detail view #10019

Merged
merged 8 commits into from
Dec 22, 2024
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);
wasnertobias marked this conversation as resolved.
Show resolved Hide resolved
}
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);
wasnertobias marked this conversation as resolved.
Show resolved Hide resolved
}
// 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();

wasnertobias marked this conversation as resolved.
Show resolved Hide resolved
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,
wasnertobias marked this conversation as resolved.
Show resolved Hide resolved
};

beforeEach(() => {
Expand Down
Loading