Skip to content

Commit

Permalink
feat: consume chat participant detection provider (microsoft#224661)
Browse files Browse the repository at this point in the history
* feat: consume chat participant detection provider

* Fix tests

* Fix more tests

There is now an async call between when a response is first added into
the UI and when the agent for that response is identified with intent
detection and invoked. This causes a race condition in the test code to
show up where waiting for `State.SHOW_REQUEST` is insufficient to ensure
that the agent has been invoked. Working around this with another
timeout for now.
  • Loading branch information
joyceerhl authored Aug 3, 2024
1 parent ee47c51 commit 9a5ce80
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 32 deletions.
3 changes: 2 additions & 1 deletion src/vs/workbench/contrib/chat/common/chatAgents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,9 @@ export interface IChatAgentService {
registerAgentImplementation(id: string, agent: IChatAgentImplementation): IDisposable;
registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable;
registerAgentCompletionProvider(id: string, provider: (query: string, token: CancellationToken) => Promise<IChatAgentCompletionItem[]>): IDisposable;
registerChatParticipantDetectionProvider(handle: number, provider: IChatParticipantDetectionProvider): IDisposable;
getAgentCompletionItems(id: string, query: string, token: CancellationToken): Promise<IChatAgentCompletionItem[]>;
registerChatParticipantDetectionProvider(handle: number, provider: IChatParticipantDetectionProvider): IDisposable;
detectAgentOrCommand(request: IChatAgentRequest, history: IChatAgentHistoryEntry[], options: { location: ChatAgentLocation }, token: CancellationToken): Promise<{ agent: IChatAgentData; command?: IChatAgentCommand } | undefined>;
invokeAgent(agent: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatAgentResult>;
getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatFollowup[]>;
getAgent(id: string): IChatAgentData | undefined;
Expand Down
81 changes: 54 additions & 27 deletions src/vs/workbench/contrib/chat/common/chatServiceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ import { revive } from 'vs/base/common/marshalling';
import { StopWatch } from 'vs/base/common/stopwatch';
import { URI, UriComponents } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ILogService } from 'vs/platform/log/common/log';
import { Progress } from 'vs/platform/progress/common/progress';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { ChatAgentLocation, IChatAgent, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { ChatAgentLocation, IChatAgent, IChatAgentCommand, IChatAgentData, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { CONTEXT_VOTE_UP_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys';
import { ChatModel, ChatRequestModel, ChatRequestRemovalReason, ChatWelcomeMessageModel, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, ISerializableChatData, ISerializableChatsData, getHistoryEntriesFromModel, updateRanges } from 'vs/workbench/contrib/chat/common/chatModel';
import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, chatAgentLeader, chatSubcommandLeader, getPromptText } from 'vs/workbench/contrib/chat/common/chatParserTypes';
Expand Down Expand Up @@ -111,7 +112,8 @@ export class ChatService extends Disposable implements IChatService {
@IChatVariablesService private readonly chatVariablesService: IChatVariablesService,
@IChatAgentService private readonly chatAgentService: IChatAgentService,
@IWorkbenchAssignmentService workbenchAssignmentService: IWorkbenchAssignmentService,
@IContextKeyService contextKeyService: IContextKeyService
@IContextKeyService contextKeyService: IContextKeyService,
@IConfigurationService private readonly configurationService: IConfigurationService
) {
super();

Expand Down Expand Up @@ -549,35 +551,60 @@ export class ChatService extends Disposable implements IChatService {
let rawResult: IChatAgentResult | null | undefined;
let agentOrCommandFollowups: Promise<IChatFollowup[] | undefined> | undefined = undefined;


if (agentPart || (defaultAgent && !commandPart)) {
const agent = (agentPart?.agent ?? defaultAgent)!;
const prepareChatAgentRequest = async (agent: IChatAgentData, command?: IChatAgentCommand, chatRequest?: ChatRequestModel) => {
const initVariableData: IChatRequestVariableData = { variables: [] };
request = chatRequest ?? model.addRequest(parsedRequest, initVariableData, attempt, agent, command, options?.confirmation);

// Variables may have changed if the agent and slash command changed, so resolve them again even if we already had a chatRequest
const variableData = await this.chatVariablesService.resolveVariables(parsedRequest, options?.attachedContext, model, progressCallback, token);
model.updateRequest(request, variableData);
const promptTextResult = getPromptText(request.message);
const updatedVariableData = updateRanges(variableData, promptTextResult.diff); // TODO bit of a hack

return {
sessionId,
requestId: request.id,
agentId: agent.id,
message: promptTextResult.message,
command: command?.name,
variables: updatedVariableData,
enableCommandDetection,
attempt,
location,
locationData: options?.locationData,
acceptedConfirmationData: options?.acceptedConfirmationData,
rejectedConfirmationData: options?.rejectedConfirmationData,
} satisfies IChatAgentRequest;
};

let detectedAgent: IChatAgentData | undefined;
let detectedCommand: IChatAgentCommand | undefined;
if (this.configurationService.getValue('chat.experimental.detectParticipant.enabled') && !agentPart && !commandPart) {
// We have no agent or command to scope history with, pass the full history to the participant detection provider
const defaultAgentHistory = getHistoryEntriesFromModel(model, defaultAgent.id);

// Prepare the request object that we will send to the participant detection provider
const chatAgentRequest = await prepareChatAgentRequest(defaultAgent, agentSlashCommandPart?.command);

const result = await this.chatAgentService.detectAgentOrCommand(chatAgentRequest, defaultAgentHistory, { location }, token);
if (result) {
// Update the response in the ChatModel to reflect the detected agent and command
request.response?.setAgent(result.agent, result.command);
detectedAgent = result.agent;
detectedCommand = result.command;
}
}

const agent = (detectedAgent ?? agentPart?.agent ?? defaultAgent)!;
const command = detectedCommand ?? agentSlashCommandPart?.command;
await this.extensionService.activateByEvent(`onChatParticipant:${agent.id}`);
const history = getHistoryEntriesFromModel(model, agentPart?.agent.id);

const initVariableData: IChatRequestVariableData = { variables: [] };
request = model.addRequest(parsedRequest, initVariableData, attempt, agent, agentSlashCommandPart?.command, options?.confirmation);
// Recompute history in case the agent or command changed
const history = getHistoryEntriesFromModel(model, agent.id);
const requestProps = await prepareChatAgentRequest(agent, command, request /* Reuse the request object if we already created it for participant detection */);
completeResponseCreated();
const variableData = await this.chatVariablesService.resolveVariables(parsedRequest, options?.attachedContext, model, progressCallback, token);
model.updateRequest(request, variableData);

const promptTextResult = getPromptText(request.message);
const updatedVariableData = updateRanges(variableData, promptTextResult.diff); // TODO bit of a hack

const requestProps: IChatAgentRequest = {
sessionId,
requestId: request.id,
agentId: agent.id,
message: promptTextResult.message,
command: agentSlashCommandPart?.command.name,
variables: updatedVariableData,
enableCommandDetection,
attempt,
location,
locationData: options?.locationData,
acceptedConfirmationData: options?.acceptedConfirmationData,
rejectedConfirmationData: options?.rejectedConfirmationData,
};

const agentResult = await this.chatAgentService.invokeAgent(agent.id, requestProps, progressCallback, history, token);
rawResult = agentResult;
agentOrCommandFollowups = this.chatAgentService.getFollowups(agent.id, requestProps, agentResult, history, followupsCancelToken);
Expand Down
3 changes: 3 additions & 0 deletions src/vs/workbench/contrib/chat/test/common/chatService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { URI } from 'vs/base/common/uri';
import { assertSnapshot } from 'vs/base/test/common/snapshot';
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
import { Range } from 'vs/editor/common/core/range';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
Expand Down Expand Up @@ -85,6 +87,7 @@ suite('ChatService', () => {
instantiationService.stub(IViewsService, new TestExtensionService());
instantiationService.stub(IWorkspaceContextService, new TestContextService());
instantiationService.stub(IChatSlashCommandService, testDisposables.add(instantiationService.createInstance(ChatSlashCommandService)));
instantiationService.stub(IConfigurationService, new TestConfigurationService());
instantiationService.stub(IChatService, new MockChatService());

chatAgentService = instantiationService.createInstance(ChatAgentService);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,6 @@ import { InlineChatController, InlineChatRunOptions, State } from 'vs/workbench/
import { Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession';
import { CTX_INLINE_CHAT_USER_DID_EDIT, EditMode, InlineChatConfigKeys } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { TestViewsService, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices';
import { IInlineChatSavingService } from '../../browser/inlineChatSavingService';
import { IInlineChatSessionService } from '../../browser/inlineChatSessionService';
import { InlineChatSessionServiceImpl } from '../../browser/inlineChatSessionServiceImpl';
import { TestWorkerService } from './testWorkerService';
import { IExtensionService, nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
import { IChatProgress, IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { ChatService } from 'vs/workbench/contrib/chat/common/chatServiceImpl';
Expand All @@ -65,6 +61,10 @@ import { CancellationToken } from 'vs/base/common/cancellation';
import { assertType } from 'vs/base/common/types';
import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService';
import { NullWorkbenchAssignmentService } from 'vs/workbench/services/assignment/test/common/nullAssignmentService';
import { IInlineChatSavingService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSavingService';
import { IInlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionService';
import { InlineChatSessionServiceImpl } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl';
import { TestWorkerService } from 'vs/workbench/contrib/inlineChat/test/browser/testWorkerService';

suite('InteractiveChatController', function () {

Expand Down Expand Up @@ -768,6 +768,7 @@ suite('InteractiveChatController', function () {
const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]);
ctrl.run({ message: 'Hello-', autoSend: true });
assert.strictEqual(await p, undefined);
await timeout(10);
assert.deepStrictEqual(attempts, [0]);

// RERUN (cancel, undo, redo)
Expand Down Expand Up @@ -806,6 +807,7 @@ suite('InteractiveChatController', function () {
// REQUEST 1
const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]);
ctrl.run({ message: 'Hello', autoSend: true });
await timeout(10);
assert.strictEqual(await p, undefined);

assertType(progress);
Expand Down

0 comments on commit 9a5ce80

Please sign in to comment.