Skip to content

Commit

Permalink
Iris: Move rate limit into admin settings (#7378)
Browse files Browse the repository at this point in the history
  • Loading branch information
Hialus authored Oct 15, 2023
1 parent 4b8bd15 commit 6fa10af
Show file tree
Hide file tree
Showing 26 changed files with 164 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ public class IrisSubSettings extends DomainObject {
@Column(name = "preferredModel")
private String preferredModel;

@Nullable
@Column(name = "rateLimit")
private Integer rateLimit;

@Nullable
@Column(name = "rateLimitTimeframeHours")
private Integer rateLimitTimeframeHours;

public boolean isEnabled() {
return enabled;
}
Expand All @@ -57,4 +65,22 @@ public String getPreferredModel() {
public void setPreferredModel(@Nullable String preferredModel) {
this.preferredModel = preferredModel;
}

@Nullable
public Integer getRateLimit() {
return rateLimit;
}

public void setRateLimit(@Nullable Integer rateLimit) {
this.rateLimit = rateLimit;
}

@Nullable
public Integer getRateLimitTimeframeHours() {
return rateLimitTimeframeHours;
}

public void setRateLimitTimeframeHours(@Nullable Integer rateLimitTimeframeHours) {
this.rateLimitTimeframeHours = rateLimitTimeframeHours;
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package de.tum.in.www1.artemis.service.iris;

import java.time.ZonedDateTime;
import java.util.Objects;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;

Expand All @@ -19,14 +19,11 @@ public class IrisRateLimitService {

private final IrisMessageRepository irisMessageRepository;

@Value("${artemis.iris.rate-limit:5}")
private int rateLimit;
private final IrisSettingsService irisSettingsService;

@Value("${artemis.iris.rate-limit-timeframe-hours:24}")
private int rateLimitTimeframeHours;

public IrisRateLimitService(IrisMessageRepository irisMessageRepository) {
public IrisRateLimitService(IrisMessageRepository irisMessageRepository, IrisSettingsService irisSettingsService) {
this.irisMessageRepository = irisMessageRepository;
this.irisSettingsService = irisSettingsService;
}

/**
Expand All @@ -37,11 +34,15 @@ public IrisRateLimitService(IrisMessageRepository irisMessageRepository) {
* @return the rate limit information
*/
public IrisRateLimitInformation getRateLimitInformation(User user) {
var globalSettings = irisSettingsService.getGlobalSettings();
var irisChatSettings = globalSettings.getIrisChatSettings();
var rateLimitTimeframeHours = Objects.requireNonNullElse(irisChatSettings.getRateLimitTimeframeHours(), 0);
var start = ZonedDateTime.now().minusHours(rateLimitTimeframeHours);
var end = ZonedDateTime.now();
var currentMessageCount = irisMessageRepository.countLlmResponsesOfUserWithinTimeframe(user.getId(), start, end);
var rateLimit = Objects.requireNonNullElse(irisChatSettings.getRateLimit(), -1);

return new IrisRateLimitInformation(currentMessageCount, rateLimit);
return new IrisRateLimitInformation(currentMessageCount, rateLimit, rateLimitTimeframeHours);
}

/**
Expand All @@ -65,7 +66,7 @@ public void checkRateLimitElseThrow(User user) {
* @param currentMessageCount the current rate limit
* @param rateLimit the max rate limit
*/
public record IrisRateLimitInformation(int currentMessageCount, int rateLimit) {
public record IrisRateLimitInformation(int currentMessageCount, int rateLimit, int rateLimitTimeframeHours) {

/**
* Checks if the rate limit is exceeded.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,8 @@ private IrisSubSettings copyIrisSubSettings(IrisSubSettings target, IrisSubSetti
if (authCheckService.isAdmin()) {
target.setEnabled(source.isEnabled());
target.setPreferredModel(source.getPreferredModel());
target.setRateLimit(source.getRateLimit());
target.setRateLimitTimeframeHours(source.getRateLimitTimeframeHours());
}
if (!Objects.equals(source.getTemplate(), target.getTemplate())) {
target.setTemplate(source.getTemplate());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,9 @@ public ResponseEntity<IrisHealthDTO> isIrisActive(@PathVariable Long sessionId)

var rateLimitInfo = irisRateLimitService.getRateLimitInformation(user);

return ResponseEntity.ok(new IrisHealthDTO(specificModelStatus, rateLimitInfo.currentMessageCount(), rateLimitInfo.rateLimit()));
return ResponseEntity.ok(new IrisHealthDTO(specificModelStatus, rateLimitInfo));
}

public record IrisHealthDTO(boolean active, int currentMessageCount, int rateLimit) {
public record IrisHealthDTO(boolean active, IrisRateLimitService.IrisRateLimitInformation rateLimitInfo) {
}
}
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="20230922222222" author="morrien">
<addColumn tableName="iris_sub_settings">
<column name="rate_limit" type="integer"/>
<column name="rate_limit_timeframe_hours" type="integer"/>
</addColumn>
</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 @@ -61,6 +61,7 @@
<include file="classpath:config/liquibase/changelog/20230713113211_changelog.xml" relativeToChangelogFile="false"/>
<include file="classpath:config/liquibase/changelog/20230907114600_changelog.xml" relativeToChangelogFile="false"/>
<include file="classpath:config/liquibase/changelog/20230907225501_changelog.xml" relativeToChangelogFile="false"/>
<include file="classpath:config/liquibase/changelog/20230922222222_changelog.xml" relativeToChangelogFile="false"/>
<include file="classpath:config/liquibase/changelog/20230920133000_changelog.xml" relativeToChangelogFile="false"/>
<include file="classpath:config/liquibase/changelog/20230927125606_changelog.xml" relativeToChangelogFile="false"/>
<!-- NOTE: please use the format "YYYYMMDDhhmmss_changelog.xml", i.e. year month day hour minutes seconds and not something else! -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ export class IrisSubSettings implements BaseEntity {
enabled = false;
template?: IrisTemplate;
preferredModel?: string;
rateLimit?: number;
rateLimitTimeframeHours?: number;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ <h3 class="header-start">
</h3>
<div>
<div class="button-container">
<span *ngIf="rateLimit >= 0" class="rate-limit" [ngbTooltip]="'artemisApp.exerciseChatbot.rateLimitTooltip' | artemisTranslate"
<span
*ngIf="rateLimit >= 0"
class="rate-limit"
[ngbTooltip]="'artemisApp.exerciseChatbot.rateLimitTooltip' | artemisTranslate: { hours: rateLimitTimeframeHours }"
>{{ currentMessageCount }}/{{ rateLimit }}</span
>
<button *ngIf="isClearChatEnabled()" id="clear-chat-session" (click)="onClearSession(clearConfirmModal)" class="header-icon">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export class ExerciseChatWidgetComponent implements OnInit, OnDestroy, AfterView
shouldShowEmptyMessageError = false;
currentMessageCount: number;
rateLimit: number;
rateLimitTimeframeHours: number;

// User preferences
userAccepted: boolean;
Expand Down Expand Up @@ -179,6 +180,7 @@ export class ExerciseChatWidgetComponent implements OnInit, OnDestroy, AfterView
}
this.currentMessageCount = state.currentMessageCount;
this.rateLimit = state.rateLimit;
this.rateLimitTimeframeHours = state.rateLimitTimeframeHours;
});

// Focus on message textarea
Expand Down Expand Up @@ -633,7 +635,9 @@ export class ExerciseChatWidgetComponent implements OnInit, OnDestroy, AfterView
if (error.status === 403) {
this.stateStore.dispatch(new ConversationErrorOccurredAction(IrisErrorMessageKey.IRIS_DISABLED));
} else if (error.status === 429) {
this.stateStore.dispatch(new ConversationErrorOccurredAction(IrisErrorMessageKey.RATE_LIMIT_EXCEEDED));
const map = new Map<string, any>();
map.set('hours', this.rateLimitTimeframeHours);
this.stateStore.dispatch(new ConversationErrorOccurredAction(IrisErrorMessageKey.RATE_LIMIT_EXCEEDED, map));
} else {
this.stateStore.dispatch(new ConversationErrorOccurredAction(IrisErrorMessageKey.SEND_MESSAGE_FAILED));
}
Expand Down Expand Up @@ -670,7 +674,7 @@ export class ExerciseChatWidgetComponent implements OnInit, OnDestroy, AfterView

getConvertedErrorMap() {
if (this.error?.paramsMap) {
return Object.fromEntries(Object.entries(this.error.paramsMap as Map<string, any>));
return Object.fromEntries(this.error.paramsMap);
}
return null;
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/webapp/app/iris/heartbeat.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class IrisHeartbeatService implements OnDestroy {
if (!response.body.active) {
this.stateStore.dispatch(new ConversationErrorOccurredAction(IrisErrorMessageKey.IRIS_NOT_AVAILABLE));
}
this.stateStore.dispatch(new RateLimitUpdatedAction(response.body!.currentMessageCount, response.body!.rateLimit));
this.stateStore.dispatch(new RateLimitUpdatedAction(response.body!.rateLimitInfo));
} else {
this.stateStore.dispatch(new ConversationErrorOccurredAction(IrisErrorMessageKey.IRIS_NOT_AVAILABLE));
}
Expand Down
5 changes: 3 additions & 2 deletions src/main/webapp/app/iris/http-session.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import { Injectable } from '@angular/core';
import { HttpClient, HttpResponse } from '@angular/common/http';
import { Observable } from 'rxjs';
import { IrisSession } from 'app/entities/iris/iris-session.model';
import { IrisRateLimitInformation } from 'app/iris/websocket.service';

type EntityResponseType = HttpResponse<IrisSession>;

export class HeartbeatDTO {
active: boolean;
currentMessageCount: number;
rateLimit: number;
rateLimitInfo: IrisRateLimitInformation;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ <h3 jhiTranslate="artemisApp.iris.settings.subSettings.chatSettings">Chat Settin
[models]="irisModels ?? []"
[templateOptional]="settingType !== GLOBAL"
[modelOptional]="settingType !== GLOBAL"
[rateLimitSettable]="settingType === GLOBAL"
></jhi-iris-sub-settings-update>
</div>
<hr class="hr" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,26 @@
</div>
</div>

<div *ngIf="rateLimitSettable">
<label class="form-check-label" for="rateLimit" jhiTranslate="artemisApp.iris.settings.subSettings.rateLimit">Rate Limit</label>
<jhi-help-icon [text]="'artemisApp.iris.settings.subSettings.rateLimitTooltip'"></jhi-help-icon>
<input id="rateLimit" name="rateLimit" class="form-control" type="number" [customMin]="-1" [customMax]="1000000" [(ngModel)]="subSettings!.rateLimit" />
</div>

<div *ngIf="rateLimitSettable">
<label class="form-check-label" for="rateLimitTimeframeHours" jhiTranslate="artemisApp.iris.settings.subSettings.rateLimitTimeframeHours">Rate Limit Timeframe (Hours)</label>
<jhi-help-icon [text]="'artemisApp.iris.settings.subSettings.rateLimitTimeframeHoursTooltip'"></jhi-help-icon>
<input
id="rateLimitTimeframeHours"
name="rateLimitTimeframeHours"
class="form-control"
type="number"
[customMin]="0"
[customMax]="1000000"
[(ngModel)]="subSettings!.rateLimitTimeframeHours"
/>
</div>

<div class="mb-3">
<label class="form-label fs-4 fw-bold" for="template-editor" jhiTranslate="artemisApp.iris.settings.subSettings.template.title"> Template </label>
<div *ngIf="templateOptional" class="form-check form-switch">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export class IrisSubSettingsUpdateComponent {
@Input()
templateOptional = false;

@Input()
rateLimitSettable = false;

previousTemplate?: IrisTemplate;

isAdmin: boolean;
Expand Down
7 changes: 3 additions & 4 deletions src/main/webapp/app/iris/state-store.model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { IrisClientMessage, IrisMessage, IrisServerMessage } from 'app/entities/iris/iris-message.model';
import { IrisErrorMessageKey, IrisErrorType, errorMessages } from 'app/entities/iris/iris-errors.model';
import { IrisRateLimitInformation } from 'app/iris/websocket.service';

export enum ActionType {
NUM_NEW_MESSAGES_RESET = 'num-new-messages-reset',
Expand Down Expand Up @@ -93,10 +94,7 @@ export class RateMessageSuccessAction implements MessageStoreAction {
export class RateLimitUpdatedAction implements MessageStoreAction {
readonly type: ActionType;

public constructor(
public readonly currentMessageCount: number,
public readonly rateLimit: number,
) {
public constructor(public readonly rateLimitInfo: IrisRateLimitInformation) {
this.type = ActionType.RATE_LIMIT_UPDATED;
}
}
Expand Down Expand Up @@ -143,5 +141,6 @@ export class MessageStoreState {
public serverResponseTimeout: ReturnType<typeof setTimeout> | null,
public currentMessageCount: number,
public rateLimit: number,
public rateLimitTimeframeHours: number,
) {}
}
16 changes: 13 additions & 3 deletions src/main/webapp/app/iris/state-store.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export class IrisStateStore implements OnDestroy {
serverResponseTimeout: null,
currentMessageCount: -1,
rateLimit: -1,
rateLimitTimeframeHours: -1,
};

private readonly action = new Subject<ResolvableAction>();
Expand Down Expand Up @@ -157,6 +158,7 @@ export class IrisStateStore implements OnDestroy {
serverResponseTimeout: null,
currentMessageCount: state.currentMessageCount,
rateLimit: state.rateLimit,
rateLimitTimeframeHours: state.rateLimitTimeframeHours,
};
}
if (isConversationErrorOccurredAction(action)) {
Expand Down Expand Up @@ -227,11 +229,19 @@ export class IrisStateStore implements OnDestroy {
}
if (isRateLimitUpdatedAction(action)) {
const castedAction = action as RateLimitUpdatedAction;
const rateLimitInfo = castedAction.rateLimitInfo;
let errorMessage: IrisErrorType | null = null;
if (rateLimitInfo.rateLimit >= 0 && rateLimitInfo.currentMessageCount >= rateLimitInfo.rateLimit) {
errorMessage = errorMessages[IrisErrorMessageKey.RATE_LIMIT_EXCEEDED];
errorMessage.paramsMap = new Map<string, any>();
errorMessage.paramsMap.set('hours', rateLimitInfo.rateLimitTimeframeHours);
}
return {
...state,
error: castedAction.rateLimit >= 0 && castedAction.currentMessageCount >= castedAction.rateLimit ? errorMessages[IrisErrorMessageKey.RATE_LIMIT_EXCEEDED] : null,
currentMessageCount: castedAction.currentMessageCount,
rateLimit: castedAction.rateLimit,
error: errorMessage,
currentMessageCount: rateLimitInfo.currentMessageCount,
rateLimit: rateLimitInfo.rateLimit,
rateLimitTimeframeHours: rateLimitInfo.rateLimitTimeframeHours,
};
}

Expand Down
11 changes: 9 additions & 2 deletions src/main/webapp/app/iris/websocket.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,16 @@ export enum IrisWebsocketMessageType {
ERROR = 'ERROR',
}

class IrisRateLimitInformation {
export class IrisRateLimitInformation {
currentMessageCount: number;
rateLimit: number;
rateLimitTimeframeHours: number;

constructor(currentMessageCount: number, rateLimit: number, rateLimitTimeframeHours: number) {
this.currentMessageCount = currentMessageCount;
this.rateLimit = rateLimit;
this.rateLimitTimeframeHours = rateLimitTimeframeHours;
}
}

/**
Expand Down Expand Up @@ -95,7 +102,7 @@ export class IrisWebsocketService implements OnDestroy {
this.jhiWebsocketService.subscribe(this.subscriptionChannel);
this.jhiWebsocketService.receive(this.subscriptionChannel).subscribe((websocketResponse: IrisWebsocketDTO) => {
if (websocketResponse.rateLimitInfo) {
this.stateStore.dispatch(new RateLimitUpdatedAction(websocketResponse.rateLimitInfo.currentMessageCount, websocketResponse.rateLimitInfo.rateLimit));
this.stateStore.dispatch(new RateLimitUpdatedAction(websocketResponse.rateLimitInfo));
}

if (websocketResponse.type === IrisWebsocketMessageType.ERROR) {
Expand Down
4 changes: 2 additions & 2 deletions src/main/webapp/i18n/de/exerciseChatbot.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@
"parseResponse": "Ein Fehler ist beim Parsen der Antwort von Iris aufgetreten. Ursache: {{ cause }}",
"technicalError": "Es ist ein technischer Fehler aufgetreten. Bitte wende dich an Artemis Administratoren sollte der Fehler weiterhin zu sehen sein.",
"irisNotAvailable": "Iris antwortet nicht. Bitte versuche es später!",
"rateLimitExceeded": "Du hast die maximale Anzahl von Nachrichten, die du in einem 24-Stunden-Zeitfenster an Iris senden kannst, erreicht. Bitte versuche es später erneut!"
"rateLimitExceeded": "Du hast die maximale Anzahl von Nachrichten, die du in einem {{ hours }}-Stunden-Zeitfenster an Iris senden kannst, erreicht. Bitte versuche es später erneut!"
},
"firstMessage": "Hallo, ich bin Iris! Ich kann dir bei deiner Programmieraufgabe helfen. Du kannst <a href='/about-iris' target='_blank'>hier</a> mehr über mich erfahren.",
"rateLimitTooltip": "Dies ist die maximale Anzahl von Nachrichten, die du in einem 24-Stunden-Zeitfenster an Iris senden kannst."
"rateLimitTooltip": "Dies ist die maximale Anzahl von Nachrichten, die du in einem {{ hours }}-Stunden-Zeitfenster an Iris senden kannst."
}
}
}
2 changes: 1 addition & 1 deletion src/main/webapp/i18n/de/iris.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"preferredModel": "Präferiertes Modell",
"inheritModel": "Vererbe Modell",
"rateLimit": "Rate Limit",
"rateLimitTooltip": "Die maximale Anzahl an Antworten, die ein Benutzer vom LLM in einem bestimmten Zeitraum erhalten kann. Der Zeitraum beträgt derzeit 3h.",
"rateLimitTooltip": "Die maximale Anzahl an Antworten, die ein Benutzer vom LLM in einem bestimmten Zeitraum erhalten kann.",
"template": {
"title": "Template",
"inherit": "Inherit Template"
Expand Down
4 changes: 2 additions & 2 deletions src/main/webapp/i18n/en/exerciseChatbot.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@
"parseResponse": "An error occurred while parsing the response from Iris. Cause: {{ cause }}",
"technicalError": "There has been a technical error. Please contact Artemis Administrators if this error message persists.",
"irisNotAvailable": "Iris is not available. Please try again later!",
"rateLimitExceeded": "You have reached the maximum number of messages you can send to Iris in a 24 hour window. Please try again later!"
"rateLimitExceeded": "You have reached the maximum number of messages you can send to Iris in a {{ hours }} hour window. Please try again later!"
},
"firstMessage": "Hi, I'm Iris! I can help you with your programming exercise. You can learn more about me <a href='/about-iris' target='_blank'>here</a>.",
"rateLimitTooltip": "This is the maximum number of messages you can send to Iris in a 24 hour window."
"rateLimitTooltip": "This is the maximum number of messages you can send to Iris in a {{ hours }} hour window."
}
}
}
4 changes: 3 additions & 1 deletion src/main/webapp/i18n/en/iris.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
"preferredModel": "Preferred Model",
"inheritModel": "Inherit Model",
"rateLimit": "Rate Limit",
"rateLimitTooltip": "The maximum number of answers a user can receive from the LLM in a given time period. The time period is currently 3h.",
"rateLimitTooltip": "The maximum number of answers a user can receive from the LLM in a given time period.",
"rateLimitTimeframeHours": "Rate Limit Timeframe (Hours)",
"rateLimitTimeframeHoursTooltip": "The time period in which the rate limit applies in hours.",
"template": {
"title": "Template",
"inherit": "Inherit Template"
Expand Down
Loading

0 comments on commit 6fa10af

Please sign in to comment.