Path: blob/main/src/vs/workbench/api/common/extHostLanguageModelTools.ts
3296 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 type * as vscode from 'vscode';6import { raceCancellation } from '../../../base/common/async.js';7import { CancellationToken } from '../../../base/common/cancellation.js';8import { CancellationError } from '../../../base/common/errors.js';9import { IDisposable, toDisposable } from '../../../base/common/lifecycle.js';10import { revive } from '../../../base/common/marshalling.js';11import { generateUuid } from '../../../base/common/uuid.js';12import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js';13import { IPreparedToolInvocation, isToolInvocationContext, IToolInvocation, IToolInvocationContext, IToolInvocationPreparationContext, IToolResult, ToolInvocationPresentation } from '../../contrib/chat/common/languageModelToolsService.js';14import { ExtensionEditToolId, InternalEditToolId } from '../../contrib/chat/common/tools/editFileTool.js';15import { InternalFetchWebPageToolId } from '../../contrib/chat/common/tools/tools.js';16import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js';17import { Dto, SerializableObjectWithBuffers } from '../../services/extensions/common/proxyIdentifier.js';18import { ExtHostLanguageModelToolsShape, IMainContext, IToolDataDto, MainContext, MainThreadLanguageModelToolsShape } from './extHost.protocol.js';19import { ExtHostLanguageModels } from './extHostLanguageModels.js';20import * as typeConvert from './extHostTypeConverters.js';21import { SearchExtensionsToolId } from '../../contrib/extensions/common/searchExtensionsTool.js';22import { Lazy } from '../../../base/common/lazy.js';2324class Tool {2526private _data: IToolDataDto;27private _apiObject = new Lazy<vscode.LanguageModelToolInformation>(() => {28const that = this;29return Object.freeze({30get name() { return that._data.id; },31get description() { return that._data.modelDescription; },32get inputSchema() { return that._data.inputSchema; },33get tags() { return that._data.tags ?? []; },34get source() { return undefined; }35});36});3738private _apiObjectWithChatParticipantAdditions = new Lazy<vscode.LanguageModelToolInformation>(() => {39const that = this;40const source = typeConvert.LanguageModelToolSource.to(that._data.source);4142return Object.freeze({43get name() { return that._data.id; },44get description() { return that._data.modelDescription; },45get inputSchema() { return that._data.inputSchema; },46get tags() { return that._data.tags ?? []; },47get source() { return source; }48});49});5051constructor(data: IToolDataDto) {52this._data = data;53}5455update(newData: IToolDataDto): void {56this._data = newData;57}5859get data(): IToolDataDto {60return this._data;61}6263get apiObject(): vscode.LanguageModelToolInformation {64return this._apiObject.value;65}6667get apiObjectWithChatParticipantAdditions() {68return this._apiObjectWithChatParticipantAdditions.value;69}70}7172export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape {73/** A map of tools that were registered in this EH */74private readonly _registeredTools = new Map<string, { extension: IExtensionDescription; tool: vscode.LanguageModelTool<Object> }>();75private readonly _proxy: MainThreadLanguageModelToolsShape;76private readonly _tokenCountFuncs = new Map</* call ID */string, (text: string, token?: vscode.CancellationToken) => Thenable<number>>();7778/** A map of all known tools, from other EHs or registered in vscode core */79private readonly _allTools = new Map<string, Tool>();8081constructor(82mainContext: IMainContext,83private readonly _languageModels: ExtHostLanguageModels,84) {85this._proxy = mainContext.getProxy(MainContext.MainThreadLanguageModelTools);8687this._proxy.$getTools().then(tools => {88for (const tool of tools) {89this._allTools.set(tool.id, new Tool(revive(tool)));90}91});92}9394async $countTokensForInvocation(callId: string, input: string, token: CancellationToken): Promise<number> {95const fn = this._tokenCountFuncs.get(callId);96if (!fn) {97throw new Error(`Tool invocation call ${callId} not found`);98}99100return await fn(input, token);101}102103async invokeTool(extension: IExtensionDescription, toolId: string, options: vscode.LanguageModelToolInvocationOptions<any>, token?: CancellationToken): Promise<vscode.LanguageModelToolResult> {104const callId = generateUuid();105if (options.tokenizationOptions) {106this._tokenCountFuncs.set(callId, options.tokenizationOptions.countTokens);107}108109try {110if (options.toolInvocationToken && !isToolInvocationContext(options.toolInvocationToken)) {111throw new Error(`Invalid tool invocation token`);112}113114if ((toolId === InternalEditToolId || toolId === ExtensionEditToolId) && !isProposedApiEnabled(extension, 'chatParticipantPrivate')) {115throw new Error(`Invalid tool: ${toolId}`);116}117118// Making the round trip here because not all tools were necessarily registered in this EH119const result = await this._proxy.$invokeTool({120toolId,121callId,122parameters: options.input,123tokenBudget: options.tokenizationOptions?.tokenBudget,124context: options.toolInvocationToken as IToolInvocationContext | undefined,125chatRequestId: isProposedApiEnabled(extension, 'chatParticipantPrivate') ? options.chatRequestId : undefined,126chatInteractionId: isProposedApiEnabled(extension, 'chatParticipantPrivate') ? options.chatInteractionId : undefined,127}, token);128129const dto: Dto<IToolResult> = result instanceof SerializableObjectWithBuffers ? result.value : result;130return typeConvert.LanguageModelToolResult2.to(revive(dto));131} finally {132this._tokenCountFuncs.delete(callId);133}134}135136$onDidChangeTools(tools: IToolDataDto[]): void {137138const oldTools = new Set(this._registeredTools.keys());139140for (const tool of tools) {141oldTools.delete(tool.id);142const existing = this._allTools.get(tool.id);143if (existing) {144existing.update(tool);145} else {146this._allTools.set(tool.id, new Tool(revive(tool)));147}148}149150for (const id of oldTools) {151this._allTools.delete(id);152}153}154155getTools(extension: IExtensionDescription): vscode.LanguageModelToolInformation[] {156const hasParticipantAdditions = isProposedApiEnabled(extension, 'chatParticipantPrivate');157return Array.from(this._allTools.values())158.map(tool => hasParticipantAdditions ? tool.apiObjectWithChatParticipantAdditions : tool.apiObject)159.filter(tool => {160switch (tool.name) {161case InternalEditToolId:162case ExtensionEditToolId:163case InternalFetchWebPageToolId:164case SearchExtensionsToolId:165return isProposedApiEnabled(extension, 'chatParticipantPrivate');166default:167return true;168}169});170}171172async $invokeTool(dto: IToolInvocation, token: CancellationToken): Promise<Dto<IToolResult> | SerializableObjectWithBuffers<Dto<IToolResult>>> {173const item = this._registeredTools.get(dto.toolId);174if (!item) {175throw new Error(`Unknown tool ${dto.toolId}`);176}177178const options: vscode.LanguageModelToolInvocationOptions<Object> = {179input: dto.parameters,180toolInvocationToken: dto.context as vscode.ChatParticipantToolToken | undefined,181};182if (isProposedApiEnabled(item.extension, 'chatParticipantPrivate')) {183options.chatRequestId = dto.chatRequestId;184options.chatInteractionId = dto.chatInteractionId;185options.chatSessionId = dto.context?.sessionId;186}187188if (isProposedApiEnabled(item.extension, 'chatParticipantAdditions') && dto.modelId) {189options.model = await this.getModel(dto.modelId, item.extension);190}191192if (dto.tokenBudget !== undefined) {193options.tokenizationOptions = {194tokenBudget: dto.tokenBudget,195countTokens: this._tokenCountFuncs.get(dto.callId) || ((value, token = CancellationToken.None) =>196this._proxy.$countTokensForInvocation(dto.callId, value, token))197};198}199200let progress: vscode.Progress<{ message?: string | vscode.MarkdownString; increment?: number }> | undefined;201if (isProposedApiEnabled(item.extension, 'toolProgress')) {202progress = {203report: value => {204this._proxy.$acceptToolProgress(dto.callId, {205message: typeConvert.MarkdownString.fromStrict(value.message),206increment: value.increment,207total: 100,208});209}210};211}212213// todo: 'any' cast because TS can't handle the overloads214const extensionResult = await raceCancellation(Promise.resolve((item.tool.invoke as any)(options, token, progress!)), token);215if (!extensionResult) {216throw new CancellationError();217}218219return typeConvert.LanguageModelToolResult2.from(extensionResult, item.extension);220}221222private async getModel(modelId: string, extension: IExtensionDescription): Promise<vscode.LanguageModelChat> {223let model: vscode.LanguageModelChat | undefined;224if (modelId) {225model = await this._languageModels.getLanguageModelByIdentifier(extension, modelId);226}227if (!model) {228model = await this._languageModels.getDefaultLanguageModel(extension);229if (!model) {230throw new Error('Language model unavailable');231}232}233234return model;235}236237async $prepareToolInvocation(toolId: string, context: IToolInvocationPreparationContext, token: CancellationToken): Promise<IPreparedToolInvocation | undefined> {238const item = this._registeredTools.get(toolId);239if (!item) {240throw new Error(`Unknown tool ${toolId}`);241}242243const options: vscode.LanguageModelToolInvocationPrepareOptions<any> = {244input: context.parameters,245chatRequestId: context.chatRequestId,246chatSessionId: context.chatSessionId,247chatInteractionId: context.chatInteractionId248};249if (item.tool.prepareInvocation) {250const result = await item.tool.prepareInvocation(options, token);251if (!result) {252return undefined;253}254255if (result.pastTenseMessage || result.presentation) {256checkProposedApiEnabled(item.extension, 'chatParticipantPrivate');257}258259return {260confirmationMessages: result.confirmationMessages ? {261title: typeof result.confirmationMessages.title === 'string' ? result.confirmationMessages.title : typeConvert.MarkdownString.from(result.confirmationMessages.title),262message: typeof result.confirmationMessages.message === 'string' ? result.confirmationMessages.message : typeConvert.MarkdownString.from(result.confirmationMessages.message),263} : undefined,264invocationMessage: typeConvert.MarkdownString.fromStrict(result.invocationMessage),265pastTenseMessage: typeConvert.MarkdownString.fromStrict(result.pastTenseMessage),266presentation: result.presentation as ToolInvocationPresentation | undefined267};268}269270return undefined;271}272273registerTool(extension: IExtensionDescription, id: string, tool: vscode.LanguageModelTool<any>): IDisposable {274this._registeredTools.set(id, { extension, tool });275this._proxy.$registerTool(id);276277return toDisposable(() => {278this._registeredTools.delete(id);279this._proxy.$unregisterTool(id);280});281}282}283284285