committed by
GitHub
40 changed files with 1738 additions and 41 deletions
@ -0,0 +1,54 @@ |
|||
///
|
|||
/// Copyright © 2016-2025 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Injectable } from '@angular/core'; |
|||
import { HttpClient } from '@angular/common/http'; |
|||
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; |
|||
import { Observable } from 'rxjs'; |
|||
import { AiModel, AiModelWithUserMsg, CheckConnectivityResult } from '@shared/models/ai-model.models'; |
|||
import { PageLink } from '@shared/models/page/page-link'; |
|||
import { PageData } from '@shared/models/page/page-data'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root' |
|||
}) |
|||
export class AiModelService { |
|||
|
|||
constructor( |
|||
private http: HttpClient |
|||
) {} |
|||
|
|||
public saveAiModel(aiModel: AiModel, config?: RequestConfig): Observable<AiModel> { |
|||
return this.http.post<AiModel>('/api/ai/model', aiModel, defaultHttpOptionsFromConfig(config)); |
|||
} |
|||
|
|||
public getAiModels(pageLink: PageLink, config?: RequestConfig): Observable<PageData<AiModel>> { |
|||
return this.http.get<PageData<AiModel>>(`/api/ai/model${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); |
|||
} |
|||
|
|||
public getAiModelById(aiModelId: string, config?: RequestConfig): Observable<AiModel> { |
|||
return this.http.get<AiModel>(`/api/ai/model/${aiModelId}`, defaultHttpOptionsFromConfig(config)); |
|||
} |
|||
|
|||
public deleteAiModel(aiModelId: string, config?: RequestConfig) { |
|||
return this.http.delete(`/api/ai/model/${aiModelId}`, defaultHttpOptionsFromConfig(config)); |
|||
} |
|||
|
|||
public checkConnectivity(aiModelWithUserMsg: AiModelWithUserMsg, config?: RequestConfig): Observable<CheckConnectivityResult> { |
|||
return this.http.post<CheckConnectivityResult>('/api/ai/model/chat', aiModelWithUserMsg, defaultHttpOptionsFromConfig(config)); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,265 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2025 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<mat-toolbar color="primary"> |
|||
<h2>{{ dialogTitle | translate }}</h2> |
|||
<span class="flex-1"></span> |
|||
<div tb-help="aiModels"></div> |
|||
<button mat-icon-button |
|||
(click)="cancel()" |
|||
type="button"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
|||
</mat-progress-bar> |
|||
<div *ngIf="!(isLoading$ | async)"></div> |
|||
<div mat-dialog-content> |
|||
<form [formGroup]="aiModelForms"> |
|||
<section class="tb-form-panel outlined no-border no-padding"> |
|||
<mat-form-field appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-label translate>ai-models.name</mat-label> |
|||
<input matInput required formControlName="name"> |
|||
<mat-hint></mat-hint> |
|||
<mat-error *ngIf="aiModelForms.get('name').hasError('required')"> |
|||
{{ 'ai-models.name-required' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="aiModelForms.get('name').hasError('maxlength')"> |
|||
{{ 'ai-models.name-max-length' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</section> |
|||
<div formGroupName="configuration" class="tb-form-panel no-border no-padding"> |
|||
<section class="tb-form-panel stroked no-padding-bottom"> |
|||
<div class="tb-form-panel-title" translate>ai-models.provider</div> |
|||
<section class="tb-form-panel outlined no-border no-padding"> |
|||
<mat-form-field class="mat-block flex-1" appearance="outline"> |
|||
<mat-label translate>ai-models.ai-provider</mat-label> |
|||
<mat-select required formControlName="provider"> |
|||
<mat-option *ngFor="let provider of providerMap" [value]="provider"> |
|||
{{providerTranslationMap.get(provider) | translate}} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<div formGroupName="providerConfig" class="tb-form-panel no-border no-padding"> |
|||
@if (providerFieldsList.includes('personalAccessToken')) { |
|||
<mat-form-field class="mat-block flex-1" appearance="outline"> |
|||
<mat-label translate>ai-models.personal-access-token</mat-label> |
|||
<input type="password" required matInput formControlName="personalAccessToken"> |
|||
<tb-toggle-password matSuffix></tb-toggle-password> |
|||
<mat-error *ngIf="aiModelForms.get('configuration').get('providerConfig').get('personalAccessToken').hasError('required')"> |
|||
{{ 'ai-models.personal-access-token-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
} |
|||
@if (providerFieldsList.includes('projectId')) { |
|||
<mat-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-label translate>ai-models.project-id</mat-label> |
|||
<input matInput required formControlName="projectId"> |
|||
<mat-error *ngIf="aiModelForms.get('configuration').get('providerConfig').get('projectId').hasError('required')"> |
|||
{{ 'ai-models.project-id-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
} |
|||
@if (providerFieldsList.includes('location')) { |
|||
<mat-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-label translate>ai-models.location</mat-label> |
|||
<input matInput required formControlName="location"> |
|||
<mat-error *ngIf="aiModelForms.get('configuration').get('providerConfig').get('location').hasError('required')"> |
|||
{{ 'ai-models.location-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
} |
|||
@if (providerFieldsList.includes('serviceAccountKey')) { |
|||
<tb-file-input formControlName="serviceAccountKey" |
|||
[existingFileName]="aiModelForms.get('configuration').get('providerConfig').get('fileName').value" |
|||
(fileNameChanged)="aiModelForms.get('configuration').get('providerConfig').get('fileName').setValue($event)" |
|||
required |
|||
requiredAsError |
|||
accept=".json,application/json" |
|||
allowedExtensions="json" |
|||
label="{{'ai-models.service-account-key-file' | translate}}" |
|||
noFileText="ai-models.no-file" |
|||
dropLabel="{{'ai-models.drop-file' | translate}}"> |
|||
</tb-file-input> |
|||
} |
|||
@if (providerFieldsList.includes('endpoint')) { |
|||
<mat-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-label translate>ai-models.endpoint</mat-label> |
|||
<input required matInput formControlName="endpoint"> |
|||
<mat-error *ngIf="aiModelForms.get('configuration').get('providerConfig').get('endpoint').hasError('required')"> |
|||
{{ 'ai-models.endpoint-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
} |
|||
@if (providerFieldsList.includes('serviceVersion')) { |
|||
<mat-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-label translate>ai-models.service-version</mat-label> |
|||
<input matInput formControlName="serviceVersion"> |
|||
</mat-form-field> |
|||
} |
|||
@if (providerFieldsList.includes('apiKey')) { |
|||
<mat-form-field class="mat-block flex-1" appearance="outline"> |
|||
<mat-label translate>ai-models.api-key</mat-label> |
|||
<input type="password" required matInput formControlName="apiKey"> |
|||
<tb-toggle-password matSuffix></tb-toggle-password> |
|||
<mat-error *ngIf="aiModelForms.get('configuration').get('providerConfig').get('apiKey').hasError('required')"> |
|||
{{ 'ai-models.api-key-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
} |
|||
</div> |
|||
</section> |
|||
</section> |
|||
<section class="tb-form-panel stroked"> |
|||
<div class="tb-form-panel-title" translate>ai-models.configuration</div> |
|||
<section class="tb-form-panel no-border no-padding"> |
|||
<section class="tb-form-panel outlined no-border no-padding"> |
|||
<tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)" |
|||
additionalClass="tb-suffix-show-on-hover" |
|||
appearance="outline" |
|||
panelWidth="" |
|||
required |
|||
[label]="(provider === aiProvider.AZURE_OPENAI ? 'ai-models.deployment-name': 'ai-models.model-id') | translate" |
|||
[errorText]="(provider === aiProvider.AZURE_OPENAI ? 'ai-models.deployment-name-required': 'ai-models.model-id-required') | translate" |
|||
formControlName="modelId"> |
|||
</tb-string-autocomplete> |
|||
</section> |
|||
@if (modelFieldsList.includes('temperature')) { |
|||
<div class="tb-form-row space-between"> |
|||
<div tb-hint-tooltip-icon="{{ 'ai-models.temperature-hint' | translate }}"> |
|||
{{ 'ai-models.temperature' | translate }} |
|||
</div> |
|||
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> |
|||
<input matInput formControlName="temperature" |
|||
type="number" min="0" step="0.1" placeholder="{{ 'ai-models.set' | translate }}"> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="'ai-models.temperature-min' | translate" |
|||
*ngIf="aiModelForms.get('configuration').get('temperature').hasError('min')" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
} |
|||
@if (modelFieldsList.includes('topP')) { |
|||
<div class="tb-form-row space-between"> |
|||
<div tb-hint-tooltip-icon="{{ 'ai-models.top-p-hint' | translate }}"> |
|||
{{ 'ai-models.top-p' | translate }} |
|||
</div> |
|||
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> |
|||
<input matInput formControlName="topP" |
|||
type="number" min="0" step="0.1" max="1" placeholder="{{ 'ai-models.set' | translate }}"> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="'ai-models.top-p-min-max' | translate" |
|||
*ngIf="aiModelForms.get('configuration').get('topP').hasError('min') || |
|||
aiModelForms.get('configuration').get('topP').hasError('max')" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
} |
|||
@if (modelFieldsList.includes('topK')) { |
|||
<div class="tb-form-row space-between"> |
|||
<div tb-hint-tooltip-icon="{{ 'ai-models.top-k-hint' | translate }}"> |
|||
{{ 'ai-models.top-k' | translate }} |
|||
</div> |
|||
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> |
|||
<input matInput formControlName="topK" |
|||
type="number" min="0" step="1" placeholder="{{ 'ai-models.set' | translate }}"> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="'ai-models.top-k-min' | translate" |
|||
*ngIf="aiModelForms.get('configuration').get('topK').hasError('min')" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
} |
|||
@if (modelFieldsList.includes('presencePenalty')) { |
|||
<div class="tb-form-row space-between"> |
|||
<div tb-hint-tooltip-icon="{{ 'ai-models.presence-penalty-hint' | translate }}"> |
|||
{{ 'ai-models.presence-penalty' | translate }} |
|||
</div> |
|||
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> |
|||
<input matInput formControlName="presencePenalty" |
|||
type="number" step="1" placeholder="{{ 'ai-models.set' | translate }}"> |
|||
</mat-form-field> |
|||
</div> |
|||
} |
|||
|
|||
@if (modelFieldsList.includes('frequencyPenalty')) { |
|||
<div class="tb-form-row space-between"> |
|||
<div tb-hint-tooltip-icon="{{ 'ai-models.frequency-penalty-hint' | translate }}"> |
|||
{{ 'ai-models.frequency-penalty' | translate }} |
|||
</div> |
|||
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> |
|||
<input matInput formControlName="frequencyPenalty" |
|||
type="number" step="1" placeholder="{{ 'ai-models.set' | translate }}"> |
|||
</mat-form-field> |
|||
</div> |
|||
} |
|||
@if (modelFieldsList.includes('maxOutputTokens')) { |
|||
<div class="tb-form-row space-between"> |
|||
<div tb-hint-tooltip-icon="{{ 'ai-models.max-output-token-hint' | translate }}"> |
|||
{{ 'ai-models.max-output-token' | translate }} |
|||
</div> |
|||
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> |
|||
<input matInput formControlName="maxOutputTokens" |
|||
type="number" min="1" step="1" placeholder="{{ 'ai-models.set' | translate }}"> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="'ai-models.max-output-token-min' | translate" |
|||
*ngIf="aiModelForms.get('configuration').get('maxOutputTokens').hasError('min')" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
} |
|||
</section> |
|||
</section> |
|||
</div> |
|||
</form> |
|||
</div> |
|||
<div mat-dialog-actions> |
|||
<button mat-button color="primary" |
|||
type="button" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="cancel()" cdkFocusInitial> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
<span class="flex-1"></span> |
|||
<button mat-button mat-stroked-button color="primary" |
|||
type="button" (click)="checkConnectivity()" |
|||
[disabled]="(isLoading$ | async) || aiModelForms.invalid"> |
|||
{{ 'ai-models.check-connectivity' | translate }} |
|||
</button> |
|||
<button mat-button mat-raised-button color="primary" |
|||
type="button" (click)="add()" |
|||
[disabled]="(isLoading$ | async) || aiModelForms.invalid || !aiModelForms.dirty"> |
|||
{{ 'action.save' | translate }} |
|||
</button> |
|||
</div> |
|||
@ -0,0 +1,23 @@ |
|||
/** |
|||
* Copyright © 2016-2025 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
|
|||
:host { |
|||
width: 850px; |
|||
height: 100%; |
|||
max-width: 100%; |
|||
max-height: 100vh; |
|||
display: grid; |
|||
} |
|||
@ -0,0 +1,164 @@ |
|||
///
|
|||
/// Copyright © 2016-2025 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, Inject } from '@angular/core'; |
|||
import { DialogComponent } from '@shared/components/dialog.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { Router } from '@angular/router'; |
|||
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; |
|||
import { Observable, of } from 'rxjs'; |
|||
import { StepperOrientation } from '@angular/cdk/stepper'; |
|||
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
|||
import { EntityType } from '@shared/models/entity-type.models'; |
|||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|||
import { |
|||
AiModel, |
|||
AiModelMap, |
|||
AiProvider, |
|||
AiProviderTranslations, |
|||
ModelType, |
|||
ProviderFieldsAllList |
|||
} from '@shared/models/ai-model.models'; |
|||
import { AiModelService } from '@core/http/ai-model.service'; |
|||
import { CheckConnectivityDialogComponent } from '@home/components/ai-model/check-connectivity-dialog.component'; |
|||
import { map } from 'rxjs/operators'; |
|||
|
|||
export interface AIModelDialogData { |
|||
AIModel?: AiModel; |
|||
isAdd?: boolean; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-ai-model-dialog', |
|||
templateUrl: './ai-model-dialog.component.html', |
|||
styleUrls: ['./ai-model-dialog.component.scss'] |
|||
}) |
|||
export class AIModelDialogComponent extends DialogComponent<AIModelDialogComponent, AiModel> { |
|||
|
|||
readonly entityType = EntityType; |
|||
|
|||
selectedIndex = 0; |
|||
|
|||
dialogTitle = 'ai-models.ai-model'; |
|||
|
|||
stepperOrientation: Observable<StepperOrientation>; |
|||
|
|||
aiProvider = AiProvider; |
|||
providerMap: AiProvider[] = Object.keys(AiProvider) as AiProvider[]; |
|||
providerTranslationMap = AiProviderTranslations; |
|||
|
|||
provider: AiProvider = AiProvider.OPENAI; |
|||
|
|||
aiModelForms: FormGroup; |
|||
|
|||
isAdd = false; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
protected dialogRef: MatDialogRef<AIModelDialogComponent, AiModel>, |
|||
@Inject(MAT_DIALOG_DATA) public data: AIModelDialogData, |
|||
private fb: FormBuilder, |
|||
private aiModelService: AiModelService, |
|||
private dialog: MatDialog) { |
|||
super(store, router, dialogRef); |
|||
|
|||
if (this.data.isAdd) { |
|||
this.isAdd = true; |
|||
} |
|||
|
|||
this.provider = this.data.AIModel ? this.data.AIModel.configuration.provider : AiProvider.OPENAI; |
|||
|
|||
this.aiModelForms = this.fb.group({ |
|||
name: [this.data.AIModel ? this.data.AIModel.name : '', [Validators.required, Validators.maxLength(255)]], |
|||
modelType: [ModelType.CHAT], |
|||
configuration: this.fb.group({ |
|||
provider: [this.provider, [Validators.required]], |
|||
providerConfig: this.fb.group({ |
|||
apiKey: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.apiKey : '', [Validators.required]], |
|||
personalAccessToken: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.personalAccessToken : '', [Validators.required]], |
|||
endpoint: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.endpoint : '', [Validators.required]], |
|||
serviceVersion: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.serviceVersion : ''], |
|||
projectId: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.projectId : '', [Validators.required]], |
|||
location: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.location : '', [Validators.required]], |
|||
serviceAccountKey: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.serviceAccountKey : '', [Validators.required]], |
|||
fileName: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.fileName : '', [Validators.required]], |
|||
}), |
|||
modelId: [this.data.AIModel ? this.data.AIModel.configuration?.modelId : '', [Validators.required]], |
|||
temperature: [this.data.AIModel ? this.data.AIModel.configuration?.temperature : null, [Validators.min(0)]], |
|||
topP: [this.data.AIModel ? this.data.AIModel.configuration?.topP : null, [Validators.min(0.1), Validators.max(1)]], |
|||
topK: [this.data.AIModel ? this.data.AIModel.configuration?.topK : null, [Validators.min(0)]], |
|||
frequencyPenalty: [this.data.AIModel ? this.data.AIModel.configuration?.frequencyPenalty : null], |
|||
presencePenalty: [this.data.AIModel ? this.data.AIModel.configuration?.presencePenalty : null], |
|||
maxOutputTokens: [this.data.AIModel ? this.data.AIModel.configuration?.maxOutputTokens : null, [Validators.min(1)]] |
|||
}) |
|||
}); |
|||
|
|||
this.aiModelForms.get('configuration.provider').valueChanges.pipe( |
|||
takeUntilDestroyed() |
|||
).subscribe((provider: AiProvider) => { |
|||
this.provider = provider; |
|||
this.aiModelForms.get('configuration.modelId').reset(''); |
|||
this.aiModelForms.get('configuration.providerConfig').reset({}); |
|||
this.updateValidation(provider); |
|||
}) |
|||
|
|||
this.updateValidation(this.provider); |
|||
} |
|||
|
|||
fetchOptions(searchText: string): Observable<Array<string>> { |
|||
const search = searchText ? searchText?.toLowerCase() : ''; |
|||
return of(this.provider ? AiModelMap.get(this.provider).modelList || [] : []).pipe( |
|||
map(name => name?.filter(option => option.toLowerCase().includes(search))), |
|||
); |
|||
} |
|||
|
|||
private updateValidation(provider: AiProvider) { |
|||
ProviderFieldsAllList.forEach(key => { |
|||
if (AiModelMap.get(provider).providerFieldsList.includes(key)) { |
|||
this.aiModelForms.get('configuration.providerConfig').get(key).enable(); |
|||
} else { |
|||
this.aiModelForms.get('configuration.providerConfig').get(key).disable(); |
|||
} |
|||
}) |
|||
} |
|||
|
|||
get providerFieldsList(): string[] { |
|||
return AiModelMap.get(this.provider).providerFieldsList; |
|||
} |
|||
get modelFieldsList(): string[] { |
|||
return AiModelMap.get(this.provider).modelFieldsList; |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.dialogRef.close(null); |
|||
} |
|||
|
|||
checkConnectivity() { |
|||
return this.dialog.open<CheckConnectivityDialogComponent, AIModelDialogData>(CheckConnectivityDialogComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: { |
|||
AIModel: this.aiModelForms.value |
|||
} |
|||
}).afterClosed(); |
|||
} |
|||
|
|||
add(): void { |
|||
const aiModel = {...this.data.AIModel, ...this.aiModelForms.value} as AiModel; |
|||
this.aiModelService.saveAiModel(aiModel).subscribe(aiModel => this.dialogRef.close(aiModel)); |
|||
} |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2025 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<mat-toolbar class="transparent"> |
|||
<h2 translate>ai-models.check-connectivity</h2> |
|||
<span class="flex-1"></span> |
|||
<button mat-icon-button |
|||
(click)="cancel()" |
|||
type="button"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<div mat-dialog-content> |
|||
<div class="flex h-full flex-1 flex-col items-center justify-center"> |
|||
<mat-progress-spinner color="warn" mode="indeterminate" |
|||
strokeWidth="5" *ngIf="!showCheckSuccess && !checkErrMsg"> |
|||
</mat-progress-spinner> |
|||
<div *ngIf="showCheckSuccess"> |
|||
<div class="flex flex-1 items-center justify-center"> |
|||
<mat-icon class="success connection-icon">check_circle</mat-icon> |
|||
</div> |
|||
<span class="connection-status success">{{ "ai-models.check-connectivity-success" | translate }}</span> |
|||
</div> |
|||
<div *ngIf="checkErrMsg" class="w-full"> |
|||
<div class="flex flex-1 items-center justify-center"> |
|||
<mat-icon class="error connection-icon">cancel</mat-icon> |
|||
</div> |
|||
<div class="connection-status error">{{ "ai-models.check-connectivity-failed" | translate }}</div> |
|||
<tb-json-object-edit class="json-editor" |
|||
readonly |
|||
[ngModel]="checkErrMsg"> |
|||
</tb-json-object-edit> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div mat-dialog-actions class="justify-end"> |
|||
<button mat-button color="primary" |
|||
type="button" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="cancel()" cdkFocusInitial> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
</div> |
|||
@ -0,0 +1,68 @@ |
|||
/** |
|||
* Copyright © 2016-2025 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
|
|||
:host { |
|||
width: 560px; |
|||
height: 100%; |
|||
max-width: 100%; |
|||
max-height: 100vh; |
|||
display: grid; |
|||
|
|||
.transparent { |
|||
background-color: transparent; |
|||
} |
|||
|
|||
.connection-status { |
|||
font-weight: 500; |
|||
letter-spacing: 0.25px; |
|||
text-align: center; |
|||
} |
|||
|
|||
.connection-icon { |
|||
height: 32px; |
|||
font-size: 32px; |
|||
width: 32px; |
|||
margin-bottom: 4px; |
|||
} |
|||
|
|||
.error_msg { |
|||
text-align: center; |
|||
margin-top: 8px; |
|||
font-size: 14px; |
|||
line-height: 130%; |
|||
letter-spacing: 0.25px; |
|||
opacity: 0.9; |
|||
} |
|||
|
|||
.success { |
|||
color: #198038; |
|||
} |
|||
|
|||
.error { |
|||
color: #D12730; |
|||
} |
|||
|
|||
::ng-deep { |
|||
.json-editor { |
|||
.tb-json-object-toolbar { |
|||
display: none; |
|||
} |
|||
.tb-json-panel { |
|||
margin: 0; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,89 @@ |
|||
///
|
|||
/// Copyright © 2016-2025 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, Inject } from '@angular/core'; |
|||
import { DialogComponent } from '@shared/components/dialog.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { Router } from '@angular/router'; |
|||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
|||
import { AiModel, AiModelWithUserMsg, ModelType } from '@shared/models/ai-model.models'; |
|||
import { AiModelService } from '@core/http/ai-model.service'; |
|||
|
|||
export interface AIModelDialogData { |
|||
AIModel?: AiModel; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-check-connectivity-dialog', |
|||
templateUrl: './check-connectivity-dialog.component.html', |
|||
styleUrls: ['./check-connectivity-dialog.component.scss'] |
|||
}) |
|||
export class CheckConnectivityDialogComponent extends DialogComponent<CheckConnectivityDialogComponent> { |
|||
|
|||
showCheckSuccess = false; |
|||
checkErrMsg = ''; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
protected dialogRef: MatDialogRef<CheckConnectivityDialogComponent>, |
|||
@Inject(MAT_DIALOG_DATA) public data: AIModelDialogData, |
|||
private aiModelService: AiModelService) { |
|||
super(store, router, dialogRef); |
|||
|
|||
if (this.data.AIModel) { |
|||
const aiModelWithMsg: AiModelWithUserMsg = { |
|||
userMessage: { |
|||
contents: [ |
|||
{ |
|||
contentType: "TEXT", |
|||
text: "What is the capital of Ukraine?" |
|||
} |
|||
] |
|||
}, |
|||
chatModelConfig: { |
|||
modelType: ModelType.CHAT, |
|||
provider: this.data.AIModel.configuration.provider, |
|||
providerConfig: {...this.data.AIModel.configuration.providerConfig}, |
|||
modelId: this.data.AIModel.configuration.modelId, |
|||
maxRetries: 0, |
|||
timeoutSeconds: 20 |
|||
} |
|||
} |
|||
this.aiModelService.checkConnectivity(aiModelWithMsg, { |
|||
ignoreErrors: true, |
|||
ignoreLoading: true |
|||
}).subscribe({ |
|||
next: (result) => { |
|||
if (result.status === 'SUCCESS') { |
|||
this.showCheckSuccess = true; |
|||
} else { |
|||
try { |
|||
this.checkErrMsg = JSON.parse(result.errorDetails); |
|||
} catch (e) { |
|||
this.checkErrMsg = result.errorDetails; |
|||
} |
|||
} |
|||
}, |
|||
error: err => this.checkErrMsg = err.error.message |
|||
}); |
|||
} |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.dialogRef.close(null); |
|||
} |
|||
} |
|||
@ -0,0 +1,133 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2025 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<section [formGroup]="aiConfigForm" class="flex flex-col gap-4"> |
|||
<section class="tb-form-panel stroked no-padding-bottom"> |
|||
<div class="tb-form-panel-title" tb-hint-tooltip-icon="{{ 'rule-node-config.ai.ai-model-hint' | translate }}"> |
|||
{{ 'rule-node-config.ai.ai-model' | translate }} |
|||
</div> |
|||
<section class="flex flex-col"> |
|||
<tb-entity-autocomplete |
|||
allowCreateNew |
|||
useFullEntityId |
|||
required |
|||
appearance="outline" |
|||
labelText="ai-models.ai-model" |
|||
(entityChanged)="onEntityChange($event)" |
|||
[entityType]="entityType.AI_MODEL" |
|||
(createNew)="createModelAi('modelId')" |
|||
formControlName="modelId"> |
|||
</tb-entity-autocomplete> |
|||
</section> |
|||
</section> |
|||
|
|||
<div class="tb-form-panel stroked no-padding no-gap"> |
|||
<mat-expansion-panel class="tb-settings" expanded> |
|||
<mat-expansion-panel-header style="padding: 16px"> |
|||
<mat-panel-title> |
|||
{{'rule-node-config.ai.prompt-settings' | translate}} |
|||
</mat-panel-title> |
|||
</mat-expansion-panel-header> |
|||
<div class="tb-form-panel no-border no-padding-top" style="margin-top: 0"> |
|||
<tb-example-hint |
|||
[hintText]="'rule-node-config.ai.prompt-settings-hint'" |
|||
[popupHelpLink]="'rulenode/ai_node_prompt_settings'"> |
|||
</tb-example-hint> |
|||
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-label translate>rule-node-config.ai.system-prompt</mat-label> |
|||
<textarea matInput rows="4" formControlName="systemPrompt"></textarea> |
|||
<mat-error *ngIf="aiConfigForm.get('systemPrompt').hasError('maxlength')"> |
|||
{{ 'rule-node-config.ai.system-prompt-max-length' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="aiConfigForm.get('systemPrompt').hasError('pattern')"> |
|||
{{ 'rule-node-config.ai.system-prompt-blank' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-label translate>rule-node-config.ai.user-prompt</mat-label> |
|||
<textarea required matInput rows="4" formControlName="userPrompt"></textarea> |
|||
<mat-error *ngIf="aiConfigForm.get('userPrompt').hasError('required')"> |
|||
{{ 'rule-node-config.ai.user-prompt-required' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="aiConfigForm.get('userPrompt').hasError('maxlength')"> |
|||
{{ 'rule-node-config.ai.user-prompt-max-length' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="aiConfigForm.get('userPrompt').hasError('pattern')"> |
|||
{{ 'rule-node-config.ai.user-prompt-blank' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</div> |
|||
</mat-expansion-panel> |
|||
</div> |
|||
|
|||
<div class="tb-form-panel stroked" formGroupName="responseFormat"> |
|||
<div class="flex flex-row items-center justify-between xs:flex-col xs:items-start xs:gap-3"> |
|||
<div class="tb-form-panel-title" tb-hint-tooltip-icon="{{ 'rule-node-config.ai.response-format-hint' | translate }}"> |
|||
{{ 'rule-node-config.ai.response-format' | translate }} |
|||
</div> |
|||
<tb-toggle-select formControlName="type"> |
|||
<tb-toggle-option [value]="responseFormat.TEXT">{{ 'rule-node-config.ai.response-text' | translate }}</tb-toggle-option> |
|||
<tb-toggle-option [value]="responseFormat.JSON">{{ 'rule-node-config.ai.response-json' | translate }}</tb-toggle-option> |
|||
<tb-toggle-option [value]="responseFormat.JSON_SCHEMA">{{ 'rule-node-config.ai.response-json-schema' | translate }}</tb-toggle-option> |
|||
</tb-toggle-select> |
|||
</div> |
|||
@if (aiConfigForm.get('responseFormat.type').value === responseFormat.JSON_SCHEMA) { |
|||
<tb-json-object-edit |
|||
jsonRequired |
|||
label="{{ 'rule-node-config.ai.response-json-schema' | translate }}" |
|||
formControlName="schema"> |
|||
<button mat-icon-button class="tb-mat-32" |
|||
toolbarSuffixButton |
|||
matTooltip="{{ 'rule-node-config.ai.response-json-schema-hint' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon style="color: #7b7b7b">info</mat-icon> |
|||
</button> |
|||
</tb-json-object-edit> |
|||
} |
|||
</div> |
|||
|
|||
<div class="tb-form-panel stroked no-padding no-gap"> |
|||
<mat-expansion-panel class="tb-settings"> |
|||
<mat-expansion-panel-header style="padding: 16px"> |
|||
<mat-panel-title translate>rule-node-config.ai.advanced-settings</mat-panel-title> |
|||
</mat-expansion-panel-header> |
|||
<div class="tb-form-panel no-border no-padding-top" style="margin-top: 0"> |
|||
<div class="tb-form-row space-between flex-1 columns-xs"> |
|||
<div translate tb-hint-tooltip-icon="{{'rule-node-config.ai.timeout-hint' | translate}}">{{ 'rule-node-config.ai.timeout' | translate }}</div> |
|||
<div class="flex flex-row items-center justify-start gap-2"> |
|||
<tb-time-unit-input |
|||
required |
|||
inlineField |
|||
requiredText="{{ 'rule-node-config.ai.timeout-required' | translate }}" |
|||
minErrorText="{{ 'rule-node-config.ai.timeout-validation' | translate }}" |
|||
maxErrorText="{{ 'rule-node-config.ai.timeout-validation' | translate }}" |
|||
[maxTime]="600" |
|||
[minTime]="1" |
|||
formControlName="timeoutSeconds"> |
|||
</tb-time-unit-input> |
|||
</div> |
|||
</div> |
|||
<div tb-hint-tooltip-icon="{{ 'rule-node-config.ai.force-acknowledgement-hint' | translate }}" |
|||
class="tb-form-row same-padding"> |
|||
<mat-slide-toggle class="mat-slide margin" formControlName="forceAck"> |
|||
{{ 'rule-node-config.ai.force-acknowledgement' | translate }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
</div> |
|||
</mat-expansion-panel> |
|||
</div> |
|||
</section> |
|||
@ -0,0 +1,104 @@ |
|||
///
|
|||
/// Copyright © 2016-2025 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component } from '@angular/core'; |
|||
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
|||
import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; |
|||
import { EntityType } from '@shared/models/entity-type.models'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { AIModelDialogComponent, AIModelDialogData } from '@home/components/ai-model/ai-model-dialog.component'; |
|||
import { AiModel, AiRuleNodeResponseFormatTypeOnlyText, ResponseFormat } from '@shared/models/ai-model.models'; |
|||
import { deepTrim } from '@core/utils'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-external-node-ai-config', |
|||
templateUrl: './ai-config.component.html', |
|||
styleUrls: [] |
|||
}) |
|||
export class AiConfigComponent extends RuleNodeConfigurationComponent { |
|||
|
|||
aiConfigForm: UntypedFormGroup; |
|||
|
|||
entityType = EntityType; |
|||
|
|||
responseFormat = ResponseFormat; |
|||
|
|||
constructor(private fb: UntypedFormBuilder, |
|||
private dialog: MatDialog) { |
|||
super(); |
|||
} |
|||
|
|||
protected configForm(): UntypedFormGroup { |
|||
return this.aiConfigForm; |
|||
} |
|||
|
|||
protected onConfigurationSet(configuration: RuleNodeConfiguration) { |
|||
this.aiConfigForm = this.fb.group({ |
|||
modelId: [configuration?.modelId ?? null, [Validators.required]], |
|||
systemPrompt: [configuration?.systemPrompt ?? '', [Validators.maxLength(10000), Validators.pattern(/.*\S.*/)]], |
|||
userPrompt: [configuration?.userPrompt ?? '', [Validators.required, Validators.maxLength(10000), Validators.pattern(/.*\S.*/)]], |
|||
responseFormat: this.fb.group({ |
|||
type: [configuration?.responseFormat?.type ?? ResponseFormat.JSON, []], |
|||
schema: [configuration?.responseFormat?.schema ?? null, [Validators.required]], |
|||
}), |
|||
timeoutSeconds: [configuration?.timeoutSeconds ?? 60, []], |
|||
forceAck: [configuration?.forceAck ?? true, []] |
|||
}); |
|||
} |
|||
|
|||
protected validatorTriggers(): string[] { |
|||
return ['responseFormat.type']; |
|||
} |
|||
|
|||
protected updateValidators(emitEvent: boolean) { |
|||
if (this.aiConfigForm.get('responseFormat.type').value === ResponseFormat.JSON_SCHEMA) { |
|||
this.aiConfigForm.get('responseFormat.schema').enable({emitEvent: false}); |
|||
} else { |
|||
this.aiConfigForm.get('responseFormat.schema').disable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
protected prepareOutputConfig(configuration: RuleNodeConfiguration): RuleNodeConfiguration { |
|||
return deepTrim(configuration); |
|||
} |
|||
|
|||
onEntityChange($event: AiModel) { |
|||
if ($event) { |
|||
if (AiRuleNodeResponseFormatTypeOnlyText.includes($event.configuration.provider)) { |
|||
this.aiConfigForm.get('responseFormat.type').patchValue(ResponseFormat.TEXT, {emitEvent: false}); |
|||
this.aiConfigForm.get('responseFormat.type').disable({emitEvent: false}); |
|||
} |
|||
} else { |
|||
this.aiConfigForm.get('responseFormat.type').enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
createModelAi(formControl: string) { |
|||
this.dialog.open<AIModelDialogComponent, AIModelDialogData, AiModel>(AIModelDialogComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: { |
|||
isAdd: true |
|||
} |
|||
}).afterClosed() |
|||
.subscribe((model) => { |
|||
if (model) { |
|||
this.aiConfigForm.get(formControl).patchValue(model.id); |
|||
this.aiConfigForm.get(formControl).markAsDirty(); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,49 @@ |
|||
///
|
|||
/// Copyright © 2016-2025 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
|
|||
import { RouterModule, Routes } from '@angular/router'; |
|||
import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; |
|||
import { Authority } from '@shared/models/authority.enum'; |
|||
import { MenuId } from '@core/services/menu.models'; |
|||
import { AiModelsTableConfigResolver } from '@home/pages/ai-model/ai-model-table-config.resolve'; |
|||
import { NgModule } from '@angular/core'; |
|||
|
|||
export const aiModelRoutes: Routes = [ |
|||
{ |
|||
path: 'ai-models', |
|||
component: EntitiesTableComponent, |
|||
data: { |
|||
auth: [Authority.TENANT_ADMIN], |
|||
title: 'ai-models.ai-models', |
|||
breadcrumb: { |
|||
menuId: MenuId.ai_models |
|||
} |
|||
}, |
|||
resolve: { |
|||
entitiesTableConfig: AiModelsTableConfigResolver |
|||
} |
|||
} |
|||
]; |
|||
|
|||
@NgModule({ |
|||
providers: [ |
|||
AiModelsTableConfigResolver |
|||
], |
|||
imports: [RouterModule.forChild(aiModelRoutes)], |
|||
exports: [RouterModule], |
|||
}) |
|||
export class AiModelRoutingModule { } |
|||
@ -0,0 +1,127 @@ |
|||
///
|
|||
/// Copyright © 2016-2025 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Injectable } from '@angular/core'; |
|||
import { |
|||
CellActionDescriptor, |
|||
DateEntityTableColumn, |
|||
EntityTableColumn, |
|||
EntityTableConfig |
|||
} from '@home/models/entity/entities-table-config.models'; |
|||
import { ActivatedRouteSnapshot } from '@angular/router'; |
|||
import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; |
|||
import { Direction } from '@shared/models/page/sort-order'; |
|||
import { DatePipe } from '@angular/common'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { Observable } from 'rxjs'; |
|||
import { AiModel, AiProviderTranslations } from '@shared/models/ai-model.models'; |
|||
import { AiModelService } from '@core/http/ai-model.service'; |
|||
import { AiModelTableHeaderComponent } from '@home/pages/ai-model/ai-model-table-header.component'; |
|||
import { AIModelDialogComponent, AIModelDialogData } from '@home/components/ai-model/ai-model-dialog.component'; |
|||
import { map } from 'rxjs/operators'; |
|||
|
|||
@Injectable() |
|||
export class AiModelsTableConfigResolver { |
|||
|
|||
private readonly config: EntityTableConfig<AiModel> = new EntityTableConfig<AiModel>(); |
|||
|
|||
constructor( |
|||
private datePipe: DatePipe, |
|||
private aiModelService: AiModelService, |
|||
private translate : TranslateService, |
|||
private dialog: MatDialog |
|||
) { |
|||
this.config.selectionEnabled = true; |
|||
this.config.entityType = EntityType.AI_MODEL; |
|||
this.config.addAsTextButton = true; |
|||
this.config.rowPointer = true; |
|||
this.config.detailsPanelEnabled = false; |
|||
this.config.entityTranslations = entityTypeTranslations.get(EntityType.AI_MODEL); |
|||
this.config.entityResources = entityTypeResources.get(EntityType.AI_MODEL); |
|||
|
|||
this.config.headerComponent = AiModelTableHeaderComponent; |
|||
this.config.addDialogStyle = {width: '850px', maxHeight: '100vh'}; |
|||
this.config.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; |
|||
|
|||
this.config.addEntity = () => this.addModel(null, true); |
|||
|
|||
this.config.columns.push( |
|||
new DateEntityTableColumn<AiModel>('createdTime', 'common.created-time', this.datePipe, '170px'), |
|||
new EntityTableColumn<AiModel>('name', 'ai-models.name', '33%'), |
|||
new EntityTableColumn<AiModel>('provider', 'ai-models.ai-provider', '33%', |
|||
entity => this.translate.instant(AiProviderTranslations.get(entity.configuration.provider)) |
|||
), |
|||
new EntityTableColumn<AiModel>('aiModel', 'ai-models.ai-model', '33%', |
|||
entity => entity.configuration.modelId, () => ({}), false |
|||
) |
|||
) |
|||
|
|||
this.config.deleteEntityTitle = model => this.translate.instant('ai-models.delete-model-title', {modelName: model.name}); |
|||
this.config.deleteEntityContent = () => this.translate.instant('ai-models.delete-model-text'); |
|||
this.config.deleteEntitiesTitle = count => this.translate.instant('ai-models.delete-models-title', {count}); |
|||
this.config.deleteEntitiesContent = () => this.translate.instant('ai-models.delete-models-text'); |
|||
|
|||
this.config.deleteEntity = id => this.aiModelService.deleteAiModel(id.id); |
|||
|
|||
this.config.entitiesFetchFunction = pageLink => this.aiModelService.getAiModels(pageLink); |
|||
|
|||
this.config.cellActionDescriptors = this.configureCellActions(); |
|||
|
|||
this.config.handleRowClick = ($event, model) => { |
|||
this.editModel($event, model); |
|||
return true; |
|||
}; |
|||
} |
|||
|
|||
resolve(_route: ActivatedRouteSnapshot): EntityTableConfig<AiModel> { |
|||
return this.config; |
|||
} |
|||
|
|||
private configureCellActions(): Array<CellActionDescriptor<AiModel>> { |
|||
return [ |
|||
{ |
|||
name: this.translate.instant('action.edit'), |
|||
icon: 'edit', |
|||
isEnabled: () => true, |
|||
onAction: ($event, entity) => this.editModel($event, entity) |
|||
} |
|||
]; |
|||
} |
|||
|
|||
private editModel($event, AIModel: AiModel): void { |
|||
$event?.stopPropagation(); |
|||
this.addModel(AIModel, false).subscribe(); |
|||
} |
|||
|
|||
private addModel(AIModel: AiModel, isAdd = false): Observable<AiModel> { |
|||
return this.dialog.open<AIModelDialogComponent, AIModelDialogData, AiModel>(AIModelDialogComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: { |
|||
isAdd, |
|||
AIModel |
|||
} |
|||
}).afterClosed().pipe(map(res => { |
|||
if (res) { |
|||
this.config.updateData(); |
|||
return res; |
|||
} else { |
|||
return null; |
|||
} |
|||
})); |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2025 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<section class="flex items-center"> |
|||
<div tbTruncateWithTooltip translate>ai-models.ai-models</div> |
|||
<div tb-help="aiModels"></div> |
|||
</section> |
|||
@ -0,0 +1,33 @@ |
|||
///
|
|||
/// Copyright © 2016-2025 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component } from '@angular/core'; |
|||
import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { AiModel } from '@shared/models/ai-model.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-ai-model-table-header', |
|||
templateUrl: './ai-model-table-header.component.html', |
|||
styleUrls: [] |
|||
}) |
|||
export class AiModelTableHeaderComponent extends EntityTableHeaderComponent<AiModel> { |
|||
|
|||
constructor(protected store: Store<AppState>) { |
|||
super(store); |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
///
|
|||
/// Copyright © 2016-2025 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { NgModule } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { SharedModule } from '@shared/shared.module'; |
|||
import { AiModelRoutingModule } from '@home/pages/ai-model/ai-model-routing.module'; |
|||
import { AiModelTableHeaderComponent } from '@home/pages/ai-model/ai-model-table-header.component'; |
|||
|
|||
@NgModule({ |
|||
declarations: [ |
|||
AiModelTableHeaderComponent |
|||
], |
|||
imports: [ |
|||
CommonModule, |
|||
SharedModule, |
|||
AiModelRoutingModule |
|||
] |
|||
}) |
|||
export class AiModelModule { } |
|||
@ -0,0 +1,224 @@ |
|||
///
|
|||
/// Copyright © 2016-2025 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { BaseData, ExportableEntity } from '@shared/models/base-data'; |
|||
import { HasTenantId } from '@shared/models/entity.models'; |
|||
import { AiModelId } from '@shared/models/id/ai-model-id'; |
|||
|
|||
export interface AiModel extends Omit<BaseData<AiModelId>, 'label'>, HasTenantId, ExportableEntity<AiModelId> { |
|||
modelType: ModelType; |
|||
configuration: { |
|||
provider: AiProvider |
|||
providerConfig: { |
|||
apiKey?: string; |
|||
personalAccessToken?: string; |
|||
endpoint?: string; |
|||
serviceVersion?: string; |
|||
projectId?: string; |
|||
location?: string; |
|||
serviceAccountKey?: string; |
|||
fileName?: string |
|||
}; |
|||
modelId: string; |
|||
temperature?: number; |
|||
topP?: number; |
|||
topK?: number; |
|||
frequencyPenalty?: number; |
|||
presencePenalty?: number; |
|||
maxOutputTokens?: number; |
|||
} |
|||
} |
|||
|
|||
export enum ModelType { |
|||
CHAT = 'CHAT' |
|||
} |
|||
|
|||
export enum AiProvider { |
|||
OPENAI = 'OPENAI', |
|||
AZURE_OPENAI = 'AZURE_OPENAI', |
|||
GOOGLE_AI_GEMINI = 'GOOGLE_AI_GEMINI', |
|||
GOOGLE_VERTEX_AI_GEMINI = 'GOOGLE_VERTEX_AI_GEMINI', |
|||
MISTRAL_AI = 'MISTRAL_AI', |
|||
ANTHROPIC = 'ANTHROPIC', |
|||
AMAZON_BEDROCK = 'AMAZON_BEDROCK', |
|||
GITHUB_MODELS = 'GITHUB_MODELS' |
|||
} |
|||
|
|||
export const AiProviderTranslations = new Map<AiProvider, string>( |
|||
[ |
|||
[AiProvider.OPENAI , 'ai-models.ai-providers.openai'], |
|||
[AiProvider.AZURE_OPENAI , 'ai-models.ai-providers.azure-openai'], |
|||
[AiProvider.GOOGLE_AI_GEMINI , 'ai-models.ai-providers.google-ai-gemini'], |
|||
[AiProvider.GOOGLE_VERTEX_AI_GEMINI , 'ai-models.ai-providers.google-vertex-ai-gemini'], |
|||
[AiProvider.MISTRAL_AI , 'ai-models.ai-providers.mistral-ai'], |
|||
[AiProvider.ANTHROPIC , 'ai-models.ai-providers.anthropic'], |
|||
[AiProvider.AMAZON_BEDROCK , 'ai-models.ai-providers.amazon-bedrock'], |
|||
[AiProvider.GITHUB_MODELS , 'ai-models.ai-providers.github-models'] |
|||
] |
|||
); |
|||
|
|||
export const ProviderFieldsAllList = [ |
|||
'apiKey', |
|||
'personalAccessToken', |
|||
'projectId', |
|||
'location', |
|||
'serviceAccountKey', |
|||
'fileName', |
|||
'endpoint', |
|||
'serviceVersion' |
|||
]; |
|||
|
|||
export const ModelFieldsAllList = ['temperature', 'topP', 'topK', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens']; |
|||
|
|||
export const AiModelMap = new Map<AiProvider, { modelList: string[], providerFieldsList: string[], modelFieldsList: string[] }>([ |
|||
[ |
|||
AiProvider.OPENAI, |
|||
{ |
|||
modelList: [ |
|||
'o4-mini', |
|||
'o3-pro', |
|||
'o3', |
|||
'o3-mini', |
|||
'o1', |
|||
'gpt-4.1', |
|||
'gpt-4.1-mini', |
|||
'gpt-4.1-nano', |
|||
'gpt-4o', |
|||
'gpt-4o-mini', |
|||
], |
|||
providerFieldsList: ['apiKey'], |
|||
modelFieldsList: ['temperature', 'topP', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens'], |
|||
}, |
|||
], |
|||
[ |
|||
AiProvider.AZURE_OPENAI, |
|||
{ |
|||
modelList: [], |
|||
providerFieldsList: ['apiKey', 'endpoint', 'serviceVersion'], |
|||
modelFieldsList: ['temperature', 'topP', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens'], |
|||
}, |
|||
], |
|||
[ |
|||
AiProvider.GOOGLE_AI_GEMINI, |
|||
{ |
|||
modelList: [ |
|||
'gemini-2.5-pro', |
|||
'gemini-2.5-flash', |
|||
'gemini-2.0-flash', |
|||
'gemini-2.0-flash-lite', |
|||
], |
|||
providerFieldsList: ['apiKey'], |
|||
modelFieldsList: ['temperature', 'topP', 'topK', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens'], |
|||
}, |
|||
], |
|||
[ |
|||
AiProvider.GOOGLE_VERTEX_AI_GEMINI, |
|||
{ |
|||
modelList: [ |
|||
'gemini-2.5-pro', |
|||
'gemini-2.5-flash', |
|||
'gemini-2.0-flash', |
|||
'gemini-2.0-flash-lite', |
|||
], |
|||
providerFieldsList: ['projectId', 'location', 'serviceAccountKey', 'fileName'], |
|||
modelFieldsList: ['temperature', 'topP', 'topK', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens'], |
|||
}, |
|||
], |
|||
[ |
|||
AiProvider.MISTRAL_AI, |
|||
{ |
|||
modelList: [ |
|||
'magistral-medium-latest', |
|||
'magistral-small-latest', |
|||
'mistral-large-latest', |
|||
'mistral-medium-latest', |
|||
'mistral-small-latest', |
|||
'pixtral-large-latest', |
|||
'ministral-8b-latest', |
|||
'ministral-3b-latest', |
|||
'open-mistral-nemo', |
|||
], |
|||
providerFieldsList: ['apiKey'], |
|||
modelFieldsList: ['temperature', 'topP', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens'], |
|||
}, |
|||
], |
|||
[ |
|||
AiProvider.ANTHROPIC, |
|||
{ |
|||
modelList: [ |
|||
'claude-opus-4-0', |
|||
'claude-sonnet-4-0', |
|||
'claude-3-7-sonnet-latest', |
|||
'claude-3-5-sonnet-latest', |
|||
'claude-3-5-haiku-latest', |
|||
], |
|||
providerFieldsList: ['apiKey'], |
|||
modelFieldsList: ['temperature', 'topP', 'topK', 'maxOutputTokens'], |
|||
}, |
|||
], |
|||
[ |
|||
AiProvider.AMAZON_BEDROCK, |
|||
{ |
|||
modelList: [], |
|||
providerFieldsList: ['apiKey'], |
|||
modelFieldsList: ['temperature', 'topP', 'maxOutputTokens'], |
|||
}, |
|||
], |
|||
[ |
|||
AiProvider.GITHUB_MODELS, |
|||
{ |
|||
modelList: [], |
|||
providerFieldsList: ['personalAccessToken'], |
|||
modelFieldsList: ['temperature', 'topP', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens'], |
|||
}, |
|||
], |
|||
]); |
|||
|
|||
export const AiRuleNodeResponseFormatTypeOnlyText: AiProvider[] = [AiProvider.AMAZON_BEDROCK, AiProvider.ANTHROPIC, AiProvider.GITHUB_MODELS]; |
|||
|
|||
export enum ResponseFormat { |
|||
TEXT = 'TEXT', |
|||
JSON = 'JSON', |
|||
JSON_SCHEMA = 'JSON_SCHEMA' |
|||
} |
|||
|
|||
export interface AiModelWithUserMsg { |
|||
userMessage: { |
|||
contents: Array<{contentType: string; text: string}>; |
|||
} |
|||
chatModelConfig: { |
|||
modelType: string; |
|||
provider: AiProvider |
|||
providerConfig: { |
|||
apiKey?: string; |
|||
personalAccessToken?: string; |
|||
endpoint?: string; |
|||
serviceVersion?: string; |
|||
projectId?: string; |
|||
location?: string; |
|||
serviceAccountKey?: string; |
|||
fileName?: string |
|||
}; |
|||
modelId: string; |
|||
maxRetries: number; |
|||
timeoutSeconds: number; |
|||
} |
|||
} |
|||
|
|||
export interface CheckConnectivityResult { |
|||
status: string; |
|||
errorDetails: string; |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
///
|
|||
/// Copyright © 2016-2025 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { EntityId } from '@shared/models/id/entity-id'; |
|||
import { EntityType } from '@shared/models/entity-type.models'; |
|||
|
|||
export class AiModelId implements EntityId { |
|||
entityType = EntityType.AI_MODEL; |
|||
id: string; |
|||
|
|||
constructor(id: string) { |
|||
this.id = id; |
|||
} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
### Prompt settings |
|||
Loading…
Reference in new issue