Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/prompt/node/githubPullRequestTitleAndDescriptionGenerator.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
import { RenderPromptResult } from '@vscode/prompt-tsx';
7
import { IAuthenticationService } from '../../../platform/authentication/common/authentication';
8
import { ChatFetchResponseType, ChatLocation } from '../../../platform/chat/common/commonTypes';
9
import { IConversationOptions } from '../../../platform/chat/common/conversationOptions';
10
import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';
11
import { IIgnoreService } from '../../../platform/ignore/common/ignoreService';
12
import { ILogService } from '../../../platform/log/common/logService';
13
import { INotificationService } from '../../../platform/notification/common/notificationService';
14
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
15
import { DisposableStore, } from '../../../util/vs/base/common/lifecycle';
16
import { isStringArray } from '../../../util/vs/base/common/types';
17
import { URI } from '../../../util/vs/base/common/uri';
18
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
19
import { TitleAndDescriptionProvider } from '../../githubPullRequest';
20
import { PromptRenderer } from '../../prompts/node/base/promptRenderer';
21
import { GitHubPullRequestPrompt } from '../../prompts/node/github/pullRequestDescriptionPrompt';
22
23
export class GitHubPullRequestTitleAndDescriptionGenerator implements TitleAndDescriptionProvider {
24
protected readonly disposables: DisposableStore = new DisposableStore();
25
private lastContext: { commitMessages: string[]; patches: string[] } = { commitMessages: [], patches: [] };
26
27
constructor(
28
@ILogService protected readonly logService: ILogService,
29
@IConversationOptions private readonly options: IConversationOptions,
30
@IIgnoreService private readonly ignoreService: IIgnoreService,
31
@IEndpointProvider private readonly endpointProvider: IEndpointProvider,
32
@IInstantiationService private readonly instantiationService: IInstantiationService,
33
@INotificationService private readonly notificationService: INotificationService,
34
@IAuthenticationService private readonly authService: IAuthenticationService,
35
) {
36
this.logService.info('[githubTitleAndDescriptionProvider] Initializing GitHub PR title and description provider provider.');
37
}
38
39
dispose() {
40
this.disposables.dispose();
41
}
42
43
private isRegenerate(commitMessages: string[], patches: string[]): boolean {
44
if (commitMessages.length !== this.lastContext.commitMessages.length || patches.length !== this.lastContext.patches.length) {
45
return false;
46
}
47
for (let i = 0; i < commitMessages.length; i++) {
48
if (commitMessages[i] !== this.lastContext.commitMessages[i]) {
49
return false;
50
}
51
}
52
for (let i = 0; i < patches.length; i++) {
53
if (patches[i] !== this.lastContext.patches[i]) {
54
return false;
55
}
56
}
57
return true;
58
}
59
60
private async excludePatches(allPatches: { patch: string; fileUri?: string; previousFileUri?: string }[]): Promise<string[]> {
61
const patches: string[] = [];
62
for (const patch of allPatches) {
63
if (patch.fileUri && await this.ignoreService.isCopilotIgnored(URI.parse(patch.fileUri))) {
64
continue;
65
}
66
67
if (patch.previousFileUri && patch.previousFileUri !== patch.fileUri && await this.ignoreService.isCopilotIgnored(URI.parse(patch.previousFileUri))) {
68
continue;
69
}
70
71
patches.push(patch.patch);
72
}
73
return patches;
74
}
75
76
async provideTitleAndDescription(context: { commitMessages: string[]; patches: string[] | { patch: string; fileUri: string; previousFileUri?: string }[]; issues?: { reference: string; content: string }[]; template?: string; compareBranch?: string }, token: CancellationToken): Promise<{ title: string; description?: string } | undefined> {
77
const commitMessages: string[] = context.commitMessages;
78
const allPatches: { patch: string; fileUri?: string; previousFileUri?: string }[] = isStringArray(context.patches) ? context.patches.map(patch => ({ patch })) : context.patches;
79
const patches = await this.excludePatches(allPatches);
80
const issues: { reference: string; content: string }[] | undefined = context.issues;
81
const template: string | undefined = context.template;
82
const compareBranch: string | undefined = context.compareBranch;
83
84
const endpoint = await this.endpointProvider.getChatEndpoint('copilot-fast');
85
const charLimit = Math.floor((endpoint.modelMaxPromptTokens * 4) / 3);
86
87
const prompt = await this.createPRTitleAndDescriptionPrompt(commitMessages, patches, issues, template, compareBranch, charLimit);
88
const fetchResult = await endpoint
89
.makeChatRequest(
90
'githubPullRequestTitleAndDescriptionGenerator',
91
prompt.messages,
92
undefined,
93
token,
94
ChatLocation.Other,
95
undefined,
96
{
97
temperature: this.isRegenerate(commitMessages, patches) ? this.options.temperature + 0.1 : this.options.temperature,
98
},
99
);
100
101
this.lastContext = { commitMessages, patches };
102
if (fetchResult.type === ChatFetchResponseType.QuotaExceeded || (fetchResult.type === ChatFetchResponseType.RateLimited && this.authService.copilotToken?.isNoAuthUser)) {
103
await this.notificationService.showQuotaExceededDialog({ isNoAuthUser: this.authService.copilotToken?.isNoAuthUser ?? false });
104
}
105
106
if (fetchResult.type !== ChatFetchResponseType.Success) {
107
return undefined;
108
}
109
110
return GitHubPullRequestTitleAndDescriptionGenerator.parseFetchResult(fetchResult.value, !!template);
111
}
112
113
public static parseFetchResult(value: string, hasTemplate: boolean = false, retry: boolean = true): { title: string; description?: string } | undefined {
114
value = value.trim();
115
let workingValue = value;
116
let delimiter = '+++';
117
const firstIndexOfDelimiter = workingValue.indexOf(delimiter);
118
if (firstIndexOfDelimiter === -1) {
119
return undefined;
120
}
121
122
// adjust delimter as the model sometimes adds more +s
123
while (workingValue.charAt(firstIndexOfDelimiter + delimiter.length) === '+') {
124
delimiter += '+';
125
}
126
127
const lastIndexOfDelimiter = workingValue.lastIndexOf(delimiter);
128
workingValue = workingValue.substring(firstIndexOfDelimiter + delimiter.length, lastIndexOfDelimiter > firstIndexOfDelimiter + delimiter.length ? lastIndexOfDelimiter : undefined).trim().replace(/\++?(\n)\++/, delimiter);
129
const splitOnPlus = workingValue.split(delimiter).filter(s => s.trim().length > 0);
130
let splitOnLines: string[];
131
if (splitOnPlus.length === 1) {
132
// If there's only one line, split on newlines as the model has left out some +++ delimiters
133
splitOnLines = splitOnPlus[0].split('\n');
134
} else if (splitOnPlus.length > 1) {
135
if (hasTemplate) {
136
// When using a template, keep description whitespace as-is.
137
splitOnLines = splitOnPlus;
138
} else {
139
const descriptionLines = splitOnPlus.slice(1).map(line => line.split('\n')).flat().filter(s => s.trim().length > 0);
140
splitOnLines = [splitOnPlus[0], ...descriptionLines];
141
}
142
} else {
143
return undefined;
144
}
145
146
let title: string | undefined;
147
let description: string | undefined;
148
if (splitOnLines.length === 1) {
149
title = splitOnLines[0].trim();
150
if (retry && value.includes('\n') && (value.split(delimiter).length === 3)) {
151
return this.parseFetchResult(value + delimiter, hasTemplate, false);
152
}
153
} else if (splitOnLines.length > 1) {
154
title = splitOnLines[0].trim();
155
156
description = '';
157
const descriptionLines = splitOnLines.slice(1);
158
// The description can be kind of self referential. Clean it up.
159
for (const line of descriptionLines) {
160
if (line.includes('commit message')) {
161
continue;
162
}
163
description += `${line.trim()}\n\n`;
164
}
165
}
166
if (title) {
167
title = title.replace(/Title\:\s/, '').trim();
168
title = title.replace(/^\"(?<title>.+)\"$/, (_match, title) => title);
169
if (description && !hasTemplate) {
170
description = description.replace(/Description\:\s/, '').trim();
171
}
172
return { title, description };
173
}
174
}
175
176
private async createPRTitleAndDescriptionPrompt(commitMessages: string[], patches: string[], issues: { reference: string; content: string }[] | undefined, template: string | undefined, compareBranch: string | undefined, charLimit: number): Promise<RenderPromptResult> {
177
// Reserve 20% of the character limit for the safety rules and instructions
178
const availableChars = charLimit - Math.floor(charLimit * 0.2);
179
180
// Remove diffs if needed (shortest diffs first)
181
let totalChars = patches.join('\n\n').length;
182
if (totalChars > availableChars) {
183
// Sort diffs by length
184
patches.sort((a, b) => a.length - b.length);
185
186
// Remove diff(s) until we are under the character limit
187
while (totalChars > availableChars && patches.length > 0) {
188
const lastPatch = patches.pop()!;
189
totalChars -= lastPatch.length;
190
}
191
}
192
193
const endpoint = await this.endpointProvider.getChatEndpoint('copilot-fast');
194
const promptRenderer = PromptRenderer.create(this.instantiationService, endpoint, GitHubPullRequestPrompt, { commitMessages, issues, patches, template, compareBranch });
195
return promptRenderer.render(undefined, undefined);
196
}
197
}
198
199