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

Athena: Add LLM token usage tracking #9554

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package de.tum.cit.aet.artemis.athena.dto;

import java.util.List;

import com.fasterxml.jackson.annotation.JsonInclude;

import de.tum.cit.aet.artemis.core.domain.LLMRequest;

/**
* DTO representing the meta information in the Athena response.
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public record ResponseMetaDTO(TotalUsage totalUsage, List<LLMRequest> llmRequests) {

public record TotalUsage(Integer numInputTokens, Integer numOutputTokens, Integer numTotalTokens, Float cost) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,18 @@
import de.tum.cit.aet.artemis.athena.dto.ExerciseBaseDTO;
import de.tum.cit.aet.artemis.athena.dto.ModelingFeedbackDTO;
import de.tum.cit.aet.artemis.athena.dto.ProgrammingFeedbackDTO;
import de.tum.cit.aet.artemis.athena.dto.ResponseMetaDTO;
import de.tum.cit.aet.artemis.athena.dto.SubmissionBaseDTO;
import de.tum.cit.aet.artemis.athena.dto.TextFeedbackDTO;
import de.tum.cit.aet.artemis.core.domain.LLMRequest;
import de.tum.cit.aet.artemis.core.domain.LLMServiceType;
import de.tum.cit.aet.artemis.core.domain.User;
import de.tum.cit.aet.artemis.core.exception.ConflictException;
import de.tum.cit.aet.artemis.core.exception.NetworkingException;
import de.tum.cit.aet.artemis.core.service.LLMTokenUsageService;
import de.tum.cit.aet.artemis.exercise.domain.Exercise;
import de.tum.cit.aet.artemis.exercise.domain.Submission;
import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation;
import de.tum.cit.aet.artemis.modeling.domain.ModelingExercise;
import de.tum.cit.aet.artemis.modeling.domain.ModelingSubmission;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise;
Expand Down Expand Up @@ -48,36 +56,40 @@ public class AthenaFeedbackSuggestionsService {

private final AthenaDTOConverterService athenaDTOConverterService;

private final LLMTokenUsageService llmTokenUsageService;

/**
* Create a new AthenaFeedbackSuggestionsService to receive feedback suggestions from the Athena service.
*
* @param athenaRestTemplate REST template used for the communication with Athena
* @param athenaModuleService Athena module serviced used to determine the urls for different modules
* @param athenaDTOConverterService Service to convert exr
* @param athenaDTOConverterService Service to convert exrcises and submissions to DTOs
* @param llmTokenUsageService Service to store the usage of LLM tokens
*/
public AthenaFeedbackSuggestionsService(@Qualifier("athenaRestTemplate") RestTemplate athenaRestTemplate, AthenaModuleService athenaModuleService,
AthenaDTOConverterService athenaDTOConverterService) {
AthenaDTOConverterService athenaDTOConverterService, LLMTokenUsageService llmTokenUsageService) {
textAthenaConnector = new AthenaConnector<>(athenaRestTemplate, ResponseDTOText.class);
programmingAthenaConnector = new AthenaConnector<>(athenaRestTemplate, ResponseDTOProgramming.class);
modelingAthenaConnector = new AthenaConnector<>(athenaRestTemplate, ResponseDTOModeling.class);
this.athenaDTOConverterService = athenaDTOConverterService;
this.athenaModuleService = athenaModuleService;
this.llmTokenUsageService = llmTokenUsageService;
}

@JsonInclude(JsonInclude.Include.NON_EMPTY)
private record RequestDTO(ExerciseBaseDTO exercise, SubmissionBaseDTO submission, boolean isGraded) {
}

@JsonInclude(JsonInclude.Include.NON_EMPTY)
private record ResponseDTOText(List<TextFeedbackDTO> data) {
private record ResponseDTOText(List<TextFeedbackDTO> data, ResponseMetaDTO meta) {
}

@JsonInclude(JsonInclude.Include.NON_EMPTY)
private record ResponseDTOProgramming(List<ProgrammingFeedbackDTO> data) {
private record ResponseDTOProgramming(List<ProgrammingFeedbackDTO> data, ResponseMetaDTO meta) {
}

@JsonInclude(JsonInclude.Include.NON_EMPTY)
private record ResponseDTOModeling(List<ModelingFeedbackDTO> data) {
private record ResponseDTOModeling(List<ModelingFeedbackDTO> data, ResponseMetaDTO meta) {
}

/**
Expand All @@ -100,6 +112,7 @@ public List<TextFeedbackDTO> getTextFeedbackSuggestions(TextExercise exercise, T
final RequestDTO request = new RequestDTO(athenaDTOConverterService.ofExercise(exercise), athenaDTOConverterService.ofSubmission(exercise.getId(), submission), isGraded);
ResponseDTOText response = textAthenaConnector.invokeWithRetry(athenaModuleService.getAthenaModuleUrl(exercise) + "/feedback_suggestions", request, 0);
log.info("Athena responded to '{}' feedback suggestions request: {}", isGraded ? "Graded" : "Non Graded", response.data);
storeTokenUsage(exercise, submission, response.meta, !isGraded);
return response.data.stream().toList();
}

Expand All @@ -117,6 +130,7 @@ public List<ProgrammingFeedbackDTO> getProgrammingFeedbackSuggestions(Programmin
final RequestDTO request = new RequestDTO(athenaDTOConverterService.ofExercise(exercise), athenaDTOConverterService.ofSubmission(exercise.getId(), submission), isGraded);
ResponseDTOProgramming response = programmingAthenaConnector.invokeWithRetry(athenaModuleService.getAthenaModuleUrl(exercise) + "/feedback_suggestions", request, 0);
log.info("Athena responded to '{}' feedback suggestions request: {}", isGraded ? "Graded" : "Non Graded", response.data);
storeTokenUsage(exercise, submission, response.meta, !isGraded);
return response.data.stream().toList();
}

Expand All @@ -139,6 +153,30 @@ public List<ModelingFeedbackDTO> getModelingFeedbackSuggestions(ModelingExercise
final RequestDTO request = new RequestDTO(athenaDTOConverterService.ofExercise(exercise), athenaDTOConverterService.ofSubmission(exercise.getId(), submission), isGraded);
ResponseDTOModeling response = modelingAthenaConnector.invokeWithRetry(athenaModuleService.getAthenaModuleUrl(exercise) + "/feedback_suggestions", request, 0);
log.info("Athena responded to '{}' feedback suggestions request: {}", isGraded ? "Graded" : "Non Graded", response.data);
storeTokenUsage(exercise, submission, response.meta, !isGraded);
return response.data;
}

/**
* Store the usage of LLM tokens for a given submission
*
* @param exercise the exercise the submission belongs to
* @param submission the submission for which the tokens were used
* @param meta the meta information of the response from Athena
* @param isPreliminaryFeedback whether the feedback is preliminary or not
*/
private void storeTokenUsage(Exercise exercise, Submission submission, ResponseMetaDTO meta, Boolean isPreliminaryFeedback) {
if (meta == null) {
return;
}
Long courseId = exercise.getCourseViaExerciseGroupOrCourseMember().getId();
Long userId = ((StudentParticipation) submission.getParticipation()).getStudent().map(User::getId).orElse(null);
List<LLMRequest> llmRequests = meta.llmRequests();
if (llmRequests == null) {
return;
}

llmTokenUsageService.saveLLMTokenUsage(llmRequests, LLMServiceType.ATHENA,
(llmTokenUsageBuilder -> llmTokenUsageBuilder.withCourse(courseId).withExercise(exercise.getId()).withUser(userId)));
}
}
Loading