Path: blob/main/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts
4780 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 { isFalsyOrEmpty } from '../../../../../base/common/arrays.js';6import { MarkdownString } from '../../../../../base/common/htmlContent.js';7import { IJSONSchema } from '../../../../../base/common/jsonSchema.js';8import { Disposable, DisposableMap, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js';9import { transaction } from '../../../../../base/common/observable.js';10import { joinPath } from '../../../../../base/common/resources.js';11import { isFalsyOrWhitespace } from '../../../../../base/common/strings.js';12import { ThemeIcon } from '../../../../../base/common/themables.js';13import { localize } from '../../../../../nls.js';14import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';15import { ExtensionIdentifier, IExtensionManifest } from '../../../../../platform/extensions/common/extensions.js';16import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js';17import { IProductService } from '../../../../../platform/product/common/productService.js';18import { Registry } from '../../../../../platform/registry/common/platform.js';19import { IWorkbenchContribution } from '../../../../common/contributions.js';20import { Extensions, IExtensionFeaturesRegistry, IExtensionFeatureTableRenderer, IRenderedData, IRowData, ITableData } from '../../../../services/extensionManagement/common/extensionFeatures.js';21import { isProposedApiEnabled } from '../../../../services/extensions/common/extensions.js';22import * as extensionsRegistry from '../../../../services/extensions/common/extensionsRegistry.js';23import { ILanguageModelToolsService, IToolData, ToolDataSource, ToolSet } from './languageModelToolsService.js';24import { toolsParametersSchemaSchemaId } from './languageModelToolsParametersSchema.js';2526export interface IRawToolContribution {27name: string;28displayName: string;29modelDescription: string;30toolReferenceName?: string;31legacyToolReferenceFullNames?: string[];32icon?: string | { light: string; dark: string };33when?: string;34tags?: string[];35userDescription?: string;36inputSchema?: IJSONSchema;37canBeReferencedInPrompt?: boolean;38}3940const languageModelToolsExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint<IRawToolContribution[]>({41extensionPoint: 'languageModelTools',42activationEventsGenerator: function* (contributions: readonly IRawToolContribution[]) {43for (const contrib of contributions) {44yield `onLanguageModelTool:${contrib.name}`;45}46},47jsonSchema: {48description: 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.'),49type: 'array',50items: {51additionalProperties: false,52type: 'object',53defaultSnippets: [{54body: {55name: '${1}',56modelDescription: '${2}',57inputSchema: {58type: 'object',59properties: {60'${3:name}': {61type: 'string',62description: '${4:description}'63}64}65},66}67}],68required: ['name', 'displayName', 'modelDescription'],69properties: {70name: {71description: 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."),72type: 'string',73// [\\w-]+ is OpenAI's requirement for tool names74pattern: '^(?!copilot_|vscode_)[\\w-]+$'75},76toolReferenceName: {77markdownDescription: 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`'),78type: 'string',79pattern: '^[\\w-]+$'80},81displayName: {82description: localize('toolDisplayName', "A human-readable name for this tool that may be used to describe it in the UI."),83type: 'string'84},85userDescription: {86description: localize('toolUserDescription', "A description of this tool that may be shown to the user."),87type: 'string'88},89// eslint-disable-next-line local/code-no-localized-model-description90modelDescription: {91description: localize('toolModelDescription', "A description of this tool that may be used by a language model to select it."),92type: 'string'93},94inputSchema: {95description: 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."),96$ref: toolsParametersSchemaSchemaId97},98canBeReferencedInPrompt: {99markdownDescription: 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`'),100type: 'boolean'101},102icon: {103markdownDescription: 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)"'),104anyOf: [{105type: 'string'106},107{108type: 'object',109properties: {110light: {111description: localize('icon.light', 'Icon path when a light theme is used'),112type: 'string'113},114dark: {115description: localize('icon.dark', 'Icon path when a dark theme is used'),116type: 'string'117}118}119}]120},121when: {122markdownDescription: 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."),123type: 'string'124},125tags: {126description: 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."),127type: 'array',128items: {129type: 'string',130pattern: '^(?!copilot_|vscode_)'131}132}133}134}135}136});137138export interface IRawToolSetContribution {139name: string;140/**141* @deprecated142*/143referenceName?: string;144legacyFullNames?: string[];145description: string;146icon?: string;147tools: string[];148}149150const languageModelToolSetsExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint<IRawToolSetContribution[]>({151extensionPoint: 'languageModelToolSets',152deps: [languageModelToolsExtensionPoint],153jsonSchema: {154description: localize('vscode.extension.contributes.toolSets', 'Contributes a set of language model tools that can be used together.'),155type: 'array',156items: {157additionalProperties: false,158type: 'object',159defaultSnippets: [{160body: {161name: '${1}',162description: '${2}',163tools: ['${3}']164}165}],166required: ['name', 'description', 'tools'],167properties: {168name: {169description: localize('toolSetName', "A name for this tool set. Used as reference and should not contain whitespace."),170type: 'string',171pattern: '^[\\w-]+$'172},173description: {174description: localize('toolSetDescription', "A description of this tool set."),175type: 'string'176},177icon: {178markdownDescription: localize('toolSetIcon', "An icon that represents this tool set, like `$(zap)`"),179type: 'string'180},181tools: {182markdownDescription: 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`."),183type: 'array',184minItems: 1,185items: {186type: 'string'187}188}189}190}191}192});193194function toToolKey(extensionIdentifier: ExtensionIdentifier, toolName: string) {195return `${extensionIdentifier.value}/${toolName}`;196}197198function toToolSetKey(extensionIdentifier: ExtensionIdentifier, toolName: string) {199return `toolset:${extensionIdentifier.value}/${toolName}`;200}201202export class LanguageModelToolsExtensionPointHandler implements IWorkbenchContribution {203static readonly ID = 'workbench.contrib.toolsExtensionPointHandler';204205private _registrationDisposables = new DisposableMap<string>();206207constructor(208@IProductService productService: IProductService,209@ILanguageModelToolsService languageModelToolsService: ILanguageModelToolsService,210) {211212languageModelToolsExtensionPoint.setHandler((_extensions, delta) => {213for (const extension of delta.added) {214for (const rawTool of extension.value) {215if (!rawTool.name || !rawTool.modelDescription || !rawTool.displayName) {216extension.collector.error(`Extension '${extension.description.identifier.value}' CANNOT register tool without name, modelDescription, and displayName: ${JSON.stringify(rawTool)}`);217continue;218}219220if (!rawTool.name.match(/^[\w-]+$/)) {221extension.collector.error(`Extension '${extension.description.identifier.value}' CANNOT register tool with invalid id: ${rawTool.name}. The id must match /^[\\w-]+$/.`);222continue;223}224225if (rawTool.canBeReferencedInPrompt && !rawTool.toolReferenceName) {226extension.collector.error(`Extension '${extension.description.identifier.value}' CANNOT register tool with 'canBeReferencedInPrompt' set without a 'toolReferenceName': ${JSON.stringify(rawTool)}`);227continue;228}229230if ((rawTool.name.startsWith('copilot_') || rawTool.name.startsWith('vscode_')) && !isProposedApiEnabled(extension.description, 'chatParticipantPrivate')) {231extension.collector.error(`Extension '${extension.description.identifier.value}' CANNOT register tool with name starting with "vscode_" or "copilot_"`);232continue;233}234235if (rawTool.tags?.some(tag => tag.startsWith('copilot_') || tag.startsWith('vscode_')) && !isProposedApiEnabled(extension.description, 'chatParticipantPrivate')) {236extension.collector.error(`Extension '${extension.description.identifier.value}' CANNOT register tool with tags starting with "vscode_" or "copilot_"`);237}238239if (rawTool.legacyToolReferenceFullNames && !isProposedApiEnabled(extension.description, 'chatParticipantPrivate')) {240extension.collector.error(`Extension '${extension.description.identifier.value}' CANNOT use 'legacyToolReferenceFullNames' without the 'chatParticipantPrivate' API proposal enabled`);241continue;242}243244const rawIcon = rawTool.icon;245let icon: IToolData['icon'] | undefined;246if (typeof rawIcon === 'string') {247icon = ThemeIcon.fromString(rawIcon) ?? {248dark: joinPath(extension.description.extensionLocation, rawIcon),249light: joinPath(extension.description.extensionLocation, rawIcon)250};251} else if (rawIcon) {252icon = {253dark: joinPath(extension.description.extensionLocation, rawIcon.dark),254light: joinPath(extension.description.extensionLocation, rawIcon.light)255};256}257258// If OSS and the product.json is not set up, fall back to checking api proposal259const isBuiltinTool = productService.defaultChatAgent?.chatExtensionId ?260ExtensionIdentifier.equals(extension.description.identifier, productService.defaultChatAgent.chatExtensionId) :261isProposedApiEnabled(extension.description, 'chatParticipantPrivate');262263const source: ToolDataSource = isBuiltinTool264? ToolDataSource.Internal265: { type: 'extension', label: extension.description.displayName ?? extension.description.name, extensionId: extension.description.identifier };266267const tool: IToolData = {268...rawTool,269source,270inputSchema: rawTool.inputSchema,271id: rawTool.name,272icon,273when: rawTool.when ? ContextKeyExpr.deserialize(rawTool.when) : undefined,274alwaysDisplayInputOutput: !isBuiltinTool,275};276try {277const disposable = languageModelToolsService.registerToolData(tool);278this._registrationDisposables.set(toToolKey(extension.description.identifier, rawTool.name), disposable);279} catch (e) {280extension.collector.error(`Failed to register tool '${rawTool.name}': ${e}`);281}282}283}284285for (const extension of delta.removed) {286for (const tool of extension.value) {287this._registrationDisposables.deleteAndDispose(toToolKey(extension.description.identifier, tool.name));288}289}290});291292languageModelToolSetsExtensionPoint.setHandler((_extensions, delta) => {293294for (const extension of delta.added) {295296if (!isProposedApiEnabled(extension.description, 'contribLanguageModelToolSets')) {297extension.collector.error(`Extension '${extension.description.identifier.value}' CANNOT register language model tools because the 'contribLanguageModelToolSets' API proposal is not enabled.`);298continue;299}300301const isBuiltinTool = productService.defaultChatAgent?.chatExtensionId ?302ExtensionIdentifier.equals(extension.description.identifier, productService.defaultChatAgent.chatExtensionId) :303isProposedApiEnabled(extension.description, 'chatParticipantPrivate');304305const source: ToolDataSource = isBuiltinTool306? ToolDataSource.Internal307: { type: 'extension', label: extension.description.displayName ?? extension.description.name, extensionId: extension.description.identifier };308309310for (const toolSet of extension.value) {311312if (isFalsyOrWhitespace(toolSet.name)) {313extension.collector.error(`Tool set '${toolSet.name}' CANNOT have an empty name`);314continue;315}316317if (toolSet.legacyFullNames && !isProposedApiEnabled(extension.description, 'contribLanguageModelToolSets')) {318extension.collector.error(`Tool set '${toolSet.name}' CANNOT use 'legacyFullNames' without the 'contribLanguageModelToolSets' API proposal enabled`);319continue;320}321322if (isFalsyOrEmpty(toolSet.tools)) {323extension.collector.error(`Tool set '${toolSet.name}' CANNOT have an empty tools array`);324continue;325}326327const tools: IToolData[] = [];328const toolSets: ToolSet[] = [];329330for (const toolName of toolSet.tools) {331const toolObj = languageModelToolsService.getToolByName(toolName, true);332if (toolObj) {333tools.push(toolObj);334continue;335}336const toolSetObj = languageModelToolsService.getToolSetByName(toolName);337if (toolSetObj) {338toolSets.push(toolSetObj);339continue;340}341extension.collector.warn(`Tool set '${toolSet.name}' CANNOT find tool or tool set by name: ${toolName}`);342}343344if (toolSets.length === 0 && tools.length === 0) {345extension.collector.error(`Tool set '${toolSet.name}' CANNOT have an empty tools array (none of the tools were found)`);346continue;347}348349const store = new DisposableStore();350const referenceName = toolSet.referenceName ?? toolSet.name;351const existingToolSet = languageModelToolsService.getToolSetByName(referenceName);352const mergeExisting = isBuiltinTool && existingToolSet?.source === ToolDataSource.Internal;353354let obj: ToolSet & IDisposable;355// Allow built-in tool to update the tool set if it already exists356if (mergeExisting) {357obj = existingToolSet as ToolSet & IDisposable;358} else {359obj = languageModelToolsService.createToolSet(360source,361toToolSetKey(extension.description.identifier, toolSet.name),362referenceName,363{ icon: toolSet.icon ? ThemeIcon.fromString(toolSet.icon) : undefined, description: toolSet.description, legacyFullNames: toolSet.legacyFullNames }364);365}366367transaction(tx => {368if (!mergeExisting) {369store.add(obj);370}371tools.forEach(tool => store.add(obj.addTool(tool, tx)));372toolSets.forEach(toolSet => store.add(obj.addToolSet(toolSet, tx)));373});374375this._registrationDisposables.set(toToolSetKey(extension.description.identifier, toolSet.name), store);376}377}378379for (const extension of delta.removed) {380for (const toolSet of extension.value) {381this._registrationDisposables.deleteAndDispose(toToolSetKey(extension.description.identifier, toolSet.name));382}383}384});385}386}387388389// --- render390391class LanguageModelToolDataRenderer extends Disposable implements IExtensionFeatureTableRenderer {392readonly type = 'table';393394shouldRender(manifest: IExtensionManifest): boolean {395return !!manifest.contributes?.languageModelTools;396}397398render(manifest: IExtensionManifest): IRenderedData<ITableData> {399const contribs = manifest.contributes?.languageModelTools ?? [];400if (!contribs.length) {401return { data: { headers: [], rows: [] }, dispose: () => { } };402}403404const headers = [405localize('toolTableName', "Name"),406localize('toolTableDisplayName', "Display Name"),407localize('toolTableDescription', "Description"),408];409410const rows: IRowData[][] = contribs.map(t => {411return [412new MarkdownString(`\`${t.name}\``),413t.displayName,414t.userDescription ?? t.modelDescription,415];416});417418return {419data: {420headers,421rows422},423dispose: () => { }424};425}426}427428Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({429id: 'languageModelTools',430label: localize('langModelTools', "Language Model Tools"),431access: {432canToggle: false433},434renderer: new SyncDescriptor(LanguageModelToolDataRenderer),435});436437438class LanguageModelToolSetDataRenderer extends Disposable implements IExtensionFeatureTableRenderer {439440readonly type = 'table';441442shouldRender(manifest: IExtensionManifest): boolean {443return !!manifest.contributes?.languageModelToolSets;444}445446render(manifest: IExtensionManifest): IRenderedData<ITableData> {447const contribs = manifest.contributes?.languageModelToolSets ?? [];448if (!contribs.length) {449return { data: { headers: [], rows: [] }, dispose: () => { } };450}451452const headers = [453localize('name', "Name"),454localize('reference', "Reference Name"),455localize('tools', "Tools"),456localize('descriptions', "Description"),457];458459const rows: IRowData[][] = contribs.map(t => {460return [461new MarkdownString(`\`${t.name}\``),462t.referenceName ? new MarkdownString(`\`#${t.referenceName}\``) : 'none',463t.tools.join(', '),464t.description,465];466});467468return {469data: {470headers,471rows472},473dispose: () => { }474};475}476}477478Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({479id: 'languageModelToolSets',480label: localize('langModelToolSets', "Language Model Tool Sets"),481access: {482canToggle: false483},484renderer: new SyncDescriptor(LanguageModelToolSetDataRenderer),485});486487488