Fix retrieving dynamic references for chat agents (#196320)

* Fix retrieving dynamic references for chat agents
Not a perfect fix to the underlying issue but solves the problem for now

* Fix tests

* Fix tests

* Fix integration tests
This commit is contained in:
Rob Lourens
2023-10-23 16:26:45 -07:00
committed by GitHub
parent b2cba04163
commit 6113b646d5
11 changed files with 71 additions and 34 deletions
@@ -26,7 +26,7 @@ export class ChatDynamicReferenceModel extends Disposable implements IChatWidget
private readonly _references: IDynamicReference[] = [];
get references(): ReadonlyArray<IDynamicReference> {
return this._references;
return [...this._references];
}
get id() {
@@ -126,7 +126,7 @@ export class SelectAndInsertFileAction extends Action2 {
const fileName = basename(resource);
const editor = context.widget.inputEditor;
const text = `$file:${fileName}`;
const text = `#file:${fileName}`;
const range = context.range;
const success = editor.executeEdits('chatInsertFile', [{ range, text: text + ' ' }]);
if (!success) {
@@ -501,7 +501,7 @@ class AgentCompletions extends Disposable {
}
class BuiltinDynamicCompletions extends Disposable {
private static readonly VariableNameDef = /\$\w*/g; // MUST be using `g`-flag
private static readonly VariableNameDef = new RegExp(`${chatVariableLeader}\\w*`, 'g'); // MUST be using `g`-flag
constructor(
@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,
@@ -513,7 +513,7 @@ class BuiltinDynamicCompletions extends Disposable {
this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, {
_debugDisplayName: 'chatDynamicCompletions',
triggerCharacters: ['$'],
triggerCharacters: [chatVariableLeader],
provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => {
const fileVariablesEnabled = this.configurationService.getValue('chat.experimental.fileVariables') ?? this.productService.quality !== 'stable';
if (!fileVariablesEnabled) {
@@ -544,8 +544,8 @@ class BuiltinDynamicCompletions extends Disposable {
return <CompletionList>{
suggestions: [
<CompletionItem>{
label: '$file',
insertText: '$file:',
label: `${chatVariableLeader}file`,
insertText: `${chatVariableLeader}file:`,
detail: localize('pickFileLabel', "Pick a file"),
range: { insert, replace },
kind: CompletionItemKind.Text,
@@ -54,16 +54,19 @@
@keyframes ellipsis {
0% {
content: "";
}
25% {
content: ".";
}
33% {
50% {
content: "..";
}
66% {
75% {
content: "...";
}
100% {
content: ".";
content: "";
}
}
@@ -66,7 +66,7 @@ export class ChatRequestAgentPart implements IParsedChatRequestPart {
constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly agent: IChatAgent) { }
get text(): string {
return `@${this.agent.id}`;
return `${chatAgentLeader}${this.agent.id}`;
}
get promptText(): string {
@@ -83,7 +83,7 @@ export class ChatRequestAgentSubcommandPart implements IParsedChatRequestPart {
constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly command: IChatAgentCommand) { }
get text(): string {
return `/${this.command.name}`;
return `${chatVariableLeader}${this.command.name}`;
}
get promptText(): string {
@@ -100,16 +100,16 @@ export class ChatRequestSlashCommandPart implements IParsedChatRequestPart {
constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly slashCommand: ISlashCommand) { }
get text(): string {
return `/${this.slashCommand.command}`;
return `${chatSubcommandLeader}${this.slashCommand.command}`;
}
get promptText(): string {
return `/${this.slashCommand.command}`;
return `${chatSubcommandLeader}${this.slashCommand.command}`;
}
}
/**
* An invocation of a dynamic reference like '$file:'
* An invocation of a dynamic reference like '#file:'
*/
export class ChatRequestDynamicReferencePart implements IParsedChatRequestPart {
static readonly Kind = 'dynamic';
@@ -121,7 +121,7 @@ export class ChatRequestDynamicReferencePart implements IParsedChatRequestPart {
}
get text(): string {
return `$${this.referenceText}`;
return `${chatVariableLeader}${this.referenceText}`;
}
get promptText(): string {
@@ -8,14 +8,14 @@ import { OffsetRange } from 'vs/editor/common/core/offsetRange';
import { IPosition, Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicReferencePart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart, IParsedChatRequest, IParsedChatRequestPart, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicReferencePart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart, IParsedChatRequest, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables';
import { IChatVariablesService, IDynamicReference } from 'vs/workbench/contrib/chat/common/chatVariables';
const agentReg = /^@([\w_\-]+)(?=(\s|$|\b))/i; // An @-agent
const variableReg = /^#([\w_\-]+)(:\d+)?(?=(\s|$|\b))/i; // A #-variable with an optional numeric : arg (@response:2)
const slashReg = /\/([\w_\-]+)(?=(\s|$|\b))/i; // A / command
const dollarSignVarReg = /\$([\w_\-]+):([\w_\-\.]+)(?=(\s|$|\b))/i; // A / command
const variableWithArgReg = /\#([\w_\-]+):([\w_\-\.]+)(?=(\s|$|\b))/i; // A variable with a string : arg (#file:foo.ts)
export class ChatRequestParser {
constructor(
@@ -26,6 +26,7 @@ export class ChatRequestParser {
async parseChatRequest(sessionId: string, message: string): Promise<IParsedChatRequest> {
const parts: IParsedChatRequestPart[] = [];
const references = this.variableService.getDynamicReferences(sessionId); // must access this list before any async calls
let lineNumber = 1;
let column = 1;
@@ -35,14 +36,13 @@ 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);
} else if (char === '@') {
newPart = this.tryToParseVariable(message.slice(i), i, new Position(lineNumber, column), parts) ||
await this.tryToParseDynamicVariable(message.slice(i), i, new Position(lineNumber, column), references);
} else if (char === chatAgentLeader) {
newPart = this.tryToParseAgent(message.slice(i), message, i, new Position(lineNumber, column), parts);
} else if (char === '/') {
} else if (char === chatSubcommandLeader) {
// TODO try to make this sync
newPart = await this.tryToParseSlashCommand(sessionId, message.slice(i), message, i, new Position(lineNumber, column), parts);
} else if (char === '$') {
newPart = await this.tryToParseDynamicVariable(sessionId, message.slice(i), i, new Position(lineNumber, column), parts);
}
}
@@ -185,8 +185,8 @@ export class ChatRequestParser {
return;
}
private async tryToParseDynamicVariable(sessionId: string, message: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>): Promise<ChatRequestDynamicReferencePart | undefined> {
const nextVarMatch = message.match(dollarSignVarReg);
private async tryToParseDynamicVariable(message: string, offset: number, position: IPosition, references: ReadonlyArray<IDynamicReference>): Promise<ChatRequestDynamicReferencePart | undefined> {
const nextVarMatch = message.match(variableWithArgReg);
if (!nextVarMatch) {
return;
}
@@ -200,12 +200,9 @@ export class ChatRequestParser {
return;
}
const references = this.variableService.getDynamicReferences(sessionId);
const refAtThisPosition = references.find(r =>
r.range.startLineNumber === position.lineNumber &&
r.range.startColumn === position.column &&
r.range.endLineNumber === position.lineNumber &&
r.range.endColumn === position.column + full.length);
r.range.startColumn === position.column);
if (refAtThisPosition) {
return new ChatRequestDynamicReferencePart(range, editorRange, name, arg, refAtThisPosition.data);
}
@@ -680,9 +680,9 @@ export class ChatService extends Disposable implements IChatService {
}
}
async sendRequestToProvider(sessionId: string, message: IChatDynamicRequest): Promise<void> {
async sendRequestToProvider(sessionId: string, message: IChatDynamicRequest): Promise<{ responseCompletePromise: Promise<void> } | undefined> {
this.trace('sendRequestToProvider', `sessionId: ${sessionId}`);
await this.sendRequest(sessionId, message.message);
return await this.sendRequest(sessionId, message.message);
}
getProviders(): string[] {
@@ -13,6 +13,7 @@ import { ChatVariablesService } from 'vs/workbench/contrib/chat/browser/chatVari
import { ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser';
import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables';
import { MockChatWidgetService } from 'vs/workbench/contrib/chat/test/browser/mockChatWidget';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
@@ -23,7 +24,7 @@ suite('ChatVariables', function () {
const testDisposables = ensureNoDisposablesAreLeakedInTestSuite();
setup(function () {
service = new ChatVariablesService(null!);
service = new ChatVariablesService(new MockChatWidgetService());
instantiationService = testDisposables.add(new TestInstantiationService());
instantiationService.stub(IStorageService, testDisposables.add(new TestStorageService()));
instantiationService.stub(ILogService, new NullLogService());
@@ -0,0 +1,31 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
export class MockChatWidgetService implements IChatWidgetService {
readonly _serviceBrand: undefined;
/**
* Returns the most recently focused widget if any.
*/
readonly lastFocusedWidget: IChatWidget | undefined;
/**
* Returns whether a view was successfully revealed.
*/
async revealViewForProvider(providerId: string): Promise<IChatWidget | undefined> {
return undefined;
}
getWidgetByInputUri(uri: URI): IChatWidget | undefined {
return undefined;
}
getWidgetBySessionId(sessionId: string): IChatWidget | undefined {
return undefined;
}
}
@@ -28,6 +28,10 @@ suite('ChatRequestParser', () => {
instantiationService.stub(ILogService, new NullLogService());
instantiationService.stub(IExtensionService, new TestExtensionService());
instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService)));
const varService = mockObject<IChatVariablesService>()({});
varService.getDynamicReferences.returns([]);
instantiationService.stub(IChatVariablesService, varService as any);
});
test('plain text', async () => {
@@ -230,7 +230,8 @@ suite('Chat', () => {
const model = testDisposables.add(testService.startSession('testProvider', CancellationToken.None));
assert.strictEqual(model.getRequests().length, 0);
await testService.sendRequestToProvider(model.sessionId, { message: 'test request' });
const response = await testService.sendRequestToProvider(model.sessionId, { message: 'test request' });
await response?.responseCompletePromise;
assert.strictEqual(model.getRequests().length, 1);
});
@@ -24,7 +24,7 @@ export class MockChatVariablesService implements IChatVariablesService {
}
getDynamicReferences(sessionId: string): readonly IDynamicReference[] {
throw new Error('Method not implemented.');
return [];
}
async resolveVariables(prompt: IParsedChatRequest, model: IChatModel, token: CancellationToken): Promise<IChatVariableResolveResult> {