/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { DisposableMap } from 'vs/base/common/lifecycle'; import { joinPath } from 'vs/base/common/resources'; import { ThemeIcon } from 'vs/base/common/themables'; import { localize } from 'vs/nls'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { ILanguageModelToolsService, IToolData } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; import * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry'; interface IRawToolContribution { id: string; name?: string; icon?: string | { light: string; dark: string }; displayName?: string; description: string; parametersSchema?: IJSONSchema; canBeInvokedManually?: boolean; } const languageModelToolsExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'languageModelTools', activationEventsGenerator: (contributions: IRawToolContribution[], result) => { for (const contrib of contributions) { result.push(`onLanguageModelTool:${contrib.name}`); } }, jsonSchema: { description: localize('vscode.extension.contributes.tools', 'Contributes a tool that can be invoked by a language model.'), type: 'array', items: { additionalProperties: false, type: 'object', defaultSnippets: [{ body: { name: '', description: '' } }], required: ['id', 'description'], properties: { id: { description: localize('toolId', "A unique id for this tool."), type: 'string', // Borrow OpenAI's requirement for tool names pattern: '^[\\w-]+$' }, name: { description: localize('toolName', "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.", '`canBeInvokedManually`'), type: 'string', pattern: '^[\\w-]+$' }, displayName: { description: localize('toolDisplayName', "A human-readable name for this tool that may be used to describe it in the UI."), type: 'string' }, description: { description: localize('toolDescription', "A description of this tool that may be passed to a language model."), type: 'string' }, parametersSchema: { description: localize('parametersSchema', "A JSON schema for the parameters this tool accepts."), type: 'object', $ref: 'http://json-schema.org/draft-07/schema#' }, canBeInvokedManually: { description: localize('canBeInvokedManually', "Whether this tool can be invoked manually by the user through the chat UX."), type: 'boolean' }, icon: { description: 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)`"), anyOf: [{ type: 'string' }, { type: 'object', properties: { light: { description: localize('icon.light', 'Icon path when a light theme is used'), type: 'string' }, dark: { description: localize('icon.dark', 'Icon path when a dark theme is used'), type: 'string' } } }] } } } } }); function toToolKey(extensionIdentifier: ExtensionIdentifier, toolName: string) { return `${extensionIdentifier.value}/${toolName}`; } export class LanguageModelToolsExtensionPointHandler implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.toolsExtensionPointHandler'; private _registrationDisposables = new DisposableMap(); constructor( @ILanguageModelToolsService languageModelToolsService: ILanguageModelToolsService, @ILogService logService: ILogService, ) { languageModelToolsExtensionPoint.setHandler((extensions, delta) => { for (const extension of delta.added) { for (const rawTool of extension.value) { if (!rawTool.name || !rawTool.description) { logService.error(`Invalid tool contribution from ${extension.description.identifier.value}: ${JSON.stringify(rawTool)}`); continue; } if (!rawTool.id.match(/^[\w-]+$/)) { logService.error(`Extension '${extension.description.identifier.value}' CANNOT register participant with invalid id: ${rawTool.id}. The id must match /^[\\w-]+$/.`); continue; } const rawIcon = rawTool.icon; let icon: IToolData['icon'] | undefined; if (typeof rawIcon === 'string') { icon = ThemeIcon.fromString(rawIcon) ?? { dark: joinPath(extension.description.extensionLocation, rawIcon), light: joinPath(extension.description.extensionLocation, rawIcon) }; } else if (rawIcon) { icon = { dark: joinPath(extension.description.extensionLocation, rawIcon.dark), light: joinPath(extension.description.extensionLocation, rawIcon.light) }; } const tool: IToolData = { ...rawTool, icon }; const disposable = languageModelToolsService.registerToolData(tool); this._registrationDisposables.set(toToolKey(extension.description.identifier, rawTool.id), disposable); } } for (const extension of delta.removed) { for (const tool of extension.value) { this._registrationDisposables.deleteAndDispose(toToolKey(extension.description.identifier, tool.id)); } } }); } }