/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as l10n from '@vscode/l10n'; import { BasePromptElementProps, PromptElement, PromptElementProps, PromptPiece, PromptReference, PromptSizing } from '@vscode/prompt-tsx'; import type * as vscode from 'vscode'; import { FileChunkAndScore } from '../../../platform/chunking/common/chunk'; import { IRunCommandExecutionService } from '../../../platform/commands/common/runCommandExecutionService'; import { GithubRepoId, toGithubNwo } from '../../../platform/git/common/gitService'; import { IGithubCodeSearchService } from '../../../platform/remoteCodeSearch/common/githubCodeSearchService'; import { RemoteCodeSearchIndexStatus } from '../../../platform/remoteCodeSearch/common/remoteCodeSearch'; import { ITelemetryService } from '../../../platform/telemetry/common/telemetry'; import { GithubAvailableEmbeddingTypesService, IGithubAvailableEmbeddingTypesService } from '../../../platform/workspaceChunkSearch/common/githubAvailableEmbeddingTypes'; import { Result } from '../../../util/common/result'; import { TelemetryCorrelationId } from '../../../util/common/telemetryCorrelationId'; import { isLocation, isUri } from '../../../util/common/types'; import { raceCancellationError, timeout } from '../../../util/vs/base/common/async'; import { CancellationToken } from '../../../util/vs/base/common/cancellation'; import { URI } from '../../../util/vs/base/common/uri'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; import { ExtendedLanguageModelToolResult, LanguageModelPromptTsxPart, MarkdownString } from '../../../vscodeTypes'; import { getUniqueReferences } from '../../prompt/common/conversation'; import { renderPromptElementJSON } from '../../prompts/node/base/promptRenderer'; import { WorkspaceChunkList } from '../../prompts/node/panel/workspace/workspaceContext'; import { ToolName } from '../common/toolNames'; import { ICopilotTool, ToolRegistry } from '../common/toolsRegistry'; export interface GithubRepoToolParams { readonly repo: string; readonly query: string; } interface PrepareError { readonly message: string; readonly id: string; readonly details?: string; } export class GithubRepoSemanticSearchTool implements ICopilotTool { public static readonly toolName = ToolName.GithubSemanticRepoSearch; constructor( @IRunCommandExecutionService _commandService: IRunCommandExecutionService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IGithubCodeSearchService private readonly _githubCodeSearch: IGithubCodeSearchService, @IGithubAvailableEmbeddingTypesService private readonly _availableEmbeddingTypesManager: GithubAvailableEmbeddingTypesService, @ITelemetryService private readonly _telemetryService: ITelemetryService, ) { } async invoke(options: vscode.LanguageModelToolInvocationOptions, token: CancellationToken): Promise { const githubRepoId = GithubRepoId.parse(options.input.repo); if (!githubRepoId) { throw new Error('Invalid input. Could not parse repo'); } const embeddingType = await this._availableEmbeddingTypesManager.getPreferredType(false); if (!embeddingType) { throw new Error('No embedding models available'); } const searchResults = await this._githubCodeSearch.semanticSearch({ silent: true }, embeddingType, { kind: 'repo', githubRepoId, localRepoRoot: undefined, indexedCommit: undefined }, options.input.query, 64, {}, new TelemetryCorrelationId('github-repo-tool'), token); // Map the chunks to URIs using the remote URL and ref from the search response const repoBaseUrl = searchResults.remoteUrl ?? `https://github.com/${toGithubNwo(githubRepoId)}`; const ref = searchResults.refName ?? 'main'; const chunks = searchResults.chunks.map((entry): FileChunkAndScore => ({ chunk: { ...entry.chunk, file: URI.joinPath(URI.parse(repoBaseUrl), 'tree', ref, entry.chunk.file.path).with({ fragment: `L${entry.chunk.range.startLineNumber}-L${entry.chunk.range.endLineNumber}`, }), }, distance: entry.distance, })); let references: PromptReference[] = []; const json = await renderPromptElementJSON(this._instantiationService, GithubChunkSearchResults, { chunks, referencesOut: references, }); const result = new ExtendedLanguageModelToolResult([ new LanguageModelPromptTsxPart(json), ]); references = getUniqueReferences(references); result.toolResultMessage = references.length === 0 ? new MarkdownString(l10n.t`Searched ${githubRepoId.toString()} for "${options.input.query}", no results`) : references.length === 1 ? new MarkdownString(l10n.t`Searched ${githubRepoId.toString()} for "${options.input.query}", 1 result`) : new MarkdownString(l10n.t`Searched ${githubRepoId.toString()} for "${options.input.query}", ${references.length} results`); result.toolResultDetails = references .map(r => r.anchor) .filter(r => isUri(r) || isLocation(r)); return result; } async prepareInvocation(options: vscode.LanguageModelToolInvocationPrepareOptions, token: vscode.CancellationToken): Promise { const prepareResult = await raceCancellationError(this.doPrepare(options, token), token); if (prepareResult.isOk()) { return { invocationMessage: l10n.t("Searching '{0}' for relevant code snippets", options.input.repo), }; } /* __GDPR__ "githubRepoTool.prepare.error" : { "owner": "mjbvz", "comment": "Tracks errors for the GitHub repo tool prepare step", "errorId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "General reason why the search failed" }, "errorDetails": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "More detailed info about the failure" } } */ this._telemetryService.sendMSFTTelemetryEvent('githubRepoTool.prepare.error', { errorId: prepareResult.err.id, errorDetails: prepareResult.err.details, }); throw new Error(prepareResult.err.message); } private async doPrepare(options: vscode.LanguageModelToolInvocationPrepareOptions, token: vscode.CancellationToken): Promise> { if (!options.input.repo) { return Result.error({ message: l10n.t`Invalid input. No 'repo' argument provided`, id: 'no-repo-arg', }); } let githubRepoId = GithubRepoId.parse(options.input.repo); if (!githubRepoId) { // We may have been passed a full URL try { const uri = URI.parse(options.input.repo); if (uri.scheme === 'https' && uri.authority === 'github.com') { const pathParts = uri.path.split('/'); if (pathParts.length >= 3) { githubRepoId = new GithubRepoId(pathParts[1], pathParts[2]); } } } catch { // Noop } } if (!githubRepoId) { return Result.error({ message: l10n.t`Invalid input. Could not parse 'repo' argument`, id: 'could-not-parse-repo', }); } const checkIndexReady = async (): Promise> => { const state = await raceCancellationError(this._githubCodeSearch.getRemoteIndexState({ silent: true }, githubRepoId, new TelemetryCorrelationId('GitHubRepoTool'), token), token); if (!state.isOk()) { if (state.err.type === 'not-authorized') { return Result.error({ message: l10n.t`Not authenticated`, id: 'no-auth-token', }); } else { return Result.error({ message: l10n.t`Could not check status of Github repo index`, id: 'could-not-check-status', }); } } if (state.val.status === RemoteCodeSearchIndexStatus.Ready) { return Result.ok(true); } return Result.error({ message: l10n.t`GitHub repo index not yet ready`, id: 'unexpected-status', details: `status: ${state.val.status}`, }); }; if ((await checkIndexReady()).isOk()) { return Result.ok(githubRepoId); } if (!await this._githubCodeSearch.triggerIndexing({ silent: true }, 'tool', githubRepoId, new TelemetryCorrelationId('GitHubRepoTool'))) { return Result.error({ message: l10n.t`Could not index Github repo. Repo may not exist or you may not have access to it.`, id: 'trigger-indexing-failed', }); } const pollAttempts = 10; const pollDelay = 1000; for (let i = 0; i < pollAttempts; i++) { await raceCancellationError(timeout(pollDelay), token); if ((await raceCancellationError(checkIndexReady(), token)).isOk()) { return Result.ok(githubRepoId); } } return Result.error({ message: l10n.t`Github repo index not yet. Please try again shortly`, id: 'not-ready-after-polling', }); } } interface GithubChunkSearchResultsProps extends BasePromptElementProps { readonly chunks: FileChunkAndScore[]; readonly referencesOut: PromptReference[]; } class GithubChunkSearchResults extends PromptElement { constructor( props: PromptElementProps, ) { super(props); } override render(_state: void, _sizing: PromptSizing, _progress?: vscode.Progress, _token?: vscode.CancellationToken): Promise | PromptPiece | undefined { return ; } } ToolRegistry.registerTool(GithubRepoSemanticSearchTool);