37 changed files with 1892 additions and 43 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/settings', aiModel, defaultHttpOptionsFromConfig(config)); |
||||
|
} |
||||
|
|
||||
|
public getAiModels(pageLink: PageLink, config?: RequestConfig): Observable<PageData<AiModel>> { |
||||
|
return this.http.get<PageData<AiModel>>(`/api/ai/model/settings${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); |
||||
|
} |
||||
|
|
||||
|
public getAiModelById(aiModelId: string, config?: RequestConfig): Observable<AiModel> { |
||||
|
return this.http.get<AiModel>(`/api/ai/model/settings/${aiModelId}`, defaultHttpOptionsFromConfig(config)); |
||||
|
} |
||||
|
|
||||
|
public deleteAiModel(aiModelId: string, config?: RequestConfig) { |
||||
|
return this.http.delete(`/api/ai/model/settings/${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,115 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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_SETTINGS" |
||||
|
(createNew)="createModelAi('modelSettingsId')" |
||||
|
formControlName="modelSettingsId"> |
||||
|
</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 tb-hint-tooltip-icon="{{ 'rule-node-config.ai.prompt-settings-hint' | translate }}"> |
||||
|
{{'rule-node-config.ai.prompt-settings' | translate}} |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
<div class="tb-form-panel no-border no-padding-bottom no-gap" style="margin-top: 0"> |
||||
|
<mat-form-field class="flex" appearance="outline"> |
||||
|
<mat-label translate>rule-node-config.ai.system-prompt</mat-label> |
||||
|
<textarea matInput rows="2" 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"> |
||||
|
<mat-label translate>rule-node-config.ai.user-prompt</mat-label> |
||||
|
<textarea required matInput rows="2" 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"> |
||||
|
</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" style="margin-top: 0"> |
||||
|
<tb-time-unit-input |
||||
|
required |
||||
|
outlined |
||||
|
filterTimeUnitsByMaxTime |
||||
|
labelText="{{ 'rule-node-config.ai.timeout' | translate }}" |
||||
|
labelIconHintTooltipText="{{ 'rule-node-config.ai.timeout-hint' | translate }}" |
||||
|
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> |
||||
|
</mat-expansion-panel> |
||||
|
</div> |
||||
|
</section> |
||||
@ -0,0 +1,115 @@ |
|||||
|
///
|
||||
|
/// 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 '@shared/components/ai-model/ai-model-dialog.component'; |
||||
|
import { AiModel, AiProvider } from '@shared/models/ai-model.models'; |
||||
|
import { deepTrim } from '@core/utils'; |
||||
|
|
||||
|
enum ResponseFormat { |
||||
|
TEXT = 'TEXT', |
||||
|
JSON = 'JSON', |
||||
|
JSON_SCHEMA = 'JSON_SCHEMA' |
||||
|
} |
||||
|
|
||||
|
@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({ |
||||
|
modelSettingsId: [configuration ? configuration.modelSettingsId : null, [Validators.required]], |
||||
|
systemPrompt: [configuration ? configuration.systemPrompt : '', [Validators.maxLength(10000), Validators.pattern(/.*\S.*/)]], |
||||
|
userPrompt: [configuration ? configuration.userPrompt : '', [Validators.required, Validators.maxLength(10000), Validators.pattern(/.*\S.*/)]], |
||||
|
responseFormat: this.fb.group({ |
||||
|
type: [configuration ? configuration.responseFormat.type : ResponseFormat.JSON, []], |
||||
|
schema: [configuration ? configuration.responseFormat.schema : null, []], |
||||
|
}), |
||||
|
timeoutSeconds: [configuration ? configuration.timeoutSeconds : 60, []] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
protected validatorTriggers(): string[] { |
||||
|
return ['responseFormat.type']; |
||||
|
} |
||||
|
|
||||
|
protected updateValidators(emitEvent: boolean) { |
||||
|
const responseFormatType = this.aiConfigForm.get('responseFormat.type').value; |
||||
|
if (responseFormatType === ResponseFormat.JSON_SCHEMA) { |
||||
|
this.aiConfigForm.get('responseFormat.schema').setValidators([Validators.required]); |
||||
|
this.aiConfigForm.get('responseFormat.schema').enable(); |
||||
|
} else { |
||||
|
this.aiConfigForm.get('responseFormat.schema').setValidators([]); |
||||
|
this.aiConfigForm.get('responseFormat.schema').disable(); |
||||
|
} |
||||
|
this.aiConfigForm.get('responseFormat.schema').updateValueAndValidity({emitEvent}); |
||||
|
} |
||||
|
|
||||
|
protected prepareOutputConfig(configuration: RuleNodeConfiguration): RuleNodeConfiguration { |
||||
|
return deepTrim(configuration); |
||||
|
} |
||||
|
|
||||
|
onEntityChange($event: AiModel) { |
||||
|
if ($event) { |
||||
|
if ($event.configuration.provider === AiProvider.AMAZON_BEDROCK || |
||||
|
$event.configuration.provider === AiProvider.ANTHROPIC || |
||||
|
$event.configuration.provider === AiProvider.GITHUB_MODELS) { |
||||
|
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,124 @@ |
|||||
|
///
|
||||
|
/// 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 '@shared/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_SETTINGS; |
||||
|
this.config.addAsTextButton = true; |
||||
|
this.config.detailsPanelEnabled = false; |
||||
|
this.config.entityTranslations = entityTypeTranslations.get(EntityType.AI_MODEL_SETTINGS); |
||||
|
this.config.entityResources = entityTypeResources.get(EntityType.AI_MODEL_SETTINGS); |
||||
|
|
||||
|
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.modelConfig.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(); |
||||
|
} |
||||
|
|
||||
|
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(entity) |
||||
|
} |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
private editModel(AIModel: AiModel): void { |
||||
|
this.addModel(AIModel, false).subscribe((res) => { |
||||
|
if (res) { |
||||
|
this.config.updateData(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
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,18 @@ |
|||||
|
/** |
||||
|
* 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: 100%; |
||||
|
} |
||||
@ -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: ['./ai-model-table-header.component.scss'] |
||||
|
}) |
||||
|
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,248 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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 (provider === aiProvider.GITHUB_MODELS) { |
||||
|
<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> |
||||
|
} @else if (provider === aiProvider.GOOGLE_VERTEX_AI_GEMINI) { |
||||
|
<mat-form-field class="mat-block flex-1" appearance="outline"> |
||||
|
<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> |
||||
|
<mat-form-field class="mat-block flex-1" appearance="outline"> |
||||
|
<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> |
||||
|
<tb-file-input formControlName="serviceAccountKey" |
||||
|
[existingFileName]="aiModelForms.get('configuration').get('providerConfig').get('serviceAccountKeyFileName').value" |
||||
|
(fileNameChanged)="aiModelForms.get('configuration').get('providerConfig').get('serviceAccountKeyFileName').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> |
||||
|
} @else { |
||||
|
@if (provider === aiProvider.AZURE_OPENAI) { |
||||
|
<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> |
||||
|
<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> |
||||
|
} |
||||
|
<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" formGroupName="modelConfig"> |
||||
|
<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-models-list-autocomplete formControlName="modelId" |
||||
|
[provider]="provider" |
||||
|
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"> |
||||
|
</tb-models-list-autocomplete> |
||||
|
</section> |
||||
|
<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('modelConfig').get('temperature').hasError('min')" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<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('modelConfig').get('topP').hasError('min') || |
||||
|
aiModelForms.get('configuration').get('modelConfig').get('topP').hasError('max')" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
|
||||
|
@if (provider === aiProvider.GOOGLE_AI_GEMINI || provider === aiProvider.GOOGLE_VERTEX_AI_GEMINI || provider === aiProvider.ANTHROPIC) { |
||||
|
<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('modelConfig').get('topK').hasError('min')" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
} |
||||
|
|
||||
|
@if (provider !== aiProvider.ANTHROPIC && provider !== aiProvider.AMAZON_BEDROCK) { |
||||
|
<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="frequencyPenalty" |
||||
|
type="number" step="1" placeholder="{{ 'ai-models.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<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="presencePenalty" |
||||
|
type="number" step="1" placeholder="{{ 'ai-models.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
} |
||||
|
|
||||
|
<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('modelConfig').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,168 @@ |
|||||
|
///
|
||||
|
/// 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 } 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, AiProvider, AiProviderTranslations, AiProviderWithApiKey } from '@shared/models/ai-model.models'; |
||||
|
import { AiModelService } from '@core/http/ai-model.service'; |
||||
|
import { CheckConnectivityDialogComponent } from '@shared/components/ai-model/check-connectivity-dialog.component'; |
||||
|
|
||||
|
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; |
||||
|
|
||||
|
aiProviderWithApiKey: AiProvider[] = AiProviderWithApiKey; |
||||
|
|
||||
|
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: ['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]], |
||||
|
serviceAccountKeyFileName: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.serviceAccountKeyFileName : '', [Validators.required]], |
||||
|
}), |
||||
|
modelConfig: this.fb.group({ |
||||
|
modelId: [this.data.AIModel ? this.data.AIModel.configuration.modelConfig?.modelId : '', [Validators.required]], |
||||
|
temperature: [this.data.AIModel ? this.data.AIModel.configuration.modelConfig?.temperature : null, [Validators.min(0)]], |
||||
|
topP: this.data.AIModel ? this.data.AIModel.configuration.modelConfig?.topP : [null, [Validators.min(0.1), Validators.max(1)]], |
||||
|
topK: [this.data.AIModel ? this.data.AIModel.configuration.modelConfig?.topK : null, [Validators.min(0)]], |
||||
|
frequencyPenalty: [this.data.AIModel ? this.data.AIModel.configuration.modelConfig?.frequencyPenalty : null], |
||||
|
presencePenalty: [this.data.AIModel ? this.data.AIModel.configuration.modelConfig?.presencePenalty : null], |
||||
|
maxOutputTokens: [this.data.AIModel ? this.data.AIModel.configuration.modelConfig?.maxOutputTokens : null, [Validators.min(1)]] |
||||
|
}) |
||||
|
}) |
||||
|
}); |
||||
|
|
||||
|
this.aiModelForms.get('configuration.provider').valueChanges.pipe( |
||||
|
takeUntilDestroyed() |
||||
|
).subscribe((provider: AiProvider) => { |
||||
|
this.provider = provider; |
||||
|
this.aiModelForms.get('configuration.modelConfig').reset({}); |
||||
|
this.aiModelForms.get('configuration.providerConfig').reset({}); |
||||
|
this.updateValidation(provider); |
||||
|
}) |
||||
|
|
||||
|
this.updateValidation(this.provider); |
||||
|
} |
||||
|
|
||||
|
private updateValidation(provider: AiProvider) { |
||||
|
const providerConfig = this.aiModelForms.get('configuration.providerConfig'); |
||||
|
if (this.aiProviderWithApiKey.includes(provider)) { |
||||
|
providerConfig.get('apiKey').enable(); |
||||
|
} else { |
||||
|
providerConfig.get('apiKey').disable(); |
||||
|
} |
||||
|
if (provider === AiProvider.GITHUB_MODELS) { |
||||
|
providerConfig.get('personalAccessToken').enable(); |
||||
|
} else { |
||||
|
providerConfig.get('personalAccessToken').disable(); |
||||
|
} |
||||
|
if (provider === AiProvider.GOOGLE_VERTEX_AI_GEMINI) { |
||||
|
providerConfig.get('projectId').enable(); |
||||
|
providerConfig.get('location').enable(); |
||||
|
providerConfig.get('serviceAccountKey').enable(); |
||||
|
providerConfig.get('serviceAccountKeyFileName').enable(); |
||||
|
} else { |
||||
|
providerConfig.get('projectId').disable(); |
||||
|
providerConfig.get('location').disable(); |
||||
|
providerConfig.get('serviceAccountKey').disable(); |
||||
|
providerConfig.get('serviceAccountKeyFileName').disable(); |
||||
|
} |
||||
|
if (provider === AiProvider.AZURE_OPENAI) { |
||||
|
providerConfig.get('endpoint').enable(); |
||||
|
providerConfig.get('serviceVersion').enable(); |
||||
|
} else { |
||||
|
providerConfig.get('endpoint').disable(); |
||||
|
providerConfig.get('serviceVersion').disable(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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,58 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<div class="flex h-6 flex-row"> |
||||
|
<h2 class="connectivity-title" 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> |
||||
|
</div> |
||||
|
<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> |
||||
|
<span class="flex-1"></span> |
||||
|
<button mat-button color="primary" |
||||
|
type="button" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="cancel()" cdkFocusInitial> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
@ -0,0 +1,71 @@ |
|||||
|
/** |
||||
|
* 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; |
||||
|
|
||||
|
.connectivity-title { |
||||
|
font-size: 18px; |
||||
|
font-weight: 500; |
||||
|
margin: 0; |
||||
|
padding-left: 16px; |
||||
|
} |
||||
|
|
||||
|
.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,87 @@ |
|||||
|
///
|
||||
|
/// 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 } 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?" |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
chatModel: { |
||||
|
modelType: "CHAT", |
||||
|
provider: this.data.AIModel.configuration.provider, |
||||
|
providerConfig: {...this.data.AIModel.configuration.providerConfig}, |
||||
|
modelConfig: { |
||||
|
modelId: this.data.AIModel.configuration.modelConfig.modelId, |
||||
|
maxRetries: 0, |
||||
|
timeoutSeconds: 20 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
this.aiModelService.checkConnectivity(aiModelWithMsg, { |
||||
|
ignoreErrors: true, |
||||
|
ignoreLoading: true |
||||
|
}).subscribe({ |
||||
|
next: (result) => { |
||||
|
if (result.status === 'SUCCESS') { |
||||
|
this.showCheckSuccess = true; |
||||
|
} else { |
||||
|
this.checkErrMsg = JSON.parse(result.errorDetails); |
||||
|
} |
||||
|
}, |
||||
|
error: err => this.checkErrMsg = err.error.message |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
cancel(): void { |
||||
|
this.dialogRef.close(null); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,41 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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-form-field [appearance]="appearance" [subscriptSizing]="subscriptSizing" style="width: 100%"> |
||||
|
<mat-label *ngIf="label">{{label}}</mat-label> |
||||
|
<input matInput #nameInput [formControl]="selectionFormControl" |
||||
|
[placeholder]="placeholderText" |
||||
|
(focusin)="onFocus()" |
||||
|
[matAutocomplete]="optionsAutocomplete"> |
||||
|
<button *ngIf="selectionFormControl.value && !disabled" |
||||
|
type="button" |
||||
|
class="tb-icon-24 mr-2" |
||||
|
matSuffix mat-icon-button aria-label="Clear" |
||||
|
(click)="clear()"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
<mat-error *ngIf="selectionFormControl.hasError('required')"> |
||||
|
{{errorText}} |
||||
|
</mat-error> |
||||
|
<mat-autocomplete |
||||
|
#optionsAutocomplete="matAutocomplete" |
||||
|
class="tb-autocomplete tb-options-input-autocomplete"> |
||||
|
<mat-option *ngFor="let option of filteredOptions$ | async" [value]="option"> |
||||
|
<span class="tb-option-name flex-1" [innerHTML]="option | highlight:searchText:true:'ig'"></span> |
||||
|
</mat-option> |
||||
|
</mat-autocomplete> |
||||
|
</mat-form-field> |
||||
@ -0,0 +1,161 @@ |
|||||
|
///
|
||||
|
/// 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, ElementRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; |
||||
|
import { ControlValueAccessor, FormBuilder, FormControl, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; |
||||
|
import { Observable } from 'rxjs'; |
||||
|
import { map, startWith, tap } from 'rxjs/operators'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { coerceBoolean } from '@shared/decorators/coercion'; |
||||
|
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; |
||||
|
import { AiModelMap, AiProvider } from '@shared/models/ai-model.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-models-list-autocomplete', |
||||
|
templateUrl: './models-list-autocomplete.component.html', |
||||
|
styleUrls: [], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => ModelsListAutocompleteComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class ModelsListAutocompleteComponent implements ControlValueAccessor, OnInit, OnChanges { |
||||
|
|
||||
|
@ViewChild('nameInput', {static: true}) nameInput: ElementRef; |
||||
|
|
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
@Input() |
||||
|
@coerceBoolean() |
||||
|
required = false; |
||||
|
|
||||
|
@Input() |
||||
|
provider: AiProvider; |
||||
|
|
||||
|
@Input() |
||||
|
placeholderText: string = this.translate.instant('widget-config.set'); |
||||
|
|
||||
|
@Input() |
||||
|
subscriptSizing: SubscriptSizing = 'dynamic'; |
||||
|
|
||||
|
@Input() |
||||
|
appearance: MatFormFieldAppearance = 'outline'; |
||||
|
|
||||
|
@Input() |
||||
|
label: string; |
||||
|
|
||||
|
@Input() |
||||
|
errorText: string; |
||||
|
|
||||
|
selectionFormControl: FormControl; |
||||
|
modelValue: string | null; |
||||
|
|
||||
|
filteredOptions$: Observable<Array<string>>; |
||||
|
|
||||
|
searchText = ''; |
||||
|
|
||||
|
private dirty = false; |
||||
|
|
||||
|
private propagateChange = (_val: any) => {}; |
||||
|
|
||||
|
constructor(private fb: FormBuilder, |
||||
|
private translate: TranslateService) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.selectionFormControl = this.fb.control('', this.required ? [Validators.required] : []); |
||||
|
this.setupFilteredOptions(); |
||||
|
} |
||||
|
|
||||
|
ngOnChanges(changes: SimpleChanges) { |
||||
|
if (changes.provider && !changes.provider.isFirstChange()) { |
||||
|
this.setupFilteredOptions(); |
||||
|
this.selectionFormControl.setValue(null, {emitEvent: false}); |
||||
|
this.modelValue = null; |
||||
|
this.propagateChange(null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private setupFilteredOptions() { |
||||
|
this.filteredOptions$ = this.selectionFormControl.valueChanges.pipe( |
||||
|
startWith(''), |
||||
|
tap(value => this.updateView(value)), |
||||
|
map(value => { |
||||
|
const search = value ? value.toLowerCase() : ''; |
||||
|
const options = this.provider ? AiModelMap.get(this.provider) || [] : []; |
||||
|
return search ? options.filter(option => option.toLowerCase().includes(search)) : options; |
||||
|
}) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
writeValue(option?: string): void { |
||||
|
this.searchText = ''; |
||||
|
this.modelValue = option ? option : null; |
||||
|
|
||||
|
if (option) { |
||||
|
this.selectionFormControl.patchValue(option, { emitEvent: false }); |
||||
|
this.dirty = true; |
||||
|
} else { |
||||
|
this.selectionFormControl.patchValue(null, { emitEvent: false }); |
||||
|
this.dirty = true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onFocus() { |
||||
|
if (this.dirty) { |
||||
|
this.selectionFormControl.updateValueAndValidity({onlySelf: true, emitEvent: true}); |
||||
|
this.dirty = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
updateView(value: string) { |
||||
|
this.searchText = value ? value : ''; |
||||
|
if (this.modelValue !== value && value) { |
||||
|
this.modelValue = value; |
||||
|
this.propagateChange(this.modelValue); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (this.disabled) { |
||||
|
this.selectionFormControl.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.selectionFormControl.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
clear() { |
||||
|
this.selectionFormControl.patchValue(null, {emitEvent: false}); |
||||
|
this.propagateChange(null); |
||||
|
this.modelValue = null; |
||||
|
setTimeout(() => { |
||||
|
this.nameInput.nativeElement.blur(); |
||||
|
this.nameInput.nativeElement.focus(); |
||||
|
}, 0); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,159 @@ |
|||||
|
///
|
||||
|
/// 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 } 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 { |
||||
|
modelType: string; |
||||
|
configuration: { |
||||
|
provider: AiProvider |
||||
|
providerConfig: { |
||||
|
apiKey?: string; |
||||
|
personalAccessToken?: string; |
||||
|
endpoint?: string; |
||||
|
serviceVersion?: string; |
||||
|
projectId?: string; |
||||
|
location?: string; |
||||
|
serviceAccountKey?: string; |
||||
|
serviceAccountKeyFileName?: string |
||||
|
}; |
||||
|
modelConfig: { |
||||
|
modelId: string; |
||||
|
temperature?: number | null; |
||||
|
topP?: number; |
||||
|
topK?: number; |
||||
|
frequencyPenalty?: number; |
||||
|
presencePenalty?: number; |
||||
|
maxOutputTokens?: number; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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 AiProviderWithApiKey: AiProvider[] = [ |
||||
|
AiProvider.OPENAI, |
||||
|
AiProvider.AZURE_OPENAI, |
||||
|
AiProvider.GOOGLE_AI_GEMINI, |
||||
|
AiProvider.MISTRAL_AI, |
||||
|
AiProvider.ANTHROPIC, |
||||
|
AiProvider.AMAZON_BEDROCK |
||||
|
] |
||||
|
|
||||
|
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 AiModelMap = new Map<AiProvider, string[]>( |
||||
|
[ |
||||
|
[AiProvider.OPENAI , [ |
||||
|
'o4-mini', |
||||
|
'o3-pro', |
||||
|
'o3', |
||||
|
'o3-mini', |
||||
|
'o1', |
||||
|
'gpt-4.1', |
||||
|
'gpt-4.1-mini', |
||||
|
'gpt-4.1-nano', |
||||
|
'gpt-4o', |
||||
|
'gpt-4o-mini' |
||||
|
]], |
||||
|
[AiProvider.AZURE_OPENAI , []], |
||||
|
[AiProvider.GOOGLE_AI_GEMINI , [ |
||||
|
'gemini-2.5-pro', |
||||
|
'gemini-2.5-flash', |
||||
|
'gemini-2.0-flash', |
||||
|
'gemini-2.0-flash-lite', |
||||
|
]], |
||||
|
[AiProvider.GOOGLE_VERTEX_AI_GEMINI , [ |
||||
|
'gemini-2.5-pro', |
||||
|
'gemini-2.5-flash', |
||||
|
'gemini-2.0-flash', |
||||
|
'gemini-2.0-flash-lite', |
||||
|
]], |
||||
|
[AiProvider.MISTRAL_AI , [ |
||||
|
'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' |
||||
|
]], |
||||
|
[AiProvider.ANTHROPIC , [ |
||||
|
'claude-opus-4-0', |
||||
|
'claude-sonnet-4-0', |
||||
|
'claude-3-7-sonnet-latest', |
||||
|
'claude-3-5-sonnet-latest', |
||||
|
'claude-3-5-haiku-latest' |
||||
|
]], |
||||
|
[AiProvider.AMAZON_BEDROCK , []], |
||||
|
[AiProvider.GITHUB_MODELS , []] |
||||
|
] |
||||
|
); |
||||
|
|
||||
|
export interface AiModelWithUserMsg { |
||||
|
userMessage: { |
||||
|
contents: Array<{contentType: string; text: string}>; |
||||
|
} |
||||
|
chatModel: { |
||||
|
modelType: string; |
||||
|
provider: AiProvider |
||||
|
providerConfig: { |
||||
|
apiKey?: string; |
||||
|
personalAccessToken?: string; |
||||
|
endpoint?: string; |
||||
|
serviceVersion?: string; |
||||
|
projectId?: string; |
||||
|
location?: string; |
||||
|
serviceAccountKey?: string; |
||||
|
serviceAccountKeyFileName?: string |
||||
|
}; |
||||
|
modelConfig: { |
||||
|
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_SETTINGS; |
||||
|
id: string; |
||||
|
|
||||
|
constructor(id: string) { |
||||
|
this.id = id; |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue