Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/common/extHostLanguageModelTools.ts
3296 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 type * as vscode from 'vscode';
7
import { raceCancellation } from '../../../base/common/async.js';
8
import { CancellationToken } from '../../../base/common/cancellation.js';
9
import { CancellationError } from '../../../base/common/errors.js';
10
import { IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
11
import { revive } from '../../../base/common/marshalling.js';
12
import { generateUuid } from '../../../base/common/uuid.js';
13
import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js';
14
import { IPreparedToolInvocation, isToolInvocationContext, IToolInvocation, IToolInvocationContext, IToolInvocationPreparationContext, IToolResult, ToolInvocationPresentation } from '../../contrib/chat/common/languageModelToolsService.js';
15
import { ExtensionEditToolId, InternalEditToolId } from '../../contrib/chat/common/tools/editFileTool.js';
16
import { InternalFetchWebPageToolId } from '../../contrib/chat/common/tools/tools.js';
17
import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js';
18
import { Dto, SerializableObjectWithBuffers } from '../../services/extensions/common/proxyIdentifier.js';
19
import { ExtHostLanguageModelToolsShape, IMainContext, IToolDataDto, MainContext, MainThreadLanguageModelToolsShape } from './extHost.protocol.js';
20
import { ExtHostLanguageModels } from './extHostLanguageModels.js';
21
import * as typeConvert from './extHostTypeConverters.js';
22
import { SearchExtensionsToolId } from '../../contrib/extensions/common/searchExtensionsTool.js';
23
import { Lazy } from '../../../base/common/lazy.js';
24
25
class Tool {
26
27
private _data: IToolDataDto;
28
private _apiObject = new Lazy<vscode.LanguageModelToolInformation>(() => {
29
const that = this;
30
return Object.freeze({
31
get name() { return that._data.id; },
32
get description() { return that._data.modelDescription; },
33
get inputSchema() { return that._data.inputSchema; },
34
get tags() { return that._data.tags ?? []; },
35
get source() { return undefined; }
36
});
37
});
38
39
private _apiObjectWithChatParticipantAdditions = new Lazy<vscode.LanguageModelToolInformation>(() => {
40
const that = this;
41
const source = typeConvert.LanguageModelToolSource.to(that._data.source);
42
43
return Object.freeze({
44
get name() { return that._data.id; },
45
get description() { return that._data.modelDescription; },
46
get inputSchema() { return that._data.inputSchema; },
47
get tags() { return that._data.tags ?? []; },
48
get source() { return source; }
49
});
50
});
51
52
constructor(data: IToolDataDto) {
53
this._data = data;
54
}
55
56
update(newData: IToolDataDto): void {
57
this._data = newData;
58
}
59
60
get data(): IToolDataDto {
61
return this._data;
62
}
63
64
get apiObject(): vscode.LanguageModelToolInformation {
65
return this._apiObject.value;
66
}
67
68
get apiObjectWithChatParticipantAdditions() {
69
return this._apiObjectWithChatParticipantAdditions.value;
70
}
71
}
72
73
export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape {
74
/** A map of tools that were registered in this EH */
75
private readonly _registeredTools = new Map<string, { extension: IExtensionDescription; tool: vscode.LanguageModelTool<Object> }>();
76
private readonly _proxy: MainThreadLanguageModelToolsShape;
77
private readonly _tokenCountFuncs = new Map</* call ID */string, (text: string, token?: vscode.CancellationToken) => Thenable<number>>();
78
79
/** A map of all known tools, from other EHs or registered in vscode core */
80
private readonly _allTools = new Map<string, Tool>();
81
82
constructor(
83
mainContext: IMainContext,
84
private readonly _languageModels: ExtHostLanguageModels,
85
) {
86
this._proxy = mainContext.getProxy(MainContext.MainThreadLanguageModelTools);
87
88
this._proxy.$getTools().then(tools => {
89
for (const tool of tools) {
90
this._allTools.set(tool.id, new Tool(revive(tool)));
91
}
92
});
93
}
94
95
async $countTokensForInvocation(callId: string, input: string, token: CancellationToken): Promise<number> {
96
const fn = this._tokenCountFuncs.get(callId);
97
if (!fn) {
98
throw new Error(`Tool invocation call ${callId} not found`);
99
}
100
101
return await fn(input, token);
102
}
103
104
async invokeTool(extension: IExtensionDescription, toolId: string, options: vscode.LanguageModelToolInvocationOptions<any>, token?: CancellationToken): Promise<vscode.LanguageModelToolResult> {
105
const callId = generateUuid();
106
if (options.tokenizationOptions) {
107
this._tokenCountFuncs.set(callId, options.tokenizationOptions.countTokens);
108
}
109
110
try {
111
if (options.toolInvocationToken && !isToolInvocationContext(options.toolInvocationToken)) {
112
throw new Error(`Invalid tool invocation token`);
113
}
114
115
if ((toolId === InternalEditToolId || toolId === ExtensionEditToolId) && !isProposedApiEnabled(extension, 'chatParticipantPrivate')) {
116
throw new Error(`Invalid tool: ${toolId}`);
117
}
118
119
// Making the round trip here because not all tools were necessarily registered in this EH
120
const result = await this._proxy.$invokeTool({
121
toolId,
122
callId,
123
parameters: options.input,
124
tokenBudget: options.tokenizationOptions?.tokenBudget,
125
context: options.toolInvocationToken as IToolInvocationContext | undefined,
126
chatRequestId: isProposedApiEnabled(extension, 'chatParticipantPrivate') ? options.chatRequestId : undefined,
127
chatInteractionId: isProposedApiEnabled(extension, 'chatParticipantPrivate') ? options.chatInteractionId : undefined,
128
}, token);
129
130
const dto: Dto<IToolResult> = result instanceof SerializableObjectWithBuffers ? result.value : result;
131
return typeConvert.LanguageModelToolResult2.to(revive(dto));
132
} finally {
133
this._tokenCountFuncs.delete(callId);
134
}
135
}
136
137
$onDidChangeTools(tools: IToolDataDto[]): void {
138
139
const oldTools = new Set(this._registeredTools.keys());
140
141
for (const tool of tools) {
142
oldTools.delete(tool.id);
143
const existing = this._allTools.get(tool.id);
144
if (existing) {
145
existing.update(tool);
146
} else {
147
this._allTools.set(tool.id, new Tool(revive(tool)));
148
}
149
}
150
151
for (const id of oldTools) {
152
this._allTools.delete(id);
153
}
154
}
155
156
getTools(extension: IExtensionDescription): vscode.LanguageModelToolInformation[] {
157
const hasParticipantAdditions = isProposedApiEnabled(extension, 'chatParticipantPrivate');
158
return Array.from(this._allTools.values())
159
.map(tool => hasParticipantAdditions ? tool.apiObjectWithChatParticipantAdditions : tool.apiObject)
160
.filter(tool => {
161
switch (tool.name) {
162
case InternalEditToolId:
163
case ExtensionEditToolId:
164
case InternalFetchWebPageToolId:
165
case SearchExtensionsToolId:
166
return isProposedApiEnabled(extension, 'chatParticipantPrivate');
167
default:
168
return true;
169
}
170
});
171
}
172
173
async $invokeTool(dto: IToolInvocation, token: CancellationToken): Promise<Dto<IToolResult> | SerializableObjectWithBuffers<Dto<IToolResult>>> {
174
const item = this._registeredTools.get(dto.toolId);
175
if (!item) {
176
throw new Error(`Unknown tool ${dto.toolId}`);
177
}
178
179
const options: vscode.LanguageModelToolInvocationOptions<Object> = {
180
input: dto.parameters,
181
toolInvocationToken: dto.context as vscode.ChatParticipantToolToken | undefined,
182
};
183
if (isProposedApiEnabled(item.extension, 'chatParticipantPrivate')) {
184
options.chatRequestId = dto.chatRequestId;
185
options.chatInteractionId = dto.chatInteractionId;
186
options.chatSessionId = dto.context?.sessionId;
187
}
188
189
if (isProposedApiEnabled(item.extension, 'chatParticipantAdditions') && dto.modelId) {
190
options.model = await this.getModel(dto.modelId, item.extension);
191
}
192
193
if (dto.tokenBudget !== undefined) {
194
options.tokenizationOptions = {
195
tokenBudget: dto.tokenBudget,
196
countTokens: this._tokenCountFuncs.get(dto.callId) || ((value, token = CancellationToken.None) =>
197
this._proxy.$countTokensForInvocation(dto.callId, value, token))
198
};
199
}
200
201
let progress: vscode.Progress<{ message?: string | vscode.MarkdownString; increment?: number }> | undefined;
202
if (isProposedApiEnabled(item.extension, 'toolProgress')) {
203
progress = {
204
report: value => {
205
this._proxy.$acceptToolProgress(dto.callId, {
206
message: typeConvert.MarkdownString.fromStrict(value.message),
207
increment: value.increment,
208
total: 100,
209
});
210
}
211
};
212
}
213
214
// todo: 'any' cast because TS can't handle the overloads
215
const extensionResult = await raceCancellation(Promise.resolve((item.tool.invoke as any)(options, token, progress!)), token);
216
if (!extensionResult) {
217
throw new CancellationError();
218
}
219
220
return typeConvert.LanguageModelToolResult2.from(extensionResult, item.extension);
221
}
222
223
private async getModel(modelId: string, extension: IExtensionDescription): Promise<vscode.LanguageModelChat> {
224
let model: vscode.LanguageModelChat | undefined;
225
if (modelId) {
226
model = await this._languageModels.getLanguageModelByIdentifier(extension, modelId);
227
}
228
if (!model) {
229
model = await this._languageModels.getDefaultLanguageModel(extension);
230
if (!model) {
231
throw new Error('Language model unavailable');
232
}
233
}
234
235
return model;
236
}
237
238
async $prepareToolInvocation(toolId: string, context: IToolInvocationPreparationContext, token: CancellationToken): Promise<IPreparedToolInvocation | undefined> {
239
const item = this._registeredTools.get(toolId);
240
if (!item) {
241
throw new Error(`Unknown tool ${toolId}`);
242
}
243
244
const options: vscode.LanguageModelToolInvocationPrepareOptions<any> = {
245
input: context.parameters,
246
chatRequestId: context.chatRequestId,
247
chatSessionId: context.chatSessionId,
248
chatInteractionId: context.chatInteractionId
249
};
250
if (item.tool.prepareInvocation) {
251
const result = await item.tool.prepareInvocation(options, token);
252
if (!result) {
253
return undefined;
254
}
255
256
if (result.pastTenseMessage || result.presentation) {
257
checkProposedApiEnabled(item.extension, 'chatParticipantPrivate');
258
}
259
260
return {
261
confirmationMessages: result.confirmationMessages ? {
262
title: typeof result.confirmationMessages.title === 'string' ? result.confirmationMessages.title : typeConvert.MarkdownString.from(result.confirmationMessages.title),
263
message: typeof result.confirmationMessages.message === 'string' ? result.confirmationMessages.message : typeConvert.MarkdownString.from(result.confirmationMessages.message),
264
} : undefined,
265
invocationMessage: typeConvert.MarkdownString.fromStrict(result.invocationMessage),
266
pastTenseMessage: typeConvert.MarkdownString.fromStrict(result.pastTenseMessage),
267
presentation: result.presentation as ToolInvocationPresentation | undefined
268
};
269
}
270
271
return undefined;
272
}
273
274
registerTool(extension: IExtensionDescription, id: string, tool: vscode.LanguageModelTool<any>): IDisposable {
275
this._registeredTools.set(id, { extension, tool });
276
this._proxy.$registerTool(id);
277
278
return toDisposable(() => {
279
this._registeredTools.delete(id);
280
this._proxy.$unregisterTool(id);
281
});
282
}
283
}
284
285