mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-24 17:31:37 +01:00
cf82f857de
Fixes #312210 This is using the old search endpoint. We'll start switching over the new one once it's ready Co-authored-by: Copilot <copilot@github.com>
233 lines
9.7 KiB
TypeScript
233 lines
9.7 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* 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<GithubRepoToolParams> {
|
|
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<GithubRepoToolParams>, token: CancellationToken): Promise<vscode.LanguageModelToolResult> {
|
|
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<GithubRepoToolParams>, token: vscode.CancellationToken): Promise<vscode.PreparedToolInvocation> {
|
|
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<GithubRepoToolParams>, token: vscode.CancellationToken): Promise<Result<GithubRepoId, PrepareError>> {
|
|
if (!options.input.repo) {
|
|
return Result.error<PrepareError>({
|
|
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<PrepareError>({
|
|
message: l10n.t`Invalid input. Could not parse 'repo' argument`,
|
|
id: 'could-not-parse-repo',
|
|
});
|
|
}
|
|
|
|
const checkIndexReady = async (): Promise<Result<boolean, PrepareError>> => {
|
|
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<PrepareError>({
|
|
message: l10n.t`Not authenticated`,
|
|
id: 'no-auth-token',
|
|
});
|
|
} else {
|
|
return Result.error<PrepareError>({
|
|
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<PrepareError>({
|
|
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<PrepareError>({
|
|
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<PrepareError>({
|
|
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<GithubChunkSearchResultsProps> {
|
|
constructor(
|
|
props: PromptElementProps<GithubChunkSearchResultsProps>,
|
|
) {
|
|
super(props);
|
|
}
|
|
|
|
override render(_state: void, _sizing: PromptSizing, _progress?: vscode.Progress<vscode.ChatResponsePart>, _token?: vscode.CancellationToken): Promise<PromptPiece | undefined> | PromptPiece | undefined {
|
|
return <WorkspaceChunkList
|
|
result={{ chunks: this.props.chunks }}
|
|
referencesOut={this.props.referencesOut}
|
|
absolutePaths={true}
|
|
isToolCall={true} />;
|
|
}
|
|
}
|
|
|
|
|
|
ToolRegistry.registerTool(GithubRepoSemanticSearchTool);
|