Add ChatAgentResult2#metadata (#204851)

* Support serializable metadata on 'result' object from chat agent

* Fix 'errorDetails' on the VM

* Fix acceptAction, get rid of generic parameter on ChatAgent

* Use result metadata for followups

* Use serialized result for history

* Don't share metadata between agents

* Add a test for result metadata

* Fix build
This commit is contained in:
Rob Lourens 2024-02-10 10:40:01 -03:00 committed by GitHub
parent 56c54e7fef
commit b49c1c1170
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 215 additions and 165 deletions

View file

@ -5,7 +5,7 @@
import * as assert from 'assert';
import 'mocha';
import { CancellationToken, chat, ChatAgentRequest, ChatVariableLevel, Disposable, interactive, InteractiveSession, ProviderResult } from 'vscode';
import { CancellationToken, chat, ChatAgentRequest, ChatAgentResult2, ChatVariableLevel, Disposable, interactive, InteractiveSession, ProviderResult } from 'vscode';
import { assertNoRpc, closeAllEditors, DeferredPromise, disposeAll } from '../utils';
suite('chat', () => {
@ -67,4 +67,37 @@ suite('chat', () => {
assert.strictEqual(request.prompt, 'hi [#myVar](values:myVar)');
assert.strictEqual(request.variables['myVar'][0].value, 'myValue');
});
test('result metadata is returned to the followup provider', async () => {
disposables.push(interactive.registerInteractiveSessionProvider('provider', {
prepareSession: (_token: CancellationToken): ProviderResult<InteractiveSession> => {
return {
requester: { name: 'test' },
responder: { name: 'test' },
};
},
}));
const deferred = new DeferredPromise<ChatAgentResult2>();
const agent = chat.createChatAgent('agent', (_request, _context, _progress, _token) => {
return { metadata: { key: 'value' } };
});
agent.isDefault = true;
agent.subCommandProvider = {
provideSubCommands: (_token) => {
return [{ name: 'hello', description: 'Hello' }];
}
};
agent.followupProvider = {
provideFollowups(result, _token) {
deferred.complete(result);
return [];
},
};
disposables.push(agent);
interactive.sendInteractiveRequestToProvider('provider', { message: '@agent /hello friend' });
const result = await deferred.p;
assert.deepStrictEqual(result.metadata, { key: 'value' });
});
});

View file

@ -59,9 +59,9 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
for (const [handle, agent] of this._agents) {
if (agent.name === e.agentId) {
if (e.action.kind === 'vote') {
this._proxy.$acceptFeedback(handle, e.sessionId, e.requestId, e.action.direction);
this._proxy.$acceptFeedback(handle, e.result ?? {}, e.action.direction);
} else {
this._proxy.$acceptAction(handle, e.sessionId, e.requestId, e);
this._proxy.$acceptAction(handle, e.result || {}, e);
}
break;
}
@ -87,12 +87,12 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
this._pendingProgress.delete(request.requestId);
}
},
provideFollowups: async (sessionId, token): Promise<IChatFollowup[]> => {
provideFollowups: async (result, token): Promise<IChatFollowup[]> => {
if (!this._agents.get(handle)?.hasFollowups) {
return [];
}
return this._proxy.$provideFollowups(handle, sessionId, token);
return this._proxy.$provideFollowups(handle, result, token);
},
get lastSlashCommands() {
return lastSlashCommands;

View file

@ -1221,9 +1221,9 @@ export type IChatAgentHistoryEntryDto = {
export interface ExtHostChatAgentsShape2 {
$invokeAgent(handle: number, request: IChatAgentRequest, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise<IChatAgentResult | undefined>;
$provideSlashCommands(handle: number, token: CancellationToken): Promise<IChatAgentCommand[]>;
$provideFollowups(handle: number, sessionId: string, token: CancellationToken): Promise<IChatFollowup[]>;
$acceptFeedback(handle: number, sessionId: string, requestId: string, vote: InteractiveSessionVoteDirection, reportIssue?: boolean): void;
$acceptAction(handle: number, sessionId: string, requestId: string, action: IChatUserActionEvent): void;
$provideFollowups(handle: number, result: IChatAgentResult, token: CancellationToken): Promise<IChatFollowup[]>;
$acceptFeedback(handle: number, result: IChatAgentResult, vote: InteractiveSessionVoteDirection, reportIssue?: boolean): void;
$acceptAction(handle: number, result: IChatAgentResult, action: IChatUserActionEvent): void;
$invokeCompletionProvider(handle: number, query: string, token: CancellationToken): Promise<IChatAgentCompletionItem[]>;
$provideWelcomeMessage(handle: number, token: CancellationToken): Promise<(string | IMarkdownString)[] | undefined>;
$provideSampleQuestions(handle: number, token: CancellationToken): Promise<IChatFollowup[] | undefined>;

View file

@ -157,11 +157,9 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
private static _idPool = 0;
private readonly _agents = new Map<number, ExtHostChatAgent<any>>();
private readonly _agents = new Map<number, ExtHostChatAgent>();
private readonly _proxy: MainThreadChatAgentsShape2;
private readonly _previousResultMap: Map<string, vscode.ChatAgentResult2> = new Map();
private readonly _resultsBySessionAndRequestId: Map<string, Map<string, vscode.ChatAgentResult2>> = new Map();
private readonly _sessionDisposables: DisposableMap<string, DisposableStore> = new DisposableMap();
constructor(
@ -173,9 +171,9 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
this._proxy = mainContext.getProxy(MainContext.MainThreadChatAgents2);
}
createChatAgent<TResult extends vscode.ChatAgentResult2>(extension: IExtensionDescription, name: string, handler: vscode.ChatAgentExtendedHandler): vscode.ChatAgent2<TResult> {
createChatAgent(extension: IExtensionDescription, name: string, handler: vscode.ChatAgentExtendedHandler): vscode.ChatAgent2 {
const handle = ExtHostChatAgents2._idPool++;
const agent = new ExtHostChatAgent<TResult>(extension, name, this._proxy, handle, handler);
const agent = new ExtHostChatAgent(extension, name, this._proxy, handle, handler);
this._agents.set(handle, agent);
this._proxy.$registerAgent(handle, name, {});
@ -183,10 +181,6 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
}
async $invokeAgent(handle: number, request: IChatAgentRequest, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise<IChatAgentResult | undefined> {
// Clear the previous result so that $acceptFeedback or $acceptAction during a request will be ignored.
// We may want to support sending those during a request.
this._previousResultMap.delete(request.sessionId);
const agent = this._agents.get(handle);
if (!agent) {
throw new Error(`[CHAT](${handle}) CANNOT invoke agent because the agent is not registered`);
@ -203,7 +197,7 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
const stream = new ChatAgentResponseStream(agent.extension, request, this._proxy, this._logService, this.commands.converter, sessionDisposables);
try {
const convertedHistory = await this.prepareHistory(agent, request, context);
const convertedHistory = await this.prepareHistory(request, context);
const task = agent.invoke(
typeConvert.ChatAgentRequest.to(request),
{ history: convertedHistory },
@ -212,21 +206,16 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
);
return await raceCancellation(Promise.resolve(task).then((result) => {
if (result) {
this._previousResultMap.set(request.sessionId, result);
let sessionResults = this._resultsBySessionAndRequestId.get(request.sessionId);
if (!sessionResults) {
sessionResults = new Map();
this._resultsBySessionAndRequestId.set(request.sessionId, sessionResults);
if (result?.metadata) {
try {
JSON.stringify(result.metadata);
} catch (err) {
const msg = `result.metadata MUST be JSON.stringify-able. Got error: ${err.message}`;
this._logService.error(`[${agent.extension.identifier.value}] [@${agent.id}] ${msg}`, agent.extension);
return { errorDetails: { message: msg }, timings: stream.timings };
}
sessionResults.set(request.requestId, result);
return { errorDetails: result.errorDetails, timings: stream.timings };
} else {
this._previousResultMap.delete(request.sessionId);
}
return undefined;
return { errorDetails: result?.errorDetails, timings: stream.timings, metadata: result?.metadata };
}), token);
} catch (e) {
@ -239,22 +228,22 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
}
}
private async prepareHistory<T extends vscode.ChatAgentResult2>(agent: ExtHostChatAgent<T>, request: IChatAgentRequest, context: { history: IChatAgentHistoryEntryDto[] }): Promise<vscode.ChatAgentHistoryEntry[]> {
private async prepareHistory(request: IChatAgentRequest, context: { history: IChatAgentHistoryEntryDto[] }): Promise<vscode.ChatAgentHistoryEntry[]> {
return coalesce(await Promise.all(context.history
.map(async h => {
const result = request.agentId === h.request.agentId && this._resultsBySessionAndRequestId.get(request.sessionId)?.get(h.request.requestId)
|| h.result;
const ehResult = typeConvert.ChatAgentResult.to(h.result);
const result: vscode.ChatAgentResult2 = request.agentId === h.request.agentId ?
ehResult :
{ ...ehResult, metadata: undefined };
return {
request: typeConvert.ChatAgentRequest.to(h.request),
response: coalesce(h.response.map(r => typeConvert.ChatResponsePart.from(r, this.commands.converter))),
result
result,
} satisfies vscode.ChatAgentHistoryEntry;
})));
}
$releaseSession(sessionId: string): void {
this._previousResultMap.delete(sessionId);
this._resultsBySessionAndRequestId.delete(sessionId);
this._sessionDisposables.deleteAndDispose(sessionId);
}
@ -267,30 +256,23 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
return agent.provideSlashCommands(token);
}
$provideFollowups(handle: number, sessionId: string, token: CancellationToken): Promise<IChatFollowup[]> {
$provideFollowups(handle: number, result: IChatAgentResult, token: CancellationToken): Promise<IChatFollowup[]> {
const agent = this._agents.get(handle);
if (!agent) {
return Promise.resolve([]);
}
const result = this._previousResultMap.get(sessionId);
if (!result) {
return Promise.resolve([]);
}
return agent.provideFollowups(result, token);
const ehResult = typeConvert.ChatAgentResult.to(result);
return agent.provideFollowups(ehResult, token);
}
$acceptFeedback(handle: number, sessionId: string, requestId: string, vote: InteractiveSessionVoteDirection, reportIssue?: boolean): void {
$acceptFeedback(handle: number, result: IChatAgentResult, vote: InteractiveSessionVoteDirection, reportIssue?: boolean): void {
const agent = this._agents.get(handle);
if (!agent) {
return;
}
const result = this._resultsBySessionAndRequestId.get(sessionId)?.get(requestId);
if (!result) {
return;
}
const ehResult = typeConvert.ChatAgentResult.to(result);
let kind: extHostTypes.ChatAgentResultFeedbackKind;
switch (vote) {
case InteractiveSessionVoteDirection.Down:
@ -300,29 +282,28 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
kind = extHostTypes.ChatAgentResultFeedbackKind.Helpful;
break;
}
agent.acceptFeedback(reportIssue ? Object.freeze({ result, kind, reportIssue }) : Object.freeze({ result, kind }));
agent.acceptFeedback(reportIssue ?
Object.freeze({ result: ehResult, kind, reportIssue }) :
Object.freeze({ result: ehResult, kind }));
}
$acceptAction(handle: number, sessionId: string, requestId: string, action: IChatUserActionEvent): void {
$acceptAction(handle: number, result: IChatAgentResult, action: IChatUserActionEvent): void {
const agent = this._agents.get(handle);
if (!agent) {
return;
}
const result = this._resultsBySessionAndRequestId.get(sessionId)?.get(requestId);
if (!result) {
return;
}
if (action.action.kind === 'vote') {
// handled by $acceptFeedback
return;
}
const ehResult = typeConvert.ChatAgentResult.to(result);
if (action.action.kind === 'command') {
const commandAction: vscode.ChatAgentCommandAction = { kind: 'command', commandButton: typeConvert.ChatResponseProgress.toProgressContent(action.action.commandButton, this.commands.converter) as vscode.ChatAgentCommandButton };
agent.acceptAction(Object.freeze({ action: commandAction, result }));
agent.acceptAction(Object.freeze({ action: commandAction, result: ehResult }));
return;
} else {
agent.acceptAction(Object.freeze({ action: action.action, result }));
agent.acceptAction(Object.freeze({ action: action.action, result: ehResult }));
}
}
@ -355,10 +336,10 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
}
}
class ExtHostChatAgent<TResult extends vscode.ChatAgentResult2> {
class ExtHostChatAgent {
private _subCommandProvider: vscode.ChatAgentSubCommandProvider | undefined;
private _followupProvider: vscode.ChatAgentFollowupProvider<TResult> | undefined;
private _followupProvider: vscode.ChatAgentFollowupProvider | undefined;
private _description: string | undefined;
private _fullName: string | undefined;
private _iconPath: vscode.Uri | { light: vscode.Uri; dark: vscode.Uri } | vscode.ThemeIcon | undefined;
@ -367,7 +348,7 @@ class ExtHostChatAgent<TResult extends vscode.ChatAgentResult2> {
private _helpTextPostfix: string | vscode.MarkdownString | undefined;
private _sampleRequest?: string;
private _isSecondary: boolean | undefined;
private _onDidReceiveFeedback = new Emitter<vscode.ChatAgentResult2Feedback<TResult>>();
private _onDidReceiveFeedback = new Emitter<vscode.ChatAgentResult2Feedback>();
private _onDidPerformAction = new Emitter<vscode.ChatAgentUserActionEvent>();
private _supportIssueReporting: boolean | undefined;
private _agentVariableProvider?: { provider: vscode.ChatAgentCompletionItemProvider; triggerCharacters: string[] };
@ -381,7 +362,7 @@ class ExtHostChatAgent<TResult extends vscode.ChatAgentResult2> {
private readonly _callback: vscode.ChatAgentExtendedHandler,
) { }
acceptFeedback(feedback: vscode.ChatAgentResult2Feedback<TResult>) {
acceptFeedback(feedback: vscode.ChatAgentResult2Feedback) {
this._onDidReceiveFeedback.fire(feedback);
}
@ -415,7 +396,7 @@ class ExtHostChatAgent<TResult extends vscode.ChatAgentResult2> {
}));
}
async provideFollowups(result: TResult, token: CancellationToken): Promise<IChatFollowup[]> {
async provideFollowups(result: vscode.ChatAgentResult2, token: CancellationToken): Promise<IChatFollowup[]> {
if (!this._followupProvider) {
return [];
}
@ -458,7 +439,7 @@ class ExtHostChatAgent<TResult extends vscode.ChatAgentResult2> {
return content?.map(f => typeConvert.ChatFollowup.from(f));
}
get apiAgent(): vscode.ChatAgent2<TResult> {
get apiAgent(): vscode.ChatAgent2 {
let disposed = false;
let updateScheduled = false;
const updateMetadataSoon = () => {
@ -630,7 +611,7 @@ class ExtHostChatAgent<TResult extends vscode.ChatAgentResult2> {
that._onDidReceiveFeedback.dispose();
that._proxy.$unregisterAgent(that._handle);
},
} satisfies vscode.ChatAgent2<TResult>;
} satisfies vscode.ChatAgent2;
}
invoke(request: vscode.ChatAgentRequest, context: vscode.ChatAgentContext, response: vscode.ChatAgentExtendedResponseStream, token: CancellationToken): vscode.ProviderResult<vscode.ChatAgentResult2> {

View file

@ -34,7 +34,7 @@ import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol';
import { getPrivateApiFor } from 'vs/workbench/api/common/extHostTestingPrivateApi';
import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from 'vs/workbench/common/editor';
import { IViewBadge } from 'vs/workbench/common/views';
import { IChatAgentRequest } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents';
import * as chatProvider from 'vs/workbench/contrib/chat/common/chatProvider';
import { IChatCommandButton, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgressMessage, IChatTreeData } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables';
@ -2628,6 +2628,15 @@ export namespace ChatAgentCompletionItem {
}
}
export namespace ChatAgentResult {
export function to(result: IChatAgentResult): vscode.ChatAgentResult2 {
return {
errorDetails: result.errorDetails,
metadata: result.metadata,
};
}
}
export namespace TerminalQuickFix {
export function from(quickFix: vscode.TerminalQuickFixTerminalCommand | vscode.TerminalQuickFixOpener | vscode.Command, converter: Command.ICommandsConverter, disposables: DisposableStore): extHostProtocol.ITerminalQuickFixTerminalCommandDto | extHostProtocol.ITerminalQuickFixOpenerDto | extHostProtocol.ICommandDto | undefined {

View file

@ -108,6 +108,7 @@ export function registerChatCodeBlockActions() {
agentId: context.element.agent?.id,
sessionId: context.element.sessionId,
requestId: context.element.requestId,
result: context.element.result,
action: {
kind: 'copy',
codeBlockIndex: context.codeBlockIndex,
@ -151,6 +152,7 @@ export function registerChatCodeBlockActions() {
agentId: context.element.agent?.id,
sessionId: context.element.sessionId,
requestId: context.element.requestId,
result: context.element.result,
action: {
kind: 'copy',
codeBlockIndex: context.codeBlockIndex,
@ -325,6 +327,7 @@ export function registerChatCodeBlockActions() {
agentId: context.element.agent?.id,
sessionId: context.element.sessionId,
requestId: context.element.requestId,
result: context.element.result,
action: {
kind: 'insert',
codeBlockIndex: context.codeBlockIndex,
@ -370,6 +373,7 @@ export function registerChatCodeBlockActions() {
agentId: context.element.agent?.id,
sessionId: context.element.sessionId,
requestId: context.element.requestId,
result: context.element.result,
action: {
kind: 'insert',
codeBlockIndex: context.codeBlockIndex,
@ -464,6 +468,7 @@ export function registerChatCodeBlockActions() {
agentId: context.element.agent?.id,
sessionId: context.element.sessionId,
requestId: context.element.requestId,
result: context.element.result,
action: {
kind: 'runInTerminal',
codeBlockIndex: context.codeBlockIndex,

View file

@ -54,6 +54,7 @@ export function registerChatTitleActions() {
agentId: item.agent?.id,
sessionId: item.sessionId,
requestId: item.requestId,
result: item.result,
action: {
kind: 'vote',
direction: InteractiveSessionVoteDirection.Up,
@ -93,6 +94,7 @@ export function registerChatTitleActions() {
agentId: item.agent?.id,
sessionId: item.sessionId,
requestId: item.requestId,
result: item.result,
action: {
kind: 'vote',
direction: InteractiveSessionVoteDirection.Down,
@ -131,6 +133,7 @@ export function registerChatTitleActions() {
agentId: item.agent?.id,
sessionId: item.sessionId,
requestId: item.requestId,
result: item.result,
action: {
kind: 'bug'
}

View file

@ -289,13 +289,13 @@ class QuickChat extends Disposable {
}
for (const request of this.model.getRequests()) {
if (request.response?.response.value || request.response?.errorDetails) {
if (request.response?.response.value || request.response?.result) {
this.chatService.addCompleteRequest(widget.viewModel.sessionId,
request.message as IParsedChatRequest,
request.variableData,
{
message: request.response.response.value,
errorDetails: request.response.errorDetails,
result: request.response.result,
followups: request.response.followups
});
} else if (request.message) {

View file

@ -455,7 +455,8 @@ export class ChatWidget extends Disposable implements IChatWidget {
providerId: this.viewModel.providerId,
sessionId: this.viewModel.sessionId,
requestId: e.response.requestId,
agentId: e.response?.agent?.id,
agentId: e.response.agent?.id,
result: e.response.result,
action: {
kind: 'followUp',
followup: e.followup

View file

@ -31,7 +31,7 @@ export interface IChatAgentData {
export interface IChatAgent extends IChatAgentData {
invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatAgentResult>;
provideFollowups?(sessionId: string, token: CancellationToken): Promise<IChatFollowup[]>;
provideFollowups?(result: IChatAgentResult, token: CancellationToken): Promise<IChatFollowup[]>;
lastSlashCommands?: IChatAgentCommand[];
provideSlashCommands(token: CancellationToken): Promise<IChatAgentCommand[]>;
provideWelcomeMessage?(token: CancellationToken): ProviderResult<(string | IMarkdownString)[] | undefined>;
@ -89,13 +89,13 @@ export interface IChatAgentRequest {
}
export interface IChatAgentResult {
// delete, keep while people are still using the previous API
followUp?: IChatFollowup[];
errorDetails?: IChatResponseErrorDetails;
timings?: {
firstProgress?: number;
totalElapsed: number;
};
/** Extra properties that the agent can use to identify a result */
readonly metadata?: { readonly [key: string]: any };
}
export const IChatAgentService = createDecorator<IChatAgentService>('chatAgentService');
@ -105,7 +105,7 @@ export interface IChatAgentService {
readonly onDidChangeAgents: Event<void>;
registerAgent(agent: IChatAgent): IDisposable;
invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatAgentResult>;
getFollowups(id: string, sessionId: string, token: CancellationToken): Promise<IChatFollowup[]>;
getFollowups(id: string, result: IChatAgentResult, token: CancellationToken): Promise<IChatFollowup[]>;
getAgents(): Array<IChatAgent>;
getAgent(id: string): IChatAgent | undefined;
getDefaultAgent(): IChatAgent | undefined;
@ -184,7 +184,7 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
return await data.agent.invoke(request, progress, history, token);
}
async getFollowups(id: string, sessionId: string, token: CancellationToken): Promise<IChatFollowup[]> {
async getFollowups(id: string, result: IChatAgentResult, token: CancellationToken): Promise<IChatFollowup[]> {
const data = this._agents.get(id);
if (!data) {
throw new Error(`No agent with id ${id}`);
@ -194,6 +194,6 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
return [];
}
return data.agent.provideFollowups(sessionId, token);
return data.agent.provideFollowups(result, token);
}
}

View file

@ -14,9 +14,9 @@ import { URI, UriComponents, UriDto } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';
import { OffsetRange } from 'vs/editor/common/core/offsetRange';
import { ILogService } from 'vs/platform/log/common/log';
import { IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IChatAgentCommand, IChatAgentData, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { IChat, IChatAgentMarkdownContentWithVulnerability, IChatContent, IChatContentInlineReference, IChatContentReference, IChatMarkdownContent, IChatProgress, IChatProgressMessage, IChatFollowup, IChatResponse, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatTreeData, IChatUsedContext, InteractiveSessionVoteDirection, isIUsedContext, IChatCommandButton } from 'vs/workbench/contrib/chat/common/chatService';
import { IChat, IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatContent, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgress, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatTreeData, IChatUsedContext, InteractiveSessionVoteDirection, isIUsedContext } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables';
export interface IChatRequestVariableData {
@ -73,7 +73,7 @@ export interface IChatResponseModel {
readonly isStale: boolean;
readonly vote: InteractiveSessionVoteDirection | undefined;
readonly followups?: IChatFollowup[] | undefined;
readonly errorDetails?: IChatResponseErrorDetails;
readonly result?: IChatAgentResult;
setVote(vote: InteractiveSessionVoteDirection): void;
}
@ -227,8 +227,8 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel
return this._response;
}
public get errorDetails(): IChatResponseErrorDetails | undefined {
return this._errorDetails;
public get result(): IChatAgentResult | undefined {
return this._result;
}
public get providerId(): string {
@ -282,7 +282,7 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel
private _isComplete: boolean = false,
private _isCanceled = false,
private _vote?: InteractiveSessionVoteDirection,
private _errorDetails?: IChatResponseErrorDetails,
private _result?: IChatAgentResult,
followups?: ReadonlyArray<IChatFollowup>
) {
super();
@ -321,13 +321,13 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel
this._onDidChange.fire();
}
setErrorDetails(errorDetails?: IChatResponseErrorDetails): void {
this._errorDetails = errorDetails;
setResult(result: IChatAgentResult): void {
this._result = result;
this._onDidChange.fire();
}
complete(errorDetails?: IChatResponseErrorDetails): void {
if (errorDetails?.responseIsRedacted) {
complete(): void {
if (this._result?.errorDetails?.responseIsRedacted) {
this._response.clear();
}
@ -380,7 +380,8 @@ export interface ISerializableChatRequestData {
response: ReadonlyArray<IMarkdownString | IChatResponseProgressFileTreeData | IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability> | undefined;
agent?: ISerializableChatAgentData;
slashCommand?: IChatAgentCommand;
responseErrorDetails: IChatResponseErrorDetails | undefined;
// responseErrorDetails: IChatResponseErrorDetails | undefined;
result?: IChatAgentResult; // Optional for backcompat
followups: ReadonlyArray<IChatFollowup> | undefined;
isCanceled: boolean | undefined;
vote: InteractiveSessionVoteDirection | undefined;
@ -561,13 +562,18 @@ export class ChatModel extends Disposable implements IChatModel {
typeof raw.message === 'string'
? this.getParsedRequestFromString(raw.message)
: reviveParsedChatRequest(raw.message);
// Only old messages don't have variableData
const variableData: IChatRequestVariableData = raw.variableData ?? { message: parsedRequest.text, variables: {} };
const request = new ChatRequestModel(this, parsedRequest, variableData);
if (raw.response || raw.responseErrorDetails) {
if (raw.response || raw.result || (raw as any).responseErrorDetails) {
const agent = (raw.agent && 'metadata' in raw.agent) ? // Check for the new format, ignore entries in the old format
revive<ISerializableChatAgentData>(raw.agent) : undefined;
request.response = new ChatResponseModel(raw.response ?? [new MarkdownString(raw.response)], this, agent, raw.slashCommand, request.id, true, raw.isCanceled, raw.vote, raw.responseErrorDetails, raw.followups);
// Port entries from old format
const result = 'responseErrorDetails' in raw ?
{ errorDetails: raw.responseErrorDetails } as IChatAgentResult : raw.result;
request.response = new ChatResponseModel(raw.response ?? [new MarkdownString(raw.response)], this, agent, raw.slashCommand, request.id, true, raw.isCanceled, raw.vote, result, raw.followups);
if (raw.usedContext) { // @ulugbekna: if this's a new vscode sessions, doc versions are incorrect anyway?
request.response.applyReference(revive(raw.usedContext));
}
@ -700,7 +706,7 @@ export class ChatModel extends Disposable implements IChatModel {
}
}
setResponse(request: ChatRequestModel, rawResponse: IChatResponse): void {
setResponse(request: ChatRequestModel, result: IChatAgentResult): void {
if (!this._session) {
throw new Error('completeResponse: No session');
}
@ -709,15 +715,15 @@ export class ChatModel extends Disposable implements IChatModel {
request.response = new ChatResponseModel([], this, undefined, undefined, request.id);
}
request.response.setErrorDetails(rawResponse.errorDetails);
request.response.setResult(result);
}
completeResponse(request: ChatRequestModel, errorDetails: IChatResponseErrorDetails | undefined): void {
completeResponse(request: ChatRequestModel): void {
if (!request.response) {
throw new Error('Call setResponse before completeResponse');
}
request.response.complete(errorDetails);
request.response.complete();
}
setFollowups(request: ChatRequestModel, followups: IChatFollowup[] | undefined): void {
@ -748,8 +754,12 @@ export class ChatModel extends Disposable implements IChatModel {
}
}),
requests: this._requests.map((r): ISerializableChatRequestData => {
const message = {
...r.message,
parts: r.message.parts.map(p => p && 'toJSON' in p ? (p.toJSON as Function)() : p)
};
return {
message: r.message,
message,
variableData: r.variableData,
response: r.response ?
r.response.response.value.map(item => {
@ -763,7 +773,7 @@ export class ChatModel extends Disposable implements IChatModel {
}
})
: undefined,
responseErrorDetails: r.response?.errorDetails,
result: r.response?.result,
followups: r.response?.followups,
isCanceled: r.response?.isCanceled,
vote: r.response?.vote,

View file

@ -78,6 +78,21 @@ export class ChatRequestAgentPart implements IParsedChatRequestPart {
get promptText(): string {
return '';
}
/**
* Don't stringify all the agent methods, just data.
*/
toJSON(): any {
return {
kind: this.kind,
range: this.range,
editorRange: this.editorRange,
agent: {
id: this.agent.id,
metadata: this.agent.metadata
}
};
}
}
/**

View file

@ -12,7 +12,7 @@ import { IRange, Range } from 'vs/editor/common/core/range';
import { Command, Location, ProviderResult } from 'vs/editor/common/languages';
import { FileType } from 'vs/platform/files/common/files';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IChatAgentCommand, IChatAgentData } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IChatAgentCommand, IChatAgentData, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents';
import { ChatModel, IChatModel, IChatRequestVariableData, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel';
import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables';
@ -40,15 +40,6 @@ export interface IChatResponseErrorDetails {
responseIsRedacted?: boolean;
}
export interface IChatResponse {
session: IChat;
errorDetails?: IChatResponseErrorDetails;
timings?: {
firstProgress?: number;
totalElapsed: number;
};
}
export interface IChatResponseProgressFileTreeData {
label: string;
uri: URI;
@ -234,6 +225,7 @@ export interface IChatUserActionEvent {
agentId: string | undefined;
sessionId: string;
requestId: string;
result: IChatAgentResult | undefined;
}
export interface IChatDynamicRequest {
@ -250,7 +242,7 @@ export interface IChatDynamicRequest {
export interface IChatCompleteResponse {
message: string | ReadonlyArray<IChatProgress>;
errorDetails?: IChatResponseErrorDetails;
result?: IChatAgentResult;
followups?: IChatFollowup[];
}

View file

@ -20,13 +20,13 @@ 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 { IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys';
import { ChatModel, ChatModelInitState, ChatRequestModel, ChatWelcomeMessageModel, IChatModel, IChatRequestVariableData, ISerializableChatData, ISerializableChatsData } from 'vs/workbench/contrib/chat/common/chatModel';
import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, getPromptText } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { ChatMessageRole, IChatMessage } from 'vs/workbench/contrib/chat/common/chatProvider';
import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser';
import { ChatAgentCopyKind, IChat, IChatCompleteResponse, IChatDetail, IChatDynamicRequest, IChatFollowup, IChatProgress, IChatProvider, IChatProviderInfo, IChatResponse, IChatService, IChatTransferredSessionData, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
import { ChatAgentCopyKind, IChat, IChatCompleteResponse, IChatDetail, IChatDynamicRequest, IChatFollowup, IChatProgress, IChatProvider, IChatProviderInfo, IChatService, IChatTransferredSessionData, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
@ -516,7 +516,7 @@ export class ChatService extends Disposable implements IChatService {
this._onDidSubmitAgent.fire({ agent: agentPart.agent, slashCommand: agentSlashCommandPart.command, sessionId: model.sessionId });
}
let rawResponse: IChatResponse | null | undefined;
let rawResult: IChatAgentResult | null | undefined;
let agentOrCommandFollowups: Promise<IChatFollowup[] | undefined> | undefined = undefined;
const defaultAgent = this.chatAgentService.getDefaultAgent();
@ -536,7 +536,7 @@ export class ChatService extends Disposable implements IChatService {
variables: request.variableData.variables,
command: request.response.slashCommand?.name
};
history.push({ request: historyRequest, response: request.response.response.value, result: { errorDetails: request.response.errorDetails } });
history.push({ request: historyRequest, response: request.response.response.value, result: request.response.result ?? {} });
}
const initVariableData: IChatRequestVariableData = { message: getPromptText(parsedRequest.parts), variables: {} };
@ -554,13 +554,8 @@ export class ChatService extends Disposable implements IChatService {
};
const agentResult = await this.chatAgentService.invokeAgent(agent.id, requestProps, progressCallback, history, token);
rawResponse = {
session: model.session!,
errorDetails: agentResult.errorDetails,
timings: agentResult.timings
};
agentOrCommandFollowups = agentResult?.followUp ? Promise.resolve(agentResult.followUp) :
this.chatAgentService.getFollowups(agent.id, sessionId, CancellationToken.None);
rawResult = agentResult;
agentOrCommandFollowups = this.chatAgentService.getFollowups(agent.id, agentResult, CancellationToken.None);
} else if (commandPart && this.chatSlashCommandService.hasCommand(commandPart.slashCommand.command)) {
request = model.addRequest(parsedRequest, { message, variables: {} });
// contributed slash commands
@ -577,7 +572,7 @@ export class ChatService extends Disposable implements IChatService {
progressCallback(p);
}), history, token);
agentOrCommandFollowups = Promise.resolve(commandResult?.followUp);
rawResponse = { session: model.session! };
rawResult = {};
} else {
throw new Error(`Cannot handle request`);
@ -586,36 +581,36 @@ export class ChatService extends Disposable implements IChatService {
if (token.isCancellationRequested) {
return;
} else {
if (!rawResponse) {
if (!rawResult) {
this.trace('sendRequest', `Provider returned no response for session ${model.sessionId}`);
rawResponse = { session: model.session!, errorDetails: { message: localize('emptyResponse', "Provider returned null response") } };
rawResult = { errorDetails: { message: localize('emptyResponse', "Provider returned null response") } };
}
const result = rawResponse.errorDetails?.responseIsFiltered ? 'filtered' :
rawResponse.errorDetails && gotProgress ? 'errorWithOutput' :
rawResponse.errorDetails ? 'error' :
const result = rawResult.errorDetails?.responseIsFiltered ? 'filtered' :
rawResult.errorDetails && gotProgress ? 'errorWithOutput' :
rawResult.errorDetails ? 'error' :
'success';
this.telemetryService.publicLog2<ChatProviderInvokedEvent, ChatProviderInvokedClassification>('interactiveSessionProviderInvoked', {
providerId: provider.id,
timeToFirstProgress: rawResponse.timings?.firstProgress,
totalTime: rawResponse.timings?.totalElapsed,
timeToFirstProgress: rawResult.timings?.firstProgress,
totalTime: rawResult.timings?.totalElapsed,
result,
requestType,
agent: agentPart?.agent.id ?? '',
slashCommand: agentSlashCommandPart ? agentSlashCommandPart.command.name : commandPart?.slashCommand.command,
chatSessionId: model.sessionId
});
model.setResponse(request, rawResponse);
model.setResponse(request, rawResult);
this.trace('sendRequest', `Provider returned response for session ${model.sessionId}`);
// TODO refactor this or rethink the API https://github.com/microsoft/vscode-copilot/issues/593
if (agentOrCommandFollowups) {
agentOrCommandFollowups.then(followups => {
model.setFollowups(request, followups);
model.completeResponse(request, rawResponse?.errorDetails);
model.completeResponse(request);
});
} else {
model.completeResponse(request, rawResponse?.errorDetails);
model.completeResponse(request);
}
}
} finally {
@ -674,14 +669,11 @@ export class ChatService extends Disposable implements IChatService {
model.acceptResponseProgress(request, part, true);
}
}
model.setResponse(request, {
session: model.session!,
errorDetails: response.errorDetails,
});
model.setResponse(request, response.result || {});
if (response.followups !== undefined) {
model.setFollowups(request, response.followups);
}
model.completeResponse(request, response.errorDetails);
model.completeResponse(request);
}
cancelCurrentRequestForSession(sessionId: string): void {

View file

@ -8,7 +8,7 @@ import { Disposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ILogService } from 'vs/platform/log/common/log';
import { IChatAgentCommand, IChatAgentData } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IChatAgentCommand, IChatAgentData, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents';
import { ChatModelInitState, IChatModel, IChatRequestModel, IChatResponseModel, IChatWelcomeMessageContent, IResponse } from 'vs/workbench/contrib/chat/common/chatModel';
import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { IChatContentReference, IChatProgressMessage, IChatFollowup, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatUsedContext, InteractiveSessionVoteDirection, IChatCommandButton } from 'vs/workbench/contrib/chat/common/chatService';
@ -122,6 +122,7 @@ export interface IChatResponseViewModel {
readonly vote: InteractiveSessionVoteDirection | undefined;
readonly replyFollowups?: IChatFollowup[];
readonly errorDetails?: IChatResponseErrorDetails;
readonly result?: IChatAgentResult;
readonly contentUpdateTimings?: IChatLiveUpdateData;
renderData?: IChatResponseRenderData;
agentAvatarHasBeenRendered?: boolean;
@ -203,7 +204,7 @@ export class ChatViewModel extends Disposable implements IChatViewModel {
if (typeof responseIdx === 'number' && responseIdx >= 0) {
const items = this._items.splice(responseIdx, 1);
const item = items[0];
if (isResponseVM(item)) {
if (item instanceof ChatResponseViewModel) {
item.dispose();
}
}
@ -334,8 +335,12 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi
return this._model.followups?.filter((f): f is IChatFollowup => f.kind === 'reply');
}
get errorDetails() {
return this._model.errorDetails;
get result() {
return this._model.result;
}
get errorDetails(): IChatResponseErrorDetails | undefined {
return this.result?.errorDetails;
}
get vote() {

View file

@ -10,6 +10,7 @@
text: "@ChatProviderWithUsedContext test request",
parts: [
{
kind: "agent",
range: {
start: 0,
endExclusive: 28
@ -22,11 +23,8 @@
},
agent: {
id: "ChatProviderWithUsedContext",
metadata: { },
provideSlashCommands: [Function provideSlashCommands],
invoke: [Function invoke]
},
kind: "agent"
metadata: { }
}
},
{
range: {
@ -49,8 +47,8 @@
variables: { }
},
response: [ ],
responseErrorDetails: undefined,
followups: [ ],
result: { metadata: { metadataKey: "value" } },
followups: undefined,
isCanceled: false,
vote: undefined,
agent: {

View file

@ -9,6 +9,7 @@
message: {
parts: [
{
kind: "agent",
range: {
start: 0,
endExclusive: 28
@ -21,11 +22,8 @@
},
agent: {
id: "ChatProviderWithUsedContext",
metadata: { },
provideSlashCommands: [Function provideSlashCommands],
invoke: [Function invoke]
},
kind: "agent"
metadata: { }
}
},
{
range: {
@ -49,7 +47,7 @@
variables: { }
},
response: [ ],
responseErrorDetails: undefined,
result: { metadata: { metadataKey: "value" } },
followups: undefined,
isCanceled: false,
vote: undefined,

View file

@ -75,7 +75,10 @@ const chatAgentWithUsedContext: IChatAgent = {
kind: 'usedContext'
});
return {};
return { metadata: { metadataKey: 'value' } };
},
async provideFollowups(sessionId, token) {
return [{ kind: 'reply', message: 'Something else', tooltip: 'a tooltip' }];
},
};
@ -110,6 +113,9 @@ suite('Chat', () => {
async invoke(request, progress, history, token) {
return {};
},
async provideSlashCommands(token) {
return [];
},
} as IChatAgent;
testDisposables.add(chatAgentService.registerAgent(agent));
});
@ -224,6 +230,7 @@ suite('Chat', () => {
const response = await testService.sendRequest(model.sessionId, `@${chatAgentWithUsedContextId} test request`);
assert(response);
await response.responseCompletePromise;
assert.strictEqual(model.getRequests().length, 1);

View file

@ -44,7 +44,7 @@ suite('VoiceChat', () => {
readonly onDidChangeAgents = Event.None;
registerAgent(agent: IChatAgent): IDisposable { throw new Error(); }
invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatAgentResult> { throw new Error(); }
getFollowups(id: string, sessionId: string, token: CancellationToken): Promise<IChatFollowup[]> { throw new Error(); }
getFollowups(id: string, result: IChatAgentResult, token: CancellationToken): Promise<IChatFollowup[]> { throw new Error(); }
getAgents(): Array<IChatAgent> { return agents; }
getAgent(id: string): IChatAgent | undefined { throw new Error(); }
getDefaultAgent(): IChatAgent | undefined { throw new Error(); }

View file

@ -86,9 +86,10 @@ declare module 'vscode' {
*/
errorDetails?: ChatAgentErrorDetails;
// TODO@API
// add CATCH-all signature [name:string]: string|boolean|number instead of `T extends...`
// readonly metadata: { readonly [key: string]: any };
/**
* Arbitrary metadata for this result. Can be anything but must be JSON-stringifyable.
*/
readonly metadata?: { readonly [key: string]: any };
}
/**
@ -109,12 +110,12 @@ declare module 'vscode' {
/**
* Represents user feedback for a result.
*/
export interface ChatAgentResult2Feedback<TResult extends ChatAgentResult2> {
export interface ChatAgentResult2Feedback {
/**
* This instance of ChatAgentResult2 is the same instance that was returned from the chat agent,
* and it can be extended with arbitrary properties if needed.
*/
readonly result: TResult;
readonly result: ChatAgentResult2;
/**
* The kind of feedback that was received.
@ -196,16 +197,16 @@ declare module 'vscode' {
/**
* Will be invoked once after each request to get suggested followup questions to show the user. The user can click the followup to send it to the chat.
*/
export interface ChatAgentFollowupProvider<TResult extends ChatAgentResult2> {
export interface ChatAgentFollowupProvider {
/**
*
* @param result The same instance of the result object that was returned by the chat agent, and it can be extended with arbitrary properties if needed.
* @param token A cancellation token.
*/
provideFollowups(result: TResult, token: CancellationToken): ProviderResult<ChatAgentFollowup[]>;
provideFollowups(result: ChatAgentResult2, token: CancellationToken): ProviderResult<ChatAgentFollowup[]>;
}
export interface ChatAgent2<TResult extends ChatAgentResult2> {
export interface ChatAgent2 {
/**
* The short name by which this agent is referred to in the UI, e.g `workspace`.
@ -244,7 +245,7 @@ declare module 'vscode' {
/**
* This provider will be called once after each request to retrieve suggested followup questions.
*/
followupProvider?: ChatAgentFollowupProvider<TResult>;
followupProvider?: ChatAgentFollowupProvider;
// TODO@API
@ -268,7 +269,7 @@ declare module 'vscode' {
* The passed {@link ChatAgentResult2Feedback.result result} is guaranteed to be the same instance that was
* previously returned from this chat agent.
*/
onDidReceiveFeedback: Event<ChatAgentResult2Feedback<TResult>>;
onDidReceiveFeedback: Event<ChatAgentResult2Feedback>;
/**
* Dispose this agent and free resources
@ -452,7 +453,7 @@ declare module 'vscode' {
* @param handler The reply-handler of the agent.
* @returns A new chat agent
*/
export function createChatAgent<TResult extends ChatAgentResult2>(name: string, handler: ChatAgentHandler): ChatAgent2<TResult>;
export function createChatAgent(name: string, handler: ChatAgentHandler): ChatAgent2;
/**
* Register a variable which can be used in a chat request to any agent.

View file

@ -5,7 +5,7 @@
declare module 'vscode' {
export interface ChatAgent2<TResult extends ChatAgentResult2> {
export interface ChatAgent2 {
onDidPerformAction: Event<ChatAgentUserActionEvent>;
supportIssueReporting?: boolean;
}
@ -152,7 +152,7 @@ declare module 'vscode' {
report(value: ChatAgentExtendedProgress): void;
};
export interface ChatAgent2<TResult extends ChatAgentResult2> {
export interface ChatAgent2 {
/**
* Provide a set of variables that can only be used with this agent.
*/
@ -179,7 +179,7 @@ declare module 'vscode' {
/**
* Create a chat agent with the extended progress type
*/
export function createChatAgent<TResult extends ChatAgentResult2>(name: string, handler: ChatAgentExtendedHandler): ChatAgent2<TResult>;
export function createChatAgent(name: string, handler: ChatAgentExtendedHandler): ChatAgent2;
}
/*

View file

@ -12,7 +12,7 @@ declare module 'vscode' {
provideSampleQuestions?(token: CancellationToken): ProviderResult<ChatAgentFollowup[]>;
}
export interface ChatAgent2<TResult extends ChatAgentResult2> {
export interface ChatAgent2 {
/**
* When true, this agent is invoked by default when no other agent is being invoked
*/