Fix variables in chatAgents2 API requests, add tests (#195529)

* Fix variables in chatAgents2 API requests

* Enable file references and the 'used references' section by default in Insiders

* Add integration tests for chat

* Fix equality

* fix test
This commit is contained in:
Rob Lourens 2023-10-12 21:27:30 -07:00 committed by GitHub
parent e13be231e7
commit 6c02f61149
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 172 additions and 6 deletions

View file

@ -6,6 +6,8 @@
"license": "MIT",
"enabledApiProposals": [
"authSession",
"chatAgents2",
"chatVariables",
"contribViewsRemote",
"contribStatusBarItems",
"createFileSystemWatcher",
@ -20,6 +22,7 @@
"fileSearchProvider",
"findTextInFiles",
"fsChunks",
"interactive",
"mappedEditsProvider",
"notebookCellExecutionState",
"notebookDeprecated",
@ -165,6 +168,12 @@
]
}
],
"interactiveSession": [
{
"id": "provider",
"label": "Provider"
}
],
"notebooks": [
{
"type": "notebookCoreTest",

View file

@ -0,0 +1,80 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import 'mocha';
import { CancellationToken, chat, ChatAgentRequest, ChatVariableLevel, CompletionItemKind, Disposable, interactive, InteractiveProgress, InteractiveRequest, InteractiveResponseForProgress, InteractiveSession, InteractiveSessionState, Progress, ProviderResult } from 'vscode';
import { assertNoRpc, closeAllEditors, DeferredPromise, disposeAll } from '../utils';
suite('chat', () => {
let disposables: Disposable[] = [];
setup(() => {
disposables = [];
});
teardown(async function () {
assertNoRpc();
await closeAllEditors();
disposeAll(disposables);
});
function getDeferredForRequest(): DeferredPromise<ChatAgentRequest> {
disposables.push(interactive.registerInteractiveSessionProvider('provider', {
prepareSession: (_initialState: InteractiveSessionState | undefined, _token: CancellationToken): ProviderResult<InteractiveSession> => {
return {
requester: { name: 'test' },
responder: { name: 'test' },
};
},
provideResponseWithProgress: (_request: InteractiveRequest, _progress: Progress<InteractiveProgress>, _token: CancellationToken): ProviderResult<InteractiveResponseForProgress> => {
return null;
},
provideSlashCommands: (_session, _token) => {
return [{ command: 'hello', title: 'Hello', kind: CompletionItemKind.Text }];
},
removeRequest: (_session: InteractiveSession, _requestId: string): void => {
throw new Error('Function not implemented.');
}
}));
const deferred = new DeferredPromise<ChatAgentRequest>();
const agent = chat.createChatAgent('agent', (request, _context, _progress, _token) => {
deferred.complete(request);
return null;
});
agent.slashCommandProvider = {
provideSlashCommands: (_token) => {
return [{ name: 'hello', description: 'Hello' }];
}
};
disposables.push(agent);
return deferred;
}
test('agent and slash command', async () => {
const deferred = getDeferredForRequest();
interactive.sendInteractiveRequestToProvider('provider', { message: '@agent /hello friend' });
const lastResult = await deferred.p;
assert.deepStrictEqual(lastResult.slashCommand, { name: 'hello', description: 'Hello' });
assert.strictEqual(lastResult.prompt, 'friend');
});
test('agent and variable', async () => {
disposables.push(chat.registerVariable('myVar', 'My variable', {
resolve(_name, _context, _token) {
return [{ level: ChatVariableLevel.Full, value: 'myValue' }];
}
}));
const deferred = getDeferredForRequest();
interactive.sendInteractiveRequestToProvider('provider', { message: '@agent hi #myVar' });
const lastResult = await deferred.p;
assert.strictEqual(lastResult.prompt, 'hi [#myVar](values:myVar)');
assert.strictEqual(lastResult.variables['myVar'][0].value, 'myValue');
});
});

View file

@ -0,0 +1,62 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import 'mocha';
import { CancellationToken, CompletionItemKind, Disposable, interactive, InteractiveProgress, InteractiveRequest, InteractiveResponseForProgress, InteractiveSession, InteractiveSessionState, Progress, ProviderResult } from 'vscode';
import { assertNoRpc, closeAllEditors, DeferredPromise, disposeAll } from '../utils';
suite('InteractiveSessionProvider', () => {
let disposables: Disposable[] = [];
setup(async () => {
disposables = [];
});
teardown(async function () {
assertNoRpc();
await closeAllEditors();
disposeAll(disposables);
});
function getDeferredForRequest(): DeferredPromise<InteractiveRequest> {
const deferred = new DeferredPromise<InteractiveRequest>();
disposables.push(interactive.registerInteractiveSessionProvider('provider', {
prepareSession: (_initialState: InteractiveSessionState | undefined, _token: CancellationToken): ProviderResult<InteractiveSession> => {
return {
requester: { name: 'test' },
responder: { name: 'test' },
};
},
provideResponseWithProgress: (request: InteractiveRequest, _progress: Progress<InteractiveProgress>, _token: CancellationToken): ProviderResult<InteractiveResponseForProgress> => {
deferred.complete(request);
return null;
},
provideSlashCommands: (_session, _token) => {
return [{ command: 'hello', title: 'Hello', kind: CompletionItemKind.Text }];
},
removeRequest: (_session: InteractiveSession, _requestId: string): void => {
throw new Error('Function not implemented.');
}
}));
return deferred;
}
test('plain text query', async () => {
const deferred = getDeferredForRequest();
interactive.sendInteractiveRequestToProvider('provider', { message: 'hello' });
const lastResult = await deferred.p;
assert.strictEqual(lastResult.message, 'hello');
});
test('slash command', async () => {
const deferred = getDeferredForRequest();
interactive.sendInteractiveRequestToProvider('provider', { message: '/hello' });
const lastResult = await deferred.p;
assert.strictEqual(lastResult.message, '/hello');
});
});

View file

@ -67,11 +67,13 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
? await agent.validateSlashCommand(request.command)
: undefined;
try {
const task = agent.invoke(
{ prompt: request.message, variables: {}, slashCommand },
{
prompt: request.message,
variables: typeConvert.ChatVariable.objectTo(request.variables),
slashCommand
},
{ history: context.history.map(typeConvert.ChatMessage.to) },
new Progress<vscode.InteractiveProgress>(p => {
throwIfDone();

View file

@ -2236,6 +2236,15 @@ export namespace ChatMessageRole {
}
export namespace ChatVariable {
export function objectTo(variableObject: Record<string, IChatRequestVariableValue[]>): Record<string, vscode.ChatVariableValue[]> {
const result: Record<string, vscode.ChatVariableValue[]> = {};
for (const key of Object.keys(variableObject)) {
result[key] = variableObject[key].map(ChatVariable.to);
}
return result;
}
export function to(variable: IChatRequestVariableValue): vscode.ChatVariableValue {
return {
level: ChatVariableLevel.to(variable.level),

View file

@ -55,6 +55,7 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle
import { WorkbenchCompressibleAsyncDataTree, WorkbenchList } from 'vs/platform/list/browser/listService';
import { ILogService } from 'vs/platform/log/common/log';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IProductService } from 'vs/platform/product/common/productService';
import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels';
@ -145,6 +146,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IChatService private readonly chatService: IChatService,
@IEditorService private readonly editorService: IEditorService,
@IProductService productService: IProductService,
) {
super();
this.renderer = this.instantiationService.createInstance(MarkdownRenderer, {});
@ -152,10 +154,10 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
this._treePool = this._register(this.instantiationService.createInstance(TreePool, this._onDidChangeVisibility.event));
this._contentReferencesListPool = this._register(this.instantiationService.createInstance(ContentReferencesListPool, this._onDidChangeVisibility.event));
this._usedReferencesEnabled = configService.getValue('chat.experimental.usedReferences');
this._usedReferencesEnabled = configService.getValue('chat.experimental.usedReferences') ?? productService.quality !== 'stable';
this._register(configService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('chat.experimental.usedReferences')) {
this._usedReferencesEnabled = configService.getValue('chat.experimental.usedReferences');
this._usedReferencesEnabled = configService.getValue('chat.experimental.usedReferences') ?? productService.quality !== 'stable';
}
}));
}

View file

@ -17,6 +17,7 @@ import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeat
import { localize } from 'vs/nls';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IProductService } from 'vs/platform/product/common/productService';
import { Registry } from 'vs/platform/registry/common/platform';
import { inputPlaceholderForeground } from 'vs/platform/theme/common/colorRegistry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
@ -435,6 +436,7 @@ class BuiltinDynamicCompletions extends Disposable {
@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,
@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IProductService private readonly productService: IProductService,
) {
super();
@ -442,7 +444,7 @@ class BuiltinDynamicCompletions extends Disposable {
_debugDisplayName: 'chatDynamicCompletions',
triggerCharacters: ['$'],
provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => {
const fileVariablesEnabled = this.configurationService.getValue('chat.experimental.fileVariables');
const fileVariablesEnabled = this.configurationService.getValue('chat.experimental.fileVariables') ?? this.productService.quality !== 'stable';
if (!fileVariablesEnabled) {
return;
}