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