Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/conversation/vscode-node/feedbackReporter.ts
13399 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
7
import { Raw } from '@vscode/prompt-tsx';
8
import * as vscode from 'vscode';
9
import { ChatLocation } from '../../../platform/chat/common/commonTypes';
10
import { getTextPart, roleToString } from '../../../platform/chat/common/globalStringUtils';
11
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
12
import { IEditLogService } from '../../../platform/multiFileEdit/common/editLogService';
13
import { ILoggedPendingRequest, IRequestLogger, LoggedInfoKind, LoggedRequestKind } from '../../../platform/requestLogger/common/requestLogger';
14
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';
15
import { Disposable } from '../../../util/vs/base/common/lifecycle';
16
import { IObservable } from '../../../util/vs/base/common/observableInternal';
17
import { basename } from '../../../util/vs/base/common/resources';
18
import { splitLinesIncludeSeparators } from '../../../util/vs/base/common/strings';
19
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
20
import { EXTENSION_ID } from '../../common/constants';
21
import { InteractionOutcome, PromptQuery } from '../../inlineChat/node/promptCraftingTypes';
22
import { Conversation, RequestDebugInformation, Turn } from '../../prompt/common/conversation';
23
import { IntentInvocationMetadata } from '../../prompt/node/conversation';
24
import { IFeedbackReporter } from '../../prompt/node/feedbackReporter';
25
import { SearchFeedbackKind, SemanticSearchTextSearchProvider } from '../../workspaceSemanticSearch/node/semanticSearchTextSearchProvider';
26
import { WorkspaceStateSnapshotHelper } from './logWorkspaceState';
27
28
const SEPARATOR = '---------------------------------';
29
30
export class FeedbackReporter extends Disposable implements IFeedbackReporter {
31
32
declare readonly _serviceBrand: undefined;
33
34
readonly canReport: IObservable<boolean>;
35
36
constructor(
37
@IInstantiationService private readonly _instantiationService: IInstantiationService,
38
@IConfigurationService private readonly _configurationService: IConfigurationService,
39
@IRequestLogger private readonly _requestLogger: IRequestLogger,
40
@ITelemetryService private readonly telemetryService: ITelemetryService,
41
@IEditLogService private readonly _editLogService: IEditLogService,
42
) {
43
super();
44
45
this.canReport = this._configurationService.getConfigObservable(ConfigKey.TeamInternal.DebugReportFeedback);
46
}
47
48
private _findChatParamsForTurn(turn: Turn): ILoggedPendingRequest | undefined {
49
for (const request of this._requestLogger.getRequests().reverse()) {
50
if (request.kind !== LoggedInfoKind.Request) {
51
continue;
52
}
53
if (request.entry.type === LoggedRequestKind.MarkdownContentRequest) {
54
continue;
55
}
56
if (request.entry.chatParams.ourRequestId === turn.id) {
57
return (<ILoggedPendingRequest>request.entry.chatParams);
58
}
59
}
60
}
61
62
async reportInline(conversation: Conversation, promptQuery: PromptQuery, interactionOutcome: InteractionOutcome): Promise<void> {
63
if (!this.canReport) {
64
return;
65
}
66
67
const turn = conversation.getLatestTurn();
68
const latestMessages = this._findChatParamsForTurn(turn)?.messages;
69
70
const intentDump = promptQuery.intent ? this._embedCodeblock('INTENT', promptQuery.intent.id) : '';
71
const contextDump = this._embedCodeblock('CONTEXT', JSON.stringify({
72
document: promptQuery.document.uri.toString(),
73
fileIndentInfo: promptQuery.fileIndentInfo,
74
language: promptQuery.language,
75
wholeRange: promptQuery.wholeRange,
76
selection: promptQuery.selection,
77
}, null, '\t'));
78
let messagesDump = '';
79
80
if (latestMessages && latestMessages.length > 0) {
81
const messagesInfo = latestMessages.map(message => this._embedCodeblock(roleToString(message.role).toUpperCase(), getTextPart(message.content))).join('\n');
82
messagesDump = `\t${SEPARATOR}\n${this._headerSeparator()}PROMPT MESSAGES:\n${messagesInfo}`;
83
} else {
84
messagesDump = this._embedCodeblock(turn.request.type.toUpperCase(), turn.request.message);
85
}
86
87
const responseDump = this._embedCodeblock('ASSISTANT', turn.responseMessage?.message || '');
88
const parsedReplyDump = this._embedCodeblock('Interaction outcome', JSON.stringify(interactionOutcome, null, '\t'));
89
90
const output: string[] = [];
91
appendPromptDetailsSection(output, intentDump, contextDump, messagesDump, responseDump, parsedReplyDump);
92
await appendSTestSection(output, turn);
93
await this._reportIssue('Feedback for inline chat', output.join('\n'));
94
}
95
96
async reportChat(turn: Turn): Promise<void> {
97
if (!this.canReport) {
98
return;
99
}
100
101
let messagesDump = '';
102
const params = this._findChatParamsForTurn(turn);
103
104
if (params?.messages && params.messages.length > 0) {
105
const messagesInfo = params.messages.map(message => {
106
let content = getTextPart(message.content);
107
108
if (message.content.some(part => part.type === Raw.ChatCompletionContentPartKind.CacheBreakpoint)) {
109
content += `\ncopilot_cache_control: { type: 'ephemeral' }`;
110
}
111
if (message.role === Raw.ChatRole.Assistant && message.toolCalls?.length) {
112
if (content) {
113
content += '\n';
114
}
115
content += message.toolCalls.map(c => {
116
let argsStr = c.function.arguments;
117
try {
118
const parsedArgs = JSON.parse(c.function.arguments);
119
argsStr = JSON.stringify(parsedArgs, undefined, 2);
120
} catch (e) { }
121
return `🛠️ ${c.function.name} (${c.id}) ${argsStr}`;
122
}).join('\n');
123
} else if (message.role === Raw.ChatRole.Tool) {
124
content = `🛠️ ${message.toolCallId}\n${content}`;
125
}
126
127
return this._embedCodeblock(roleToString(message.role).toUpperCase(), content);
128
}).join('\n');
129
messagesDump += `\t${SEPARATOR}\n${this._headerSeparator()}PROMPT MESSAGES:\n${messagesInfo}`;
130
} else {
131
messagesDump += this._embedCodeblock(turn.request.type.toUpperCase(), turn.request.message);
132
}
133
134
const intent = turn.getMetadata(IntentInvocationMetadata)?.value.intent;
135
const intentDump = intent ? this._embedCodeblock('INTENT', `[${intent.id}] ${intent.description}`) : '';
136
const responseDump = this._embedCodeblock('ASSISTANT', turn.responseMessage?.message || '');
137
const workspaceState = await this._instantiationService.createInstance(WorkspaceStateSnapshotHelper).captureWorkspaceStateSnapshot([]);
138
const workspaceStateDump = this._embedCodeblock('WORKSPACE STATE', JSON.stringify(workspaceState, null, 2));
139
const toolsDump = params?.body?.tools ? this._embedCodeblock('TOOLS', JSON.stringify(params.body.tools, null, 2)) : '';
140
const metadata = this._embedCodeblock('METADATA', `requestID: ${turn.id}\nmodel: ${params?.model}`);
141
const edits = (await this._editLogService.getEditLog(turn.id))?.map((edit, i) => {
142
return this._embedCodeblock(`EDIT ${i + 1}`, JSON.stringify(edit, null, 2));
143
}).join('\n') || '';
144
145
const output: string[] = [];
146
147
appendPromptDetailsSection(output, intentDump, messagesDump, responseDump, workspaceStateDump, toolsDump, metadata, edits);
148
await appendSTestSection(output, turn);
149
150
await this._reportIssue('Feedback for sidebar chat', output.join('\n'));
151
}
152
153
async reportSearch(kind: SearchFeedbackKind): Promise<void> {
154
/* __GDPR__
155
"copilot.search.feedback" : {
156
"owner": "osortega",
157
"comment": "Feedback telemetry for copilot search",
158
"kind": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Feedback provided by the user." },
159
"chunkCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Count of copilot search code chunks." },
160
"rankResult": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Result of the copilot search ranking." },
161
"rankResultsCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Count of the results from copilot search ranking." },
162
"combinedResultsCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Count of combined results from copilot search." },
163
"chunkSearchDuration": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true, "comment": "Duration of the chunk search" },
164
"llmFilteringDuration": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true, "comment": "Duration of the LLM filtering" }
165
}
166
*/
167
this.telemetryService.sendMSFTTelemetryEvent('copilot.search.feedback', {
168
kind,
169
rankResult: SemanticSearchTextSearchProvider.feedBackTelemetry.rankResult,
170
}, {
171
chunkCount: SemanticSearchTextSearchProvider.feedBackTelemetry.chunkCount,
172
rankResultsCount: SemanticSearchTextSearchProvider.feedBackTelemetry.rankResultsCount,
173
combinedResultsCount: SemanticSearchTextSearchProvider.feedBackTelemetry.combinedResultsCount,
174
chunkSearchDuration: SemanticSearchTextSearchProvider.feedBackTelemetry.chunkSearchDuration,
175
llmFilteringDuration: SemanticSearchTextSearchProvider.feedBackTelemetry.llmFilteringDuration,
176
});
177
}
178
179
private _embedCodeblock(header: string, text: string) {
180
const body = this._bodySeparator() + text.split('\n').join(`\n${this._bodySeparator()}`);
181
return `\t${SEPARATOR}\n${this._headerSeparator()}${header}:\n${body}`;
182
}
183
184
private _headerSeparator() {
185
return `\t`;
186
}
187
188
private _bodySeparator() {
189
return `\t\t`;
190
}
191
192
private async _reportIssue(title: string, body: string) {
193
openIssueReporter({ title, data: body });
194
}
195
}
196
197
export async function openIssueReporter(args: { title: string; issueBody?: string; data: string; public?: boolean }) {
198
await vscode.commands.executeCommand('workbench.action.openIssueReporter', {
199
extensionId: EXTENSION_ID,
200
issueTitle: args.title,
201
data: args.data,
202
issueBody: args.issueBody ?? '',
203
// team -> vscode-copilot
204
uri: vscode.Uri.parse(args.public ? 'https://github.com/microsoft/vscode' : 'https://github.com/microsoft/vscode-copilot-issues'),
205
});
206
}
207
208
function appendPromptDetailsSection(output: string[], ...dumps: string[]): void {
209
output.push(
210
`<details><summary>Prompt Details</summary>`,
211
`<p>`,
212
'', // Necessary for the indentation to render as a codeblock inside the <p>
213
...dumps,
214
`</p>`,
215
`</details>`,
216
);
217
}
218
219
async function appendSTestSection(output: string[], turn: Turn): Promise<void> {
220
const test = await generateSTest(turn);
221
if (test) {
222
output.push(
223
`<details><summary>STest</summary>`,
224
`<p>`,
225
`STest code:`,
226
``,
227
'```ts',
228
...test,
229
'```',
230
`</p>`,
231
`</details>`,
232
);
233
}
234
}
235
236
export async function generateSTest(turn: Turn): Promise<string[] | undefined> {
237
const intentInvocation = turn.getMetadata(IntentInvocationMetadata)?.value;
238
if (intentInvocation) {
239
if (intentInvocation.location === ChatLocation.Editor) {
240
return generateInlineChatSTest(turn);
241
}
242
}
243
return undefined;
244
}
245
246
247
export function generateInlineChatSTest(turn: Turn): string[] | undefined {
248
const requestInfo = turn.getMetadata(RequestDebugInformation);
249
if (!requestInfo) {
250
return undefined;
251
}
252
const fileName = basename(requestInfo.uri);
253
const str = (val: unknown) => JSON.stringify(val);
254
255
return [
256
`stest({ description: 'Issue #XXXXX', language: ${str(requestInfo.languageId)}, model }, (testingServiceCollection) => {`,
257
` return simulateInlineChat(testingServiceCollection, {`,
258
` files: [toFile({`,
259
` fileName: ${str(`${requestInfo.intentId}/issue-XXXXX/${fileName}`)},`,
260
` fileContents: [`,
261
...splitLinesIncludeSeparators(requestInfo.initialDocumentText).map(line => ` ${str(line)},`),
262
` ]`,
263
` })],`,
264
` queries: [`,
265
` {`,
266
` file: ${str(fileName)},`,
267
` selection: ${str(selectionAsArray(requestInfo.userSelection))},`,
268
` query: ${str(requestInfo.userPrompt)},`,
269
` diagnostics: 'tsc',`,
270
` expectedIntent: ${str(requestInfo.intentId)},`,
271
` validate: async (outcome, workspace, accessor) => {`,
272
` assertInlineEdit(outcome);`,
273
` await assertNoDiagnosticsAsync(accessor, outcome, workspace, KnownDiagnosticProviders.tscIgnoreImportErrors);`,
274
` }`,
275
` }`,
276
` ]`,
277
` });`,
278
`});`
279
];
280
}
281
282
function selectionAsArray(range: vscode.Range) {
283
return [range.start.line, range.start.character, range.end.line, range.end.character];
284
}
285
286