Path: blob/main/src/vs/workbench/contrib/chat/browser/languageModelsConfigurationService.ts
5221 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { VSBuffer } from '../../../../base/common/buffer.js';6import { Emitter, Event } from '../../../../base/common/event.js';7import { Disposable } from '../../../../base/common/lifecycle.js';8import { Mutable } from '../../../../base/common/types.js';9import { URI } from '../../../../base/common/uri.js';10import { IFileService } from '../../../../platform/files/common/files.js';11import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';12import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';13import { ITextEditorService } from '../../../services/textfile/common/textEditorService.js';14import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js';15import { equals } from '../../../../base/common/objects.js';16import { IRange } from '../../../../editor/common/core/range.js';17import { JSONVisitor, visit } from '../../../../base/common/json.js';18import { ITextModel } from '../../../../editor/common/model.js';19import { ITextModelService } from '../../../../editor/common/services/resolverService.js';20import { ITextFileService } from '../../../services/textfile/common/textfiles.js';21import { getCodeEditor } from '../../../../editor/browser/editorBrowser.js';22import { SnippetController2 } from '../../../../editor/contrib/snippet/browser/snippetController2.js';23import { ConfigureLanguageModelsOptions, ILanguageModelsConfigurationService, ILanguageModelsProviderGroup } from '../common/languageModelsConfiguration.js';24import { IJSONContributionRegistry, Extensions as JSONExtensions } from '../../../../platform/jsonschemas/common/jsonContributionRegistry.js';25import { Registry } from '../../../../platform/registry/common/platform.js';26import { IWorkbenchContribution } from '../../../common/contributions.js';27import { ILanguageModelsService } from '../common/languageModels.js';28import { IJSONSchema } from '../../../../base/common/jsonSchema.js';2930type LanguageModelsProviderGroups = Mutable<ILanguageModelsProviderGroup>[];3132export class LanguageModelsConfigurationService extends Disposable implements ILanguageModelsConfigurationService {3334declare _serviceBrand: undefined;3536private readonly modelsConfigurationFile: URI;37get configurationFile(): URI { return this.modelsConfigurationFile; }3839private readonly _onDidChangeLanguageModelGroups = new Emitter<readonly ILanguageModelsProviderGroup[]>();40readonly onDidChangeLanguageModelGroups: Event<readonly ILanguageModelsProviderGroup[]> = this._onDidChangeLanguageModelGroups.event;4142private languageModelsProviderGroups: LanguageModelsProviderGroups = [];4344constructor(45@IFileService private readonly fileService: IFileService,46@ITextFileService private readonly textFileService: ITextFileService,47@ITextModelService private readonly textModelService: ITextModelService,48@IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService,49@ITextEditorService private readonly textEditorService: ITextEditorService,50@IUserDataProfileService userDataProfileService: IUserDataProfileService,51@IUriIdentityService uriIdentityService: IUriIdentityService,52) {53super();54this.modelsConfigurationFile = uriIdentityService.extUri.joinPath(userDataProfileService.currentProfile.location, 'chatLanguageModels.json');55this.updateLanguageModelsConfiguration();56this._register(fileService.watch(this.modelsConfigurationFile));57this._register(fileService.onDidFilesChange(e => {58if (e.contains(this.modelsConfigurationFile)) {59this.updateLanguageModelsConfiguration();60}61}));62}6364private setLanguageModelsConfiguration(languageModelsConfiguration: LanguageModelsProviderGroups): void {65const changedGroups: ILanguageModelsProviderGroup[] = [];66const oldGroupMap = new Map(this.languageModelsProviderGroups.map(g => [`${g.vendor}:${g.name}`, g]));67const newGroupMap = new Map(languageModelsConfiguration.map(g => [`${g.vendor}:${g.name}`, g]));6869// Find added or modified groups70for (const [key, newGroup] of newGroupMap) {71const oldGroup = oldGroupMap.get(key);72if (!oldGroup || !equals(oldGroup, newGroup)) {73changedGroups.push(newGroup);74}75}7677// Find removed groups78for (const [key, oldGroup] of oldGroupMap) {79if (!newGroupMap.has(key)) {80changedGroups.push(oldGroup);81}82}8384this.languageModelsProviderGroups = languageModelsConfiguration;85if (changedGroups.length > 0) {86this._onDidChangeLanguageModelGroups.fire(changedGroups);87}88}8990private async updateLanguageModelsConfiguration(): Promise<void> {91const languageModelsProviderGroups = await this.withLanguageModelsProviderGroups();92this.setLanguageModelsConfiguration(languageModelsProviderGroups);93}9495getLanguageModelsProviderGroups(): readonly ILanguageModelsProviderGroup[] {96return this.languageModelsProviderGroups;97}9899async addLanguageModelsProviderGroup(toAdd: ILanguageModelsProviderGroup): Promise<ILanguageModelsProviderGroup> {100await this.withLanguageModelsProviderGroups(async languageModelsProviderGroups => {101if (languageModelsProviderGroups.some(({ name, vendor }) => name === toAdd.name && vendor === toAdd.vendor)) {102throw new Error(`Language model group with name ${toAdd.name} already exists for vendor ${toAdd.vendor}`);103}104languageModelsProviderGroups.push(toAdd);105return languageModelsProviderGroups;106});107108await this.updateLanguageModelsConfiguration();109const result = this.getLanguageModelsProviderGroups().find(group => group.name === toAdd.name && group.vendor === toAdd.vendor);110if (!result) {111throw new Error(`Language model group with name ${toAdd.name} not found for vendor ${toAdd.vendor}`);112}113return result;114}115116async updateLanguageModelsProviderGroup(from: ILanguageModelsProviderGroup, to: ILanguageModelsProviderGroup): Promise<ILanguageModelsProviderGroup> {117await this.withLanguageModelsProviderGroups(async languageModelsProviderGroups => {118const result: LanguageModelsProviderGroups = [];119for (const group of languageModelsProviderGroups) {120if (group.name === from.name && group.vendor === from.vendor) {121result.push(to);122} else {123result.push(group);124}125}126return result;127});128129await this.updateLanguageModelsConfiguration();130const result = this.getLanguageModelsProviderGroups().find(group => group.name === to.name && group.vendor === to.vendor);131if (!result) {132throw new Error(`Language model group with name ${to.name} not found for vendor ${to.vendor}`);133}134return result;135}136137async removeLanguageModelsProviderGroup(toRemove: ILanguageModelsProviderGroup): Promise<void> {138await this.withLanguageModelsProviderGroups(async languageModelsProviderGroups => {139const result: LanguageModelsProviderGroups = [];140for (const group of languageModelsProviderGroups) {141if (group.name === toRemove.name && group.vendor === toRemove.vendor) {142continue;143}144result.push(group);145}146return result;147});148await this.updateLanguageModelsConfiguration();149}150151async configureLanguageModels(options?: ConfigureLanguageModelsOptions): Promise<void> {152const editor = await this.editorGroupsService.activeGroup.openEditor(this.textEditorService.createTextEditor({ resource: this.modelsConfigurationFile }));153if (!editor || !options?.group) {154return;155}156157const codeEditor = getCodeEditor(editor.getControl());158if (!codeEditor) {159return;160}161162if (!options.group.range) {163return;164}165166if (options.snippet) {167// Insert snippet at the end of the last property line (before the closing brace line), with comma prepended168const model = codeEditor.getModel();169if (!model) {170return;171}172const lastPropertyLine = options.group.range.endLineNumber - 1;173const lastPropertyLineLength = model.getLineLength(lastPropertyLine);174const insertPosition = { lineNumber: lastPropertyLine, column: lastPropertyLineLength + 1 };175codeEditor.setPosition(insertPosition);176codeEditor.revealPositionNearTop(insertPosition);177codeEditor.focus();178SnippetController2.get(codeEditor)?.insert(',\n' + options.snippet);179} else {180const position = { lineNumber: options.group.range.startLineNumber, column: options.group.range.startColumn };181codeEditor.setPosition(position);182codeEditor.revealPositionNearTop(position);183codeEditor.focus();184}185}186187private async withLanguageModelsProviderGroups(update?: (languageModelsProviderGroups: LanguageModelsProviderGroups) => Promise<LanguageModelsProviderGroups>): Promise<LanguageModelsProviderGroups> {188const exists = await this.fileService.exists(this.modelsConfigurationFile);189if (!exists) {190await this.fileService.writeFile(this.modelsConfigurationFile, VSBuffer.fromString(JSON.stringify([], undefined, '\t')));191}192const ref = await this.textModelService.createModelReference(this.modelsConfigurationFile);193const model = ref.object.textEditorModel;194try {195const languageModelsProviderGroups = parseLanguageModelsProviderGroups(model);196if (!update) {197return languageModelsProviderGroups;198}199const updatedLanguageModelsProviderGroups = await update(languageModelsProviderGroups);200for (const group of updatedLanguageModelsProviderGroups) {201delete group.range;202}203model.setValue(JSON.stringify(updatedLanguageModelsProviderGroups, undefined, '\t'));204await this.textFileService.save(this.modelsConfigurationFile);205return updatedLanguageModelsProviderGroups;206} finally {207ref.dispose();208}209}210}211212export function parseLanguageModelsProviderGroups(model: ITextModel): LanguageModelsProviderGroups {213const configuration: LanguageModelsProviderGroups = [];214let currentProperty: string | null = null;215let currentParent: unknown = configuration;216const previousParents: unknown[] = [];217218function onValue(value: unknown, offset: number, length: number) {219if (Array.isArray(currentParent)) {220(currentParent as unknown[]).push(value);221} else if (currentProperty !== null) {222(currentParent as Record<string, unknown>)[currentProperty] = value;223}224}225226const visitor: JSONVisitor = {227onObjectBegin: (offset: number, length: number) => {228const object: Record<string, unknown> & { range?: IRange } = {};229if (previousParents.length === 1 && Array.isArray(currentParent)) {230const start = model.getPositionAt(offset);231const end = model.getPositionAt(offset + length);232object.range = {233startLineNumber: start.lineNumber,234startColumn: start.column,235endLineNumber: end.lineNumber,236endColumn: end.column237};238}239onValue(object, offset, length);240previousParents.push(currentParent);241currentParent = object;242currentProperty = null;243},244onObjectProperty: (name: string, offset: number, length: number) => {245currentProperty = name;246},247onObjectEnd: (offset: number, length: number) => {248const parent = currentParent as Record<string, unknown> & { range?: IRange; _parentConfigurationRange?: Mutable<IRange> };249if (parent.range) {250const end = model.getPositionAt(offset + length);251parent.range = {252startLineNumber: parent.range.startLineNumber,253startColumn: parent.range.startColumn,254endLineNumber: end.lineNumber,255endColumn: end.column256};257}258if (parent._parentConfigurationRange) {259const end = model.getPositionAt(offset + length);260parent._parentConfigurationRange.endLineNumber = end.lineNumber;261parent._parentConfigurationRange.endColumn = end.column;262delete parent._parentConfigurationRange;263}264currentParent = previousParents.pop();265},266onArrayBegin: (offset: number, length: number) => {267if (currentParent === configuration && previousParents.length === 0) {268previousParents.push(currentParent);269currentProperty = null;270return;271}272const array: unknown[] = [];273onValue(array, offset, length);274previousParents.push(currentParent);275currentParent = array;276currentProperty = null;277},278onArrayEnd: (offset: number, length: number) => {279const parent = currentParent as { _parentConfigurationRange?: Mutable<IRange> };280if (parent._parentConfigurationRange) {281const end = model.getPositionAt(offset + length);282parent._parentConfigurationRange.endLineNumber = end.lineNumber;283parent._parentConfigurationRange.endColumn = end.column;284delete parent._parentConfigurationRange;285}286currentParent = previousParents.pop();287},288onLiteralValue: (value: unknown, offset: number, length: number) => {289onValue(value, offset, length);290},291};292visit(model.getValue(), visitor);293return configuration;294}295296const languageModelsSchemaId = 'vscode://schemas/language-models';297298export class ChatLanguageModelsDataContribution extends Disposable implements IWorkbenchContribution {299300static readonly ID = 'workbench.contrib.chatLanguageModelsData';301302constructor(303@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService,304@ILanguageModelsConfigurationService languageModelsConfigurationService: ILanguageModelsConfigurationService,305) {306super();307const registry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);308this._register(registry.registerSchemaAssociation(languageModelsSchemaId, languageModelsConfigurationService.configurationFile.toString()));309310this.updateSchema(registry);311this._register(this.languageModelsService.onDidChangeLanguageModels(() => this.updateSchema(registry)));312}313314private updateSchema(registry: IJSONContributionRegistry): void {315const vendors = this.languageModelsService.getVendors();316317const schema: IJSONSchema = {318type: 'array',319items: {320properties: {321vendor: {322type: 'string',323enum: vendors.map(v => v.vendor)324},325name: { type: 'string' }326},327allOf: vendors.map(vendor => ({328if: {329properties: {330vendor: { const: vendor.vendor }331}332},333then: vendor.configuration334})),335required: ['vendor', 'name']336}337};338339registry.registerSchema(languageModelsSchemaId, schema);340}341}342343344