diff --git a/package.json b/package.json index eecb59a8a4..5640aa9480 100644 --- a/package.json +++ b/package.json @@ -88,16 +88,6 @@ "displayName": "GitHub Issues" } ], - "chatParticipants": [ - { - "id": "githubpr", - "name": "githubpr", - "fullName": "GitHub Pull Requests", - "description": "Chat participant for GitHub Pull Requests extension", - "when": "config.githubPullRequests.experimental.chat", - "isSticky": true - } - ], "configuration": { "type": "object", "title": "GitHub Pull Requests", @@ -2909,32 +2899,32 @@ { "command": "notification.chatSummarizeNotification", "group": "issues_0@0", - "when": "view == notifications:github && (viewItem == 'Issue' || viewItem == 'PullRequest') && config.githubPullRequests.experimental.notificationsView && config.githubPullRequests.experimental.chat && !config.chat.disableAIFeatures" + "when": "view == notifications:github && (viewItem == 'Issue' || viewItem == 'PullRequest') && config.githubPullRequests.experimental.chat && !config.chat.disableAIFeatures" }, { "command": "notification.openOnGitHub", "group": "issues_0@1", - "when": "view == notifications:github && (viewItem == 'Issue' || viewItem == 'PullRequest') && config.githubPullRequests.experimental.notificationsView" + "when": "view == notifications:github && (viewItem == 'Issue' || viewItem == 'PullRequest')" }, { "command": "notification.markAsRead", "group": "inline@3", - "when": "view == notifications:github && (viewItem == 'Issue' || viewItem == 'PullRequest') && config.githubPullRequests.experimental.notificationsView" + "when": "view == notifications:github && (viewItem == 'Issue' || viewItem == 'PullRequest')" }, { "command": "notification.markAsRead", "group": "issues_0@2", - "when": "view == notifications:github && (viewItem == 'Issue' || viewItem == 'PullRequest') && config.githubPullRequests.experimental.notificationsView" + "when": "view == notifications:github && (viewItem == 'Issue' || viewItem == 'PullRequest')" }, { "command": "notification.markAsDone", "group": "inline@4", - "when": "view == notifications:github && (viewItem == 'Issue' || viewItem == 'PullRequest') && config.githubPullRequests.experimental.notificationsView" + "when": "view == notifications:github && (viewItem == 'Issue' || viewItem == 'PullRequest')" }, { "command": "notification.markAsDone", "group": "issues_0@3", - "when": "view == notifications:github && (viewItem == 'Issue' || viewItem == 'PullRequest') && config.githubPullRequests.experimental.notificationsView" + "when": "view == notifications:github && (viewItem == 'Issue' || viewItem == 'PullRequest')" }, { "command": "pr.openPullRequestOnGitHub", @@ -3747,6 +3737,20 @@ } } ], + "chatSkills": [ + { + "path": "./src/lm/skills/summarize-github-issue-pr-notification/SKILL.md" + }, + { + "path": "./src/lm/skills/suggest-fix-issue/SKILL.md" + }, + { + "path": "./src/lm/skills/form-github-search-query/SKILL.md" + }, + { + "path": "./src/lm/skills/show-github-search-result/SKILL.md" + } + ], "languageModelTools": [ { "name": "github-pull-request_issue_fetch", @@ -3793,304 +3797,62 @@ "when": "config.githubPullRequests.experimental.chat" }, { - "name": "github-pull-request_notification_fetch", - "tags": [ - "github", - "notification" - ], - "toolReferenceName": "notification_fetch", - "displayName": "%languageModelTools.github-pull-request_notification_fetch.displayName%", - "modelDescription": "Get a GitHub notification's details as a JSON object.", - "icon": "$(info)", - "canBeReferencedInPrompt": false, - "inputSchema": { - "type": "object", - "properties": { - "thread_id": { - "type": "string", - "description": "The notification thread id." - } - }, - "required": [ - "thread_id" - ] - }, - "when": "config.githubPullRequests.experimental.chat" - }, - { - "name": "github-pull-request_issue_summarize", - "tags": [ - "github", - "issues", - "prs" - ], - "toolReferenceName": "issue_summarize", - "displayName": "%languageModelTools.github-pull-request_issue_summarize.displayName%", - "modelDescription": "Summarizes a GitHub issue or pull request. A summary is a great way to describe an issue or pull request.", - "icon": "$(info)", - "canBeReferencedInPrompt": false, - "inputSchema": { - "type": "object", - "properties": { - "title": { - "type": "string", - "description": "The title of the issue/PR" - }, - "body": { - "type": "string", - "description": "The body of the issue/PR" - }, - "owner": { - "type": "string", - "description": "The owner of the repo in which the issue/PR is located" - }, - "repo": { - "type": "string", - "description": "The repo in which the issue/PR is located" - }, - "comments": { - "type": "array", - "items": { - "type": "object", - "properties": { - "body": { - "type": "string", - "description": "The comment body" - }, - "author": { - "type": "string", - "description": "The author of the comment" - } - } - }, - "description": "The array of associated string comments" - }, - "fileChanges": { - "type": "array", - "items": { - "type": "object", - "properties": { - "fileName": { - "type": "string", - "description": "The name of the file of the change" - }, - "patch": { - "type": "string", - "description": "The patch of the change" - } - } - }, - "description": "For a PR, the array of associated file changes" - } - }, - "required": [ - "title", - "body", - "comments", - "owner", - "repo" - ] - }, - "when": "config.githubPullRequests.experimental.chat" - }, - { - "name": "github-pull-request_notification_summarize", - "tags": [ - "github", - "notification" - ], - "toolReferenceName": "notification_summarize", - "displayName": "%languageModelTools.github-pull-request_notification_summarize.displayName%", - "modelDescription": "Summarizes a GitHub notification. A summary is a great way to describe a notification.", - "icon": "$(info)", - "canBeReferencedInPrompt": false, - "inputSchema": { - "type": "object", - "properties": { - "lastReadAt": { - "type": "string", - "description": "The last read time of the notification." - }, - "lastUpdatedAt": { - "type": "string", - "description": "The last updated time of the notification." - }, - "unread": { - "type": "boolean", - "description": "Whether the notification is unread." - }, - "title": { - "type": "string", - "description": "The title of the notification issue/PR" - }, - "body": { - "type": "string", - "description": "The body of the notification issue/PR" - }, - "owner": { - "type": "string", - "description": "The owner of the repo in which the issue/PR is located" - }, - "repo": { - "type": "string", - "description": "The repo in which the issue/PR is located" - }, - "comments": { - "type": "array", - "items": { - "type": "object", - "properties": { - "body": { - "type": "string", - "description": "The comment body" - }, - "author": { - "type": "string", - "description": "The author of the comment" - } - } - }, - "description": "The array of unread comments under the issue/PR of the notification" - }, - "threadId": { - "type": "number", - "description": "The thread id of the notification" - }, - "notificationKey": { - "type": "string", - "description": "The key of the notification" - }, - "itemNumber": { - "type": "string", - "description": "The number of the issue/PR in the notification" - }, - "itemType": { - "type": "string", - "description": "The type of the item in the notification - whether it is an issue or a PR" - }, - "fileChanges": { - "type": "array", - "items": { - "type": "object", - "properties": { - "fileName": { - "type": "string", - "description": "The name of the file of the change" - }, - "patch": { - "type": "string", - "description": "The patch of the change" - } - }, - "required": [ - "fileName", - "patch" - ] - }, - "description": "For a notification about a PR, the array of associated file changes" - } - }, - "required": [ - "title", - "comments", - "lastUpdatedAt", - "unread", - "threadId", - "notificationKey", - "owner", - "repo", - "itemNumber", - "itemType" - ] - }, - "when": "config.githubPullRequests.experimental.chat" - }, - { - "name": "github-pull-request_suggest-fix", + "name": "github-pull-request_labels_fetch", "tags": [ "github", - "issues" + "labels" ], - "toolReferenceName": "suggest-fix", - "displayName": "%languageModelTools.github-pull-request_suggest-fix.displayName%", - "modelDescription": "Summarize and suggest a fix for a GitHub issue.", - "icon": "$(info)", + "toolReferenceName": "labels_fetch", + "displayName": "%languageModelTools.github-pull-request_labels_fetch.displayName%", + "modelDescription": "Fetch all labels from a GitHub repository. Returns the label names, colors, and descriptions.", + "icon": "$(tag)", "canBeReferencedInPrompt": true, "inputSchema": { "type": "object", "properties": { "repo": { "type": "object", - "description": "The repository to get the issue from.", + "description": "The repository to fetch labels from.", "properties": { "owner": { "type": "string", - "description": "The owner of the repository to get the issue from." + "description": "The owner of the repository to fetch labels from." }, "name": { "type": "string", - "description": "The name of the repository to get the issue from." + "description": "The name of the repository to fetch labels from." } }, "required": [ "owner", "name" ] - }, - "issueNumber": { - "type": "number", - "description": "The number of the issue to get." } - }, - "required": [ - "issueNumber", - "repo" - ] + } }, "when": "config.githubPullRequests.experimental.chat" }, { - "name": "github-pull-request_formSearchQuery", + "name": "github-pull-request_notification_fetch", "tags": [ "github", - "issues", - "search", - "query", - "natural language" + "notification" ], - "toolReferenceName": "searchSyntax", - "displayName": "%languageModelTools.github-pull-request_formSearchQuery.displayName%", - "modelDescription": "Converts natural language to a GitHub search query. Should ALWAYS be called before doing a search.", - "icon": "$(search)", + "toolReferenceName": "notification_fetch", + "displayName": "%languageModelTools.github-pull-request_notification_fetch.displayName%", + "modelDescription": "Get a GitHub notification's details as a JSON object.", + "icon": "$(info)", "canBeReferencedInPrompt": true, "inputSchema": { "type": "object", "properties": { - "repo": { - "type": "object", - "description": "The repository to get the issue from.", - "properties": { - "owner": { - "type": "string", - "description": "The owner of the repository to get the issue from." - }, - "name": { - "type": "string", - "description": "The name of the repository to get the issue from." - } - }, - "required": [ - "owner", - "name" - ] - }, - "naturalLanguageString": { + "thread_id": { "type": "string", - "description": "A plain text description of what the search should be." + "description": "The notification thread id." } }, "required": [ - "naturalLanguageString" + "thread_id" ] }, "when": "config.githubPullRequests.experimental.chat" @@ -4104,7 +3866,7 @@ ], "toolReferenceName": "doSearch", "displayName": "%languageModelTools.github-pull-request_doSearch.displayName%", - "modelDescription": "Execute a GitHub search given a well formed GitHub search query. Call github-pull-request_formSearchQuery first to get good search syntax and pass the exact result in as the 'query'.", + "modelDescription": "Execute a GitHub search given a well formed GitHub search query. Make sure to form a good search query first", "icon": "$(search)", "canBeReferencedInPrompt": true, "inputSchema": { @@ -4140,137 +3902,6 @@ }, "when": "config.githubPullRequests.experimental.chat" }, - { - "name": "github-pull-request_renderIssues", - "tags": [ - "github", - "issues", - "render", - "display" - ], - "toolReferenceName": "renderIssues", - "displayName": "%languageModelTools.github-pull-request_renderIssues.displayName%", - "modelDescription": "Render issue items from an issue search in a markdown table. The markdown table will be displayed directly to the user by the tool. No further display should be done after this!", - "icon": "$(paintcan)", - "canBeReferencedInPrompt": true, - "inputSchema": { - "type": "object", - "properties": { - "arrayOfIssues": { - "type": "array", - "description": "An array of GitHub Issues.", - "items": { - "type": "object", - "properties": { - "title": { - "type": "string", - "description": "The title of the issue." - }, - "number": { - "type": "number", - "description": "The number of the issue." - }, - "url": { - "type": "string", - "description": "The URL of the issue." - }, - "state": { - "type": "string", - "description": "The state of the issue (open/closed)." - }, - "createdAt": { - "type": "string", - "description": "The creation date of the issue." - }, - "updatedAt": { - "type": "string", - "description": "The last update date of the issue." - }, - "closedAt": { - "type": "string", - "description": "The closing date of the issue." - }, - "author": { - "type": "object", - "description": "The author of the issue.", - "properties": { - "login": { - "type": "string", - "description": "The login of the author." - }, - "url": { - "type": "string", - "description": "The URL of the author's profile." - } - } - }, - "labels": { - "type": "array", - "description": "The labels associated with the issue.", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "The name of the label." - }, - "color": { - "type": "string", - "description": "The color of the label." - } - } - } - }, - "assignees": { - "type": "array", - "description": "The assignees of the issue.", - "items": { - "type": "object", - "properties": { - "login": { - "type": "string", - "description": "The login of the assignee." - }, - "url": { - "type": "string", - "description": "The URL of the assignee's profile." - } - } - } - }, - "commentCount": { - "type": "number", - "description": "The number of comments on the issue." - }, - "reactionCount": { - "type": "number", - "description": "The number of reactions on the issue." - } - }, - "required": [ - "title", - "number", - "url", - "state", - "createdAt", - "author", - "commentCount", - "reactionCount" - ] - } - }, - "totalIssues": { - "type": "number", - "description": "The total number of issues in the search." - } - }, - "required": [ - "arrayOfIssues", - "totalIssues" - ] - }, - "when": "config.githubPullRequests.experimental.chat" - }, { "name": "github-pull-request_activePullRequest", "tags": [ diff --git a/package.nls.json b/package.nls.json index 1f07a9f57e..4773db28b0 100644 --- a/package.nls.json +++ b/package.nls.json @@ -418,6 +418,7 @@ "welcome.github.activePullRequest.contents": "Loading...", "languageModelTools.github-pull-request_issue_fetch.displayName": "Get a GitHub Issue or pull request", "languageModelTools.github-pull-request_issue_summarize.displayName": "Summarize a GitHub Issue or pull request", + "languageModelTools.github-pull-request_labels_fetch.displayName": "Fetch labels from a GitHub repository", "languageModelTools.github-pull-request_notification_fetch.displayName": "Get a GitHub Notification", "languageModelTools.github-pull-request_notification_summarize.displayName": "Summarize a GitHub Notification", "languageModelTools.github-pull-request_suggest-fix.displayName": "Suggest a Fix for a GitHub Issue", diff --git a/src/common/executeCommands.ts b/src/common/executeCommands.ts index 945abba102..54dc5800d3 100644 --- a/src/common/executeCommands.ts +++ b/src/common/executeCommands.ts @@ -26,6 +26,7 @@ export namespace contexts { export namespace commands { export const OPEN_CHAT = 'workbench.action.chat.open'; export const NEW_CHAT = 'workbench.action.chat.newChat'; + export const SHOW_CHAT = 'workbench.panel.chat'; export const CHAT_SETUP_ACTION_ID = 'workbench.action.chat.triggerSetup'; export const QUICK_CHAT_OPEN = 'workbench.action.quickchat.toggle'; diff --git a/src/extension.ts b/src/extension.ts index e8a13e50aa..ffb8dc4327 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -33,7 +33,6 @@ import { GitLensIntegration } from './integrations/gitlens/gitlensImpl'; import { IssueFeatureRegistrar } from './issues/issueFeatureRegistrar'; import { StateManager } from './issues/stateManager'; import { IssueContextProvider } from './lm/issueContextProvider'; -import { ChatParticipant, ChatParticipantState } from './lm/participants'; import { PullRequestContextProvider } from './lm/pullRequestContextProvider'; import { registerTools } from './lm/tools/tools'; import { migrate } from './migrations'; @@ -300,17 +299,11 @@ async function init( } function initChat(context: vscode.ExtensionContext, credentialStore: CredentialStore, reposManager: RepositoriesManager) { - const createParticipant = () => { - const chatParticipantState = new ChatParticipantState(); - context.subscriptions.push(new ChatParticipant(context, chatParticipantState)); - registerTools(context, credentialStore, reposManager, chatParticipantState); - }; - const chatEnabled = () => vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get(EXPERIMENTAL_CHAT, false); if (chatEnabled()) { - createParticipant(); + registerTools(context, credentialStore, reposManager); } else { - initBasedOnSettingChange(PR_SETTINGS_NAMESPACE, EXPERIMENTAL_CHAT, chatEnabled, createParticipant, context.subscriptions); + initBasedOnSettingChange(PR_SETTINGS_NAMESPACE, EXPERIMENTAL_CHAT, chatEnabled, () => registerTools(context, credentialStore, reposManager), context.subscriptions); } } diff --git a/src/issues/issueFeatureRegistrar.ts b/src/issues/issueFeatureRegistrar.ts index 7abf0d1aab..c0edf0e1e7 100644 --- a/src/issues/issueFeatureRegistrar.ts +++ b/src/issues/issueFeatureRegistrar.ts @@ -565,11 +565,13 @@ export class IssueFeatureRegistrar extends Disposable { */ this.telemetry.sendTelemetryEvent('issue.chatSummarizeIssue'); if (issue instanceof IssueModel) { - commands.executeCommand(commands.NEW_CHAT, { inputValue: vscode.l10n.t('@githubpr Summarize issue {0}/{1}#{2}', issue.remote.owner, issue.remote.repositoryName, issue.number) }); + commands.executeCommand(commands.NEW_CHAT, { inputValue: vscode.l10n.t('Summarize issue {0}/{1}#{2}', issue.remote.owner, issue.remote.repositoryName, issue.number) }); + commands.executeCommand(commands.SHOW_CHAT); } else { const pullRequestModel = issue.pullRequestModel; const remote = pullRequestModel.githubRepository.remote; - commands.executeCommand(commands.NEW_CHAT, { inputValue: vscode.l10n.t('@githubpr Summarize pull request {0}/{1}#{2}', remote.owner, remote.repositoryName, pullRequestModel.number) }); + commands.executeCommand(commands.NEW_CHAT, { inputValue: vscode.l10n.t('Summarize pull request {0}/{1}#{2}', remote.owner, remote.repositoryName, pullRequestModel.number) }); + commands.executeCommand(commands.SHOW_CHAT); } }), ); @@ -582,7 +584,8 @@ export class IssueFeatureRegistrar extends Disposable { "issue.chatSuggestFix" : {} */ this.telemetry.sendTelemetryEvent('issue.chatSuggestFix'); - commands.executeCommand(commands.NEW_CHAT, { inputValue: vscode.l10n.t('@githubpr Find a fix for issue {0}/{1}#{2}', issue.remote.owner, issue.remote.repositoryName, issue.number) }); + commands.executeCommand(commands.NEW_CHAT, { inputValue: vscode.l10n.t('Find a fix for issue {0}/{1}#{2}', issue.remote.owner, issue.remote.repositoryName, issue.number) }); + commands.executeCommand(commands.SHOW_CHAT); }), ); this._register(vscode.commands.registerCommand('issues.configureIssuesViewlet', async () => { diff --git a/src/lm/participants.ts b/src/lm/participants.ts deleted file mode 100644 index 94fe90f925..0000000000 --- a/src/lm/participants.ts +++ /dev/null @@ -1,210 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; -import { renderPrompt } from '@vscode/prompt-tsx'; -import * as vscode from 'vscode'; -import { ParticipantsPrompt } from './participantsPrompt'; -import { Disposable } from '../common/lifecycle'; -import { IToolCall, TOOL_COMMAND_RESULT, TOOL_MARKDOWN_RESULT } from './tools/toolsUtils'; - -export class ChatParticipantState { - private _messages: vscode.LanguageModelChatMessage[] = []; - - get lastToolResult(): (vscode.LanguageModelTextPart | vscode.LanguageModelToolResultPart | vscode.LanguageModelToolCallPart)[] { - for (let i = this._messages.length - 1; i >= 0; i--) { - const message = this._messages[i]; - for (const part of message.content) { - if (part instanceof vscode.LanguageModelToolResultPart) { - return message.content; - } - } - } - return []; - } - - get firstUserMessage(): vscode.LanguageModelTextPart | undefined { - for (let i = 0; i < this._messages.length; i++) { - const message = this._messages[i]; - if (message.role === vscode.LanguageModelChatMessageRole.User && message.content) { - for (const part of message.content) { - if (part instanceof vscode.LanguageModelTextPart) { - return part; - } - } - } - } - return undefined; - } - - get messages(): vscode.LanguageModelChatMessage[] { - return this._messages; - } - - addMessage(message: vscode.LanguageModelChatMessage): void { - this._messages.push(message); - } - - addMessages(messages: vscode.LanguageModelChatMessage[]): void { - this._messages.push(...messages); - } - - reset(): void { - this._messages = []; - } -} - -export class ChatParticipant extends Disposable { - - constructor(context: vscode.ExtensionContext, private readonly state: ChatParticipantState) { - super(); - const ghprChatParticipant = this._register(vscode.chat.createChatParticipant('githubpr', ( - request: vscode.ChatRequest, - context: vscode.ChatContext, - stream: vscode.ChatResponseStream, - token: vscode.CancellationToken - ) => this.handleParticipantRequest(request, context, stream, token))); - ghprChatParticipant.iconPath = vscode.Uri.joinPath(context.extensionUri, 'resources/icons/github_logo.png'); - } - - async handleParticipantRequest( - request: vscode.ChatRequest, - context: vscode.ChatContext, - stream: vscode.ChatResponseStream, - token: vscode.CancellationToken - ): Promise { - this.state.reset(); - - const models = await vscode.lm.selectChatModels({ - vendor: 'copilot', - family: 'gpt-4o' - }); - const model = models[0]; - - - const allTools: vscode.LanguageModelChatTool[] = []; - for (const tool of vscode.lm.tools) { - if (request.tools.has(tool) && request.tools.get(tool)) { - allTools.push(tool); - } else if (tool.name.startsWith('github-pull-request')) { - allTools.push(tool); - } - } - - const { messages } = await renderPrompt( - ParticipantsPrompt, - { userMessage: request.prompt }, - { modelMaxPromptTokens: model.maxInputTokens }, - model); - - this.state.addMessages(messages); - - const toolReferences = [...request.toolReferences]; - const options: vscode.LanguageModelChatRequestOptions = { - justification: 'Answering user questions pertaining to GitHub.' - }; - - const commands: vscode.Command[] = []; - const runWithFunctions = async (): Promise => { - - const requestedTool = toolReferences.shift(); - if (requestedTool) { - options.toolMode = vscode.LanguageModelChatToolMode.Required; - options.tools = allTools.filter(tool => tool.name === requestedTool.name); - } else { - options.toolMode = undefined; - options.tools = allTools; - } - - const toolCalls: IToolCall[] = []; - const response = await model.sendRequest(this.state.messages, options, token); - - for await (const part of response.stream) { - - if (part instanceof vscode.LanguageModelTextPart) { - stream.markdown(part.value); - } else if (part instanceof vscode.LanguageModelToolCallPart) { - - const tool = vscode.lm.tools.find(tool => tool.name === part.name); - if (!tool) { - throw new Error('Got invalid tool choice: ' + part.name); - } - - let input: any; - try { - input = part.input; - } catch (err) { - throw new Error(`Got invalid tool use parameters: "${JSON.stringify(part.input)}". (${(err as Error).message})`); - } - - const invocationOptions: vscode.LanguageModelToolInvocationOptions = { input, toolInvocationToken: request.toolInvocationToken }; - toolCalls.push({ - call: part, - result: vscode.lm.invokeTool(tool.name, invocationOptions, token), - tool - }); - } - } - - if (toolCalls.length) { - const assistantMsg = vscode.LanguageModelChatMessage.Assistant(''); - assistantMsg.content = toolCalls.map(toolCall => new vscode.LanguageModelToolCallPart(toolCall.call.callId, toolCall.tool.name, toolCall.call.input)); - this.state.addMessage(assistantMsg); - - let shownToUser = false; - for (const toolCall of toolCalls) { - let toolCallResult = (await toolCall.result); - - const additionalContent: vscode.LanguageModelTextPart[] = []; - let result: vscode.LanguageModelToolResultPart | undefined; - - for (let i = 0; i < toolCallResult.content.length; i++) { - const part = toolCallResult.content[i]; - if (!(part instanceof vscode.LanguageModelTextPart)) { - // We only support text results for now, will change when we finish adopting prompt-tsx - result = new vscode.LanguageModelToolResultPart(toolCall.call.callId, toolCallResult.content); - continue; - } - - if (part.value === TOOL_MARKDOWN_RESULT) { - const markdown = new vscode.MarkdownString((toolCallResult.content[++i] as vscode.LanguageModelTextPart).value); - markdown.supportHtml = true; - stream.markdown(markdown); - shownToUser = true; - } else if (part.value === TOOL_COMMAND_RESULT) { - commands.push(JSON.parse((toolCallResult.content[++i] as vscode.LanguageModelTextPart).value) as vscode.Command); - } else { - if (!result) { - result = new vscode.LanguageModelToolResultPart(toolCall.call.callId, [part]); - } else { - additionalContent.push(part); - } - } - } - const message = vscode.LanguageModelChatMessage.User(''); - message.content = [result!]; - this.state.addMessage(message); - if (additionalContent.length) { - const additionalMessage = vscode.LanguageModelChatMessage.User(''); - additionalMessage.content = additionalContent; - this.state.addMessage(additionalMessage); - } - } - - this.state.addMessage(vscode.LanguageModelChatMessage.User(`Above is the result of calling the functions ${toolCalls.map(call => call.tool.name).join(', ')}. ${shownToUser ? 'The user can see the result of the tool call.' : ''}`)); - return runWithFunctions(); - } - }; - await runWithFunctions(); - this.addButtons(stream, commands); - } - - private addButtons(stream: vscode.ChatResponseStream, commands: vscode.Command[]) { - for (const command of commands) { - stream.button(command); - } - } -} - diff --git a/src/lm/participantsPrompt.ts b/src/lm/participantsPrompt.ts deleted file mode 100644 index 9533fd073d..0000000000 --- a/src/lm/participantsPrompt.ts +++ /dev/null @@ -1,43 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AssistantMessage, BasePromptElementProps, Chunk, PromptElement, PromptPiece, PromptSizing, UserMessage } from '@vscode/prompt-tsx'; - -interface ParticipantsPromptProps extends BasePromptElementProps { - readonly userMessage: string; -} - -export class ParticipantsPrompt extends PromptElement { - render(_state: void, _sizing: PromptSizing): PromptPiece { - const instructions = [ - 'Instructions:', - '- The user will ask a question related to GitHub, and it may require lots of research to answer correctly. There is a selection of tools that let you perform actions or retrieve helpful context to answer the user\'s question.', - "- If you aren't sure which tool is relevant, you can call multiple tools. You can call tools repeatedly to take actions or gather as much context as needed until you have completed the task fully. Don't give up unless you are sure the request cannot be fulfilled with the tools you have.", - "- Don't ask the user for confirmation to use tools, just use them.", - '- When talking about issues, be as concise as possible while still conveying all the information you need to. Avoid mentioning the following:', - ' - The fact that there are no comments.', - ' - Any info that seems like template info.' - ].join('\n'); - - const assistantPiece: PromptPiece = { - ctor: AssistantMessage, - props: {}, - children: [instructions] - }; - - const userPiece: PromptPiece = { - ctor: UserMessage, - props: {}, - children: [this.props.userMessage] - }; - - const container: PromptPiece = { - ctor: Chunk, - props: {}, - children: [assistantPiece, userPiece] - }; - return container; - } -} \ No newline at end of file diff --git a/src/lm/skills/form-github-search-query/SKILL.md b/src/lm/skills/form-github-search-query/SKILL.md new file mode 100644 index 0000000000..9c04763cf0 --- /dev/null +++ b/src/lm/skills/form-github-search-query/SKILL.md @@ -0,0 +1,90 @@ +--- +name: form-github-search-query +description: Forms a GitHub search query based on a natural language query and the type of search (issue or PR). This skill helps users create effective search queries to find relevant issues or pull requests on GitHub. +--- + +# Form GitHub Search Query + +## Purpose + +GitHub has a specific syntax for searching issues and pull requests. This skill takes a natural language query from the user and the type of search they want to perform (issue or PR) and converts it into a properly formatted GitHub search query. This allows users to leverage GitHub's powerful search capabilities without needing to know the specific syntax. + +## Usage + +To use this skill, provide a natural language query and specify whether you want to search for issues or pull requests. The skill will then analyze the input and generate a GitHub search query that can be used to find relevant results on GitHub. + +## Converting Natural Language to GitHub Search Syntax + +### Steps + +1. Identify if there's a repo mention in the query. +2. Fetch labels for the repo if mentioned. +3. Follow the "Tips for Forming Effective Search Queries" to convert the natural language query into GitHub search syntax. + +### Search Syntax Overview + +- is: { possibleValues: ['issue', 'pr', 'draft', 'public', 'private', 'locked', 'unlocked'] } +- assignee: { valueDescription: 'A GitHub user name or @me' } +- author: { valueDescription: 'A GitHub user name or @me' } +- mentions: { valueDescription: 'A GitHub user name or @me' } +- team: { valueDescription: 'A GitHub user name' } +- commenter: { valueDescription: 'A GitHub user name or @me' } +- involves: { valueDescription: 'A GitHub user name or @me' } +- label: { valueDescription: 'A GitHub issue/pr label' } +- type: { possibleValues: ['pr', 'issue'] } +- state: { possibleValues: ['open', 'closed', 'merged'] } +- in: { possibleValues: ['title', 'body', 'comments'] } +- user: { valueDescription: 'A GitHub user name or @me' } +- org: { valueDescription: 'A GitHub org, without the repo name' } +- repo: { valueDescription: 'A GitHub repo, without the org name' } +- linked: { possibleValues: ['pr', 'issue'] } +- milestone: { valueDescription: 'A GitHub milestone' } +- project: { valueDescription: 'A GitHub project' } +- status: { possibleValues: ['success', 'failure', 'pending'] } +- head: { valueDescription: 'A git commit sha or branch name' } +- base: { valueDescription: 'A git commit sha or branch name' } +- comments: { valueDescription: 'A number' } +- interactions: { valueDescription: 'A number' } +- reactions: { valueDescription: 'A number' } +- draft: { possibleValues: ['true', 'false'] } +- review: { possibleValues: ['none', 'required', 'approved', 'changes_requested'] } +- reviewedBy: { valueDescription: 'A GitHub user name or @me' } +- reviewRequested: { valueDescription: 'A GitHub user name or @me' } +- userReviewRequested: { valueDescription: 'A GitHub user name or @me' } +- teamReviewRequested: { valueDescription: 'A GitHub user name' } +- created: { valueDescription: 'A date, with an optional < >' } +- updated: { valueDescription: 'A date, with an optional < >' } +- closed: { valueDescription: 'A date, with an optional < >' } +- no: { possibleValues: ['label', 'milestone', 'assignee', 'project'] } +- sort: { possibleValues: ['updated', 'updated-asc', 'interactions', 'interactions-asc', 'author-date', 'author-date-asc', 'committer-date', 'committer-date-asc', 'reactions', 'reactions-asc', 'reactions-(+1, -1, smile, tada, heart)'] } + +### Example Queries + +- repo:microsoft/vscode is:issue state:open sort:updated-asc +- mentions:@me org:microsoft is:issue state:open sort:updated +- assignee:@me milestone:"October 2024" is:open is:issue sort:reactions +- comments:>5 org:contoso is:issue state:closed mentions:@me label:bug +- interactions:>5 repo:contoso/cli is:issue state:open +- repo:microsoft/vscode-python is:issue sort:updated -assignee:@me +- repo:contoso/cli is:issue sort:updated no:milestone + +### Tips for Forming Effective Search Queries + +- Always try to include "repo:" or "org:" in your response. +- "repo" is often formated as "owner/name". +- If the user specifies a repo, ALWAYS fetch the labels for that repo and try to match any words in the natural language query to the label names to include them in the search query (See "Adding Labels to the Search Query" section). +- Words in inline codeblocks are likely to refer to labels. Try to match them to labels in the repo and include them in the search query. +- Always include a "sort:" parameter. If multiple sorts are possible, choose the one that the user requested. +- Always include a property with the @me value if the query includes "me" or "my". +- Go through each word of the natural language query and try to match it to a syntax component. +- Use a "-" in front of a syntax component to indicate that it should be "not-ed". +- Use the "no" syntax component to indicate that a property should be empty. + +### Adding Labels to the Search Query + +- Choose labels based on what the user wants to search for, not based on the actual words in the query. +- The user might include info on how they want their search results to be displayed. Ignore all of that. +- Labels will be and-ed together, so don't pick a bunch of super specific labels. +- Try to pick just one label. +- Only choose labels that you're sure are relevant. Having no labels is preferable than labels that aren't relevant. +- Don't choose labels that the user has explicitly excluded. diff --git a/src/lm/skills/show-github-search-result/SKILL.md b/src/lm/skills/show-github-search-result/SKILL.md new file mode 100644 index 0000000000..4fa4bc7d3c --- /dev/null +++ b/src/lm/skills/show-github-search-result/SKILL.md @@ -0,0 +1,24 @@ +--- +name: show-github-search-result +description: Summarizes the results of a GitHub search query in a human friendly markdown table that is easy to read and understand. ALWAYS use this skill when displaying the results of a GitHub search query. +user-invokable: false +--- + +# Render GitHub Search Result + +## Purpose + +To take the results of a GitHub search query, which may include issues or pull requests, and render them in a human-friendly markdown table format. This skill extracts the relevant information from each search result and organizes it in a way that is easy to read and understand, allowing users to quickly grasp the key details of each issue or pull request without having to parse through raw data. + +## Usage + +To use this skill, pass raw search results from a GitHub search query. The skill will then process the data and generate a markdown table that summarizes the key information for each issue or pull request, such as the title, author, labels, state, and any other relevant details. This makes it easier for users to review and analyze the search results at a glance. + +## How to Render GitHub Search Results + +- If you have the original query, use that to help determine the most important fields to include in the table. Ex: + - If the query included a specific label, make sure to not include that label in the table as all results will have it. + - If the query included "is:pr", then focus on fields relevant to pull requests such as "review status" and "merge status". + - Include a column related to the sort value, if given. + - Don't include columns that will all have the same value for all the resulting issues. +- Always include a column for the number and title of the item. Format the number as a markdown link to the issue or PR. Ex: [#123](https://github.com/owner/repo/issues/123) \ No newline at end of file diff --git a/src/lm/skills/suggest-fix-issue/SKILL.md b/src/lm/skills/suggest-fix-issue/SKILL.md new file mode 100644 index 0000000000..612cca9772 --- /dev/null +++ b/src/lm/skills/suggest-fix-issue/SKILL.md @@ -0,0 +1,20 @@ +--- +name: suggest-fix-issue +description: Given the details of an issue, suggests a fix for the issue. +--- + +# Suggest Fix for Issue + +## Purpose + +To propose a potential solution for a given issue based on its details. This skill analyzes the issue's content, including its description, comments, and any relevant context, to generate a suggestion that could help resolve the issue effectively. + +## Usage + +To use this skill, provide the details of an issue, including its description, comments, and any relevant context. The skill will then analyze the information and generate a suggestion for how to fix the issue. This can be particularly useful for developers looking for guidance on how to address a problem or for those who may be new to troubleshooting issues. + +## How to suggest a fix + +- Carefully read through the issue's description and comments to understand the problem fully. +- Where possible output code-blocks and reference real files in the workspace with the fix. +- If the issue is related to a bug, try to identify the root cause and suggest a fix that addresses it directly. \ No newline at end of file diff --git a/src/lm/skills/summarize-github-issue-pr-notification/SKILL.md b/src/lm/skills/summarize-github-issue-pr-notification/SKILL.md new file mode 100644 index 0000000000..30c37baefa --- /dev/null +++ b/src/lm/skills/summarize-github-issue-pr-notification/SKILL.md @@ -0,0 +1,26 @@ +--- +name: summarize-github-issue-pr-notification +description: Summarizes the content of a GitHub issue, pull request (PR), or notification, providing a concise overview of the main points and key details. ALWAYS use the skill when asked to summarize an issue, PR, or notification. +--- + +# Summarize Issue + +## Purpose + +Given a json GitHub issue, PR, or notification, this skill summarizes the content, providing a concise overview of the main points and key details. This skill helps users quickly understand the essence of an issue without having to read through the entire content. + +## Usage + +To use this skill, provide a JSON representation of a GitHub issue. The skill will extract the relevant information and generate a summary that captures the main points and key details of the issue. + +## Tips on How to Summarize an Issue + +- Do not output code. When you try to summarize PR changes, summarize in a textual format. +- Output references to other issues and PRs as Markdown links. +- If a comment references for example issue or PR #123, then output either of the following in the summary depending on if it is an issue or a PR: + - [#123](https://github.com/${owner}/${repo}/issues/123) + - [#123](https://github.com/${owner}/${repo}/pull/123) +- Comments should be summarized with the author first. Ex: + - @username: This is a comment that summarizes the main point of the comment. +- If the content contains images in Markdown format (e.g., ![alt text](image-url)), always preserve them in the output exactly as they appear. Images are important visual content and should not be removed or summarized. +- Make sure the summary is at least as short or shorter than the issue or PR with the comments and the patches if there are. \ No newline at end of file diff --git a/src/lm/tools/displayIssuesTool.ts b/src/lm/tools/displayIssuesTool.ts deleted file mode 100644 index 8177718f87..0000000000 --- a/src/lm/tools/displayIssuesTool.ts +++ /dev/null @@ -1,173 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import * as vscode from 'vscode'; -import { ensureEmojis } from '../../common/emoji'; -import Logger from '../../common/logger'; -import { reviewerLabel } from '../../github/interface'; -import { makeLabel } from '../../github/utils'; -import { ChatParticipantState } from '../participants'; -import { IssueSearchResultAccount, IssueSearchResultItem, SearchToolResult } from './searchTools'; -import { concatAsyncIterable, TOOL_MARKDOWN_RESULT, ToolBase } from './toolsUtils'; - -export type DisplayIssuesParameters = SearchToolResult; - -type IssueColumn = keyof IssueSearchResultItem; - -const LLM_FIND_IMPORTANT_COLUMNS_INSTRUCTIONS = `Instructions: -You are an expert on GitHub issues. You can help the user identify the most important columns for rendering issues based on a query for issues: -- Include a column related to the sort value, if given. -- Output a newline separated list of columns only, max 4 columns. -- List the columns in the order they should be displayed. -- Don't change the casing. -- Don't include columns that will all have the same value for all the resulting issues. -Here are the possible columns: -`; - -export class DisplayIssuesTool extends ToolBase { - public static readonly toolId = 'github-pull-request_renderIssues'; - private static ID = 'DisplayIssuesTool'; - constructor(private readonly context: vscode.ExtensionContext, chatParticipantState: ChatParticipantState) { - super(chatParticipantState); - } - - private assistantPrompt(issues: IssueSearchResultItem[]): string { - const possibleColumns = Object.keys(issues[0]); - return `${LLM_FIND_IMPORTANT_COLUMNS_INSTRUCTIONS}\n${possibleColumns.map(column => `- ${column}`).join('\n')}\nHere's the data you have about the issues:\n`; - } - - private postProcess(proposedColumns: string, issues: IssueSearchResultItem[]): IssueColumn[] { - const lines = proposedColumns.split('\n'); - const possibleColumns = Object.keys(issues[0]); - const finalColumns: IssueColumn[] = []; - for (let line of lines) { - line = line.trim(); - if (line === '') { - continue; - } - if (!possibleColumns.includes(line)) { - // Check if the llm decided to use formatting, even though we asked it not to - const splitOnSpace = line.split(' '); - if (splitOnSpace.length > 1) { - const testColumn = splitOnSpace[splitOnSpace.length - 1]; - if (possibleColumns.includes(testColumn)) { - finalColumns.push(testColumn as IssueColumn); - } - } - } else { - finalColumns.push(line as IssueColumn); - } - } - return finalColumns; - } - - private async getImportantColumns(issueItemsInfo: vscode.LanguageModelTextPart | undefined, issues: IssueSearchResultItem[], token: vscode.CancellationToken): Promise { - if (!issueItemsInfo) { - return ['number', 'title', 'state']; - } - - // Try to get the llm to tell us which columns are important based on information it has about the issues - const models = await vscode.lm.selectChatModels({ - vendor: 'copilot', - family: 'gpt-4o' - }); - const model = models[0]; - const chatOptions: vscode.LanguageModelChatRequestOptions = { - justification: 'Answering user questions pertaining to GitHub.' - }; - const messages = [vscode.LanguageModelChatMessage.Assistant(this.assistantPrompt(issues))]; - messages.push(new vscode.LanguageModelChatMessage(vscode.LanguageModelChatMessageRole.User, issueItemsInfo?.value)); - const response = await model.sendRequest(messages, chatOptions, token); - const result = this.postProcess(await concatAsyncIterable(response.text), issues); - const indexOfUrl = result.indexOf('url'); - if (result.length === 0) { - return ['number', 'title', 'state']; - } else if (indexOfUrl >= 0) { - // Never include the url column - result.splice(indexOfUrl, 1); - } - - return result; - } - - private renderUser(account: IssueSearchResultAccount) { - return `[@${reviewerLabel(account)}](${account.url})`; - } - - private issueToRow(issue: IssueSearchResultItem, importantColumns: IssueColumn[]): string { - return `| ${importantColumns.map(column => { - switch (column) { - case 'number': - return `[${issue[column]}](${issue.url})`; - case 'labels': - return issue[column]?.map((label) => label?.name && label.color ? makeLabel({ name: label.name, color: label.color }) : '').join(', '); - case 'assignees': - return issue[column]?.map((assignee) => this.renderUser(assignee)).join(', '); - case 'author': - const account = issue[column]; - return account ? this.renderUser(account) : ''; - case 'createdAt': - case 'updatedAt': - const updatedAt = issue[column]; - return updatedAt ? new Date(updatedAt).toLocaleDateString() : ''; - case 'milestone': - return issue[column]; - default: - return issue[column]; - } - }).join(' | ')} |`; - } - - private foundIssuesCount(params: DisplayIssuesParameters): number { - return params.totalIssues !== undefined ? params.totalIssues : (params.arrayOfIssues?.length ?? 0); - } - - async prepareInvocation(options: vscode.LanguageModelToolInvocationPrepareOptions): Promise { - const maxDisplay = 10; - const foundIssuesCount = this.foundIssuesCount(options.input); - const actualDisplay = Math.min(maxDisplay, foundIssuesCount); - if (actualDisplay === 0) { - return { - invocationMessage: vscode.l10n.t('No issues found') - }; - } else if (actualDisplay < foundIssuesCount) { - return { - invocationMessage: vscode.l10n.t('Found {0} issues. Generating a markdown table of the first {1}', foundIssuesCount, actualDisplay) - }; - } else { - return { - invocationMessage: vscode.l10n.t('Found {0} issues. Generating a markdown table', foundIssuesCount) - }; - } - } - - async invoke(options: vscode.LanguageModelToolInvocationOptions, token: vscode.CancellationToken): Promise { - await ensureEmojis(this.context); - const issueItemsInfo: vscode.LanguageModelTextPart | undefined = this.chatParticipantState.firstUserMessage; - const issueItems: IssueSearchResultItem[] | undefined = options.input.arrayOfIssues; - if (!issueItems || issueItems.length === 0) { - return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart(vscode.l10n.t('No issues found. Please try another query.'))]); - } - Logger.debug(`Displaying ${this.foundIssuesCount(options.input)} issues, first issue ${issueItems[0].number}`, DisplayIssuesTool.ID); - const importantColumns = await this.getImportantColumns(issueItemsInfo, issueItems, token); - - const titleRow = `| ${importantColumns.join(' | ')} |`; - Logger.debug(`Columns ${titleRow} issues`, DisplayIssuesTool.ID); - const separatorRow = `| ${importantColumns.map(() => '---').join(' | ')} |\n`; - const issues = new vscode.MarkdownString(titleRow); - issues.supportHtml = true; - issues.appendMarkdown('\n'); - issues.appendMarkdown(separatorRow); - issues.appendMarkdown(issueItems.slice(0, 10).map(issue => { - return this.issueToRow(issue, importantColumns); - }).join('\n')); - - return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart(TOOL_MARKDOWN_RESULT), - new vscode.LanguageModelTextPart(issues.value), - new vscode.LanguageModelTextPart(`The issues have been shown to the user. Simply say that you've already displayed the issue or first 10 issues.`)]); - } - -} \ No newline at end of file diff --git a/src/lm/tools/fetchLabelsTool.ts b/src/lm/tools/fetchLabelsTool.ts new file mode 100644 index 0000000000..0b09746765 --- /dev/null +++ b/src/lm/tools/fetchLabelsTool.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as vscode from 'vscode'; +import { RepoToolBase } from './toolsUtils'; + +interface FetchLabelsToolParameters { + repo?: { + owner?: string; + name?: string; + }; +} + +export interface FetchLabelsResult { + owner: string; + repo: string; + labels: { + name: string; + color: string; + description?: string; + }[]; +} + +export class FetchLabelsTool extends RepoToolBase { + public static readonly toolId = 'github-pull-request_labels_fetch'; + + async invoke(options: vscode.LanguageModelToolInvocationOptions, _token: vscode.CancellationToken): Promise { + const { owner, name, folderManager } = await this.getRepoInfo({ owner: options.input.repo?.owner, name: options.input.repo?.name }); + const labels = await folderManager.getLabels(undefined, { owner, repo: name }); + const result: FetchLabelsResult = { + owner, + repo: name, + labels: labels.map(label => ({ + name: label.name, + color: label.color, + description: label.description + })) + }; + return new vscode.LanguageModelToolResult([ + new vscode.LanguageModelTextPart(JSON.stringify(result)), + new vscode.LanguageModelTextPart('Above is a stringified JSON representation of the labels for the repository.') + ]); + } + + async prepareInvocation(options: vscode.LanguageModelToolInvocationPrepareOptions): Promise { + const { owner, name } = await this.getRepoInfo({ owner: options.input.repo?.owner, name: options.input.repo?.name }); + const url = (owner && name) ? `https://github.com/${owner}/${name}/labels` : undefined; + const message = url + ? new vscode.MarkdownString(vscode.l10n.t('Fetching labels from [{0}/{1}]({2})', owner, name, url)) + : vscode.l10n.t('Fetching labels from GitHub'); + return { + invocationMessage: message, + }; + } +} diff --git a/src/lm/tools/searchTools.ts b/src/lm/tools/searchTools.ts index 4d5c99273a..92d623e7c5 100644 --- a/src/lm/tools/searchTools.ts +++ b/src/lm/tools/searchTools.ts @@ -5,19 +5,10 @@ 'use strict'; import * as vscode from 'vscode'; -import { concatAsyncIterable, RepoToolBase } from './toolsUtils'; +import { RepoToolBase } from './toolsUtils'; import Logger from '../../common/logger'; -import { FolderRepositoryManager } from '../../github/folderRepositoryManager'; -import { ILabel } from '../../github/interface'; import { escapeMarkdown } from '../../issues/util'; -interface ConvertToQuerySyntaxParameters { - naturalLanguageString?: string; - repo?: { - owner?: string; - name?: string; - }; -} interface ConvertToQuerySyntaxResult { query: string; @@ -27,375 +18,6 @@ interface ConvertToQuerySyntaxResult { }; } -enum ValidatableProperty { - is = 'is', - type = 'type', - state = 'state', - in = 'in', - linked = 'linked', - status = 'status', - draft = 'draft', - review = 'review', - no = 'no', -} - -const githubSearchSyntax = { - is: { possibleValues: ['issue', 'pr', 'draft', 'public', 'private', 'locked', 'unlocked'] }, - assignee: { valueDescription: 'A GitHub user name or @me' }, - author: { valueDescription: 'A GitHub user name or @me' }, - mentions: { valueDescription: 'A GitHub user name or @me' }, - team: { valueDescription: 'A GitHub user name' }, - commenter: { valueDescription: 'A GitHub user name or @me' }, - involves: { valueDescription: 'A GitHub user name or @me' }, - label: { valueDescription: 'A GitHub issue/pr label' }, - type: { possibleValues: ['pr', 'issue'] }, - state: { possibleValues: ['open', 'closed', 'merged'] }, - in: { possibleValues: ['title', 'body', 'comments'] }, - user: { valueDescription: 'A GitHub user name or @me' }, - org: { valueDescription: 'A GitHub org, without the repo name' }, - repo: { valueDescription: 'A GitHub repo, without the org name' }, - linked: { possibleValues: ['pr', 'issue'] }, - milestone: { valueDescription: 'A GitHub milestone' }, - project: { valueDescription: 'A GitHub project' }, - status: { possibleValues: ['success', 'failure', 'pending'] }, - head: { valueDescription: 'A git commit sha or branch name' }, - base: { valueDescription: 'A git commit sha or branch name' }, - comments: { valueDescription: 'A number' }, - interactions: { valueDescription: 'A number' }, - reactions: { valueDescription: 'A number' }, - draft: { possibleValues: ['true', 'false'] }, - review: { possibleValues: ['none', 'required', 'approved', 'changes_requested'] }, - reviewedBy: { valueDescription: 'A GitHub user name or @me' }, - reviewRequested: { valueDescription: 'A GitHub user name or @me' }, - userReviewRequested: { valueDescription: 'A GitHub user name or @me' }, - teamReviewRequested: { valueDescription: 'A GitHub user name' }, - created: { valueDescription: 'A date, with an optional < >' }, - updated: { valueDescription: 'A date, with an optional < >' }, - closed: { valueDescription: 'A date, with an optional < >' }, - no: { possibleValues: ['label', 'milestone', 'assignee', 'project'] }, - sort: { possibleValues: ['updated', 'updated-asc', 'interactions', 'interactions-asc', 'author-date', 'author-date-asc', 'committer-date', 'committer-date-asc', 'reactions', 'reactions-asc', 'reactions-(+1, -1, smile, tada, heart)'] } -}; - -const MATCH_UNQUOTED_SPACES = /(?!\B"[^"]*)\s+(?![^"]*"\B)/; - -export class ConvertToSearchSyntaxTool extends RepoToolBase { - public static readonly toolId = 'github-pull-request_formSearchQuery'; - static ID = 'ConvertToSearchSyntaxTool'; - - private async fullQueryAssistantPrompt(folderRepoManager: FolderRepositoryManager): Promise { - const remote = folderRepoManager.activePullRequest?.remote ?? folderRepoManager.activeIssue?.remote ?? (await folderRepoManager.getPullRequestDefaultRepo()).remote; - - return `Instructions: -You are an expert on GitHub issue search syntax. GitHub issues are always software engineering related. You can help the user convert a natural language query to a query that can be used to search GitHub issues. Here are some rules to follow: -- Always try to include "repo:" or "org:" in your response. -- "repo" is often formated as "owner/name". If needed, the current repo is ${remote.owner}/${remote.repositoryName}. -- Ignore display information. -- Respond with only the query. -- Always include a "sort:" parameter. If multiple sorts are possible, choose the one that the user requested. -- Always include a property with the @me value if the query includes "me" or "my". -- Here are some examples of valid queries: - - repo:microsoft/vscode is:issue state:open sort:updated-asc - - mentions:@me org:microsoft is:issue state:open sort:updated - - assignee:@me milestone:"October 2024" is:open is:issue sort:reactions - - comments:>5 org:contoso is:issue state:closed mentions:@me label:bug - - interactions:>5 repo:contoso/cli is:issue state:open - - repo:microsoft/vscode-python is:issue sort:updated -assignee:@me - - repo:contoso/cli is:issue sort:updated no:milestone -- Go through each word of the natural language query and try to match it to a syntax component. -- Use a "-" in front of a syntax component to indicate that it should be "not-ed". -- Use the "no" syntax component to indicate that a property should be empty. -- As a reminder, here are the components of the query syntax: - ${JSON.stringify(githubSearchSyntax)} -`; - } - - private async labelsAssistantPrompt(folderRepoManager: FolderRepositoryManager, labels: ILabel[]): Promise { - // It seems that AND and OR aren't supported in GraphQL, so we can't use them in the query - // Here's the prompt in case we switch to REST: - // - Use as many labels as you think fit the query. If one label fits, then there are probably more that fit. - // - Respond with a list of labels in github search syntax, separated by AND or OR. Examples: "label:bug OR label:polish", "label:accessibility AND label:editor-accessibility" - return `Instructions: -You are an expert on choosing search keywords based on a natural language search query. Here are some rules to follow: -- Choose labels based on what the user wants to search for, not based on the actual words in the query. -- The user might include info on how they want their search results to be displayed. Ignore all of that. -- Labels will be and-ed together, so don't pick a bunch of super specific labels. -- Try to pick just one label. -- Respond with a space-separated list of labels: Examples: 'bug polish', 'accessibility "feature accessibility"' -- Only choose labels that you're sure are relevant. Having no labels is preferable than lables that aren't relevant. -- Don't choose labels that the user has explicitly excluded. -- Respond with label names chosen from this JSON array of options: -${JSON.stringify(labels.filter(label => !label.name.includes('required') && !label.name.includes('search') && !label.name.includes('question') && !label.name.includes('find') && !label.name.includes('issue')).map(label => ({ name: label.name, description: label.description })))} -`; - } - - private freeFormAssistantPrompt(): string { - return `Instructions: -You are getting ready to make a GitHub search query. Given a natural language query, you should find any key words that might be good for searching: -- Only include a max of 1 key word that is relevant to the search query. -- Don't refer to issue numbers. -- Don't refer to product names. -- Don't include any key words that might be related to display or rendering. -- Respond with only your chosen key word. -- It's better to return no keywords than to return irrelevant keywords. -- If an issue is provided, choose a keyword that names the feature or bug that the issue is about. -- Don't include key words or concepts that are already covered by labels. -`; - } - - private freeFormUserPrompt(labels: string[], originalUserPrompt: string): string { - return `I've already included the following labels: [${labels.join(', ')}]. The best search keywords in "${originalUserPrompt}" are:`; - } - - private labelsUserPrompt(originalUserPrompt: string): string { - return `The following labels are most appropriate for "${originalUserPrompt}":`; - } - - private fullQueryUserPrompt(originalUserPrompt: string): string { - originalUserPrompt = originalUserPrompt.replace(/\b(me|my)\b/, (value) => value.toUpperCase()); - const date = new Date(); - return `Pretend today's date is ${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}, but only include it if needed. How should this be converted to a GitHub issue search query? ${originalUserPrompt}`; - } - - private validateSpecificQueryPart(property: ValidatableProperty | string, value: string): boolean { - switch (property) { - case ValidatableProperty.is: - return value === 'issue' || value === 'pr' || value === 'draft' || value === 'public' || value === 'private' || value === 'locked' || value === 'unlocked'; - case ValidatableProperty.type: - return value === 'pr' || value === 'issue'; - case ValidatableProperty.state: - return value === 'open' || value === 'closed' || value === 'merged'; - case ValidatableProperty.in: - return value === 'title' || value === 'body' || value === 'comments'; - case ValidatableProperty.linked: - return value === 'pr' || value === 'issue'; - case ValidatableProperty.status: - return value === 'success' || value === 'failure' || value === 'pending'; - case ValidatableProperty.draft: - return value === 'true' || value === 'false'; - case ValidatableProperty.review: - return value === 'none' || value === 'required' || value === 'approved' || value === 'changes_requested'; - case ValidatableProperty.no: - return value === 'label' || value === 'milestone' || value === 'assignee' || value === 'project'; - default: - return true; - } - } - - private validateLabelsList(labelsList: string, allLabels: ILabel[]): string[] { - // I wrote everything for AND and OR, but it isn't supported with GraphQL. - // Leaving it in for now in case we switch to REST. - const isAndOrOr = (labelOrOperator: string) => { - return labelOrOperator === 'AND' || labelOrOperator === 'OR'; - }; - - const labelsAndOperators = labelsList.split(MATCH_UNQUOTED_SPACES).map(label => label.trim()); - let goodLabels: string[] = []; - for (let labelOrOperator of labelsAndOperators) { - if (isAndOrOr(labelOrOperator)) { - if (goodLabels.length === 0) { - continue; - } else if (goodLabels.length > 0 && isAndOrOr(goodLabels[goodLabels.length - 1])) { - goodLabels[goodLabels.length - 1] = labelOrOperator; - } else { - goodLabels.push(labelOrOperator); - } - continue; - } - // Make sure it does start with `label:` - const labelPrefixRegex = /^label:(?!\B"[^"]*)\s+(?![^"]*"\B)/; - const labelPrefixMatch = labelOrOperator.match(labelPrefixRegex); - let label = labelOrOperator; - if (labelPrefixMatch) { - label = labelPrefixMatch[1]; - } - if (allLabels.find(l => l.name === label)) { - goodLabels.push(label); - } - } - if (goodLabels.length > 0 && isAndOrOr(goodLabels[goodLabels.length - 1])) { - goodLabels = goodLabels.slice(0, goodLabels.length - 1); - } - return goodLabels; - } - - private validateFreeForm(baseQuery: string, labels: string[], freeForm: string) { - // Currently, we only allow the free form to return one keyword - freeForm = freeForm.trim(); - // useless strings to search for - if (freeForm.includes('issue') || freeForm.match(MATCH_UNQUOTED_SPACES) || freeForm.toLowerCase() === 'none') { - return ''; - } - if (baseQuery.includes(freeForm)) { - return ''; - } - if (labels.includes(freeForm)) { - return ''; - } - if (labels.some(label => freeForm.includes(label) || label.includes(freeForm))) { - return ''; - } - if (Object.keys(githubSearchSyntax).find(searchPart => freeForm.includes(searchPart) || searchPart.includes(freeForm))) { - return ''; - } - return freeForm; - } - - private validateQuery(query: string, labels: string[], freeForm: string) { - let reformedQuery = ''; - const queryParts = query.split(MATCH_UNQUOTED_SPACES); - // Only keep property:value pairs and '-', no reform allowed here. - for (const part of queryParts) { - if (part.startsWith('label:')) { - continue; - } - const propAndVal = part.split(':'); - if (propAndVal.length === 2) { - const hasMinus = propAndVal[0].startsWith('-'); - const label = hasMinus ? propAndVal[0].substring(1) : propAndVal[0]; - const value = propAndVal[1]; - if (!label.match(/^[a-zA-Z]+$/)) { - continue; - } - if (!this.validateSpecificQueryPart(label, value)) { - continue; - } - if (label === 'no' && value === 'label' && labels.length > 0) { - // special case for no:label as we shouldn't have both no:label and label:label - continue; - } - } - reformedQuery = `${reformedQuery} ${part}`; - } - - const validFreeForm = this.validateFreeForm(reformedQuery, labels, freeForm); - - reformedQuery = `${reformedQuery} ${labels.map(label => `label:${label}`).join(' ')} ${validFreeForm}`; - return reformedQuery.trim(); - } - - private postProcess(queryPart: string, freeForm: string, labels: string[]): ConvertToQuerySyntaxResult | undefined { - const query = this.findQuery(queryPart); - if (!query) { - return; - } - const fixedLabels = this.validateQuery(query, labels, freeForm); - const fixedRepo = this.fixRepo(fixedLabels); - return fixedRepo; - } - - private fixRepo(query: string): ConvertToQuerySyntaxResult { - const repoRegex = /repo:([^ ]+)/; - const orgRegex = /org:([^ ]+)/; - const repoMatch = query.match(repoRegex); - const orgMatch = query.match(orgRegex); - let newQuery = query.trim(); - let owner: string | undefined; - let name: string | undefined; - if (repoMatch) { - const originalRepo = repoMatch[1]; - if (originalRepo.includes('/')) { - const ownerAndRepo = originalRepo.split('/'); - owner = ownerAndRepo[0]; - name = ownerAndRepo[1]; - } - - if (orgMatch && originalRepo.includes('/')) { - // remove the org match - newQuery = query.replace(orgRegex, ''); - } else if (orgMatch) { - // We need to add the org into the repo - newQuery = query.replace(repoRegex, `repo:${orgMatch[1]}/${originalRepo}`); - owner = orgMatch[1]; - name = originalRepo; - } - } - return { - query: newQuery, - repo: owner && name ? { owner, name } : undefined - }; - } - - private findQuery(result: string): string | undefined { - // if there's a code block, then that's all we take - if (result.includes('```')) { - const start = result.indexOf('```'); - const end = result.indexOf('```', start + 3); - return result.substring(start + 3, end); - } - // if it's only one line, we take that - const lines = result.split('\n'); - if (lines.length <= 1) { - return lines.length === 0 ? result : lines[0]; - } - // if there are multiple lines, we take the first line that has a colon - for (const line of lines) { - if (line.includes(':')) { - return line; - } - } - } - - private async generateLabelQuery(folderManager: FolderRepositoryManager, labels: ILabel[], chatOptions: vscode.LanguageModelChatRequestOptions, model: vscode.LanguageModelChat, naturalLanguageString: string, token: vscode.CancellationToken): Promise { - const messages = [vscode.LanguageModelChatMessage.Assistant(await this.labelsAssistantPrompt(folderManager, labels))]; - messages.push(vscode.LanguageModelChatMessage.User(this.labelsUserPrompt(naturalLanguageString))); - const response = await model.sendRequest(messages, chatOptions, token); - return concatAsyncIterable(response.text); - } - - private async generateFreeFormQuery(folderManager: FolderRepositoryManager, chatOptions: vscode.LanguageModelChatRequestOptions, model: vscode.LanguageModelChat, naturalLanguageString: string, labels: string[], token: vscode.CancellationToken): Promise { - const messages = [vscode.LanguageModelChatMessage.Assistant(this.freeFormAssistantPrompt())]; - messages.push(vscode.LanguageModelChatMessage.User(this.freeFormUserPrompt(labels, naturalLanguageString))); - const response = await model.sendRequest(messages, chatOptions, token); - return concatAsyncIterable(response.text); - } - - private async generateQuery(folderManager: FolderRepositoryManager, chatOptions: vscode.LanguageModelChatRequestOptions, model: vscode.LanguageModelChat, naturalLanguageString: string, token: vscode.CancellationToken): Promise { - const messages = [vscode.LanguageModelChatMessage.Assistant(await this.fullQueryAssistantPrompt(folderManager))]; - messages.push(vscode.LanguageModelChatMessage.User(this.fullQueryUserPrompt(naturalLanguageString))); - const response = await model.sendRequest(messages, chatOptions, token); - return concatAsyncIterable(response.text); - } - - async prepareInvocation(_options: vscode.LanguageModelToolInvocationPrepareOptions): Promise { - return { - invocationMessage: vscode.l10n.t('Converting to search syntax') - }; - } - - async invoke(options: vscode.LanguageModelToolInvocationOptions, token: vscode.CancellationToken): Promise { - const { owner, name, folderManager } = await this.getRepoInfo({ owner: options.input.repo?.owner, name: options.input.repo?.name }); - const firstUserMessage = `${this.chatParticipantState.firstUserMessage?.value}, ${options.input.naturalLanguageString}`; - - const allLabels = await folderManager.getLabels(undefined, { owner, repo: name }); - - const models = await vscode.lm.selectChatModels({ - vendor: 'copilot', - family: 'gpt-4o' - }); - const model = models[0]; - const chatOptions: vscode.LanguageModelChatRequestOptions = { - justification: 'Answering user questions pertaining to GitHub.' - }; - const [query, labelsList] = await Promise.all([this.generateQuery(folderManager, chatOptions, model, firstUserMessage, token), this.generateLabelQuery(folderManager, allLabels, chatOptions, model, firstUserMessage, token)]); - const validatedLabels = this.validateLabelsList(labelsList, allLabels); - const freeForm = await this.generateFreeFormQuery(folderManager, chatOptions, model, firstUserMessage, validatedLabels, token); - const result = this.postProcess(query, freeForm, validatedLabels); - if (!result) { - throw new Error('Unable to form a query.'); - } - Logger.debug(`Query \`${result.query}\``, ConvertToSearchSyntaxTool.ID); - const json: ConvertToQuerySyntaxResult = { - query: result.query, - repo: { - owner, - name - } - }; - return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart(JSON.stringify(json)), - new vscode.LanguageModelTextPart('Above is the query in stringified json format. You can pass this VERBATIM to a tool that knows how to search.')]); - } -} - type SearchToolParameters = ConvertToQuerySyntaxResult; export interface IssueSearchResultAccount { diff --git a/src/lm/tools/suggestFixTool.ts b/src/lm/tools/suggestFixTool.ts deleted file mode 100644 index d3dfdac364..0000000000 --- a/src/lm/tools/suggestFixTool.ts +++ /dev/null @@ -1,96 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import * as vscode from 'vscode'; -import { CredentialStore } from '../../github/credentials'; -import { RepositoriesManager } from '../../github/repositoriesManager'; -import { ChatParticipantState } from '../participants'; -import { IssueResult, IssueToolParameters, RepoToolBase } from './toolsUtils'; - -export class SuggestFixTool extends RepoToolBase { - public static readonly toolId = 'github-pull-request_suggest-fix'; - - constructor(credentialStore: CredentialStore, repositoriesManager: RepositoriesManager, chatParticipantState: ChatParticipantState) { - super(credentialStore, repositoriesManager, chatParticipantState); - } - - async prepareInvocation(options: vscode.LanguageModelToolInvocationPrepareOptions): Promise { - return { - invocationMessage: options.input.issueNumber ? vscode.l10n.t('Suggesting a fix for issue #{0}', options.input.issueNumber) : vscode.l10n.t('Suggesting a fix for the issue') - }; - } - - async invoke(options: vscode.LanguageModelToolInvocationOptions, token: vscode.CancellationToken): Promise { - const repo = options.input.repo; - const owner = repo?.owner; - const name = repo?.name; - const issueNumber = options.input.issueNumber; - if (!repo || !owner || !name || !issueNumber) { - return undefined; - } - const { folderManager } = await this.getRepoInfo(repo); - if (!folderManager) { - throw new Error(`No folder manager found for ${repo.owner}/${repo.name}. Make sure to have the repository open.`); - } - const issue = await folderManager.resolveIssue(owner, name, issueNumber, true); - if (!issue) { - throw new Error(`No issue found for ${repo.owner}/${repo.name}/${options.input.issueNumber}. Make sure the issue exists.`); - } - - const result: IssueResult = { - title: issue.title, - body: issue.body, - comments: issue.item.comments?.map(c => ({ body: c.body })) ?? [] - }; - - const messages: vscode.LanguageModelChatMessage[] = []; - messages.push(vscode.LanguageModelChatMessage.Assistant(`You are a world-class developer who is capable of solving very difficult bugs and issues.`)); - messages.push(vscode.LanguageModelChatMessage.Assistant(`The user will give you an issue title, body and a list of comments from GitHub. The user wants you to suggest a fix.`)); - messages.push(vscode.LanguageModelChatMessage.Assistant(`Analyze the issue content, the workspace context below and using all this information suggest a fix.`)); - messages.push(vscode.LanguageModelChatMessage.Assistant(`Where possible output code-blocks and reference real files in the workspace with the fix.`)); - messages.push(vscode.LanguageModelChatMessage.User(`The issue content is as follows: `)); - messages.push(vscode.LanguageModelChatMessage.User(`Issue Title: ${result.title}`)); - messages.push(vscode.LanguageModelChatMessage.User(`Issue Body: ${result.body}`)); - result.comments.forEach((comment, index) => { - messages.push(vscode.LanguageModelChatMessage.User(`Comment ${index}: ${comment.body}`)); - }); - - const codeSearchTool = vscode.lm.tools.find(value => value.tags.includes('vscode_codesearch')); - if (!codeSearchTool) { - throw new Error('Could not find the code search tool'); - } - - const copilotCodebaseResult = await vscode.lm.invokeTool(codeSearchTool.name, { - toolInvocationToken: undefined, - input: { - query: result.title - } - }, token); - - const plainTextResult = copilotCodebaseResult.content[0]; - if (plainTextResult instanceof vscode.LanguageModelTextPart) { - messages.push(vscode.LanguageModelChatMessage.User(`Below is some potential relevant workspace context to the issue. The user cannot see this result, so you should explain it to the user if referencing it in your answer.`)); - const toolMessage = vscode.LanguageModelChatMessage.User(''); - toolMessage.content = [plainTextResult]; - messages.push(toolMessage); - } - - const models = await vscode.lm.selectChatModels({ - vendor: 'copilot', - family: 'gpt-4o' - }); - const model = models[0]; - const response = await model.sendRequest(messages, {}, token); - - let responseResult = ''; - for await (const chunk of response.text) { - responseResult += chunk; - } - return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart(responseResult)]); - } - - -} \ No newline at end of file diff --git a/src/lm/tools/summarizeIssueTool.ts b/src/lm/tools/summarizeIssueTool.ts deleted file mode 100644 index 182dba4a5f..0000000000 --- a/src/lm/tools/summarizeIssueTool.ts +++ /dev/null @@ -1,96 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import * as vscode from 'vscode'; -import { FetchIssueResult } from './fetchIssueTool'; -import { concatAsyncIterable } from './toolsUtils'; - -export class IssueSummarizationTool implements vscode.LanguageModelTool { - public static readonly toolId = 'github-pull-request_issue_summarize'; - - async prepareInvocation(options: vscode.LanguageModelToolInvocationPrepareOptions): Promise { - if (!options.input.title) { - return { - invocationMessage: vscode.l10n.t('Summarizing issue') - }; - } - const shortenedTitle = options.input.title.length > 40; - const maxLengthTitle = shortenedTitle ? options.input.title.substring(0, 40) : options.input.title; - return { - invocationMessage: vscode.l10n.t('Summarizing "{0}', maxLengthTitle) - }; - } - - async invoke(options: vscode.LanguageModelToolInvocationOptions, _token: vscode.CancellationToken): Promise { - let issueOrPullRequestInfo: string = ` -Title : ${options.input.title} -Body : ${options.input.body} -`; - const fileChanges = options.input.fileChanges; - if (fileChanges) { - issueOrPullRequestInfo += ` -The following are the files changed: -`; - for (const fileChange of fileChanges.values()) { - issueOrPullRequestInfo += ` -File : ${fileChange.fileName} -Patch: ${fileChange.patch} -`; - } - } - const comments = options.input.comments; - if (comments) { - for (const [index, comment] of comments.entries()) { - issueOrPullRequestInfo += ` -Comment ${index} : -Author: ${comment.author} -Body: ${comment.body} -`; - } - } - const models = await vscode.lm.selectChatModels({ - vendor: 'copilot', - family: 'gpt-4o' - }); - const model = models[0]; - const repo = options.input.repo; - const owner = options.input.owner; - - if (model && repo && owner) { - const messages = [vscode.LanguageModelChatMessage.User(this.summarizeInstructions(repo, owner))]; - messages.push(vscode.LanguageModelChatMessage.User(`The issue or pull request information is as follows:`)); - messages.push(vscode.LanguageModelChatMessage.User(issueOrPullRequestInfo)); - const response = await model.sendRequest(messages, {}); - const responseText = await concatAsyncIterable(response.text); - return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart(responseText)]); - } else { - return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart(issueOrPullRequestInfo)]); - } - } - - private summarizeInstructions(repo: string, owner: string): string { - return ` -You are an AI assistant who is very proficient in summarizing issues and pull requests (PRs). -You will be given information relative to an issue or PR : the title, the body and the comments. In the case of a PR you will also be given patches of the PR changes. -Your task is to output a summary of all this information. -Do not output code. When you try to summarize PR changes, summarize in a textual format. -Output references to other issues and PRs as Markdown links. The current issue has owner ${owner} and is in the repo ${repo}. -If a comment references for example issue or PR #123, then output either of the following in the summary depending on if it is an issue or a PR: - -[#123](https://github.com/${owner}/${repo}/issues/123) -[#123](https://github.com/${owner}/${repo}/pull/123) - -When you summarize comments, always give a summary of each comment and always mention the author clearly before the comment. If the author is called 'joe' and the comment is 'this is a comment', then the output should be: - -joe: this is a comment - -If the content contains images in Markdown format (e.g., ![alt text](image-url)), always preserve them in the output exactly as they appear. Images are important visual content and should not be removed or summarized. - -Make sure the summary is at least as short or shorter than the issue or PR with the comments and the patches if there are. -`; - } - -} \ No newline at end of file diff --git a/src/lm/tools/summarizeNotificationsTool.ts b/src/lm/tools/summarizeNotificationsTool.ts deleted file mode 100644 index 736b3ebeff..0000000000 --- a/src/lm/tools/summarizeNotificationsTool.ts +++ /dev/null @@ -1,146 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import * as vscode from 'vscode'; -import { FetchNotificationResult } from './fetchNotificationTool'; -import { concatAsyncIterable, TOOL_COMMAND_RESULT } from './toolsUtils'; - -export class NotificationSummarizationTool implements vscode.LanguageModelTool { - public static readonly toolId = 'github-pull-request_notification_summarize'; - - async prepareInvocation(options: vscode.LanguageModelToolInvocationPrepareOptions): Promise { - const parameters = options.input; - if (!parameters.itemType || !parameters.itemNumber) { - return { - invocationMessage: vscode.l10n.t('Summarizing notification') - }; - } - const type = parameters.itemType === 'issue' ? 'issues' : 'pull'; - const url = `https://github.com/${parameters.owner}/${parameters.repo}/${type}/${parameters.itemNumber}`; - return { - invocationMessage: new vscode.MarkdownString(vscode.l10n.t('Summarizing item [#{0}]({1})', parameters.itemNumber, url)) - }; - } - - async invoke(options: vscode.LanguageModelToolInvocationOptions, _token: vscode.CancellationToken): Promise { - let notificationInfo: string = ''; - const lastReadAt = options.input.lastReadAt; - if (!lastReadAt) { - // First time the thread is viewed, so no lastReadAt field - notificationInfo += `This thread is viewed for the first time. Here is the main item information of the thread:`; - } - notificationInfo += ` -Title : ${options.input.title} -Body : ${options.input.body} -`; - const fileChanges = options.input.fileChanges; - if (fileChanges) { - notificationInfo += ` -The following are the files changed: -`; - for (const fileChange of fileChanges.values()) { - notificationInfo += ` -File : ${fileChange.fileName} -Patch: ${fileChange.patch} -`; - } - } - - const unreadComments = options.input.comments; - if (unreadComments && unreadComments.length > 0) { - notificationInfo += ` -The following are the unread comments of the thread: -`; - for (const [index, comment] of unreadComments.entries()) { - notificationInfo += ` -Comment ${index} : -Author: ${comment.author} -Body: ${comment.body} -`; - } - } - const models = await vscode.lm.selectChatModels({ - vendor: 'copilot', - family: 'gpt-4o' - }); - const model = models[0]; - const content: vscode.LanguageModelTextPart[] = []; - const threadId = options.input.threadId; - const notificationKey = options.input.notificationKey; - if (threadId && notificationKey) { - const markAsReadCommand = { - title: 'Mark As Read', - command: 'notification.markAsRead', - arguments: [{ threadId, notificationKey }] - }; - const markAsDoneCommand: vscode.Command = { - title: 'Mark As Done', - command: 'notification.markAsDone', - arguments: [{ threadId, notificationKey }] - }; - content.push(new vscode.LanguageModelTextPart(TOOL_COMMAND_RESULT)); - content.push(new vscode.LanguageModelTextPart(JSON.stringify(markAsReadCommand))); - content.push(new vscode.LanguageModelTextPart(TOOL_COMMAND_RESULT)); - content.push(new vscode.LanguageModelTextPart(JSON.stringify(markAsDoneCommand))); - } - const owner = options.input.owner; - const repo = options.input.repo; - if (model && owner && repo) { - const messages = [vscode.LanguageModelChatMessage.User(this.summarizeInstructions(owner, repo))]; - messages.push(vscode.LanguageModelChatMessage.User(`The notification information is as follows:`)); - messages.push(vscode.LanguageModelChatMessage.User(notificationInfo)); - const response = await model.sendRequest(messages, {}); - const responseText = await concatAsyncIterable(response.text); - content.push(new vscode.LanguageModelTextPart(responseText)); - } else { - content.push(new vscode.LanguageModelTextPart(notificationInfo)); - } - content.push(new vscode.LanguageModelTextPart('Above is a summary of the notification. Extract and output this notification summary directly as is to the user. Do not output the result from the call to the fetch notification tool.')); - return new vscode.LanguageModelToolResult(content); - } - - private summarizeInstructions(owner: string, repo: string): string { - return ` -You are an AI assistant who is very proficient in summarizing notification threads. -You will be given information relative to a notification thread : the title, the body and the comments. In the case of a PR you will also be given patches of the PR changes. -Since you are reviewing a notification thread, part of the content is by definition unread. You will be told what part of the content is yet unread. This can be the comments or it can be both the thread issue/PR as well as the comments. -Your task is to output a summary of all this notification thread information and give an update to the user concerning the unread part of the thread. -Output references to issues and PRs as Markdown links. The current notification is for a thread that has owner ${owner} and is in the repo ${repo}. -If a comment references for example issue or PR #123, then output either of the following in the summary depending on if it is an issue or a PR: - -[#123](https://github.com/${owner}/${repo}/issues/123) -[#123](https://github.com/${owner}/${repo}/pull/123) - -When you summarize comments, always give a summary of each comment and always mention the author clearly before the comment. If the author is called 'joe' and the comment is 'this is a comment', then the output should be: - -joe: this is a comment - -If the content contains images in Markdown format (e.g., ![alt text](image-url)), always preserve them in the output exactly as they appear. Images are important visual content and should not be removed or summarized. - -Always include in your output, which part of the thread is unread by prefixing that part with the markdown heading of level 1 with text "Unread Thread" or "Unread Comments". -Make sure the summary is at least as short or shorter than the issue or PR with the comments and the patches if there are. -Example output: - -# Unread Thread - - - -or: - - -# Unread Comments - - -Both 'Unread Thread' and 'Unread Comments' should not appear at the same time as markdown titles. The following is incorrect: - -# Unread Thread - -# Unread Comments - -`; - } - -} \ No newline at end of file diff --git a/src/lm/tools/tools.ts b/src/lm/tools/tools.ts index fef9f2ac20..983ae71070 100644 --- a/src/lm/tools/tools.ts +++ b/src/lm/tools/tools.ts @@ -5,44 +5,28 @@ 'use strict'; import * as vscode from 'vscode'; -import { CredentialStore } from '../../github/credentials'; -import { RepositoriesManager } from '../../github/repositoriesManager'; -import { ChatParticipantState } from '../participants'; import { ActivePullRequestTool } from './activePullRequestTool'; -import { DisplayIssuesTool } from './displayIssuesTool'; import { FetchIssueTool } from './fetchIssueTool'; +import { FetchLabelsTool } from './fetchLabelsTool'; import { FetchNotificationTool } from './fetchNotificationTool'; import { OpenPullRequestTool } from './openPullRequestTool'; -import { ConvertToSearchSyntaxTool, SearchTool } from './searchTools'; -import { SuggestFixTool } from './suggestFixTool'; -import { IssueSummarizationTool } from './summarizeIssueTool'; -import { NotificationSummarizationTool } from './summarizeNotificationsTool'; +import { SearchTool } from './searchTools'; +import { CredentialStore } from '../../github/credentials'; +import { RepositoriesManager } from '../../github/repositoriesManager'; -export function registerTools(context: vscode.ExtensionContext, credentialStore: CredentialStore, repositoriesManager: RepositoriesManager, chatParticipantState: ChatParticipantState) { - registerFetchingTools(context, credentialStore, repositoriesManager, chatParticipantState); - registerSummarizationTools(context); - registerSuggestFixTool(context, credentialStore, repositoriesManager, chatParticipantState); - registerSearchTools(context, credentialStore, repositoriesManager, chatParticipantState); +export function registerTools(context: vscode.ExtensionContext, credentialStore: CredentialStore, repositoriesManager: RepositoriesManager) { + registerFetchingTools(context, credentialStore, repositoriesManager); + registerSearchTools(context, credentialStore, repositoriesManager); context.subscriptions.push(vscode.lm.registerTool(ActivePullRequestTool.toolId, new ActivePullRequestTool(repositoriesManager))); context.subscriptions.push(vscode.lm.registerTool(OpenPullRequestTool.toolId, new OpenPullRequestTool(repositoriesManager))); } -function registerFetchingTools(context: vscode.ExtensionContext, credentialStore: CredentialStore, repositoriesManager: RepositoriesManager, chatParticipantState: ChatParticipantState) { - context.subscriptions.push(vscode.lm.registerTool(FetchIssueTool.toolId, new FetchIssueTool(credentialStore, repositoriesManager, chatParticipantState))); - context.subscriptions.push(vscode.lm.registerTool(FetchNotificationTool.toolId, new FetchNotificationTool(credentialStore, repositoriesManager, chatParticipantState))); -} - -function registerSummarizationTools(context: vscode.ExtensionContext) { - context.subscriptions.push(vscode.lm.registerTool(IssueSummarizationTool.toolId, new IssueSummarizationTool())); - context.subscriptions.push(vscode.lm.registerTool(NotificationSummarizationTool.toolId, new NotificationSummarizationTool())); -} - -function registerSuggestFixTool(context: vscode.ExtensionContext, credentialStore: CredentialStore, repositoriesManager: RepositoriesManager, chatParticipantState: ChatParticipantState) { - context.subscriptions.push(vscode.lm.registerTool(SuggestFixTool.toolId, new SuggestFixTool(credentialStore, repositoriesManager, chatParticipantState))); +function registerFetchingTools(context: vscode.ExtensionContext, credentialStore: CredentialStore, repositoriesManager: RepositoriesManager) { + context.subscriptions.push(vscode.lm.registerTool(FetchIssueTool.toolId, new FetchIssueTool(credentialStore, repositoriesManager))); + context.subscriptions.push(vscode.lm.registerTool(FetchLabelsTool.toolId, new FetchLabelsTool(credentialStore, repositoriesManager))); + context.subscriptions.push(vscode.lm.registerTool(FetchNotificationTool.toolId, new FetchNotificationTool(credentialStore, repositoriesManager))); } -function registerSearchTools(context: vscode.ExtensionContext, credentialStore: CredentialStore, repositoriesManager: RepositoriesManager, chatParticipantState: ChatParticipantState) { - context.subscriptions.push(vscode.lm.registerTool(ConvertToSearchSyntaxTool.toolId, new ConvertToSearchSyntaxTool(credentialStore, repositoriesManager, chatParticipantState))); - context.subscriptions.push(vscode.lm.registerTool(SearchTool.toolId, new SearchTool(credentialStore, repositoriesManager, chatParticipantState))); - context.subscriptions.push(vscode.lm.registerTool(DisplayIssuesTool.toolId, new DisplayIssuesTool(context, chatParticipantState))); +function registerSearchTools(context: vscode.ExtensionContext, credentialStore: CredentialStore, repositoriesManager: RepositoriesManager) { + context.subscriptions.push(vscode.lm.registerTool(SearchTool.toolId, new SearchTool(credentialStore, repositoriesManager))); } \ No newline at end of file diff --git a/src/lm/tools/toolsUtils.ts b/src/lm/tools/toolsUtils.ts index 9b855bbe84..07692d35d3 100644 --- a/src/lm/tools/toolsUtils.ts +++ b/src/lm/tools/toolsUtils.ts @@ -9,7 +9,6 @@ import { CredentialStore, GitHub } from '../../github/credentials'; import { FolderRepositoryManager } from '../../github/folderRepositoryManager'; import { RepositoriesManager } from '../../github/repositoriesManager'; import { hasEnterpriseUri } from '../../github/utils'; -import { ChatParticipantState } from '../participants'; export interface IToolCall { tool: vscode.LanguageModelToolInformation; @@ -37,7 +36,7 @@ export interface IssueResult { } export abstract class ToolBase implements vscode.LanguageModelTool { - constructor(protected readonly chatParticipantState: ChatParticipantState) { } + constructor() { } abstract invoke(options: vscode.LanguageModelToolInvocationOptions, token: vscode.CancellationToken): vscode.ProviderResult; } @@ -50,8 +49,8 @@ export async function concatAsyncIterable(asyncIterable: AsyncIterable): } export abstract class RepoToolBase extends ToolBase { - constructor(private readonly credentialStore: CredentialStore, private readonly repositoriesManager: RepositoriesManager, chatParticipantState: ChatParticipantState) { - super(chatParticipantState); + constructor(private readonly credentialStore: CredentialStore, private readonly repositoriesManager: RepositoriesManager) { + super(); } protected async getRepoInfo(options: { owner?: string, name?: string }): Promise<{ owner: string; name: string; folderManager: FolderRepositoryManager }> { diff --git a/src/notifications/notificationsFeatureRegistar.ts b/src/notifications/notificationsFeatureRegistar.ts index 708233f040..9ce8f846fa 100644 --- a/src/notifications/notificationsFeatureRegistar.ts +++ b/src/notifications/notificationsFeatureRegistar.ts @@ -93,7 +93,7 @@ export class NotificationsFeatureRegister extends Disposable { "notification.chatSummarizeNotification" : {} */ this._telemetry.sendTelemetryEvent('notification.chatSummarizeNotification'); - vscode.commands.executeCommand(chatCommand(), vscode.l10n.t('@githubpr Summarize notification with thread ID #{0}', notification.notification.id)); + vscode.commands.executeCommand(chatCommand(), vscode.l10n.t('Summarize notification with thread ID #{0}', notification.notification.id)); }) ); this._register(