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