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