mirror of https://github.com/microsoft/vscode.git
Add support for tool sets (#249448)
* observale map/set * adds tool sets, let mcp contribute tool set, update commands * add `languageModelToolSets` contribution point that allows extensions to define set of tools * only homogenous tool sets hide the tools they contain (that is set and all its tools are from the same source) * make sure checking/unchecking buckets works * make it more clear that the action is about tool configuration (not selecting/attaching) * simplify `IToolSet#tools` * support `*.toolset.json` files * one bucket for all MCP servers * use mcp icon * use `.toolsets.json` suffix * [tools config-pick] keep parents, peek sort order * * add "Configure Tool Sets" actions * make it `toolsets.jsonc` and register schema etc pp * fix some tools picker issues * add tool sets validation * * tools completions * Add ToolDataSource.Internal constant * add missing return * make tool set a class * allow a tool set to contain other tool sets * use dynamic schema for `toolsets.jsonc` language features * fix tests
This commit is contained in:
parent
9880502f0e
commit
2e59a77991
|
@ -130,6 +130,10 @@
|
|||
"fileMatch": "%APP_SETTINGS_HOME%/snippets/*.json",
|
||||
"url": "vscode://schemas/snippets"
|
||||
},
|
||||
{
|
||||
"fileMatch": "%APP_SETTINGS_HOME%/prompts/*.toolsets.jsonc",
|
||||
"url": "vscode://schemas/toolsets"
|
||||
},
|
||||
{
|
||||
"fileMatch": "%APP_SETTINGS_HOME%/profiles/*/snippets/.json",
|
||||
"url": "vscode://schemas/snippets"
|
||||
|
|
|
@ -11,7 +11,9 @@
|
|||
"scripts": {
|
||||
"update-grammar": "node ./build/update-grammars.js"
|
||||
},
|
||||
"categories": ["Programming Languages"],
|
||||
"categories": [
|
||||
"Programming Languages"
|
||||
],
|
||||
"contributes": {
|
||||
"languages": [
|
||||
{
|
||||
|
@ -58,7 +60,8 @@
|
|||
".jshintrc",
|
||||
".swcrc",
|
||||
".hintrc",
|
||||
".babelrc"
|
||||
".babelrc",
|
||||
".toolset.jsonc"
|
||||
],
|
||||
"filenames": [
|
||||
"babel.config.json",
|
||||
|
@ -84,7 +87,7 @@
|
|||
{
|
||||
"id": "snippets",
|
||||
"aliases": [
|
||||
"Code Snippets"
|
||||
"Code Snippets"
|
||||
],
|
||||
"extensions": [
|
||||
".code-snippets"
|
||||
|
|
|
@ -32,6 +32,9 @@ export { derivedConstOnceDefined, latestChangedValue } from './experimental/util
|
|||
export { observableFromEvent } from './observables/observableFromEvent.js';
|
||||
export { observableValue } from './observables/observableValue.js';
|
||||
|
||||
export { ObservableSet } from './set.js';
|
||||
export { ObservableMap } from './map.js';
|
||||
|
||||
import { addLogger, setLogObservableFn } from './logging/logging.js';
|
||||
import { ConsoleObservableLogger, logObservableToConsole } from './logging/consoleObservableLogger.js';
|
||||
import { DevToolsLogger } from './logging/debugger/devToolsLogger.js';
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { observableValueOpts, IObservable } from '../observable.js';
|
||||
|
||||
export class ObservableMap<K, V> implements Map<K, V> {
|
||||
private readonly _data = new Map<K, V>();
|
||||
|
||||
private readonly _obs = observableValueOpts({ equalsFn: () => false }, this);
|
||||
|
||||
readonly observable: IObservable<Map<K, V>> = this._obs;
|
||||
|
||||
get size(): number {
|
||||
return this._data.size;
|
||||
}
|
||||
|
||||
has(key: K): boolean {
|
||||
return this._data.has(key);
|
||||
}
|
||||
|
||||
get(key: K): V | undefined {
|
||||
return this._data.get(key);
|
||||
}
|
||||
|
||||
set(key: K, value: V): this {
|
||||
const hadKey = this._data.has(key);
|
||||
const oldValue = this._data.get(key);
|
||||
if (!hadKey || oldValue !== value) {
|
||||
this._data.set(key, value);
|
||||
this._obs.set(this, undefined);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
delete(key: K): boolean {
|
||||
const result = this._data.delete(key);
|
||||
if (result) {
|
||||
this._obs.set(this, undefined);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
if (this._data.size > 0) {
|
||||
this._data.clear();
|
||||
this._obs.set(this, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
forEach(callbackfn: (value: V, key: K, map: Map<K, V>) => void, thisArg?: any): void {
|
||||
this._data.forEach((value, key, _map) => {
|
||||
callbackfn.call(thisArg, value, key, this);
|
||||
});
|
||||
}
|
||||
|
||||
*entries(): IterableIterator<[K, V]> {
|
||||
yield* this._data.entries();
|
||||
}
|
||||
|
||||
*keys(): IterableIterator<K> {
|
||||
yield* this._data.keys();
|
||||
}
|
||||
|
||||
*values(): IterableIterator<V> {
|
||||
yield* this._data.values();
|
||||
}
|
||||
|
||||
[Symbol.iterator](): IterableIterator<[K, V]> {
|
||||
return this.entries();
|
||||
}
|
||||
|
||||
get [Symbol.toStringTag](): string {
|
||||
return 'ObservableMap';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { observableValueOpts, IObservable } from '../observable.js';
|
||||
|
||||
|
||||
export class ObservableSet<T> implements Set<T> {
|
||||
|
||||
private readonly _data = new Set<T>();
|
||||
|
||||
private _obs = observableValueOpts({ equalsFn: () => false }, this);
|
||||
|
||||
readonly observable: IObservable<Set<T>> = this._obs;
|
||||
|
||||
get size(): number {
|
||||
return this._data.size;
|
||||
}
|
||||
|
||||
has(value: T): boolean {
|
||||
return this._data.has(value);
|
||||
}
|
||||
|
||||
add(value: T): this {
|
||||
const hadValue = this._data.has(value);
|
||||
if (!hadValue) {
|
||||
this._data.add(value);
|
||||
this._obs.set(this, undefined);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
delete(value: T): boolean {
|
||||
const result = this._data.delete(value);
|
||||
if (result) {
|
||||
this._obs.set(this, undefined);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
if (this._data.size > 0) {
|
||||
this._data.clear();
|
||||
this._obs.set(this, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
forEach(callbackfn: (value: T, value2: T, set: Set<T>) => void, thisArg?: any): void {
|
||||
this._data.forEach((value, value2, _set) => {
|
||||
callbackfn.call(thisArg, value, value2, this as any);
|
||||
});
|
||||
}
|
||||
|
||||
*entries(): IterableIterator<[T, T]> {
|
||||
for (const value of this._data) {
|
||||
yield [value, value];
|
||||
}
|
||||
}
|
||||
|
||||
*keys(): IterableIterator<T> {
|
||||
yield* this._data.keys();
|
||||
}
|
||||
|
||||
*values(): IterableIterator<T> {
|
||||
yield* this._data.values();
|
||||
}
|
||||
|
||||
[Symbol.iterator](): IterableIterator<T> {
|
||||
return this.values();
|
||||
}
|
||||
|
||||
get [Symbol.toStringTag](): string {
|
||||
return 'ObservableSet';
|
||||
}
|
||||
}
|
|
@ -385,6 +385,7 @@ export const enum CompletionItemKind {
|
|||
TypeParameter,
|
||||
User,
|
||||
Issue,
|
||||
Tool,
|
||||
Snippet, // <- highest value (used for compare!)
|
||||
}
|
||||
|
||||
|
@ -423,6 +424,7 @@ export namespace CompletionItemKinds {
|
|||
byKind.set(CompletionItemKind.TypeParameter, Codicon.symbolTypeParameter);
|
||||
byKind.set(CompletionItemKind.User, Codicon.account);
|
||||
byKind.set(CompletionItemKind.Issue, Codicon.issues);
|
||||
byKind.set(CompletionItemKind.Tool, Codicon.tools);
|
||||
|
||||
/**
|
||||
* @internal
|
||||
|
@ -468,6 +470,7 @@ export namespace CompletionItemKinds {
|
|||
case CompletionItemKind.TypeParameter: return localize('suggestWidget.kind.typeParameter', 'Type Parameter');
|
||||
case CompletionItemKind.User: return localize('suggestWidget.kind.user', 'User');
|
||||
case CompletionItemKind.Issue: return localize('suggestWidget.kind.issue', 'Issue');
|
||||
case CompletionItemKind.Tool: return localize('suggestWidget.kind.tool', 'Tool');
|
||||
case CompletionItemKind.Snippet: return localize('suggestWidget.kind.snippet', 'Snippet');
|
||||
default: return '';
|
||||
}
|
||||
|
@ -504,6 +507,7 @@ export namespace CompletionItemKinds {
|
|||
data.set('typeParameter', CompletionItemKind.TypeParameter);
|
||||
data.set('account', CompletionItemKind.User);
|
||||
data.set('issue', CompletionItemKind.Issue);
|
||||
data.set('tool', CompletionItemKind.Tool);
|
||||
|
||||
/**
|
||||
* @internal
|
||||
|
|
|
@ -61,7 +61,8 @@ export enum CompletionItemKind {
|
|||
TypeParameter = 24,
|
||||
User = 25,
|
||||
Issue = 26,
|
||||
Snippet = 27
|
||||
Tool = 27,
|
||||
Snippet = 28
|
||||
}
|
||||
|
||||
export enum CompletionItemTag {
|
||||
|
|
|
@ -7055,7 +7055,8 @@ declare namespace monaco.languages {
|
|||
TypeParameter = 24,
|
||||
User = 25,
|
||||
Issue = 26,
|
||||
Snippet = 27
|
||||
Tool = 27,
|
||||
Snippet = 28
|
||||
}
|
||||
|
||||
export interface CompletionItemLabel {
|
||||
|
|
|
@ -185,6 +185,14 @@ export interface IToolContribution {
|
|||
userDescription?: string;
|
||||
}
|
||||
|
||||
export interface IToolSetContribution {
|
||||
name: string;
|
||||
referenceName: string;
|
||||
description: string;
|
||||
icon?: string;
|
||||
tools: string[];
|
||||
}
|
||||
|
||||
export interface IMcpCollectionContribution {
|
||||
readonly id: string;
|
||||
readonly label: string;
|
||||
|
@ -217,6 +225,7 @@ export interface IExtensionContributions {
|
|||
readonly debugVisualizers?: IDebugVisualizationContribution[];
|
||||
readonly chatParticipants?: ReadonlyArray<IChatParticipantContribution>;
|
||||
readonly languageModelTools?: ReadonlyArray<IToolContribution>;
|
||||
readonly languageModelToolSets?: ReadonlyArray<IToolSetContribution>;
|
||||
readonly mcpServerDefinitionProviders?: ReadonlyArray<IMcpCollectionContribution>;
|
||||
}
|
||||
|
||||
|
|
|
@ -110,6 +110,9 @@ const _allApiProposals = {
|
|||
contribLabelFormatterWorkspaceTooltip: {
|
||||
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribLabelFormatterWorkspaceTooltip.d.ts',
|
||||
},
|
||||
contribLanguageModelToolSets: {
|
||||
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribLanguageModelToolSets.d.ts',
|
||||
},
|
||||
contribMenuBarHome: {
|
||||
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribMenuBarHome.d.ts',
|
||||
},
|
||||
|
|
|
@ -23,8 +23,8 @@ import { FileEditorInput } from '../../../files/browser/editors/fileEditorInput.
|
|||
import { NotebookEditorInput } from '../../../notebook/common/notebookEditorInput.js';
|
||||
import { IChatContextPickService, IChatContextValueItem, IChatContextPickerItem, IChatContextPickerPickItem } from '../chatContextPickService.js';
|
||||
import { IChatEditingService } from '../../common/chatEditingService.js';
|
||||
import { IChatRequestToolEntry, IChatRequestVariableEntry, IImageVariableEntry, OmittedState } from '../../common/chatModel.js';
|
||||
import { IToolData } from '../../common/languageModelToolsService.js';
|
||||
import { IChatRequestToolEntry, IChatRequestToolSetEntry, IChatRequestVariableEntry, IImageVariableEntry, OmittedState } from '../../common/chatModel.js';
|
||||
import { ToolDataSource, ToolSet } from '../../common/languageModelToolsService.js';
|
||||
import { IChatWidget } from '../chat.js';
|
||||
import { imageToHash, isImage } from '../chatPasteProviders.js';
|
||||
import { convertBufferToScreenshotVariable } from '../contrib/screenshot.js';
|
||||
|
@ -70,23 +70,22 @@ class ToolsContextPickerPick implements IChatContextPickerItem {
|
|||
readonly picks: Promise<(IChatContextPickerPickItem | IQuickPickSeparator)[]>;
|
||||
} {
|
||||
|
||||
type Pick = IChatContextPickerPickItem & { ordinal: number; groupLabel: string };
|
||||
type Pick = IChatContextPickerPickItem & { toolInfo: { ordinal: number; label: string } };
|
||||
const items: Pick[] = [];
|
||||
|
||||
for (const tool of widget.input.selectedToolsModel.tools.get()) {
|
||||
if (!tool.canBeReferencedInPrompt) {
|
||||
continue;
|
||||
}
|
||||
for (const entry of widget.input.selectedToolsModel.entries.get()) {
|
||||
|
||||
const label = entry.toolReferenceName ?? entry.displayName;
|
||||
const item: Pick = {
|
||||
...this._classify(tool),
|
||||
label: tool.toolReferenceName ?? tool.id,
|
||||
description: (tool.toolReferenceName ?? tool.id) !== tool.displayName ? tool.displayName : undefined,
|
||||
asAttachment: (): IChatRequestToolEntry => {
|
||||
toolInfo: ToolDataSource.classify(entry.source),
|
||||
label,
|
||||
description: label !== entry.displayName ? entry.displayName : undefined,
|
||||
asAttachment: (): IChatRequestToolEntry | IChatRequestToolSetEntry => {
|
||||
return {
|
||||
kind: 'tool',
|
||||
id: tool.id,
|
||||
name: tool.displayName,
|
||||
fullName: tool.displayName,
|
||||
kind: entry instanceof ToolSet ? 'toolset' : 'tool',
|
||||
id: entry.id,
|
||||
name: entry.displayName,
|
||||
fullName: entry.displayName,
|
||||
value: undefined,
|
||||
};
|
||||
}
|
||||
|
@ -96,7 +95,7 @@ class ToolsContextPickerPick implements IChatContextPickerItem {
|
|||
}
|
||||
|
||||
items.sort((a, b) => {
|
||||
let res = a.ordinal - b.ordinal;
|
||||
let res = a.toolInfo.ordinal - b.toolInfo.ordinal;
|
||||
if (res === 0) {
|
||||
res = a.label.localeCompare(b.label);
|
||||
}
|
||||
|
@ -108,9 +107,9 @@ class ToolsContextPickerPick implements IChatContextPickerItem {
|
|||
|
||||
|
||||
for (const item of items) {
|
||||
if (lastGroupLabel !== item.groupLabel) {
|
||||
picks.push({ type: 'separator', label: item.groupLabel });
|
||||
lastGroupLabel = item.groupLabel;
|
||||
if (lastGroupLabel !== item.toolInfo.label) {
|
||||
picks.push({ type: 'separator', label: item.toolInfo.label });
|
||||
lastGroupLabel = item.toolInfo.label;
|
||||
}
|
||||
picks.push(item);
|
||||
}
|
||||
|
@ -119,17 +118,6 @@ class ToolsContextPickerPick implements IChatContextPickerItem {
|
|||
placeholder: localize('chatContext.tools.placeholder', 'Select a tool'),
|
||||
picks: Promise.resolve(picks)
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
private _classify(tool: IToolData) {
|
||||
if (tool.source.type === 'internal' || tool.source.type === 'extension' && !tool.source.isExternalTool) {
|
||||
return { ordinal: 1, groupLabel: localize('chatContext.tools.internal', 'Built-In') };
|
||||
} else if (tool.source.type === 'mcp') {
|
||||
return { ordinal: 2, groupLabel: localize('chatContext.tools.mcp', 'MCP Servers') };
|
||||
} else {
|
||||
return { ordinal: 3, groupLabel: localize('chatContext.tools.extension', 'Extensions') };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -259,6 +259,7 @@ export function registerChatTitleActions() {
|
|||
}
|
||||
const request = chatModel?.getRequests().find(candidate => candidate.id === item.requestId);
|
||||
const languageModelId = widget?.input.currentLanguageModel;
|
||||
|
||||
let userSelectedTools: Record<string, boolean> | undefined;
|
||||
if (widget?.input.currentMode === ChatMode.Agent) {
|
||||
userSelectedTools = {};
|
||||
|
|
|
@ -7,16 +7,15 @@ import { assertNever } from '../../../../../base/common/assert.js';
|
|||
import { Codicon } from '../../../../../base/common/codicons.js';
|
||||
import { diffSets } from '../../../../../base/common/collections.js';
|
||||
import { Event } from '../../../../../base/common/event.js';
|
||||
import { Iterable } from '../../../../../base/common/iterator.js';
|
||||
import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
|
||||
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
|
||||
import { ThemeIcon } from '../../../../../base/common/themables.js';
|
||||
import { generateUuid } from '../../../../../base/common/uuid.js';
|
||||
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
|
||||
import { localize, localize2 } from '../../../../../nls.js';
|
||||
import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';
|
||||
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
|
||||
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
|
||||
import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js';
|
||||
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
|
||||
import { IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js';
|
||||
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
|
||||
|
@ -29,8 +28,9 @@ import { ChatContextKeys } from '../../common/chatContextKeys.js';
|
|||
import { IChatToolInvocation } from '../../common/chatService.js';
|
||||
import { isResponseVM } from '../../common/chatViewModel.js';
|
||||
import { ChatMode } from '../../common/constants.js';
|
||||
import { ILanguageModelToolsService, IToolData, ToolDataSource } from '../../common/languageModelToolsService.js';
|
||||
import { IToolData, ToolSet, ToolDataSource } from '../../common/languageModelToolsService.js';
|
||||
import { IChatWidget, IChatWidgetService } from '../chat.js';
|
||||
import { ConfigureToolSets } from '../tools/toolSetsContribution.js';
|
||||
import { CHAT_CATEGORY } from './chatActions.js';
|
||||
|
||||
|
||||
|
@ -81,14 +81,12 @@ class AcceptToolConfirmation extends Action2 {
|
|||
}
|
||||
}
|
||||
|
||||
export class AttachToolsAction extends Action2 {
|
||||
|
||||
static readonly id = 'workbench.action.chat.attachTools';
|
||||
class ConfigureToolsAction extends Action2 {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: AttachToolsAction.id,
|
||||
title: localize('label', "Select Tools..."),
|
||||
id: 'workbench.action.chat.configureTools',
|
||||
title: localize('label', "Configure Tools..."),
|
||||
icon: Codicon.tools,
|
||||
f1: false,
|
||||
category: CHAT_CATEGORY,
|
||||
|
@ -98,11 +96,6 @@ export class AttachToolsAction extends Action2 {
|
|||
id: MenuId.ChatExecute,
|
||||
group: 'navigation',
|
||||
order: 1,
|
||||
},
|
||||
keybinding: {
|
||||
when: ContextKeyExpr.and(ChatContextKeys.inChatInput, ChatContextKeys.chatMode.isEqualTo(ChatMode.Agent)),
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Slash,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -112,7 +105,6 @@ export class AttachToolsAction extends Action2 {
|
|||
const quickPickService = accessor.get(IQuickInputService);
|
||||
const mcpService = accessor.get(IMcpService);
|
||||
const mcpRegistry = accessor.get(IMcpRegistry);
|
||||
const toolsService = accessor.get(ILanguageModelToolsService);
|
||||
const chatWidgetService = accessor.get(IChatWidgetService);
|
||||
const telemetryService = accessor.get(ITelemetryService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
|
@ -142,133 +134,162 @@ export class AttachToolsAction extends Action2 {
|
|||
}
|
||||
}
|
||||
|
||||
const enum BucketOrdinal { Extension, Mcp, Other }
|
||||
type BucketPick = IQuickPickItem & { picked: boolean; ordinal: BucketOrdinal; status?: string; children: ToolPick[]; source: ToolDataSource };
|
||||
const enum BucketOrdinal { User, Mcp, Extension, BuiltIn }
|
||||
type BucketPick = IQuickPickItem & { picked: boolean; ordinal: BucketOrdinal; status?: string; children: (ToolPick | ToolSetPick)[] };
|
||||
type ToolSetPick = IQuickPickItem & { picked: boolean; toolset: ToolSet; parent: BucketPick };
|
||||
type ToolPick = IQuickPickItem & { picked: boolean; tool: IToolData; parent: BucketPick };
|
||||
type AddPick = IQuickPickItem & { pickable: false; run: () => void };
|
||||
type MyPick = ToolPick | BucketPick | AddPick;
|
||||
type CallbackPick = IQuickPickItem & { pickable: false; run: () => void };
|
||||
type MyPick = BucketPick | ToolSetPick | ToolPick | CallbackPick;
|
||||
type ActionableButton = IQuickInputButton & { action: () => void };
|
||||
|
||||
const addMcpPick: AddPick = { type: 'item', label: localize('addServer', "Add MCP Server..."), iconClass: ThemeIcon.asClassName(Codicon.add), pickable: false, run: () => commandService.executeCommand(AddConfigurationAction.ID) };
|
||||
const addExpPick: AddPick = { type: 'item', label: localize('addExtension', "Install Extension..."), iconClass: ThemeIcon.asClassName(Codicon.add), pickable: false, run: () => extensionWorkbenchService.openSearch('@tag:language-model-tools') };
|
||||
const addPick: AddPick = {
|
||||
function isBucketPick(obj: any): obj is BucketPick {
|
||||
return Boolean((obj as BucketPick).children);
|
||||
}
|
||||
function isToolSetPick(obj: MyPick): obj is ToolSetPick {
|
||||
return Boolean((obj as ToolSetPick).toolset);
|
||||
}
|
||||
function isToolPick(obj: MyPick): obj is ToolPick {
|
||||
return Boolean((obj as ToolPick).tool);
|
||||
}
|
||||
function isCallbackPick(obj: MyPick): obj is CallbackPick {
|
||||
return Boolean((obj as CallbackPick).run);
|
||||
}
|
||||
function isActionableButton(obj: IQuickInputButton): obj is ActionableButton {
|
||||
return typeof (obj as ActionableButton).action === 'function';
|
||||
}
|
||||
|
||||
const addMcpPick: CallbackPick = { type: 'item', label: localize('addServer', "Add MCP Server..."), iconClass: ThemeIcon.asClassName(Codicon.add), pickable: false, run: () => commandService.executeCommand(AddConfigurationAction.ID) };
|
||||
const configureToolSetsPick: CallbackPick = { type: 'item', label: localize('configToolSet', "Configure Tool Sets..."), iconClass: ThemeIcon.asClassName(Codicon.tools), pickable: false, run: () => commandService.executeCommand(ConfigureToolSets.ID) };
|
||||
const addExpPick: CallbackPick = { type: 'item', label: localize('addExtension', "Install Extension..."), iconClass: ThemeIcon.asClassName(Codicon.add), pickable: false, run: () => extensionWorkbenchService.openSearch('@tag:language-model-tools') };
|
||||
const addPick: CallbackPick = {
|
||||
type: 'item', label: localize('addAny', "Add More Tools..."), iconClass: ThemeIcon.asClassName(Codicon.add), pickable: false, run: async () => {
|
||||
const pick = await quickPickService.pick(
|
||||
[addMcpPick, addExpPick],
|
||||
{
|
||||
canPickMany: false,
|
||||
title: localize('noTools', "Add tools to chat")
|
||||
placeHolder: localize('noTools', "Add tools to chat")
|
||||
}
|
||||
);
|
||||
pick?.run();
|
||||
}
|
||||
};
|
||||
|
||||
const defaultBucket: BucketPick = {
|
||||
const builtinBucket: BucketPick = {
|
||||
type: 'item',
|
||||
children: [],
|
||||
label: localize('defaultBucketLabel', "Other Tools"),
|
||||
source: { type: 'internal' },
|
||||
ordinal: BucketOrdinal.Other,
|
||||
picked: true,
|
||||
label: localize('defaultBucketLabel', "Built-In"),
|
||||
ordinal: BucketOrdinal.BuiltIn,
|
||||
picked: false,
|
||||
};
|
||||
|
||||
const mcpBucket: BucketPick = {
|
||||
type: 'item',
|
||||
children: [],
|
||||
label: localize('mcp', "MCP Server"),
|
||||
ordinal: BucketOrdinal.Mcp,
|
||||
alwaysShow: true,
|
||||
picked: false,
|
||||
};
|
||||
|
||||
const userBucket: BucketPick = {
|
||||
type: 'item',
|
||||
children: [],
|
||||
label: localize('userBucket', "User Defined"),
|
||||
ordinal: BucketOrdinal.User,
|
||||
alwaysShow: true,
|
||||
picked: false,
|
||||
};
|
||||
|
||||
const nowSelectedTools = new Set(widget.input.selectedToolsModel.tools.get());
|
||||
const toolBuckets = new Map<string, BucketPick>();
|
||||
|
||||
for (const tool of toolsService.getTools()) {
|
||||
if (!tool.canBeReferencedInPrompt) {
|
||||
continue;
|
||||
}
|
||||
for (const [toolSetOrTool, picked] of widget.input.selectedToolsModel.entriesMap) {
|
||||
|
||||
let bucket: BucketPick | undefined;
|
||||
let buttons: ActionableButton[] | undefined;
|
||||
let description: string | undefined;
|
||||
|
||||
if (tool.source.type === 'mcp') {
|
||||
const mcpServer = mcpServerByTool.get(tool.id);
|
||||
if (toolSetOrTool.source.type === 'mcp') {
|
||||
const { definitionId } = toolSetOrTool.source;
|
||||
const mcpServer = mcpService.servers.get().find(candidate => candidate.definition.id === definitionId);
|
||||
if (!mcpServer) {
|
||||
continue;
|
||||
}
|
||||
const key = tool.source.type + mcpServer.definition.id;
|
||||
bucket = toolBuckets.get(key);
|
||||
bucket = mcpBucket;
|
||||
|
||||
if (!bucket) {
|
||||
const collection = mcpRegistry.collections.get().find(c => c.id === mcpServer.collection.id);
|
||||
const buttons: ActionableButton[] = [];
|
||||
if (collection?.presentation?.origin) {
|
||||
buttons.push({
|
||||
iconClass: ThemeIcon.asClassName(Codicon.settingsGear),
|
||||
tooltip: localize('configMcpCol', "Configure {0}", collection.label),
|
||||
action: () => editorService.openEditor({
|
||||
resource: collection!.presentation!.origin,
|
||||
})
|
||||
});
|
||||
}
|
||||
if (mcpServer.connectionState.get().state === McpConnectionState.Kind.Error) {
|
||||
buttons.push({
|
||||
iconClass: ThemeIcon.asClassName(Codicon.warning),
|
||||
tooltip: localize('mcpShowOutput', "Show Output"),
|
||||
action: () => mcpServer.showOutput(),
|
||||
});
|
||||
}
|
||||
|
||||
bucket = {
|
||||
type: 'item',
|
||||
label: localize('mcplabel', "MCP Server: {0}", mcpServer?.definition.label),
|
||||
status: localize('mcpstatus', "from {0}", mcpServer.collection.label),
|
||||
ordinal: BucketOrdinal.Mcp,
|
||||
source: tool.source,
|
||||
picked: false,
|
||||
children: [],
|
||||
buttons,
|
||||
};
|
||||
toolBuckets.set(key, bucket);
|
||||
// if (!bucket) {
|
||||
buttons = [];
|
||||
const collection = mcpRegistry.collections.get().find(c => c.id === mcpServer.collection.id);
|
||||
if (collection?.presentation?.origin) {
|
||||
buttons.push({
|
||||
iconClass: ThemeIcon.asClassName(Codicon.settingsGear),
|
||||
tooltip: localize('configMcpCol', "Configure {0}", collection.label),
|
||||
action: () => editorService.openEditor({
|
||||
resource: collection!.presentation!.origin,
|
||||
})
|
||||
});
|
||||
}
|
||||
if (mcpServer.connectionState.get().state === McpConnectionState.Kind.Error) {
|
||||
buttons.push({
|
||||
iconClass: ThemeIcon.asClassName(Codicon.warning),
|
||||
tooltip: localize('mcpShowOutput', "Show Output"),
|
||||
action: () => mcpServer.showOutput(),
|
||||
});
|
||||
}
|
||||
} else if (tool.source.type === 'extension') {
|
||||
const key = tool.source.type + ExtensionIdentifier.toKey(tool.source.extensionId);
|
||||
|
||||
description = localize('mcplabel', "MCP Server: {0}", mcpServer?.definition.label);
|
||||
|
||||
} else if (toolSetOrTool.source.type === 'extension') {
|
||||
const key = ToolDataSource.toKey(toolSetOrTool.source);
|
||||
bucket = toolBuckets.get(key) ?? {
|
||||
type: 'item',
|
||||
label: tool.source.label,
|
||||
label: toolSetOrTool.source.label,
|
||||
ordinal: BucketOrdinal.Extension,
|
||||
picked: false,
|
||||
source: tool.source,
|
||||
alwaysShow: true,
|
||||
children: []
|
||||
};
|
||||
toolBuckets.set(key, bucket);
|
||||
} else if (tool.source.type === 'internal') {
|
||||
bucket = defaultBucket;
|
||||
} else if (toolSetOrTool.source.type === 'internal') {
|
||||
bucket = builtinBucket;
|
||||
} else if (toolSetOrTool.source.type === 'user') {
|
||||
bucket = userBucket;
|
||||
} else {
|
||||
assertNever(tool.source);
|
||||
assertNever(toolSetOrTool.source);
|
||||
}
|
||||
|
||||
const picked = nowSelectedTools.has(tool);
|
||||
if (toolSetOrTool instanceof ToolSet) {
|
||||
bucket.children.push({
|
||||
parent: bucket,
|
||||
type: 'item',
|
||||
picked,
|
||||
toolset: toolSetOrTool,
|
||||
label: toolSetOrTool.displayName,
|
||||
description: description ?? toolSetOrTool.description,
|
||||
indented: true,
|
||||
buttons
|
||||
|
||||
bucket.children.push({
|
||||
tool,
|
||||
parent: bucket,
|
||||
type: 'item',
|
||||
label: tool.displayName,
|
||||
description: tool.userDescription,
|
||||
picked,
|
||||
indented: true,
|
||||
});
|
||||
});
|
||||
} else if (toolSetOrTool.canBeReferencedInPrompt) {
|
||||
bucket.children.push({
|
||||
parent: bucket,
|
||||
type: 'item',
|
||||
picked,
|
||||
tool: toolSetOrTool,
|
||||
label: toolSetOrTool.toolReferenceName ?? toolSetOrTool.displayName,
|
||||
description: toolSetOrTool.userDescription,
|
||||
indented: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (picked) {
|
||||
bucket.picked = true;
|
||||
}
|
||||
}
|
||||
|
||||
function isBucketPick(obj: any): obj is BucketPick {
|
||||
return Boolean((obj as BucketPick).children);
|
||||
}
|
||||
function isToolPick(obj: any): obj is ToolPick {
|
||||
return Boolean((obj as ToolPick).tool);
|
||||
}
|
||||
function isAddPick(obj: any): obj is AddPick {
|
||||
return Boolean((obj as AddPick).run);
|
||||
}
|
||||
function isActionableButton(obj: IQuickInputButton): obj is ActionableButton {
|
||||
return typeof (obj as ActionableButton).action === 'function';
|
||||
for (const bucket of [builtinBucket, mcpBucket, userBucket]) {
|
||||
if (bucket.children.length > 0) {
|
||||
toolBuckets.set(generateUuid(), bucket);
|
||||
}
|
||||
}
|
||||
|
||||
const store = new DisposableStore();
|
||||
|
@ -289,6 +310,7 @@ export class AttachToolsAction extends Action2 {
|
|||
picker.placeholder = localize('placeholder', "Select tools that are available to chat");
|
||||
picker.canSelectMany = true;
|
||||
picker.keepScrollPosition = true;
|
||||
picker.sortByLabel = false;
|
||||
picker.matchOnDescription = true;
|
||||
|
||||
if (picks.length === 0) {
|
||||
|
@ -301,6 +323,7 @@ export class AttachToolsAction extends Action2 {
|
|||
} else {
|
||||
picks.push(
|
||||
{ type: 'separator' },
|
||||
configureToolSetsPick,
|
||||
addPick,
|
||||
);
|
||||
}
|
||||
|
@ -316,19 +339,29 @@ export class AttachToolsAction extends Action2 {
|
|||
lastSelectedItems = new Set(items);
|
||||
picker.selectedItems = items;
|
||||
|
||||
const disableBuckets: ToolDataSource[] = [];
|
||||
const disableToolSets: ToolSet[] = [];
|
||||
const disableTools: IToolData[] = [];
|
||||
|
||||
|
||||
for (const item of picks) {
|
||||
if (item.type === 'item' && !item.picked) {
|
||||
if (isBucketPick(item)) {
|
||||
disableBuckets.push(item.source);
|
||||
} else if (isToolPick(item) && item.parent.picked) {
|
||||
if (isToolSetPick(item)) {
|
||||
disableToolSets.push(item.toolset);
|
||||
} else if (isToolPick(item)) {
|
||||
disableTools.push(item.tool);
|
||||
} else if (isBucketPick(item)) {
|
||||
for (const child of item.children) {
|
||||
if (isToolSetPick(child)) {
|
||||
disableToolSets.push(child.toolset);
|
||||
} else if (isToolPick(child)) {
|
||||
disableTools.push(child.tool);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
widget.input.selectedToolsModel.update(disableBuckets, disableTools);
|
||||
widget.input.selectedToolsModel.update(disableToolSets, disableTools);
|
||||
} finally {
|
||||
ignoreEvent = false;
|
||||
}
|
||||
|
@ -350,7 +383,7 @@ export class AttachToolsAction extends Action2 {
|
|||
return;
|
||||
}
|
||||
|
||||
const addPick = selectedPicks.find(isAddPick);
|
||||
const addPick = selectedPicks.find(isCallbackPick);
|
||||
if (addPick) {
|
||||
addPick.run();
|
||||
picker.hide();
|
||||
|
@ -367,7 +400,7 @@ export class AttachToolsAction extends Action2 {
|
|||
for (const toolPick of item.children) {
|
||||
toolPick.picked = true;
|
||||
}
|
||||
} else if (isToolPick(item)) {
|
||||
} else if (isToolPick(item) || isToolSetPick(item)) {
|
||||
// add server when tool is picked
|
||||
item.parent.picked = true;
|
||||
}
|
||||
|
@ -381,7 +414,7 @@ export class AttachToolsAction extends Action2 {
|
|||
for (const toolPick of item.children) {
|
||||
toolPick.picked = false;
|
||||
}
|
||||
} else if (isToolPick(item) && item.parent.children.every(child => !child.picked)) {
|
||||
} else if ((isToolPick(item) || isToolSetPick(item)) && item.parent.children.every(child => !child.picked)) {
|
||||
// remove LAST tool -> remove server
|
||||
item.parent.picked = false;
|
||||
}
|
||||
|
@ -391,13 +424,14 @@ export class AttachToolsAction extends Action2 {
|
|||
}));
|
||||
|
||||
store.add(picker.onDidAccept(() => {
|
||||
picker.activeItems.find(isAddPick)?.run();
|
||||
picker.activeItems.find(isCallbackPick)?.run();
|
||||
}));
|
||||
|
||||
await Promise.race([Event.toPromise(Event.any(picker.onDidAccept, picker.onDidHide))]);
|
||||
|
||||
telemetryService.publicLog2<SelectedToolData, SelectedToolClassification>('chat/selectedTools', {
|
||||
enabled: widget.input.selectedToolsModel.tools.get().length,
|
||||
total: Iterable.length(toolsService.getTools()),
|
||||
total: widget.input.selectedToolsModel.entriesMap.size,
|
||||
enabled: widget.input.selectedToolsModel.entries.get().size,
|
||||
});
|
||||
store.dispose();
|
||||
}
|
||||
|
@ -405,5 +439,5 @@ export class AttachToolsAction extends Action2 {
|
|||
|
||||
export function registerChatToolActions() {
|
||||
registerAction2(AcceptToolConfirmation);
|
||||
registerAction2(AttachToolsAction);
|
||||
registerAction2(ConfigureToolsAction);
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ import { PromptsService } from '../common/promptSyntax/service/promptsService.js
|
|||
import { IPromptsService } from '../common/promptSyntax/service/types.js';
|
||||
import { LanguageModelToolsExtensionPointHandler } from '../common/tools/languageModelToolsContribution.js';
|
||||
import { BuiltinToolsContribution } from '../common/tools/tools.js';
|
||||
import { ConfigureToolSets, UserToolSetsContributions } from './tools/toolSetsContribution.js';
|
||||
import { IVoiceChatService, VoiceChatService } from '../common/voiceChatService.js';
|
||||
import { AgentChatAccessibilityHelp, EditsChatAccessibilityHelp, PanelChatAccessibilityHelp, QuickChatAccessibilityHelp } from './actions/chatAccessibilityHelp.js';
|
||||
import { CopilotTitleBarMenuRendering, registerChatActions, ACTION_ID_NEW_CHAT } from './actions/chatActions.js';
|
||||
|
@ -105,6 +106,7 @@ import { ChatRelatedFilesContribution } from './contrib/chatInputRelatedFilesCon
|
|||
import { LanguageModelToolsService } from './languageModelToolsService.js';
|
||||
import './promptSyntax/contributions/createPromptCommand/createPromptCommand.js';
|
||||
import { ChatViewsWelcomeHandler } from './viewsWelcome/chatViewsWelcomeHandler.js';
|
||||
import { registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
|
||||
// Register configuration
|
||||
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
|
||||
|
@ -685,3 +687,6 @@ registerSingleton(IChatContextPickService, ChatContextPickService, Instantiation
|
|||
registerWorkbenchContribution2(ChatEditingNotebookFileSystemProviderContrib.ID, ChatEditingNotebookFileSystemProviderContrib, WorkbenchPhase.BlockStartup);
|
||||
|
||||
registerPromptFileContributions();
|
||||
|
||||
registerWorkbenchContribution2(UserToolSetsContributions.ID, UserToolSetsContributions, WorkbenchPhase.Eventually);
|
||||
registerAction2(ConfigureToolSets);
|
||||
|
|
|
@ -49,8 +49,6 @@ export class ChatAttachmentModel extends Disposable {
|
|||
return Array.from(this._attachments.values());
|
||||
}
|
||||
|
||||
|
||||
|
||||
get size(): number {
|
||||
return this._attachments.size;
|
||||
}
|
||||
|
@ -98,18 +96,18 @@ export class ChatAttachmentModel extends Disposable {
|
|||
|
||||
addContext(...attachments: IChatRequestVariableEntry[]) {
|
||||
attachments = attachments.filter(attachment => !this._attachments.has(attachment.id));
|
||||
this.updateContent(Iterable.empty(), attachments);
|
||||
this.updateContext(Iterable.empty(), attachments);
|
||||
}
|
||||
|
||||
clearAndSetContext(...attachments: IChatRequestVariableEntry[]) {
|
||||
this.updateContent(Array.from(this._attachments.keys()), attachments);
|
||||
this.updateContext(Array.from(this._attachments.keys()), attachments);
|
||||
}
|
||||
|
||||
delete(...variableEntryIds: string[]) {
|
||||
this.updateContent(variableEntryIds, Iterable.empty());
|
||||
this.updateContext(variableEntryIds, Iterable.empty());
|
||||
}
|
||||
|
||||
updateContent(toDelete: Iterable<string>, upsert: Iterable<IChatRequestVariableEntry>) {
|
||||
updateContext(toDelete: Iterable<string>, upsert: Iterable<IChatRequestVariableEntry>) {
|
||||
const deleted: string[] = [];
|
||||
const added: IChatRequestVariableEntry[] = [];
|
||||
const updated: IChatRequestVariableEntry[] = [];
|
||||
|
|
|
@ -4,21 +4,23 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { derived, IObservable, observableFromEvent } from '../../../../base/common/observable.js';
|
||||
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { autorun, IObservable, observableFromEvent, ObservableMap } from '../../../../base/common/observable.js';
|
||||
import { ObservableMemento, observableMemento } from '../../../../platform/observable/common/observableMemento.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
||||
import { ChatMode } from '../common/constants.js';
|
||||
import { ILanguageModelToolsService, IToolData, ToolDataSource } from '../common/languageModelToolsService.js';
|
||||
import { ILanguageModelToolsService, IToolData, ToolSet, ToolDataSource } from '../common/languageModelToolsService.js';
|
||||
|
||||
/**
|
||||
* New tools and new tool sources that come in should generally be enabled until
|
||||
* the user disables them. To store things, we store only the buckets and
|
||||
* the user disables them. To store things, we store only the tool sets and
|
||||
* individual tools that were disabled, so the new data sources that come in
|
||||
* are enabled, and new tools that come in for data sources not disabled are
|
||||
* also enabled.
|
||||
*/
|
||||
type StoredData = { disabledBuckets?: /* ToolDataSource.toKey */ readonly string[]; disabledTools?: readonly string[] };
|
||||
type StoredData = {
|
||||
disabledToolSets?: readonly string[];
|
||||
disabledTools?: readonly string[];
|
||||
};
|
||||
|
||||
const storedTools = observableMemento<StoredData>({
|
||||
defaultValue: {},
|
||||
|
@ -29,42 +31,96 @@ export class ChatSelectedTools extends Disposable {
|
|||
|
||||
private readonly _selectedTools: ObservableMemento<StoredData>;
|
||||
|
||||
readonly tools: IObservable<IToolData[]>;
|
||||
|
||||
private readonly _allTools: IObservable<Readonly<IToolData>[]>;
|
||||
|
||||
/**
|
||||
* All tools and tool sets with their enabled state.
|
||||
*/
|
||||
readonly entriesMap: ObservableMap<IToolData | ToolSet, boolean> = new ObservableMap<ToolSet | IToolData, boolean>();
|
||||
|
||||
/**
|
||||
* All enabled tools and tool sets.
|
||||
*/
|
||||
readonly entries: IObservable<ReadonlySet<IToolData | ToolSet>> = this.entriesMap.observable.map(function (value) {
|
||||
const result = new Set<IToolData | ToolSet>();
|
||||
for (const [item, enabled] of value) {
|
||||
if (enabled) {
|
||||
result.add(item);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
constructor(
|
||||
mode: IObservable<ChatMode>,
|
||||
@ILanguageModelToolsService toolsService: ILanguageModelToolsService,
|
||||
@IInstantiationService instaService: IInstantiationService,
|
||||
@IStorageService storageService: IStorageService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._selectedTools = this._register(storedTools(StorageScope.WORKSPACE, StorageTarget.MACHINE, storageService));
|
||||
this._selectedTools = this._store.add(storedTools(StorageScope.WORKSPACE, StorageTarget.MACHINE, storageService));
|
||||
|
||||
this._allTools = observableFromEvent(toolsService.onDidChangeTools, () => Array.from(toolsService.getTools()));
|
||||
|
||||
const disabledData = this._selectedTools.map(data => {
|
||||
return (data.disabledBuckets?.length || data.disabledTools?.length) && {
|
||||
buckets: new Set(data.disabledBuckets),
|
||||
toolIds: new Set(data.disabledTools),
|
||||
};
|
||||
const disabledDataObs = this._selectedTools.map(data => {
|
||||
return (data.disabledToolSets?.length || data.disabledTools?.length)
|
||||
? {
|
||||
toolSetIds: new Set(data.disabledToolSets),
|
||||
toolIds: new Set(data.disabledTools),
|
||||
}
|
||||
: undefined;
|
||||
});
|
||||
|
||||
this.tools = derived(r => {
|
||||
const tools = this._allTools.read(r);
|
||||
if (mode.read(r) !== ChatMode.Agent) {
|
||||
return tools;
|
||||
this._store.add(autorun(r => {
|
||||
|
||||
const sourceByTool = new Map<IToolData, ToolDataSource>();
|
||||
|
||||
for (const tool of this._allTools.read(r)) {
|
||||
if (!tool.canBeReferencedInPrompt) {
|
||||
continue;
|
||||
}
|
||||
sourceByTool.set(tool, tool.source);
|
||||
}
|
||||
const disabled = disabledData.read(r);
|
||||
if (!disabled) {
|
||||
return tools;
|
||||
|
||||
const toolSets = toolsService.toolSets.read(r);
|
||||
|
||||
for (const toolSet of toolSets) {
|
||||
|
||||
if (!toolSet.isHomogenous.read(r)) {
|
||||
// only homogenous tool sets can shallow tools
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const toolInSet of toolSet.getTools(r)) {
|
||||
const source = sourceByTool.get(toolInSet);
|
||||
if (source && ToolDataSource.equals(source, toolInSet.source)) {
|
||||
sourceByTool.delete(toolInSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
return tools.filter(t =>
|
||||
!(disabled.toolIds.has(t.id) || disabled.buckets.has(ToolDataSource.toKey(t.source)))
|
||||
);
|
||||
});
|
||||
|
||||
const oldItems = new Set(this.entriesMap.keys());
|
||||
|
||||
const disabledData = mode.read(r) === ChatMode.Agent
|
||||
? disabledDataObs.read(r)
|
||||
: undefined;
|
||||
|
||||
for (const tool of sourceByTool.keys()) {
|
||||
const enabled = !disabledData || !disabledData.toolIds.has(tool.id);
|
||||
this.entriesMap.set(tool, enabled);
|
||||
oldItems.delete(tool);
|
||||
}
|
||||
|
||||
for (const toolSet of toolSets) {
|
||||
const enabled = !disabledData || !disabledData.toolSetIds.has(toolSet.id);
|
||||
this.entriesMap.set(toolSet, enabled);
|
||||
oldItems.delete(toolSet);
|
||||
}
|
||||
|
||||
for (const item of oldItems) {
|
||||
this.entriesMap.delete(item);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
selectOnly(toolIds: readonly string[]): void {
|
||||
|
@ -75,19 +131,34 @@ export class ChatSelectedTools extends Disposable {
|
|||
this.update([], disabledTools);
|
||||
}
|
||||
|
||||
update(disableBuckets: readonly ToolDataSource[], disableTools: readonly IToolData[]): void {
|
||||
update(disabledToolSets: readonly ToolSet[], disableTools: readonly IToolData[]): void {
|
||||
this._selectedTools.set({
|
||||
disabledBuckets: disableBuckets.map(ToolDataSource.toKey),
|
||||
disabledToolSets: disabledToolSets.map(t => t.id),
|
||||
disabledTools: disableTools.map(t => t.id)
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
asEnablementMap(): Map<IToolData, boolean> {
|
||||
const result = new Map<IToolData, boolean>();
|
||||
const enabledTools = new Set(this.tools.get().map(t => t.id));
|
||||
for (const tool of this._allTools.get()) {
|
||||
if (tool.canBeReferencedInPrompt) {
|
||||
result.set(tool, enabledTools.has(tool.id));
|
||||
|
||||
const _set = (tool: IToolData, enabled: boolean) => {
|
||||
if (!tool.canBeReferencedInPrompt) {
|
||||
return;
|
||||
}
|
||||
// ONLY disable a tool that isn't enabled yet
|
||||
const enabledNow = result.get(tool);
|
||||
if (enabled || !enabledNow) {
|
||||
result.set(tool, enabled);
|
||||
}
|
||||
};
|
||||
|
||||
for (const [item, enabled] of this.entriesMap) {
|
||||
if (item instanceof ToolSet) {
|
||||
for (const tool of item.getTools()) {
|
||||
_set(tool, enabled);
|
||||
}
|
||||
} else {
|
||||
_set(item, enabled);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
|
|
@ -53,7 +53,7 @@ import { IHostService } from '../../../services/host/browser/host.js';
|
|||
import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js';
|
||||
import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js';
|
||||
import { IViewsService } from '../../../services/views/common/viewsService.js';
|
||||
import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolProgress } from '../../chat/common/languageModelToolsService.js';
|
||||
import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolDataSource, ToolProgress } from '../../chat/common/languageModelToolsService.js';
|
||||
import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js';
|
||||
import { IChatAgentImplementation, IChatAgentRequest, IChatAgentResult, IChatAgentService } from '../common/chatAgents.js';
|
||||
import { ChatContextKeys } from '../common/chatContextKeys.js';
|
||||
|
@ -144,9 +144,7 @@ class SetupAgent extends Disposable implements IChatAgentImplementation {
|
|||
|
||||
disposables.add(SetupTool.registerTool(instantiationService, {
|
||||
id: 'setup.tools.createNewWorkspace',
|
||||
source: {
|
||||
type: 'internal',
|
||||
},
|
||||
source: ToolDataSource.Internal,
|
||||
icon: Codicon.newFolder,
|
||||
displayName: localize('setupToolDisplayName', "New Workspace"),
|
||||
modelDescription: localize('setupToolsDescription', "Scaffold a new workspace in VS Code"),
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IChatVariablesService, IDynamicVariable } from '../common/chatVariables.js';
|
||||
import { IToolData } from '../common/languageModelToolsService.js';
|
||||
import { IToolData, ToolSet } from '../common/languageModelToolsService.js';
|
||||
import { IChatWidgetService } from './chat.js';
|
||||
import { ChatDynamicVariableModel } from './contrib/chatDynamicVariables.js';
|
||||
|
||||
|
@ -38,7 +38,17 @@ export class ChatVariablesService implements IChatVariablesService {
|
|||
if (!widget) {
|
||||
return [];
|
||||
}
|
||||
return widget.input.selectedToolsModel.tools.get();
|
||||
return Array.from(widget.input.selectedToolsModel.entries.get())
|
||||
.filter((t): t is IToolData => !(t instanceof ToolSet));
|
||||
|
||||
}
|
||||
getSelectedToolSets(sessionId: string): ReadonlyArray<ToolSet> {
|
||||
const widget = this.chatWidgetService.getWidgetBySessionId(sessionId);
|
||||
if (!widget) {
|
||||
return [];
|
||||
}
|
||||
return Array.from(widget.input.selectedToolsModel.entries.get())
|
||||
.filter((t): t is ToolSet => t instanceof ToolSet);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ import { IChatAgentCommand, IChatAgentData, IChatAgentService, IChatWelcomeMessa
|
|||
import { ChatContextKeys } from '../common/chatContextKeys.js';
|
||||
import { applyingChatEditsFailedContextKey, decidedChatEditingResourceContextKey, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, inChatEditingSessionContextKey, ModifiedFileEntryState } from '../common/chatEditingService.js';
|
||||
import { ChatPauseState, IChatModel, IChatRequestVariableEntry, IChatResponseModel } from '../common/chatModel.js';
|
||||
import { chatAgentLeader, ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestSlashPromptPart, ChatRequestToolPart, chatSubcommandLeader, formatChatQuestion, IParsedChatRequest } from '../common/chatParserTypes.js';
|
||||
import { chatAgentLeader, ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestSlashPromptPart, ChatRequestToolPart, ChatRequestToolSetPart, chatSubcommandLeader, formatChatQuestion, IParsedChatRequest } from '../common/chatParserTypes.js';
|
||||
import { ChatRequestParser } from '../common/chatRequestParser.js';
|
||||
import { IChatLocationData, IChatSendRequestOptions, IChatService } from '../common/chatService.js';
|
||||
import { IChatSlashCommandService } from '../common/chatSlashCommands.js';
|
||||
|
@ -53,7 +53,7 @@ import { ChatViewModel, IChatResponseViewModel, isRequestVM, isResponseVM } from
|
|||
import { IChatInputState } from '../common/chatWidgetHistoryService.js';
|
||||
import { CodeBlockModelCollection } from '../common/codeBlockModelCollection.js';
|
||||
import { ChatAgentLocation, ChatMode } from '../common/constants.js';
|
||||
import { ILanguageModelToolsService } from '../common/languageModelToolsService.js';
|
||||
import { ILanguageModelToolsService, ToolSet } from '../common/languageModelToolsService.js';
|
||||
import { IPromptsService } from '../common/promptSyntax/service/types.js';
|
||||
import { handleModeSwitch } from './actions/chatActions.js';
|
||||
import { ChatTreeItem, IChatAcceptInputOptions, IChatAccessibilityService, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions, IChatWidget, IChatWidgetService, IChatWidgetViewContext, IChatWidgetViewOptions } from './chat.js';
|
||||
|
@ -561,14 +561,14 @@ export class ChatWidget extends Disposable implements IChatWidget {
|
|||
|
||||
// update/insert prompt-referenced attachments
|
||||
for (const part of input.parts) {
|
||||
if (part instanceof ChatRequestToolPart || part instanceof ChatRequestDynamicVariablePart) {
|
||||
if (part instanceof ChatRequestToolPart || part instanceof ChatRequestToolSetPart || part instanceof ChatRequestDynamicVariablePart) {
|
||||
const entry = part.toVariableEntry();
|
||||
newPromptAttachments.set(entry.id, entry);
|
||||
oldPromptAttachments.delete(entry.id);
|
||||
}
|
||||
}
|
||||
|
||||
this.attachmentModel.updateContent(oldPromptAttachments, newPromptAttachments.values());
|
||||
this.attachmentModel.updateContext(oldPromptAttachments, newPromptAttachments.values());
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -1025,13 +1025,30 @@ export class ChatWidget extends Disposable implements IChatWidget {
|
|||
this.refreshParsedInput();
|
||||
this.renderFollowups();
|
||||
}));
|
||||
|
||||
|
||||
const enabledToolSetsAndTools = this.input.selectedToolsModel.entries.map(value => {
|
||||
const toolSetIds = new Set<string>();
|
||||
const toolIds = new Set<string>();
|
||||
for (const item of value) {
|
||||
if (item instanceof ToolSet) {
|
||||
toolSetIds.add(item.id);
|
||||
} else {
|
||||
toolIds.add(item.id);
|
||||
}
|
||||
}
|
||||
return { toolSetIds, toolIds };
|
||||
});
|
||||
|
||||
this._register(autorun(r => {
|
||||
const enabledTools = new Set(this.input.selectedToolsModel.tools.read(r).map(t => t.id));
|
||||
|
||||
const { toolSetIds, toolIds } = enabledToolSetsAndTools.read(r);
|
||||
|
||||
const disabledTools = this.inputPart.attachmentModel.attachments
|
||||
.filter(a => a.kind === 'tool' && !enabledTools.has(a.id))
|
||||
.filter(a => a.kind === 'tool' && !toolIds.has(a.id) || a.kind === 'toolset' && !toolSetIds.has(a.id))
|
||||
.map(a => a.id);
|
||||
|
||||
this.inputPart.attachmentModel.updateContent(disabledTools, Iterable.empty());
|
||||
this.inputPart.attachmentModel.updateContext(disabledTools, Iterable.empty());
|
||||
this.refreshParsedInput();
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -45,10 +45,11 @@ import { searchFilesAndFolders } from '../../../search/browser/chatContributions
|
|||
import { IChatAgentData, IChatAgentNameService, IChatAgentService, getFullyQualifiedId } from '../../common/chatAgents.js';
|
||||
import { IChatEditingService } from '../../common/chatEditingService.js';
|
||||
import { IChatRequestVariableEntry } from '../../common/chatModel.js';
|
||||
import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashPromptPart, ChatRequestTextPart, ChatRequestToolPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from '../../common/chatParserTypes.js';
|
||||
import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashPromptPart, ChatRequestTextPart, ChatRequestToolPart, ChatRequestToolSetPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from '../../common/chatParserTypes.js';
|
||||
import { IChatSlashCommandService } from '../../common/chatSlashCommands.js';
|
||||
import { IDynamicVariable } from '../../common/chatVariables.js';
|
||||
import { ChatAgentLocation, ChatMode } from '../../common/constants.js';
|
||||
import { ToolSet } from '../../common/languageModelToolsService.js';
|
||||
import { IPromptsService } from '../../common/promptSyntax/service/types.js';
|
||||
import { ChatSubmitAction } from '../actions/chatExecuteActions.js';
|
||||
import { IChatWidget, IChatWidgetService } from '../chat.js';
|
||||
|
@ -1101,35 +1102,50 @@ class ToolCompletions extends Disposable {
|
|||
return null;
|
||||
}
|
||||
|
||||
const usedTools = widget.parsedInput.parts.filter((p): p is ChatRequestToolPart => p instanceof ChatRequestToolPart);
|
||||
const usedToolNames = new Set(usedTools.map(v => v.toolName));
|
||||
const toolItems: CompletionItem[] = [];
|
||||
toolItems.push(...widget.input.selectedToolsModel.tools.get()
|
||||
.filter(t => t.canBeReferencedInPrompt)
|
||||
.filter(t => !usedToolNames.has(t.toolReferenceName ?? ''))
|
||||
.map((t): CompletionItem => {
|
||||
const source = t.source;
|
||||
const detail = source.type === 'mcp'
|
||||
? localize('desc', "MCP Server: {0}", source.label)
|
||||
: source.type === 'extension'
|
||||
? source.label
|
||||
: undefined;
|
||||
|
||||
const withLeader = `${chatVariableLeader}${t.toolReferenceName}`;
|
||||
return {
|
||||
label: withLeader,
|
||||
range,
|
||||
detail,
|
||||
insertText: withLeader + ' ',
|
||||
documentation: t.userDescription ?? t.modelDescription,
|
||||
kind: CompletionItemKind.Text,
|
||||
sortText: 'z'
|
||||
};
|
||||
}));
|
||||
const usedNames = new Set<string>();
|
||||
for (const part of widget.parsedInput.parts) {
|
||||
if (part instanceof ChatRequestToolPart) {
|
||||
usedNames.add(part.toolName);
|
||||
} else if (part instanceof ChatRequestToolSetPart) {
|
||||
usedNames.add(part.name);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
suggestions: toolItems
|
||||
};
|
||||
const suggestions: CompletionItem[] = [];
|
||||
|
||||
|
||||
const iter = widget.input.selectedToolsModel.entries.get();
|
||||
|
||||
for (const item of iter) {
|
||||
|
||||
if (usedNames.has(item.toolReferenceName ?? '')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let detail: string | undefined;
|
||||
|
||||
if (item instanceof ToolSet) {
|
||||
detail = item.description;
|
||||
|
||||
} else {
|
||||
const source = item.source;
|
||||
detail = localize('tool_source_completion', "{0}: {1}", source.label, item.displayName);
|
||||
}
|
||||
|
||||
const withLeader = `${chatVariableLeader}${item.toolReferenceName ?? item.displayName}`;
|
||||
suggestions.push({
|
||||
label: withLeader,
|
||||
range,
|
||||
detail,
|
||||
insertText: withLeader + ' ',
|
||||
kind: CompletionItemKind.Text,
|
||||
sortText: 'z',
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
return { suggestions };
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ import { inputPlaceholderForeground } from '../../../../../platform/theme/common
|
|||
import { IThemeService } from '../../../../../platform/theme/common/themeService.js';
|
||||
import { IChatAgentCommand, IChatAgentData, IChatAgentService } from '../../common/chatAgents.js';
|
||||
import { chatSlashCommandBackground, chatSlashCommandForeground } from '../../common/chatColors.js';
|
||||
import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestSlashPromptPart, ChatRequestTextPart, ChatRequestToolPart, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader } from '../../common/chatParserTypes.js';
|
||||
import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestSlashPromptPart, ChatRequestTextPart, ChatRequestToolPart, ChatRequestToolSetPart, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader } from '../../common/chatParserTypes.js';
|
||||
import { ChatRequestParser } from '../../common/chatRequestParser.js';
|
||||
import { IChatWidget } from '../chat.js';
|
||||
import { ChatWidget } from '../chatWidget.js';
|
||||
|
@ -234,7 +234,7 @@ class InputEditorDecorations extends Disposable {
|
|||
this.widget.inputEditor.setDecorationsByType(decorationDescription, slashCommandTextDecorationType, textDecorations);
|
||||
|
||||
const varDecorations: IDecorationOptions[] = [];
|
||||
const toolParts = parsedRequest.filter((p): p is ChatRequestToolPart => p instanceof ChatRequestToolPart);
|
||||
const toolParts = parsedRequest.filter((p): p is ChatRequestToolPart => p instanceof ChatRequestToolPart || p instanceof ChatRequestToolSetPart);
|
||||
for (const tool of toolParts) {
|
||||
varDecorations.push({ range: tool.editorRange });
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import { assertNever } from '../../../../base/common/assert.js';
|
|||
import { RunOnceScheduler } from '../../../../base/common/async.js';
|
||||
import { encodeBase64 } from '../../../../base/common/buffer.js';
|
||||
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
|
||||
import { Codicon } from '../../../../base/common/codicons.js';
|
||||
import { toErrorMessage } from '../../../../base/common/errorMessage.js';
|
||||
import { CancellationError, isCancellationError } from '../../../../base/common/errors.js';
|
||||
import { Emitter } from '../../../../base/common/event.js';
|
||||
|
@ -16,6 +17,8 @@ import { Iterable } from '../../../../base/common/iterator.js';
|
|||
import { Lazy } from '../../../../base/common/lazy.js';
|
||||
import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { LRUCache } from '../../../../base/common/map.js';
|
||||
import { IObservable, ObservableSet } from '../../../../base/common/observable.js';
|
||||
import { ThemeIcon } from '../../../../base/common/themables.js';
|
||||
import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js';
|
||||
import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';
|
||||
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
|
||||
|
@ -33,7 +36,7 @@ import { ChatModel } from '../common/chatModel.js';
|
|||
import { ChatToolInvocation } from '../common/chatProgressTypes/chatToolInvocation.js';
|
||||
import { IChatService } from '../common/chatService.js';
|
||||
import { ChatConfiguration } from '../common/constants.js';
|
||||
import { CountTokensCallback, createToolSchemaUri, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultInputOutputDetails, stringifyPromptTsxPart } from '../common/languageModelToolsService.js';
|
||||
import { CountTokensCallback, createToolSchemaUri, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultInputOutputDetails, ToolSet, stringifyPromptTsxPart, ToolDataSource } from '../common/languageModelToolsService.js';
|
||||
import { getToolConfirmationAlert } from './chatAccessibilityProvider.js';
|
||||
|
||||
const jsonSchemaRegistry = Registry.as<JSONContributionRegistry.IJSONContributionRegistry>(JSONContributionRegistry.Extensions.JSONContribution);
|
||||
|
@ -99,6 +102,12 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo
|
|||
|
||||
this._ctxToolsCount = ChatContextKeys.Tools.toolsCount.bindTo(_contextKeyService);
|
||||
}
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
this._callsByRequestId.forEach(calls => calls.forEach(call => call.store.dispose()));
|
||||
this._ctxToolsCount.reset();
|
||||
}
|
||||
|
||||
registerToolData(toolData: IToolData): IDisposable {
|
||||
if (this._tools.has(toolData.id)) {
|
||||
|
@ -153,14 +162,12 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo
|
|||
|
||||
getTools(): Iterable<Readonly<IToolData>> {
|
||||
const toolDatas = Iterable.map(this._tools.values(), i => i.data);
|
||||
const extensionToolsEnabled = this._configurationService.getValue(ChatConfiguration.ExtensionToolsEnabled);
|
||||
const extensionToolsEnabled = this._configurationService.getValue<boolean>(ChatConfiguration.ExtensionToolsEnabled);
|
||||
return Iterable.filter(
|
||||
toolDatas,
|
||||
toolData => {
|
||||
const satisfiesWhenClause = !toolData.when || this._contextKeyService.contextMatchesRules(toolData.when);
|
||||
const satisfiesExternalToolCheck = toolData.source.type === 'extension' && !extensionToolsEnabled ?
|
||||
!toolData.source.isExternalTool :
|
||||
true;
|
||||
const satisfiesExternalToolCheck = toolData.source.type !== 'extension' || !!extensionToolsEnabled;
|
||||
return satisfiesWhenClause && satisfiesExternalToolCheck;
|
||||
});
|
||||
}
|
||||
|
@ -447,11 +454,35 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo
|
|||
}
|
||||
}
|
||||
|
||||
public override dispose(): void {
|
||||
super.dispose();
|
||||
private readonly _toolSets = new ObservableSet<ToolSet>();
|
||||
|
||||
this._callsByRequestId.forEach(calls => calls.forEach(call => call.store.dispose()));
|
||||
this._ctxToolsCount.reset();
|
||||
readonly toolSets: IObservable<Iterable<ToolSet>> = this._toolSets.observable;
|
||||
|
||||
getToolSetByName(name: string): ToolSet | undefined {
|
||||
for (const toolSet of this._toolSets) {
|
||||
if (toolSet.toolReferenceName === name) {
|
||||
return toolSet;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
createToolSet(source: ToolDataSource, id: string, displayName: string, options?: { icon?: ThemeIcon; toolReferenceName?: string; description?: string }): ToolSet & IDisposable {
|
||||
|
||||
const that = this;
|
||||
|
||||
const result = new class extends ToolSet implements IDisposable {
|
||||
dispose(): void {
|
||||
if (that._toolSets.has(result)) {
|
||||
this._tools.clear();
|
||||
that._toolSets.delete(result);
|
||||
}
|
||||
|
||||
}
|
||||
}(id, displayName, options?.icon ?? Codicon.tools, source, options?.toolReferenceName ?? displayName, options?.description);
|
||||
|
||||
this._toolSets.add(result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,373 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { isFalsyOrEmpty } from '../../../../../base/common/arrays.js';
|
||||
import { CancellationTokenSource } from '../../../../../base/common/cancellation.js';
|
||||
import { Event } from '../../../../../base/common/event.js';
|
||||
import { Disposable, DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js';
|
||||
import { observableFromEvent, observableSignalFromEvent, autorun } from '../../../../../base/common/observable.js';
|
||||
import { basename, joinPath } from '../../../../../base/common/resources.js';
|
||||
import { isFalsyOrWhitespace } from '../../../../../base/common/strings.js';
|
||||
import { ThemeIcon } from '../../../../../base/common/themables.js';
|
||||
import { assertType, isObject } from '../../../../../base/common/types.js';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { localize, localize2 } from '../../../../../nls.js';
|
||||
import { Action2 } from '../../../../../platform/actions/common/actions.js';
|
||||
import { IFileService } from '../../../../../platform/files/common/files.js';
|
||||
import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ILogService } from '../../../../../platform/log/common/log.js';
|
||||
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js';
|
||||
import { IWorkbenchContribution } from '../../../../common/contributions.js';
|
||||
import { IExtensionService } from '../../../../services/extensions/common/extensions.js';
|
||||
import { ILifecycleService, LifecyclePhase } from '../../../../services/lifecycle/common/lifecycle.js';
|
||||
import { IUserDataProfileService } from '../../../../services/userDataProfile/common/userDataProfile.js';
|
||||
import { CHAT_CATEGORY } from '../actions/chatActions.js';
|
||||
import { ILanguageModelToolsService, IToolData, ToolSet } from '../../common/languageModelToolsService.js';
|
||||
import { IRawToolSetContribution } from '../../common/tools/languageModelToolsContribution.js';
|
||||
import { IEditorService } from '../../../../services/editor/common/editorService.js';
|
||||
import { Codicon, getAllCodicons } from '../../../../../base/common/codicons.js';
|
||||
import { isValidBasename } from '../../../../../base/common/extpath.js';
|
||||
import { ITextFileService } from '../../../../services/textfile/common/textfiles.js';
|
||||
import { parse } from '../../../../../base/common/jsonc.js';
|
||||
import { IJSONSchema } from '../../../../../base/common/jsonSchema.js';
|
||||
import * as JSONContributionRegistry from '../../../../../platform/jsonschemas/common/jsonContributionRegistry.js';
|
||||
import { Registry } from '../../../../../platform/registry/common/platform.js';
|
||||
|
||||
|
||||
const toolEnumValues: string[] = [];
|
||||
const toolEnumDescriptions: string[] = [];
|
||||
|
||||
const toolSetSchemaId = 'vscode://schemas/toolsets';
|
||||
const toolSetsSchema: IJSONSchema = {
|
||||
id: toolSetSchemaId,
|
||||
allowComments: true,
|
||||
allowTrailingCommas: true,
|
||||
defaultSnippets: [{
|
||||
label: localize('schema.default', "Empty tool set"),
|
||||
body: { '${1:toolSetName}': { 'tools': ['${2:toolName}'], 'description': '${3:description}', 'icon': '${4:$(tools)}' } }
|
||||
}],
|
||||
type: 'object',
|
||||
description: localize('toolsetSchema.json', 'User tool sets configuration'),
|
||||
|
||||
additionalProperties: {
|
||||
type: 'object',
|
||||
required: ['tools'],
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
tools: {
|
||||
description: localize('schema.tools', "A list of tools or tool sets to include in this tool set."),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
enum: toolEnumValues,
|
||||
enumDescriptions: toolEnumDescriptions,
|
||||
}
|
||||
},
|
||||
icon: {
|
||||
description: localize('schema.icon', "Icon to use for this tool set in the UI. Uses the `\\$(name)`-syntax, like `\\$(zap)`"),
|
||||
type: 'string',
|
||||
enum: Array.from(getAllCodicons(), icon => icon.id),
|
||||
markdownEnumDescriptions: Array.from(getAllCodicons(), icon => `$(${icon.id})`),
|
||||
},
|
||||
description: {
|
||||
description: localize('schema.description', "A short description of this tool set."),
|
||||
type: 'string'
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const reg = Registry.as<JSONContributionRegistry.IJSONContributionRegistry>(JSONContributionRegistry.Extensions.JSONContribution);
|
||||
|
||||
|
||||
abstract class RawToolSetsShape {
|
||||
|
||||
static readonly suffix = '.toolsets.jsonc';
|
||||
|
||||
static isToolSetFileName(uri: URI): boolean {
|
||||
return basename(uri).endsWith(RawToolSetsShape.suffix);
|
||||
}
|
||||
|
||||
static from(data: unknown) {
|
||||
if (!isObject(data)) {
|
||||
throw new Error(`Invalid tool set data`);
|
||||
}
|
||||
|
||||
const map = new Map<string, Exclude<IRawToolSetContribution, 'name'>>();
|
||||
|
||||
for (const [name, value] of Object.entries(data as RawToolSetsShape)) {
|
||||
|
||||
if (isFalsyOrWhitespace(name)) {
|
||||
throw new Error(`Tool set name cannot be empty`);
|
||||
}
|
||||
if (isFalsyOrEmpty(value.tools)) {
|
||||
throw new Error(`Tool set '${name}' cannot have an empty tools array`);
|
||||
}
|
||||
|
||||
map.set(name, {
|
||||
name,
|
||||
tools: value.tools,
|
||||
referenceName: value.referenceName,
|
||||
description: value.description,
|
||||
icon: value.icon,
|
||||
});
|
||||
}
|
||||
|
||||
return new class extends RawToolSetsShape { }(map);
|
||||
}
|
||||
|
||||
entries: ReadonlyMap<string, Exclude<IRawToolSetContribution, 'name'>>;
|
||||
|
||||
private constructor(entries: Map<string, Exclude<IRawToolSetContribution, 'name'>>) {
|
||||
this.entries = Object.freeze(new Map(entries));
|
||||
}
|
||||
}
|
||||
|
||||
export class UserToolSetsContributions extends Disposable implements IWorkbenchContribution {
|
||||
|
||||
static readonly ID = 'chat.userToolSets';
|
||||
|
||||
constructor(
|
||||
@IExtensionService extensionService: IExtensionService,
|
||||
@ILifecycleService lifecycleService: ILifecycleService,
|
||||
@ILanguageModelToolsService private readonly _languageModelToolsService: ILanguageModelToolsService,
|
||||
@IUserDataProfileService private readonly _userDataProfileService: IUserDataProfileService,
|
||||
@IFileService private readonly _fileService: IFileService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
) {
|
||||
super();
|
||||
Promise.allSettled([
|
||||
extensionService.whenInstalledExtensionsRegistered,
|
||||
lifecycleService.when(LifecyclePhase.Restored)
|
||||
]).then(() => this._initToolSets());
|
||||
|
||||
const toolsObs = observableFromEvent(this, _languageModelToolsService.onDidChangeTools, () => Array.from(_languageModelToolsService.getTools()));
|
||||
const store = this._store.add(new DisposableStore());
|
||||
|
||||
this._store.add(autorun(r => {
|
||||
const tools = toolsObs.read(r);
|
||||
const toolSets = this._languageModelToolsService.toolSets.read(r);
|
||||
|
||||
toolEnumValues.length = 0;
|
||||
toolEnumDescriptions.length = 0;
|
||||
|
||||
for (const tool of tools) {
|
||||
if (tool.toolReferenceName && tool.canBeReferencedInPrompt) {
|
||||
toolEnumValues.push(tool.toolReferenceName);
|
||||
toolEnumDescriptions.push(localize('tooldesc', "{0} ({1})", tool.userDescription ?? tool.modelDescription, tool.source.label));
|
||||
}
|
||||
}
|
||||
for (const toolSet of toolSets) {
|
||||
toolEnumValues.push(toolSet.toolReferenceName);
|
||||
toolEnumDescriptions.push(localize('toolsetdesc', "{0} ({1})", toolSet.description ?? toolSet.displayName ?? '', toolSet.source.label));
|
||||
}
|
||||
store.clear(); // reset old schema
|
||||
reg.registerSchema(toolSetSchemaId, toolSetsSchema, store);
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
private _initToolSets(): void {
|
||||
|
||||
const promptFolder = observableFromEvent(this, this._userDataProfileService.onDidChangeCurrentProfile, () => this._userDataProfileService.currentProfile.promptsHome);
|
||||
|
||||
const toolsSig = observableSignalFromEvent(this, this._languageModelToolsService.onDidChangeTools);
|
||||
const fileEventSig = observableSignalFromEvent(this, Event.filter(this._fileService.onDidFilesChange, e => e.affects(promptFolder.get())));
|
||||
|
||||
const store = this._store.add(new DisposableStore());
|
||||
|
||||
this._store.add(autorun(async r => {
|
||||
|
||||
store.clear();
|
||||
|
||||
toolsSig.read(r); // SIGNALS
|
||||
fileEventSig.read(r);
|
||||
|
||||
const uri = promptFolder.read(r);
|
||||
|
||||
const cts = new CancellationTokenSource();
|
||||
store.add(toDisposable(() => cts.dispose(true)));
|
||||
|
||||
const stat = await this._fileService.resolve(uri);
|
||||
|
||||
if (cts.token.isCancellationRequested) {
|
||||
store.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const entry of stat.children ?? []) {
|
||||
|
||||
if (!entry.isFile || !RawToolSetsShape.isToolSetFileName(entry.resource)) {
|
||||
// not interesting
|
||||
continue;
|
||||
}
|
||||
|
||||
// watch this file
|
||||
store.add(this._fileService.watch(entry.resource));
|
||||
|
||||
let data: RawToolSetsShape | undefined;
|
||||
try {
|
||||
const content = await this._fileService.readFile(entry.resource, undefined, cts.token);
|
||||
const rawObj = parse(content.value.toString());
|
||||
data = RawToolSetsShape.from(rawObj);
|
||||
|
||||
} catch (err) {
|
||||
this._logService.trace(`Error reading tool set file ${entry.resource.toString()}:`, err);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cts.token.isCancellationRequested) {
|
||||
store.dispose();
|
||||
break;
|
||||
}
|
||||
|
||||
for (const [name, value] of data.entries) {
|
||||
|
||||
const tools: IToolData[] = [];
|
||||
const toolSets: ToolSet[] = [];
|
||||
value.tools.forEach(name => {
|
||||
const tool = this._languageModelToolsService.getToolByName(name);
|
||||
if (tool) {
|
||||
tools.push(tool);
|
||||
return;
|
||||
}
|
||||
const toolSet = this._languageModelToolsService.getToolSetByName(name);
|
||||
if (toolSet) {
|
||||
toolSets.push(toolSet);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
if (tools.length === 0 && toolSets.length === 0) {
|
||||
// NO tools in this set
|
||||
continue;
|
||||
}
|
||||
|
||||
const toolset = this._languageModelToolsService.createToolSet(
|
||||
{ type: 'user', file: entry.resource, label: basename(entry.resource) },
|
||||
`user/${entry.resource.toString()}/${name}`,
|
||||
name,
|
||||
{
|
||||
// toolReferenceName: value.referenceName,
|
||||
icon: value.icon ? ThemeIcon.fromId(value.icon) : undefined,
|
||||
description: value.description
|
||||
}
|
||||
);
|
||||
|
||||
tools.forEach(tool => store.add(toolset.addTool(tool)));
|
||||
toolSets.forEach(toolSet => store.add(toolset.addToolSet(toolSet)));
|
||||
store.add(toolset);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- actions
|
||||
|
||||
export class ConfigureToolSets extends Action2 {
|
||||
|
||||
static readonly ID = 'chat.configureToolSets';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: ConfigureToolSets.ID,
|
||||
title: localize2('chat.configureToolSets', 'Configure Tool Sets...'),
|
||||
category: CHAT_CATEGORY,
|
||||
f1: true,
|
||||
});
|
||||
}
|
||||
|
||||
override async run(accessor: ServicesAccessor): Promise<void> {
|
||||
|
||||
const toolsService = accessor.get(ILanguageModelToolsService);
|
||||
const quickInputService = accessor.get(IQuickInputService);
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const userDataProfileService = accessor.get(IUserDataProfileService);
|
||||
const fileService = accessor.get(IFileService);
|
||||
const textFileService = accessor.get(ITextFileService);
|
||||
|
||||
const picks: ((IQuickPickItem & { toolset?: ToolSet }) | IQuickPickSeparator)[] = [];
|
||||
|
||||
for (const toolSet of toolsService.toolSets.get()) {
|
||||
if (toolSet.source.type !== 'user') {
|
||||
continue;
|
||||
}
|
||||
|
||||
picks.push({
|
||||
label: toolSet.displayName,
|
||||
toolset: toolSet,
|
||||
tooltip: toolSet.description,
|
||||
iconClass: ThemeIcon.asClassName(toolSet.icon)
|
||||
});
|
||||
}
|
||||
|
||||
if (picks.length !== 0) {
|
||||
picks.push({ type: 'separator' });
|
||||
}
|
||||
|
||||
picks.push({
|
||||
label: localize('chat.configureToolSets.add', 'Add Tool Sets File...'),
|
||||
alwaysShow: true,
|
||||
iconClass: ThemeIcon.asClassName(Codicon.tools)
|
||||
});
|
||||
|
||||
|
||||
const pick = await quickInputService.pick(picks, {
|
||||
canPickMany: false,
|
||||
placeHolder: localize('chat.configureToolSets.placeholder', 'Select a tool set to configure'),
|
||||
});
|
||||
|
||||
if (!pick) {
|
||||
return; // user cancelled
|
||||
}
|
||||
|
||||
let resource: URI | undefined;
|
||||
|
||||
if (!pick.toolset) {
|
||||
|
||||
const name = await quickInputService.input({
|
||||
placeHolder: localize('input.placeholder', "Type tool sets file name"),
|
||||
validateInput: async (input) => {
|
||||
if (!input) {
|
||||
return localize('bad_name1', "Invalid file name");
|
||||
}
|
||||
if (!isValidBasename(input)) {
|
||||
return localize('bad_name2', "'{0}' is not a valid file name", input);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
if (isFalsyOrWhitespace(name)) {
|
||||
return; // user cancelled
|
||||
}
|
||||
|
||||
resource = joinPath(userDataProfileService.currentProfile.promptsHome, `${name}${RawToolSetsShape.suffix}`);
|
||||
|
||||
if (!await fileService.exists(resource)) {
|
||||
await textFileService.write(resource, [
|
||||
'// Place your tool sets here...',
|
||||
'// Example:',
|
||||
'// {',
|
||||
'// \t"toolSetName": {',
|
||||
'// \t\t"tools": [',
|
||||
'// \t\t\t"toolName"',
|
||||
'// \t\t],',
|
||||
'// \t\t"description": "description",',
|
||||
'// \t\t"icon": "$(tools)"',
|
||||
'// \t}',
|
||||
'// }',
|
||||
].join('\n'));
|
||||
}
|
||||
|
||||
} else {
|
||||
assertType(pick.toolset.source.type === 'user');
|
||||
resource = pick.toolset.source.file;
|
||||
}
|
||||
|
||||
await editorService.openEditor({ resource, options: { pinned: true } });
|
||||
}
|
||||
}
|
|
@ -72,6 +72,11 @@ export interface IChatRequestToolEntry extends IBaseChatRequestVariableEntry {
|
|||
readonly kind: 'tool';
|
||||
}
|
||||
|
||||
export interface IChatRequestToolSetEntry extends IBaseChatRequestVariableEntry {
|
||||
readonly kind: 'toolset';
|
||||
readonly value: undefined;
|
||||
}
|
||||
|
||||
export interface IChatRequestImplicitVariableEntry extends IBaseChatRequestVariableEntry {
|
||||
readonly kind: 'implicit';
|
||||
readonly isFile: true;
|
||||
|
@ -211,7 +216,8 @@ export interface ISCMHistoryItemVariableEntry extends IBaseChatRequestVariableEn
|
|||
}
|
||||
|
||||
export type IChatRequestVariableEntry = IGenericChatRequestVariableEntry | IChatRequestImplicitVariableEntry | IChatRequestPasteVariableEntry
|
||||
| ISymbolVariableEntry | ICommandResultVariableEntry | IDiagnosticVariableEntry | IImageVariableEntry | IChatRequestToolEntry
|
||||
| ISymbolVariableEntry | ICommandResultVariableEntry | IDiagnosticVariableEntry | IImageVariableEntry
|
||||
| IChatRequestToolEntry | IChatRequestToolSetEntry
|
||||
| IChatRequestDirectoryEntry | IChatRequestFileEntry | INotebookOutputVariableEntry | IElementVariableEntry
|
||||
| IPromptFileVariableEntry | ISCMHistoryItemVariableEntry;
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import { ThemeIcon } from '../../../../base/common/themables.js';
|
|||
import { IOffsetRange, OffsetRange } from '../../../../editor/common/core/ranges/offsetRange.js';
|
||||
import { IRange } from '../../../../editor/common/core/range.js';
|
||||
import { IChatAgentCommand, IChatAgentData, IChatAgentService, reviveSerializedAgent } from './chatAgents.js';
|
||||
import { IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData } from './chatModel.js';
|
||||
import { IChatRequestToolSetEntry, IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData } from './chatModel.js';
|
||||
import { IChatSlashData } from './chatSlashCommands.js';
|
||||
import { IChatRequestProblemsVariable, IChatRequestVariableValue } from './chatVariables.js';
|
||||
import { ChatAgentLocation } from './constants.js';
|
||||
|
@ -93,6 +93,27 @@ export class ChatRequestToolPart implements IParsedChatRequestPart {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An invocation of a tool
|
||||
*/
|
||||
export class ChatRequestToolSetPart implements IParsedChatRequestPart {
|
||||
static readonly Kind = 'toolset';
|
||||
readonly kind = ChatRequestToolSetPart.Kind;
|
||||
constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly id: string, readonly name: string, readonly icon: ThemeIcon) { }
|
||||
|
||||
get text(): string {
|
||||
return `${chatVariableLeader}${this.name}`;
|
||||
}
|
||||
|
||||
get promptText(): string {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
toVariableEntry(): IChatRequestToolSetEntry {
|
||||
return { kind: 'toolset', id: this.id, name: this.name, range: this.range, icon: this.icon, value: undefined };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An invocation of an agent that can be resolved by the agent service
|
||||
*/
|
||||
|
@ -214,6 +235,14 @@ export function reviveParsedChatRequest(serialized: IParsedChatRequest): IParsed
|
|||
(part as ChatRequestToolPart).displayName,
|
||||
(part as ChatRequestToolPart).icon,
|
||||
);
|
||||
} else if (part.kind === ChatRequestToolSetPart.Kind) {
|
||||
return new ChatRequestToolSetPart(
|
||||
new OffsetRange(part.range.start, part.range.endExclusive),
|
||||
part.editorRange,
|
||||
(part as ChatRequestToolSetPart).id,
|
||||
(part as ChatRequestToolSetPart).name,
|
||||
(part as ChatRequestToolSetPart).icon,
|
||||
);
|
||||
} else if (part.kind === ChatRequestAgentPart.Kind) {
|
||||
let agent = (part as ChatRequestAgentPart).agent;
|
||||
agent = reviveSerializedAgent(agent);
|
||||
|
|
|
@ -7,11 +7,11 @@ import { OffsetRange } from '../../../../editor/common/core/ranges/offsetRange.j
|
|||
import { IPosition, Position } from '../../../../editor/common/core/position.js';
|
||||
import { Range } from '../../../../editor/common/core/range.js';
|
||||
import { IChatAgentData, IChatAgentService } from './chatAgents.js';
|
||||
import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, ChatRequestSlashPromptPart, ChatRequestTextPart, ChatRequestToolPart, IParsedChatRequest, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from './chatParserTypes.js';
|
||||
import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, ChatRequestSlashPromptPart, ChatRequestTextPart, ChatRequestToolPart, ChatRequestToolSetPart, IParsedChatRequest, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from './chatParserTypes.js';
|
||||
import { IChatSlashCommandService } from './chatSlashCommands.js';
|
||||
import { IChatVariablesService, IDynamicVariable } from './chatVariables.js';
|
||||
import { ChatAgentLocation, ChatMode } from './constants.js';
|
||||
import { IToolData } from './languageModelToolsService.js';
|
||||
import { IToolData, ToolSet } from './languageModelToolsService.js';
|
||||
import { IPromptsService } from './promptSyntax/service/types.js';
|
||||
|
||||
const agentReg = /^@([\w_\-\.]+)(?=(\s|$|\b))/i; // An @-agent
|
||||
|
@ -35,10 +35,13 @@ export class ChatRequestParser {
|
|||
parseChatRequest(sessionId: string, message: string, location: ChatAgentLocation = ChatAgentLocation.Panel, context?: IChatParserContext): IParsedChatRequest {
|
||||
const parts: IParsedChatRequestPart[] = [];
|
||||
const references = this.variableService.getDynamicVariables(sessionId); // must access this list before any async calls
|
||||
const toolsByName = new Map<string, IToolData>((this.variableService.getSelectedTools(sessionId))
|
||||
const toolsByName = new Map<string, IToolData>(this.variableService.getSelectedTools(sessionId)
|
||||
.filter(t => t.canBeReferencedInPrompt && t.toolReferenceName)
|
||||
.map(t => [t.toolReferenceName!, t]));
|
||||
|
||||
const toolSetsByName = new Map<string, ToolSet>(this.variableService.getSelectedToolSets(sessionId)
|
||||
.map(t => [t.displayName, t]));
|
||||
|
||||
let lineNumber = 1;
|
||||
let column = 1;
|
||||
for (let i = 0; i < message.length; i++) {
|
||||
|
@ -47,7 +50,7 @@ export class ChatRequestParser {
|
|||
let newPart: IParsedChatRequestPart | undefined;
|
||||
if (previousChar.match(/\s/) || i === 0) {
|
||||
if (char === chatVariableLeader) {
|
||||
newPart = this.tryToParseVariable(message.slice(i), i, new Position(lineNumber, column), parts, toolsByName);
|
||||
newPart = this.tryToParseVariable(message.slice(i), i, new Position(lineNumber, column), parts, toolsByName, toolSetsByName);
|
||||
} else if (char === chatAgentLeader) {
|
||||
newPart = this.tryToParseAgent(message.slice(i), message, i, new Position(lineNumber, column), parts, location, context);
|
||||
} else if (char === chatSubcommandLeader) {
|
||||
|
@ -145,7 +148,7 @@ export class ChatRequestParser {
|
|||
return new ChatRequestAgentPart(agentRange, agentEditorRange, agent);
|
||||
}
|
||||
|
||||
private tryToParseVariable(message: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>, toolsByName: ReadonlyMap<string, IToolData>): ChatRequestAgentPart | ChatRequestToolPart | undefined {
|
||||
private tryToParseVariable(message: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>, toolsByName: ReadonlyMap<string, IToolData>, toolSetsByName: ReadonlyMap<string, ToolSet>): ChatRequestToolPart | ChatRequestToolSetPart | undefined {
|
||||
const nextVariableMatch = message.match(variableReg);
|
||||
if (!nextVariableMatch) {
|
||||
return;
|
||||
|
@ -160,6 +163,11 @@ export class ChatRequestParser {
|
|||
return new ChatRequestToolPart(varRange, varEditorRange, name, tool.id, tool.displayName, tool.icon);
|
||||
}
|
||||
|
||||
const toolset = toolSetsByName.get(name);
|
||||
if (toolset) {
|
||||
return new ChatRequestToolSetPart(varRange, varEditorRange, toolset.id, toolset.displayName, toolset.icon);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import { Location } from '../../../../editor/common/languages.js';
|
|||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IChatModel, IDiagnosticVariableEntryFilterData } from './chatModel.js';
|
||||
import { IChatContentReference, IChatProgressMessage } from './chatService.js';
|
||||
import { IToolData } from './languageModelToolsService.js';
|
||||
import { IToolData, ToolSet } from './languageModelToolsService.js';
|
||||
|
||||
export interface IChatVariableData {
|
||||
id: string;
|
||||
|
@ -47,6 +47,7 @@ export interface IChatVariablesService {
|
|||
_serviceBrand: undefined;
|
||||
getDynamicVariables(sessionId: string): ReadonlyArray<IDynamicVariable>;
|
||||
getSelectedTools(sessionId: string): ReadonlyArray<IToolData>;
|
||||
getSelectedToolSets(sessionId: string): ReadonlyArray<ToolSet>;
|
||||
}
|
||||
|
||||
export interface IDynamicVariable {
|
||||
|
|
|
@ -7,7 +7,7 @@ import { CancellationToken } from '../../../../base/common/cancellation.js';
|
|||
import { Event } from '../../../../base/common/event.js';
|
||||
import { IMarkdownString } from '../../../../base/common/htmlContent.js';
|
||||
import { IJSONSchema } from '../../../../base/common/jsonSchema.js';
|
||||
import { IDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { Schemas } from '../../../../base/common/network.js';
|
||||
import { ThemeIcon } from '../../../../base/common/themables.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
|
@ -19,6 +19,8 @@ import { IProgress } from '../../../../platform/progress/common/progress.js';
|
|||
import { IChatTerminalToolInvocationData, IChatToolInputInvocationData } from './chatService.js';
|
||||
import { PromptElementJSON, stringifyPromptElementJSON } from './tools/promptTsxTypes.js';
|
||||
import { VSBuffer } from '../../../../base/common/buffer.js';
|
||||
import { derived, IObservable, IReader, ObservableSet } from '../../../../base/common/observable.js';
|
||||
import { Iterable } from '../../../../base/common/iterator.js';
|
||||
|
||||
export interface IToolData {
|
||||
id: string;
|
||||
|
@ -53,11 +55,6 @@ export type ToolDataSource =
|
|||
type: 'extension';
|
||||
label: string;
|
||||
extensionId: ExtensionIdentifier;
|
||||
/**
|
||||
* True for tools contributed through extension API from third-party extensions, so they can be disabled by policy.
|
||||
* False for built-in tools, MCP tools are handled differently.
|
||||
*/
|
||||
isExternalTool: boolean;
|
||||
}
|
||||
| {
|
||||
type: 'mcp';
|
||||
|
@ -65,16 +62,44 @@ export type ToolDataSource =
|
|||
collectionId: string;
|
||||
definitionId: string;
|
||||
}
|
||||
| { type: 'internal' };
|
||||
| {
|
||||
type: 'user';
|
||||
label: string;
|
||||
file: URI;
|
||||
}
|
||||
| {
|
||||
type: 'internal';
|
||||
label: string;
|
||||
};
|
||||
|
||||
export namespace ToolDataSource {
|
||||
|
||||
export const Internal: ToolDataSource = { type: 'internal', label: 'Built-In' };
|
||||
|
||||
export function toKey(source: ToolDataSource): string {
|
||||
switch (source.type) {
|
||||
case 'extension': return `extension:${source.extensionId.value}`;
|
||||
case 'mcp': return `mcp:${source.collectionId}:${source.definitionId}`;
|
||||
case 'user': return `user:${source.file.toString()}`;
|
||||
case 'internal': return 'internal';
|
||||
}
|
||||
}
|
||||
|
||||
export function equals(a: ToolDataSource, b: ToolDataSource): boolean {
|
||||
return toKey(a) === toKey(b);
|
||||
}
|
||||
|
||||
export function classify(source: ToolDataSource): { readonly ordinal: number; readonly label: string } {
|
||||
if (source.type === 'internal') {
|
||||
return { ordinal: 3, label: 'Built-In' };
|
||||
} else if (source.type === 'mcp') {
|
||||
return { ordinal: 1, label: 'MCP Servers' };
|
||||
} else if (source.type === 'user') {
|
||||
return { ordinal: 0, label: 'User Defined' };
|
||||
} else {
|
||||
return { ordinal: 2, label: 'Extensions' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface IToolInvocation {
|
||||
|
@ -161,6 +186,58 @@ export interface IToolImpl {
|
|||
prepareToolInvocation?(parameters: any, token: CancellationToken): Promise<IPreparedToolInvocation | undefined>;
|
||||
}
|
||||
|
||||
export class ToolSet {
|
||||
|
||||
protected readonly _tools = new ObservableSet<IToolData>();
|
||||
|
||||
protected readonly _toolSets = new ObservableSet<ToolSet>();
|
||||
|
||||
/**
|
||||
* A homogenous tool set only contains tools from the same source as the tool set itself
|
||||
*/
|
||||
readonly isHomogenous: IObservable<boolean>;
|
||||
|
||||
constructor(
|
||||
readonly id: string,
|
||||
readonly displayName: string,
|
||||
readonly icon: ThemeIcon,
|
||||
readonly source: ToolDataSource,
|
||||
readonly toolReferenceName: string,
|
||||
readonly description?: string,
|
||||
) {
|
||||
|
||||
this.isHomogenous = derived(r => {
|
||||
return !Iterable.some(this._tools.observable.read(r), tool => !ToolDataSource.equals(tool.source, this.source))
|
||||
&& !Iterable.some(this._toolSets.observable.read(r), toolSet => !ToolDataSource.equals(toolSet.source, this.source));
|
||||
});
|
||||
}
|
||||
|
||||
addTool(data: IToolData): IDisposable {
|
||||
this._tools.add(data);
|
||||
return toDisposable(() => {
|
||||
this._tools.delete(data);
|
||||
});
|
||||
}
|
||||
|
||||
addToolSet(toolSet: ToolSet): IDisposable {
|
||||
if (toolSet === this) {
|
||||
return Disposable.None;
|
||||
}
|
||||
this._toolSets.add(toolSet);
|
||||
return toDisposable(() => {
|
||||
this._toolSets.delete(toolSet);
|
||||
});
|
||||
}
|
||||
|
||||
getTools(r?: IReader): Iterable<IToolData> {
|
||||
return Iterable.concat(
|
||||
this._tools.observable.read(r),
|
||||
...Iterable.map(this._toolSets.observable.read(r), toolSet => toolSet.getTools(r))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const ILanguageModelToolsService = createDecorator<ILanguageModelToolsService>('ILanguageModelToolsService');
|
||||
|
||||
export type CountTokensCallback = (input: string, token: CancellationToken) => Promise<number>;
|
||||
|
@ -177,6 +254,10 @@ export interface ILanguageModelToolsService {
|
|||
setToolAutoConfirmation(toolId: string, scope: 'workspace' | 'profile' | 'memory', autoConfirm?: boolean): void;
|
||||
resetToolAutoConfirmation(): void;
|
||||
cancelToolCallsForRequest(requestId: string): void;
|
||||
|
||||
readonly toolSets: IObservable<Iterable<ToolSet>>;
|
||||
getToolSetByName(name: string): ToolSet | undefined;
|
||||
createToolSet(source: ToolDataSource, id: string, displayName: string, options?: { icon?: ThemeIcon; toolReferenceName?: string; description?: string }): ToolSet & IDisposable;
|
||||
}
|
||||
|
||||
export function createToolInputUri(toolOrId: IToolData | string): URI {
|
||||
|
|
|
@ -14,7 +14,7 @@ import { INotebookService } from '../../../notebook/common/notebookService.js';
|
|||
import { ICodeMapperService } from '../../common/chatCodeMapperService.js';
|
||||
import { ChatModel } from '../../common/chatModel.js';
|
||||
import { IChatService } from '../../common/chatService.js';
|
||||
import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolProgress } from '../../common/languageModelToolsService.js';
|
||||
import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolDataSource, ToolProgress } from '../../common/languageModelToolsService.js';
|
||||
|
||||
export const ExtensionEditToolId = 'vscode_editFile';
|
||||
export const InternalEditToolId = 'vscode_editFile_internal';
|
||||
|
@ -22,7 +22,7 @@ export const EditToolData: IToolData = {
|
|||
id: InternalEditToolId,
|
||||
displayName: '', // not used
|
||||
modelDescription: '', // Not used
|
||||
source: { type: 'internal' },
|
||||
source: ToolDataSource.Internal,
|
||||
};
|
||||
|
||||
export interface EditToolParams {
|
||||
|
|
|
@ -3,23 +3,24 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { isFalsyOrEmpty } from '../../../../../base/common/arrays.js';
|
||||
import { MarkdownString } from '../../../../../base/common/htmlContent.js';
|
||||
import { IJSONSchema } from '../../../../../base/common/jsonSchema.js';
|
||||
import { Disposable, DisposableMap } from '../../../../../base/common/lifecycle.js';
|
||||
import { Disposable, DisposableMap, DisposableStore } from '../../../../../base/common/lifecycle.js';
|
||||
import { joinPath } from '../../../../../base/common/resources.js';
|
||||
import { isFalsyOrWhitespace } from '../../../../../base/common/strings.js';
|
||||
import { ThemeIcon } from '../../../../../base/common/themables.js';
|
||||
import { localize } from '../../../../../nls.js';
|
||||
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
|
||||
import { ExtensionIdentifier, IExtensionManifest } from '../../../../../platform/extensions/common/extensions.js';
|
||||
import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js';
|
||||
import { ILogService } from '../../../../../platform/log/common/log.js';
|
||||
import { IProductService } from '../../../../../platform/product/common/productService.js';
|
||||
import { Registry } from '../../../../../platform/registry/common/platform.js';
|
||||
import { IWorkbenchContribution } from '../../../../common/contributions.js';
|
||||
import { Extensions, IExtensionFeaturesRegistry, IExtensionFeatureTableRenderer, IRenderedData, IRowData, ITableData } from '../../../../services/extensionManagement/common/extensionFeatures.js';
|
||||
import { isProposedApiEnabled } from '../../../../services/extensions/common/extensions.js';
|
||||
import * as extensionsRegistry from '../../../../services/extensions/common/extensionsRegistry.js';
|
||||
import { ILanguageModelToolsService, IToolData } from '../languageModelToolsService.js';
|
||||
import { ILanguageModelToolsService, IToolData, ToolDataSource, ToolSet } from '../languageModelToolsService.js';
|
||||
import { toolsParametersSchemaSchemaId } from './languageModelToolsParametersSchema.js';
|
||||
|
||||
export interface IRawToolContribution {
|
||||
|
@ -132,21 +133,80 @@ const languageModelToolsExtensionPoint = extensionsRegistry.ExtensionsRegistry.r
|
|||
}
|
||||
});
|
||||
|
||||
export interface IRawToolSetContribution {
|
||||
name: string;
|
||||
referenceName?: string;
|
||||
description: string;
|
||||
icon?: string;
|
||||
tools: string[];
|
||||
}
|
||||
|
||||
const languageModelToolSetsExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint<IRawToolSetContribution[]>({
|
||||
extensionPoint: 'languageModelToolSets',
|
||||
deps: [languageModelToolsExtensionPoint],
|
||||
jsonSchema: {
|
||||
description: localize('vscode.extension.contributes.toolSets', 'Contributes a set of language model tools that can be used together.'),
|
||||
type: 'array',
|
||||
items: {
|
||||
additionalProperties: false,
|
||||
type: 'object',
|
||||
defaultSnippets: [{
|
||||
body: {
|
||||
name: '${1}',
|
||||
description: '${2}',
|
||||
tools: ['${3}']
|
||||
}
|
||||
}],
|
||||
required: ['name', 'description', 'tools'],
|
||||
properties: {
|
||||
name: {
|
||||
description: localize('toolSetName', "A name for this tool set."),
|
||||
type: 'string',
|
||||
},
|
||||
referenceName: {
|
||||
description: localize('toolSetReferenceName', "A name that users can use to reference this tool set. Name must not contain whitespace."),
|
||||
type: 'string',
|
||||
pattern: '^[\\w-]+$'
|
||||
},
|
||||
description: {
|
||||
description: localize('toolSetDescription', "A description of this tool set."),
|
||||
type: 'string'
|
||||
},
|
||||
icon: {
|
||||
markdownDescription: localize('toolSetIcon', "An icon that represents this tool set, like `$(zap)`"),
|
||||
type: 'string'
|
||||
},
|
||||
tools: {
|
||||
description: localize('toolSetTools', "An array of tool or tool set names that are part of this set."),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function toToolKey(extensionIdentifier: ExtensionIdentifier, toolName: string) {
|
||||
return `${extensionIdentifier.value}/${toolName}`;
|
||||
}
|
||||
|
||||
function toToolSetKey(extensionIdentifier: ExtensionIdentifier, toolName: string) {
|
||||
return `toolset:${extensionIdentifier.value}/${toolName}`;
|
||||
}
|
||||
|
||||
export class LanguageModelToolsExtensionPointHandler implements IWorkbenchContribution {
|
||||
static readonly ID = 'workbench.contrib.toolsExtensionPointHandler';
|
||||
|
||||
private _registrationDisposables = new DisposableMap<string>();
|
||||
|
||||
constructor(
|
||||
@IProductService productService: IProductService,
|
||||
@ILanguageModelToolsService languageModelToolsService: ILanguageModelToolsService,
|
||||
@ILogService logService: ILogService,
|
||||
@IProductService productService: IProductService
|
||||
) {
|
||||
languageModelToolsExtensionPoint.setHandler((extensions, delta) => {
|
||||
|
||||
languageModelToolsExtensionPoint.setHandler((_extensions, delta) => {
|
||||
for (const extension of delta.added) {
|
||||
for (const rawTool of extension.value) {
|
||||
if (!rawTool.name || !rawTool.modelDescription || !rawTool.displayName) {
|
||||
|
@ -191,9 +251,14 @@ export class LanguageModelToolsExtensionPointHandler implements IWorkbenchContri
|
|||
const isBuiltinTool = productService.defaultChatAgent?.chatExtensionId ?
|
||||
ExtensionIdentifier.equals(extension.description.identifier, productService.defaultChatAgent.chatExtensionId) :
|
||||
isProposedApiEnabled(extension.description, 'chatParticipantPrivate');
|
||||
|
||||
const source: ToolDataSource = isBuiltinTool
|
||||
? ToolDataSource.Internal
|
||||
: { type: 'extension', label: extension.description.displayName ?? extension.description.name, extensionId: extension.description.identifier };
|
||||
|
||||
const tool: IToolData = {
|
||||
...rawTool,
|
||||
source: { type: 'extension', label: extension.description.displayName ?? extension.description.name, extensionId: extension.description.identifier, isExternalTool: !isBuiltinTool },
|
||||
source,
|
||||
inputSchema: rawTool.inputSchema,
|
||||
id: rawTool.name,
|
||||
icon,
|
||||
|
@ -215,9 +280,88 @@ export class LanguageModelToolsExtensionPointHandler implements IWorkbenchContri
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
languageModelToolSetsExtensionPoint.setHandler((_extensions, delta) => {
|
||||
|
||||
for (const extension of delta.added) {
|
||||
|
||||
if (!isProposedApiEnabled(extension.description, 'contribLanguageModelToolSets')) {
|
||||
extension.collector.error(`Extension '${extension.description.identifier.value}' CANNOT register language model tools because the 'contribLanguageModelToolSets' API proposal is not enabled.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const isBuiltinTool = productService.defaultChatAgent?.chatExtensionId ?
|
||||
ExtensionIdentifier.equals(extension.description.identifier, productService.defaultChatAgent.chatExtensionId) :
|
||||
isProposedApiEnabled(extension.description, 'chatParticipantPrivate');
|
||||
|
||||
const source: ToolDataSource = isBuiltinTool
|
||||
? ToolDataSource.Internal
|
||||
: { type: 'extension', label: extension.description.displayName ?? extension.description.name, extensionId: extension.description.identifier };
|
||||
|
||||
|
||||
for (const toolSet of extension.value) {
|
||||
|
||||
if (isFalsyOrWhitespace(toolSet.name)) {
|
||||
extension.collector.error(`Tool set '${toolSet.name}' CANNOT have an empty name`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isFalsyOrEmpty(toolSet.tools)) {
|
||||
extension.collector.error(`Tool set '${toolSet.name}' CANNOT have an empty tools array`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const tools: IToolData[] = [];
|
||||
const toolSets: ToolSet[] = [];
|
||||
|
||||
for (const toolName of toolSet.tools) {
|
||||
const toolObj = languageModelToolsService.getToolByName(toolName);
|
||||
if (toolObj) {
|
||||
tools.push(toolObj);
|
||||
continue;
|
||||
}
|
||||
const toolSetObj = languageModelToolsService.getToolSetByName(toolName);
|
||||
if (toolSetObj) {
|
||||
toolSets.push(toolSetObj);
|
||||
continue;
|
||||
}
|
||||
extension.collector.warn(`Tool set '${toolSet.name}' CANNOT find tool or tool set by name: ${toolName}`);
|
||||
}
|
||||
|
||||
if (toolSets.length === 0 && tools.length === 0) {
|
||||
extension.collector.error(`Tool set '${toolSet.name}' CANNOT have an empty tools array (none of the tools were found)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const store = new DisposableStore();
|
||||
|
||||
const obj = languageModelToolsService.createToolSet(
|
||||
source,
|
||||
toToolSetKey(extension.description.identifier, toolSet.name),
|
||||
toolSet.name,
|
||||
{ icon: toolSet.icon ? ThemeIcon.fromString(toolSet.icon) : undefined, toolReferenceName: toolSet.referenceName, description: toolSet.description }
|
||||
);
|
||||
|
||||
store.add(obj);
|
||||
tools.forEach(tool => store.add(obj.addTool(tool)));
|
||||
toolSets.forEach(toolSet => store.add(obj.addToolSet(toolSet)));
|
||||
|
||||
this._registrationDisposables.set(toToolSetKey(extension.description.identifier, toolSet.name), store);
|
||||
}
|
||||
}
|
||||
|
||||
for (const extension of delta.removed) {
|
||||
for (const toolSet of extension.value) {
|
||||
this._registrationDisposables.deleteAndDispose(toToolSetKey(extension.description.identifier, toolSet.name));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- render
|
||||
|
||||
class LanguageModelToolDataRenderer extends Disposable implements IExtensionFeatureTableRenderer {
|
||||
readonly type = 'table';
|
||||
|
||||
|
@ -263,3 +407,53 @@ Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).re
|
|||
},
|
||||
renderer: new SyncDescriptor(LanguageModelToolDataRenderer),
|
||||
});
|
||||
|
||||
|
||||
class LanguageModelToolSetDataRenderer extends Disposable implements IExtensionFeatureTableRenderer {
|
||||
|
||||
readonly type = 'table';
|
||||
|
||||
shouldRender(manifest: IExtensionManifest): boolean {
|
||||
return !!manifest.contributes?.languageModelToolSets;
|
||||
}
|
||||
|
||||
render(manifest: IExtensionManifest): IRenderedData<ITableData> {
|
||||
const contribs = manifest.contributes?.languageModelToolSets ?? [];
|
||||
if (!contribs.length) {
|
||||
return { data: { headers: [], rows: [] }, dispose: () => { } };
|
||||
}
|
||||
|
||||
const headers = [
|
||||
localize('name', "Name"),
|
||||
localize('reference', "Reference Name"),
|
||||
localize('tools', "Tools"),
|
||||
localize('descriptions', "Description"),
|
||||
];
|
||||
|
||||
const rows: IRowData[][] = contribs.map(t => {
|
||||
return [
|
||||
new MarkdownString(`\`${t.name}\``),
|
||||
t.referenceName ? new MarkdownString(`\`#${t.referenceName}\``) : 'none',
|
||||
t.tools.join(', '),
|
||||
t.description,
|
||||
];
|
||||
});
|
||||
|
||||
return {
|
||||
data: {
|
||||
headers,
|
||||
rows
|
||||
},
|
||||
dispose: () => { }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({
|
||||
id: 'languageModelToolSets',
|
||||
label: localize('langModelToolSets', "Language Model Tool Sets"),
|
||||
access: {
|
||||
canToggle: false
|
||||
},
|
||||
renderer: new SyncDescriptor(LanguageModelToolSetDataRenderer),
|
||||
});
|
||||
|
|
|
@ -9,7 +9,7 @@ import { ResourceSet } from '../../../../../base/common/map.js';
|
|||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { localize } from '../../../../../nls.js';
|
||||
import { IWebContentExtractorService } from '../../../../../platform/webContentExtractor/common/webContentExtractor.js';
|
||||
import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultTextPart, ToolProgress } from '../../common/languageModelToolsService.js';
|
||||
import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultTextPart, ToolDataSource, ToolProgress } from '../../common/languageModelToolsService.js';
|
||||
import { InternalFetchWebPageToolId } from '../../common/tools/tools.js';
|
||||
|
||||
export const FetchWebPageToolData: IToolData = {
|
||||
|
@ -17,7 +17,7 @@ export const FetchWebPageToolData: IToolData = {
|
|||
displayName: 'Fetch Web Page',
|
||||
canBeReferencedInPrompt: false,
|
||||
modelDescription: localize('fetchWebPage.modelDescription', 'Fetches the main content from a web page. This tool is useful for summarizing or analyzing the content of a webpage.'),
|
||||
source: { type: 'internal' },
|
||||
source: ToolDataSource.Internal,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
|
|
@ -13,7 +13,7 @@ import { workbenchInstantiationService } from '../../../../test/browser/workbenc
|
|||
import { LanguageModelToolsService } from '../../browser/languageModelToolsService.js';
|
||||
import { IChatModel } from '../../common/chatModel.js';
|
||||
import { IChatService } from '../../common/chatService.js';
|
||||
import { IToolData, IToolImpl, IToolInvocation } from '../../common/languageModelToolsService.js';
|
||||
import { IToolData, IToolImpl, IToolInvocation, ToolDataSource } from '../../common/languageModelToolsService.js';
|
||||
import { MockChatService } from '../common/mockChatService.js';
|
||||
import { CancellationError, isCancellationError } from '../../../../../base/common/errors.js';
|
||||
import { Barrier } from '../../../../../base/common/async.js';
|
||||
|
@ -40,7 +40,7 @@ suite('LanguageModelToolsService', () => {
|
|||
id: 'testTool',
|
||||
modelDescription: 'Test Tool',
|
||||
displayName: 'Test Tool',
|
||||
source: { type: 'internal' },
|
||||
source: ToolDataSource.Internal,
|
||||
};
|
||||
|
||||
const disposable = service.registerToolData(toolData);
|
||||
|
@ -54,7 +54,7 @@ suite('LanguageModelToolsService', () => {
|
|||
id: 'testTool',
|
||||
modelDescription: 'Test Tool',
|
||||
displayName: 'Test Tool',
|
||||
source: { type: 'internal' },
|
||||
source: ToolDataSource.Internal,
|
||||
};
|
||||
|
||||
store.add(service.registerToolData(toolData));
|
||||
|
@ -74,7 +74,7 @@ suite('LanguageModelToolsService', () => {
|
|||
modelDescription: 'Test Tool 1',
|
||||
when: ContextKeyEqualsExpr.create('testKey', false),
|
||||
displayName: 'Test Tool',
|
||||
source: { type: 'internal' },
|
||||
source: ToolDataSource.Internal,
|
||||
};
|
||||
|
||||
const toolData2: IToolData = {
|
||||
|
@ -82,14 +82,14 @@ suite('LanguageModelToolsService', () => {
|
|||
modelDescription: 'Test Tool 2',
|
||||
when: ContextKeyEqualsExpr.create('testKey', true),
|
||||
displayName: 'Test Tool',
|
||||
source: { type: 'internal' },
|
||||
source: ToolDataSource.Internal,
|
||||
};
|
||||
|
||||
const toolData3: IToolData = {
|
||||
id: 'testTool3',
|
||||
modelDescription: 'Test Tool 3',
|
||||
displayName: 'Test Tool',
|
||||
source: { type: 'internal' },
|
||||
source: ToolDataSource.Internal,
|
||||
};
|
||||
|
||||
store.add(service.registerToolData(toolData1));
|
||||
|
@ -107,7 +107,7 @@ suite('LanguageModelToolsService', () => {
|
|||
id: 'testTool',
|
||||
modelDescription: 'Test Tool',
|
||||
displayName: 'Test Tool',
|
||||
source: { type: 'internal' },
|
||||
source: ToolDataSource.Internal,
|
||||
};
|
||||
|
||||
store.add(service.registerToolData(toolData));
|
||||
|
@ -142,7 +142,7 @@ suite('LanguageModelToolsService', () => {
|
|||
id: 'testTool',
|
||||
modelDescription: 'Test Tool',
|
||||
displayName: 'Test Tool',
|
||||
source: { type: 'internal' },
|
||||
source: ToolDataSource.Internal,
|
||||
};
|
||||
|
||||
store.add(service.registerToolData(toolData));
|
||||
|
|
|
@ -19,7 +19,7 @@ import { IChatService } from '../../common/chatService.js';
|
|||
import { IChatSlashCommandService } from '../../common/chatSlashCommands.js';
|
||||
import { IChatVariablesService } from '../../common/chatVariables.js';
|
||||
import { ChatMode, ChatAgentLocation } from '../../common/constants.js';
|
||||
import { IToolData } from '../../common/languageModelToolsService.js';
|
||||
import { IToolData, ToolDataSource } from '../../common/languageModelToolsService.js';
|
||||
import { IPromptsService } from '../../common/promptSyntax/service/types.js';
|
||||
import { MockChatService } from './mockChatService.js';
|
||||
import { MockPromptsService } from './mockPromptsService.js';
|
||||
|
@ -44,6 +44,7 @@ suite('ChatRequestParser', () => {
|
|||
variableService = mockObject<IChatVariablesService>()();
|
||||
variableService.getDynamicVariables.returns([]);
|
||||
variableService.getSelectedTools.returns([]);
|
||||
variableService.getSelectedToolSets.returns([]);
|
||||
|
||||
instantiationService.stub(IChatVariablesService, variableService as any);
|
||||
});
|
||||
|
@ -293,8 +294,8 @@ suite('ChatRequestParser', () => {
|
|||
instantiationService.stub(IChatAgentService, agentsService as any);
|
||||
|
||||
variableService.getSelectedTools.returns([
|
||||
{ id: 'get_selection', toolReferenceName: 'selection', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: { type: 'internal' } },
|
||||
{ id: 'get_debugConsole', toolReferenceName: 'debugConsole', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: { type: 'internal' } }
|
||||
{ id: 'get_selection', toolReferenceName: 'selection', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: ToolDataSource.Internal },
|
||||
{ id: 'get_debugConsole', toolReferenceName: 'debugConsole', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: ToolDataSource.Internal }
|
||||
] satisfies IToolData[]);
|
||||
|
||||
parser = instantiationService.createInstance(ChatRequestParser);
|
||||
|
@ -308,8 +309,8 @@ suite('ChatRequestParser', () => {
|
|||
instantiationService.stub(IChatAgentService, agentsService as any);
|
||||
|
||||
variableService.getSelectedTools.returns([
|
||||
{ id: 'get_selection', toolReferenceName: 'selection', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: { type: 'internal' } },
|
||||
{ id: 'get_debugConsole', toolReferenceName: 'debugConsole', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: { type: 'internal' } }
|
||||
{ id: 'get_selection', toolReferenceName: 'selection', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: ToolDataSource.Internal },
|
||||
{ id: 'get_debugConsole', toolReferenceName: 'debugConsole', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: ToolDataSource.Internal }
|
||||
] satisfies IToolData[]);
|
||||
|
||||
parser = instantiationService.createInstance(ChatRequestParser);
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IChatVariablesService, IDynamicVariable } from '../../common/chatVariables.js';
|
||||
import { IToolData } from '../../common/languageModelToolsService.js';
|
||||
import { IToolData, ToolSet } from '../../common/languageModelToolsService.js';
|
||||
|
||||
export class MockChatVariablesService implements IChatVariablesService {
|
||||
_serviceBrand: undefined;
|
||||
|
@ -16,4 +16,8 @@ export class MockChatVariablesService implements IChatVariablesService {
|
|||
getSelectedTools(sessionId: string): readonly IToolData[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
getSelectedToolSets(sessionId: string): readonly ToolSet[] {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,9 @@
|
|||
import { CancellationToken } from '../../../../../base/common/cancellation.js';
|
||||
import { Event } from '../../../../../base/common/event.js';
|
||||
import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js';
|
||||
import { constObservable, IObservable } from '../../../../../base/common/observable.js';
|
||||
import { IProgressStep } from '../../../../../platform/progress/common/progress.js';
|
||||
import { CountTokensCallback, ILanguageModelToolsService, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../common/languageModelToolsService.js';
|
||||
import { CountTokensCallback, ILanguageModelToolsService, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolSet } from '../../common/languageModelToolsService.js';
|
||||
|
||||
export class MockLanguageModelToolsService implements ILanguageModelToolsService {
|
||||
_serviceBrand: undefined;
|
||||
|
@ -56,4 +57,14 @@ export class MockLanguageModelToolsService implements ILanguageModelToolsService
|
|||
content: [{ kind: 'text', value: 'result' }]
|
||||
};
|
||||
}
|
||||
|
||||
toolSets: IObservable<readonly ToolSet[]> = constObservable([]);
|
||||
|
||||
getToolSetByName(name: string): ToolSet | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
createToolSet(): ToolSet & IDisposable {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import { ThemeIcon } from '../../../../base/common/themables.js';
|
|||
import { localize } from '../../../../nls.js';
|
||||
import { SortBy } from '../../../../platform/extensionManagement/common/extensionManagement.js';
|
||||
import { EXTENSION_CATEGORIES } from '../../../../platform/extensions/common/extensions.js';
|
||||
import { CountTokensCallback, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolProgress } from '../../chat/common/languageModelToolsService.js';
|
||||
import { CountTokensCallback, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolDataSource, ToolProgress } from '../../chat/common/languageModelToolsService.js';
|
||||
import { ExtensionState, IExtension, IExtensionsWorkbenchService } from '../common/extensions.js';
|
||||
|
||||
export const SearchExtensionsToolId = 'vscode_searchExtensions_internal';
|
||||
|
@ -22,7 +22,7 @@ export const SearchExtensionsToolData: IToolData = {
|
|||
displayName: localize('searchExtensionsTool.displayName', 'Search Extensions'),
|
||||
modelDescription: localize('searchExtensionsTool.modelDescription', "This is a tool for browsing Visual Studio Code Extensions Marketplace. It allows the model to search for extensions and retrieve detailed information about them. The model should use this tool whenever it needs to discover extensions or resolve information about known ones. To use the tool, the model has to provide the category of the extensions, relevant search keywords, or known extension IDs. Note that search results may include false positives, so reviewing and filtering is recommended."),
|
||||
userDescription: localize('searchExtensionsTool.userDescription', 'Search for extensions in the Visual Studio Code Extensions Marketplace.'),
|
||||
source: { type: 'internal' },
|
||||
source: ToolDataSource.Internal,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
@ -148,4 +148,3 @@ export class SearchExtensionsTool implements IToolImpl {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common
|
|||
import { ILogService } from '../../../../platform/log/common/log.js';
|
||||
import { IProductService } from '../../../../platform/product/common/productService.js';
|
||||
import { StorageScope } from '../../../../platform/storage/common/storage.js';
|
||||
import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultInputOutputDetails, ToolProgress } from '../../chat/common/languageModelToolsService.js';
|
||||
import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultInputOutputDetails, ToolSet, ToolProgress } from '../../chat/common/languageModelToolsService.js';
|
||||
import { McpCommandIds } from './mcpCommandIds.js';
|
||||
import { IMcpRegistry } from './mcpRegistryTypes.js';
|
||||
import { McpServer, McpServerMetadataCache } from './mcpServer.js';
|
||||
|
@ -89,7 +89,7 @@ export class McpService extends Disposable implements IMcpService {
|
|||
await Promise.all(todo);
|
||||
}
|
||||
|
||||
private _syncTools(server: McpServer, store: DisposableStore) {
|
||||
private _syncTools(server: McpServer, toolSet: ToolSet, store: DisposableStore) {
|
||||
const tools = new Map</* tool ID */string, ISyncedToolData>();
|
||||
|
||||
store.add(autorun(reader => {
|
||||
|
@ -115,9 +115,9 @@ export class McpService extends Disposable implements IMcpService {
|
|||
const registerTool = (store: DisposableStore) => {
|
||||
store.add(this._toolsService.registerToolData(toolData));
|
||||
store.add(this._toolsService.registerToolImplementation(tool.id, this._instantiationService.createInstance(McpToolImplementation, tool, server)));
|
||||
store.add(toolSet.addTool(toolData));
|
||||
};
|
||||
|
||||
|
||||
if (existing) {
|
||||
if (!equals(existing.toolData, toolData)) {
|
||||
existing.toolData = toolData;
|
||||
|
@ -132,6 +132,7 @@ export class McpService extends Disposable implements IMcpService {
|
|||
registerTool(store);
|
||||
tools.set(tool.id, { toolData, store });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for (const id of toDelete) {
|
||||
|
@ -193,8 +194,13 @@ export class McpService extends Disposable implements IMcpService {
|
|||
!!def.collectionDefinition.lazy,
|
||||
def.collectionDefinition.scope === StorageScope.WORKSPACE ? this.workspaceCache : this.userCache,
|
||||
);
|
||||
const toolSet = this._toolsService.createToolSet(
|
||||
{ type: 'mcp', label: def.serverDefinition.label, collectionId: def.collectionDefinition.id, definitionId: def.serverDefinition.id },
|
||||
def.serverDefinition.id, def.serverDefinition.label,
|
||||
{ icon: Codicon.mcp }
|
||||
);
|
||||
store.add(object);
|
||||
this._syncTools(object, store);
|
||||
this._syncTools(object, toolSet, store);
|
||||
|
||||
nextServers.push({ object, dispose: () => store.dispose() });
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ suite('Workbench - MCP - ResourceFilesystem', () => {
|
|||
const registry = new TestMcpRegistry(parentInsta1);
|
||||
|
||||
const parentInsta2 = ds.add(parentInsta1.createChild(new ServiceCollection([IMcpRegistry, registry])));
|
||||
const mcpService = ds.add(new McpService(parentInsta2, registry, { registerToolData: () => Disposable.None, registerToolImplementation: () => Disposable.None } as any, new NullLogService()));
|
||||
const mcpService = ds.add(new McpService(parentInsta2, registry, { registerToolData: () => Disposable.None, registerToolImplementation: () => Disposable.None, createToolSet: () => Disposable.None } as any, new NullLogService()));
|
||||
mcpService.updateCollectedServers();
|
||||
|
||||
const instaService = ds.add(parentInsta2.createChild(new ServiceCollection(
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// empty placeholder declaration for the `languageModelToolSets` contribution point
|
Loading…
Reference in New Issue