Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/platform/remoteSearch/node/codeOrDocsSearchClientImpl.ts
13400 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
import { RequestMetadata, RequestType } from '@vscode/copilot-api';
6
import { TokenizerType } from '../../../util/common/tokenizer';
7
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
8
import { CancellationError, isCancellationError } from '../../../util/vs/base/common/errors';
9
import { generateUuid } from '../../../util/vs/base/common/uuid';
10
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
11
import { IAuthenticationService } from '../../authentication/common/authentication';
12
import { LogExecTime } from '../../log/common/logExecTime';
13
import { ILogService } from '../../log/common/logService';
14
import { IEndpoint, postRequest } from '../../networking/common/networking';
15
import { ITelemetryService } from '../../telemetry/common/telemetry';
16
import { ICodeOrDocsSearchBaseScopingQuery, ICodeOrDocsSearchItem, ICodeOrDocsSearchMultiRepoScopingQuery, ICodeOrDocsSearchOptions, ICodeOrDocsSearchResult, ICodeOrDocsSearchSingleRepoScopingQuery, IDocsSearchClient } from '../common/codeOrDocsSearchClient';
17
import { SearchErrorType, constructSearchError, constructSearchRepoError } from '../common/codeOrDocsSearchErrors';
18
import { formatScopingQuery } from '../common/utils';
19
20
/**
21
* What an error looks like that is returned by docssearch.
22
*/
23
interface IDocsSearchError {
24
message: string;
25
error: string;
26
repo: string;
27
}
28
29
/**
30
* What the response looks like that is returned by docssearch.
31
*/
32
interface IDocsSearchResponse {
33
results: ICodeOrDocsSearchItem[];
34
errors?: IDocsSearchError[];
35
}
36
37
class UnknownHttpError extends Error {
38
constructor(
39
readonly status: number,
40
message: string
41
) {
42
super(message);
43
}
44
}
45
46
const DEFAULT_LIMIT = 6;
47
const MAX_LIMIT = 100;
48
const DEFAULT_SIMILARITY = 0.766;
49
50
export class DocsSearchClient implements IDocsSearchClient {
51
declare readonly _serviceBrand: undefined;
52
53
private readonly slug = 'docs';
54
55
constructor(
56
@ITelemetryService private readonly _telemetryService: ITelemetryService,
57
@IAuthenticationService private readonly _authenticationService: IAuthenticationService,
58
@ILogService private readonly _logService: ILogService,
59
@IInstantiationService private readonly _instantiationService: IInstantiationService,
60
) { }
61
62
search(query: string, scopingQuery: ICodeOrDocsSearchSingleRepoScopingQuery, options?: ICodeOrDocsSearchOptions, token?: CancellationToken): Promise<ICodeOrDocsSearchItem[]>;
63
search(query: string, scopingQuery: ICodeOrDocsSearchMultiRepoScopingQuery, options?: ICodeOrDocsSearchOptions, token?: CancellationToken): Promise<ICodeOrDocsSearchResult>;
64
@LogExecTime(self => self._logService, 'CodeOrDocsSearchClientImpl::search')
65
async search(
66
query: string,
67
scopingQuery: ICodeOrDocsSearchSingleRepoScopingQuery | ICodeOrDocsSearchMultiRepoScopingQuery,
68
options: ICodeOrDocsSearchOptions = {},
69
token?: CancellationToken,
70
): Promise<ICodeOrDocsSearchItem[] | ICodeOrDocsSearchResult> {
71
// Code search requires at least one repo specified
72
if (Array.isArray(scopingQuery.repo) && !scopingQuery.repo.length) {
73
throw new Error('No repos specified');
74
}
75
76
let result: IDocsSearchResponse;
77
try {
78
result = await this.postRequestWithRetry(query, scopingQuery, options, token ?? CancellationToken.None);
79
} catch (error) {
80
if (!isCancellationError(error)) {
81
this._telemetryService.sendGHTelemetryException(error, `${this.slug} search failed`);
82
}
83
throw error;
84
}
85
const errors = result.errors?.map(constructSearchRepoError) ?? [];
86
// If we're in single repo mode, we will throw errors. If not, we're return a similar shape
87
if (!Array.isArray(scopingQuery.repo)) {
88
if (errors.length) {
89
// TODO: Can this happen?
90
if (errors.length > 1) {
91
throw new AggregateError(errors);
92
} else {
93
throw errors[0];
94
}
95
}
96
return result.results;
97
}
98
99
// Multi-repo
100
return {
101
results: result.results,
102
errors
103
};
104
}
105
106
private async postRequestWithRetry(
107
query: string,
108
scopingQuery: ICodeOrDocsSearchBaseScopingQuery,
109
options: ICodeOrDocsSearchOptions,
110
token: CancellationToken
111
): Promise<IDocsSearchResponse> {
112
const authToken = (await this._authenticationService.getGitHubSession('permissive', { silent: true }))?.accessToken ?? (await this._authenticationService.getGitHubSession('any', { silent: true }))?.accessToken;
113
if (token.isCancellationRequested) {
114
throw new CancellationError();
115
}
116
117
const MAX_RETRIES = 3;
118
let retryCount = 0;
119
120
const errorMessages = new Set<string>;
121
let error: Error | undefined;
122
while (retryCount < MAX_RETRIES) {
123
if (token.isCancellationRequested) {
124
throw new CancellationError();
125
}
126
127
try {
128
try {
129
const result = await this.postCodeOrDocsSearchRequest({ type: RequestType.SearchSkill, slug: this.slug }, authToken!, query, scopingQuery, options, token);
130
return result;
131
} catch (e) {
132
if (e instanceof UnknownHttpError) {
133
throw e;
134
}
135
error = e;
136
break;
137
}
138
} catch (error: any) {
139
retryCount++;
140
const waitTime = 100;
141
errorMessages.add(`Error fetching ${this.slug} search. ${error.message ?? error}`);
142
this._logService.warn(`[repo:${scopingQuery.repo}] Error fetching ${this.slug} search. Error: ${error.message ?? error}. Retrying in ${retryCount}ms. Query: ${query}`);
143
await new Promise(resolve => setTimeout(resolve, waitTime));
144
}
145
}
146
147
if (token.isCancellationRequested) {
148
throw new CancellationError();
149
}
150
151
if (retryCount >= MAX_RETRIES) {
152
this._logService.warn(`[repo:${scopingQuery.repo}] Max Retry Error thrown while querying '${query}'`);
153
error = constructSearchError({
154
error: SearchErrorType.maxRetriesExceeded,
155
message: `${this.slug} search timed out after ${MAX_RETRIES} retries. ${Array.from(errorMessages).join('\n')}`
156
});
157
}
158
159
throw error;
160
}
161
162
private async postCodeOrDocsSearchRequest(
163
requestMetadata: RequestMetadata,
164
authToken: string,
165
query: string,
166
scopingQuery: ICodeOrDocsSearchBaseScopingQuery,
167
options: ICodeOrDocsSearchOptions,
168
cancellationToken?: CancellationToken
169
) {
170
const limit = Math.min(options.limit ?? DEFAULT_LIMIT, MAX_LIMIT);
171
const similarity = options.similarity ?? DEFAULT_SIMILARITY;
172
const endpointInfo: IEndpoint = {
173
urlOrRequestMetadata: requestMetadata,
174
tokenizer: TokenizerType.O200K,
175
acquireTokenizer() {
176
throw new Error('Method not implemented.');
177
},
178
family: 'Code Or Doc Search',
179
name: 'Code Or Doc Search',
180
version: '2023-12-12-preview',
181
modelMaxPromptTokens: 0,
182
getExtraHeaders() {
183
const headers: Record<string, string> = {
184
// needed for errors to be in the right format
185
// TODO: should this be the default of postRequest?
186
Accept: 'application/json',
187
'X-GitHub-Api-Version': '2023-12-12-preview',
188
};
189
return headers;
190
},
191
};
192
const response = await this._instantiationService.invokeFunction(postRequest, {
193
endpointOrUrl: endpointInfo,
194
secretKey: authToken ?? '',
195
intent: 'codesearch',
196
requestId: generateUuid(),
197
body: {
198
query,
199
scopingQuery: formatScopingQuery(scopingQuery),
200
similarity,
201
limit
202
},
203
cancelToken: cancellationToken,
204
});
205
206
const text = await response.text();
207
if (response.status === 404 || (response.status === 400 && text.includes('unknown integration'))) {
208
// If the endpoint is not available for this user it will return 404.
209
this._logService.debug(`${this.slug} search endpoint not available for this user.`);
210
const error = constructSearchError({
211
error: SearchErrorType.noAccessToEndpoint,
212
message: `${this.slug}: ${text}`
213
});
214
throw error;
215
}
216
217
let result: IDocsSearchResponse;
218
try {
219
// handle 500s specifically (like blackbird queries)
220
result = JSON.parse(text);
221
} catch (e) {
222
// try again in the 500 case
223
throw new UnknownHttpError(response.status, text);
224
}
225
226
return result;
227
}
228
}
229
230