Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts
4780 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 { isFalsyOrEmpty } from '../../../../../base/common/arrays.js';
7
import { MarkdownString } from '../../../../../base/common/htmlContent.js';
8
import { IJSONSchema } from '../../../../../base/common/jsonSchema.js';
9
import { Disposable, DisposableMap, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js';
10
import { transaction } from '../../../../../base/common/observable.js';
11
import { joinPath } from '../../../../../base/common/resources.js';
12
import { isFalsyOrWhitespace } from '../../../../../base/common/strings.js';
13
import { ThemeIcon } from '../../../../../base/common/themables.js';
14
import { localize } from '../../../../../nls.js';
15
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
16
import { ExtensionIdentifier, IExtensionManifest } from '../../../../../platform/extensions/common/extensions.js';
17
import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js';
18
import { IProductService } from '../../../../../platform/product/common/productService.js';
19
import { Registry } from '../../../../../platform/registry/common/platform.js';
20
import { IWorkbenchContribution } from '../../../../common/contributions.js';
21
import { Extensions, IExtensionFeaturesRegistry, IExtensionFeatureTableRenderer, IRenderedData, IRowData, ITableData } from '../../../../services/extensionManagement/common/extensionFeatures.js';
22
import { isProposedApiEnabled } from '../../../../services/extensions/common/extensions.js';
23
import * as extensionsRegistry from '../../../../services/extensions/common/extensionsRegistry.js';
24
import { ILanguageModelToolsService, IToolData, ToolDataSource, ToolSet } from './languageModelToolsService.js';
25
import { toolsParametersSchemaSchemaId } from './languageModelToolsParametersSchema.js';
26
27
export interface IRawToolContribution {
28
name: string;
29
displayName: string;
30
modelDescription: string;
31
toolReferenceName?: string;
32
legacyToolReferenceFullNames?: string[];
33
icon?: string | { light: string; dark: string };
34
when?: string;
35
tags?: string[];
36
userDescription?: string;
37
inputSchema?: IJSONSchema;
38
canBeReferencedInPrompt?: boolean;
39
}
40
41
const languageModelToolsExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint<IRawToolContribution[]>({
42
extensionPoint: 'languageModelTools',
43
activationEventsGenerator: function* (contributions: readonly IRawToolContribution[]) {
44
for (const contrib of contributions) {
45
yield `onLanguageModelTool:${contrib.name}`;
46
}
47
},
48
jsonSchema: {
49
description: localize('vscode.extension.contributes.tools', 'Contributes a tool that can be invoked by a language model in a chat session, or from a standalone command. Registered tools can be used by all extensions.'),
50
type: 'array',
51
items: {
52
additionalProperties: false,
53
type: 'object',
54
defaultSnippets: [{
55
body: {
56
name: '${1}',
57
modelDescription: '${2}',
58
inputSchema: {
59
type: 'object',
60
properties: {
61
'${3:name}': {
62
type: 'string',
63
description: '${4:description}'
64
}
65
}
66
},
67
}
68
}],
69
required: ['name', 'displayName', 'modelDescription'],
70
properties: {
71
name: {
72
description: localize('toolName', "A unique name for this tool. This name must be a globally unique identifier, and is also used as a name when presenting this tool to a language model."),
73
type: 'string',
74
// [\\w-]+ is OpenAI's requirement for tool names
75
pattern: '^(?!copilot_|vscode_)[\\w-]+$'
76
},
77
toolReferenceName: {
78
markdownDescription: localize('toolName2', "If {0} is enabled for this tool, the user may use '#' with this name to invoke the tool in a query. Otherwise, the name is not required. Name must not contain whitespace.", '`canBeReferencedInPrompt`'),
79
type: 'string',
80
pattern: '^[\\w-]+$'
81
},
82
displayName: {
83
description: localize('toolDisplayName', "A human-readable name for this tool that may be used to describe it in the UI."),
84
type: 'string'
85
},
86
userDescription: {
87
description: localize('toolUserDescription', "A description of this tool that may be shown to the user."),
88
type: 'string'
89
},
90
// eslint-disable-next-line local/code-no-localized-model-description
91
modelDescription: {
92
description: localize('toolModelDescription', "A description of this tool that may be used by a language model to select it."),
93
type: 'string'
94
},
95
inputSchema: {
96
description: localize('parametersSchema', "A JSON schema for the input this tool accepts. The input must be an object at the top level. A particular language model may not support all JSON schema features. See the documentation for the language model family you are using for more information."),
97
$ref: toolsParametersSchemaSchemaId
98
},
99
canBeReferencedInPrompt: {
100
markdownDescription: localize('canBeReferencedInPrompt', "If true, this tool shows up as an attachment that the user can add manually to their request. Chat participants will receive the tool in {0}.", '`ChatRequest#toolReferences`'),
101
type: 'boolean'
102
},
103
icon: {
104
markdownDescription: localize('icon', 'An icon that represents this tool. Either a file path, an object with file paths for dark and light themes, or a theme icon reference, like "\\$(zap)"'),
105
anyOf: [{
106
type: 'string'
107
},
108
{
109
type: 'object',
110
properties: {
111
light: {
112
description: localize('icon.light', 'Icon path when a light theme is used'),
113
type: 'string'
114
},
115
dark: {
116
description: localize('icon.dark', 'Icon path when a dark theme is used'),
117
type: 'string'
118
}
119
}
120
}]
121
},
122
when: {
123
markdownDescription: localize('condition', "Condition which must be true for this tool to be enabled. Note that a tool may still be invoked by another extension even when its `when` condition is false."),
124
type: 'string'
125
},
126
tags: {
127
description: localize('toolTags', "A set of tags that roughly describe the tool's capabilities. A tool user may use these to filter the set of tools to just ones that are relevant for the task at hand, or they may want to pick a tag that can be used to identify just the tools contributed by this extension."),
128
type: 'array',
129
items: {
130
type: 'string',
131
pattern: '^(?!copilot_|vscode_)'
132
}
133
}
134
}
135
}
136
}
137
});
138
139
export interface IRawToolSetContribution {
140
name: string;
141
/**
142
* @deprecated
143
*/
144
referenceName?: string;
145
legacyFullNames?: string[];
146
description: string;
147
icon?: string;
148
tools: string[];
149
}
150
151
const languageModelToolSetsExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint<IRawToolSetContribution[]>({
152
extensionPoint: 'languageModelToolSets',
153
deps: [languageModelToolsExtensionPoint],
154
jsonSchema: {
155
description: localize('vscode.extension.contributes.toolSets', 'Contributes a set of language model tools that can be used together.'),
156
type: 'array',
157
items: {
158
additionalProperties: false,
159
type: 'object',
160
defaultSnippets: [{
161
body: {
162
name: '${1}',
163
description: '${2}',
164
tools: ['${3}']
165
}
166
}],
167
required: ['name', 'description', 'tools'],
168
properties: {
169
name: {
170
description: localize('toolSetName', "A name for this tool set. Used as reference and should not contain whitespace."),
171
type: 'string',
172
pattern: '^[\\w-]+$'
173
},
174
description: {
175
description: localize('toolSetDescription', "A description of this tool set."),
176
type: 'string'
177
},
178
icon: {
179
markdownDescription: localize('toolSetIcon', "An icon that represents this tool set, like `$(zap)`"),
180
type: 'string'
181
},
182
tools: {
183
markdownDescription: localize('toolSetTools', "A list of tools or tool sets to include in this tool set. Cannot be empty and must reference tools by their `toolReferenceName`."),
184
type: 'array',
185
minItems: 1,
186
items: {
187
type: 'string'
188
}
189
}
190
}
191
}
192
}
193
});
194
195
function toToolKey(extensionIdentifier: ExtensionIdentifier, toolName: string) {
196
return `${extensionIdentifier.value}/${toolName}`;
197
}
198
199
function toToolSetKey(extensionIdentifier: ExtensionIdentifier, toolName: string) {
200
return `toolset:${extensionIdentifier.value}/${toolName}`;
201
}
202
203
export class LanguageModelToolsExtensionPointHandler implements IWorkbenchContribution {
204
static readonly ID = 'workbench.contrib.toolsExtensionPointHandler';
205
206
private _registrationDisposables = new DisposableMap<string>();
207
208
constructor(
209
@IProductService productService: IProductService,
210
@ILanguageModelToolsService languageModelToolsService: ILanguageModelToolsService,
211
) {
212
213
languageModelToolsExtensionPoint.setHandler((_extensions, delta) => {
214
for (const extension of delta.added) {
215
for (const rawTool of extension.value) {
216
if (!rawTool.name || !rawTool.modelDescription || !rawTool.displayName) {
217
extension.collector.error(`Extension '${extension.description.identifier.value}' CANNOT register tool without name, modelDescription, and displayName: ${JSON.stringify(rawTool)}`);
218
continue;
219
}
220
221
if (!rawTool.name.match(/^[\w-]+$/)) {
222
extension.collector.error(`Extension '${extension.description.identifier.value}' CANNOT register tool with invalid id: ${rawTool.name}. The id must match /^[\\w-]+$/.`);
223
continue;
224
}
225
226
if (rawTool.canBeReferencedInPrompt && !rawTool.toolReferenceName) {
227
extension.collector.error(`Extension '${extension.description.identifier.value}' CANNOT register tool with 'canBeReferencedInPrompt' set without a 'toolReferenceName': ${JSON.stringify(rawTool)}`);
228
continue;
229
}
230
231
if ((rawTool.name.startsWith('copilot_') || rawTool.name.startsWith('vscode_')) && !isProposedApiEnabled(extension.description, 'chatParticipantPrivate')) {
232
extension.collector.error(`Extension '${extension.description.identifier.value}' CANNOT register tool with name starting with "vscode_" or "copilot_"`);
233
continue;
234
}
235
236
if (rawTool.tags?.some(tag => tag.startsWith('copilot_') || tag.startsWith('vscode_')) && !isProposedApiEnabled(extension.description, 'chatParticipantPrivate')) {
237
extension.collector.error(`Extension '${extension.description.identifier.value}' CANNOT register tool with tags starting with "vscode_" or "copilot_"`);
238
}
239
240
if (rawTool.legacyToolReferenceFullNames && !isProposedApiEnabled(extension.description, 'chatParticipantPrivate')) {
241
extension.collector.error(`Extension '${extension.description.identifier.value}' CANNOT use 'legacyToolReferenceFullNames' without the 'chatParticipantPrivate' API proposal enabled`);
242
continue;
243
}
244
245
const rawIcon = rawTool.icon;
246
let icon: IToolData['icon'] | undefined;
247
if (typeof rawIcon === 'string') {
248
icon = ThemeIcon.fromString(rawIcon) ?? {
249
dark: joinPath(extension.description.extensionLocation, rawIcon),
250
light: joinPath(extension.description.extensionLocation, rawIcon)
251
};
252
} else if (rawIcon) {
253
icon = {
254
dark: joinPath(extension.description.extensionLocation, rawIcon.dark),
255
light: joinPath(extension.description.extensionLocation, rawIcon.light)
256
};
257
}
258
259
// If OSS and the product.json is not set up, fall back to checking api proposal
260
const isBuiltinTool = productService.defaultChatAgent?.chatExtensionId ?
261
ExtensionIdentifier.equals(extension.description.identifier, productService.defaultChatAgent.chatExtensionId) :
262
isProposedApiEnabled(extension.description, 'chatParticipantPrivate');
263
264
const source: ToolDataSource = isBuiltinTool
265
? ToolDataSource.Internal
266
: { type: 'extension', label: extension.description.displayName ?? extension.description.name, extensionId: extension.description.identifier };
267
268
const tool: IToolData = {
269
...rawTool,
270
source,
271
inputSchema: rawTool.inputSchema,
272
id: rawTool.name,
273
icon,
274
when: rawTool.when ? ContextKeyExpr.deserialize(rawTool.when) : undefined,
275
alwaysDisplayInputOutput: !isBuiltinTool,
276
};
277
try {
278
const disposable = languageModelToolsService.registerToolData(tool);
279
this._registrationDisposables.set(toToolKey(extension.description.identifier, rawTool.name), disposable);
280
} catch (e) {
281
extension.collector.error(`Failed to register tool '${rawTool.name}': ${e}`);
282
}
283
}
284
}
285
286
for (const extension of delta.removed) {
287
for (const tool of extension.value) {
288
this._registrationDisposables.deleteAndDispose(toToolKey(extension.description.identifier, tool.name));
289
}
290
}
291
});
292
293
languageModelToolSetsExtensionPoint.setHandler((_extensions, delta) => {
294
295
for (const extension of delta.added) {
296
297
if (!isProposedApiEnabled(extension.description, 'contribLanguageModelToolSets')) {
298
extension.collector.error(`Extension '${extension.description.identifier.value}' CANNOT register language model tools because the 'contribLanguageModelToolSets' API proposal is not enabled.`);
299
continue;
300
}
301
302
const isBuiltinTool = productService.defaultChatAgent?.chatExtensionId ?
303
ExtensionIdentifier.equals(extension.description.identifier, productService.defaultChatAgent.chatExtensionId) :
304
isProposedApiEnabled(extension.description, 'chatParticipantPrivate');
305
306
const source: ToolDataSource = isBuiltinTool
307
? ToolDataSource.Internal
308
: { type: 'extension', label: extension.description.displayName ?? extension.description.name, extensionId: extension.description.identifier };
309
310
311
for (const toolSet of extension.value) {
312
313
if (isFalsyOrWhitespace(toolSet.name)) {
314
extension.collector.error(`Tool set '${toolSet.name}' CANNOT have an empty name`);
315
continue;
316
}
317
318
if (toolSet.legacyFullNames && !isProposedApiEnabled(extension.description, 'contribLanguageModelToolSets')) {
319
extension.collector.error(`Tool set '${toolSet.name}' CANNOT use 'legacyFullNames' without the 'contribLanguageModelToolSets' API proposal enabled`);
320
continue;
321
}
322
323
if (isFalsyOrEmpty(toolSet.tools)) {
324
extension.collector.error(`Tool set '${toolSet.name}' CANNOT have an empty tools array`);
325
continue;
326
}
327
328
const tools: IToolData[] = [];
329
const toolSets: ToolSet[] = [];
330
331
for (const toolName of toolSet.tools) {
332
const toolObj = languageModelToolsService.getToolByName(toolName, true);
333
if (toolObj) {
334
tools.push(toolObj);
335
continue;
336
}
337
const toolSetObj = languageModelToolsService.getToolSetByName(toolName);
338
if (toolSetObj) {
339
toolSets.push(toolSetObj);
340
continue;
341
}
342
extension.collector.warn(`Tool set '${toolSet.name}' CANNOT find tool or tool set by name: ${toolName}`);
343
}
344
345
if (toolSets.length === 0 && tools.length === 0) {
346
extension.collector.error(`Tool set '${toolSet.name}' CANNOT have an empty tools array (none of the tools were found)`);
347
continue;
348
}
349
350
const store = new DisposableStore();
351
const referenceName = toolSet.referenceName ?? toolSet.name;
352
const existingToolSet = languageModelToolsService.getToolSetByName(referenceName);
353
const mergeExisting = isBuiltinTool && existingToolSet?.source === ToolDataSource.Internal;
354
355
let obj: ToolSet & IDisposable;
356
// Allow built-in tool to update the tool set if it already exists
357
if (mergeExisting) {
358
obj = existingToolSet as ToolSet & IDisposable;
359
} else {
360
obj = languageModelToolsService.createToolSet(
361
source,
362
toToolSetKey(extension.description.identifier, toolSet.name),
363
referenceName,
364
{ icon: toolSet.icon ? ThemeIcon.fromString(toolSet.icon) : undefined, description: toolSet.description, legacyFullNames: toolSet.legacyFullNames }
365
);
366
}
367
368
transaction(tx => {
369
if (!mergeExisting) {
370
store.add(obj);
371
}
372
tools.forEach(tool => store.add(obj.addTool(tool, tx)));
373
toolSets.forEach(toolSet => store.add(obj.addToolSet(toolSet, tx)));
374
});
375
376
this._registrationDisposables.set(toToolSetKey(extension.description.identifier, toolSet.name), store);
377
}
378
}
379
380
for (const extension of delta.removed) {
381
for (const toolSet of extension.value) {
382
this._registrationDisposables.deleteAndDispose(toToolSetKey(extension.description.identifier, toolSet.name));
383
}
384
}
385
});
386
}
387
}
388
389
390
// --- render
391
392
class LanguageModelToolDataRenderer extends Disposable implements IExtensionFeatureTableRenderer {
393
readonly type = 'table';
394
395
shouldRender(manifest: IExtensionManifest): boolean {
396
return !!manifest.contributes?.languageModelTools;
397
}
398
399
render(manifest: IExtensionManifest): IRenderedData<ITableData> {
400
const contribs = manifest.contributes?.languageModelTools ?? [];
401
if (!contribs.length) {
402
return { data: { headers: [], rows: [] }, dispose: () => { } };
403
}
404
405
const headers = [
406
localize('toolTableName', "Name"),
407
localize('toolTableDisplayName', "Display Name"),
408
localize('toolTableDescription', "Description"),
409
];
410
411
const rows: IRowData[][] = contribs.map(t => {
412
return [
413
new MarkdownString(`\`${t.name}\``),
414
t.displayName,
415
t.userDescription ?? t.modelDescription,
416
];
417
});
418
419
return {
420
data: {
421
headers,
422
rows
423
},
424
dispose: () => { }
425
};
426
}
427
}
428
429
Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({
430
id: 'languageModelTools',
431
label: localize('langModelTools', "Language Model Tools"),
432
access: {
433
canToggle: false
434
},
435
renderer: new SyncDescriptor(LanguageModelToolDataRenderer),
436
});
437
438
439
class LanguageModelToolSetDataRenderer extends Disposable implements IExtensionFeatureTableRenderer {
440
441
readonly type = 'table';
442
443
shouldRender(manifest: IExtensionManifest): boolean {
444
return !!manifest.contributes?.languageModelToolSets;
445
}
446
447
render(manifest: IExtensionManifest): IRenderedData<ITableData> {
448
const contribs = manifest.contributes?.languageModelToolSets ?? [];
449
if (!contribs.length) {
450
return { data: { headers: [], rows: [] }, dispose: () => { } };
451
}
452
453
const headers = [
454
localize('name', "Name"),
455
localize('reference', "Reference Name"),
456
localize('tools', "Tools"),
457
localize('descriptions', "Description"),
458
];
459
460
const rows: IRowData[][] = contribs.map(t => {
461
return [
462
new MarkdownString(`\`${t.name}\``),
463
t.referenceName ? new MarkdownString(`\`#${t.referenceName}\``) : 'none',
464
t.tools.join(', '),
465
t.description,
466
];
467
});
468
469
return {
470
data: {
471
headers,
472
rows
473
},
474
dispose: () => { }
475
};
476
}
477
}
478
479
Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({
480
id: 'languageModelToolSets',
481
label: localize('langModelToolSets', "Language Model Tool Sets"),
482
access: {
483
canToggle: false
484
},
485
renderer: new SyncDescriptor(LanguageModelToolSetDataRenderer),
486
});
487
488