瀏覽代碼

Adds Anthropic AI support

- Adds ai.experimental.provider setting to switch between OpenAI & Anthropic
main
Eric Amodio 1 年之前
父節點
當前提交
bd413d97dd
共有 7 個檔案被更改,包括 316 行新增31 行删除
  1. +35
    -1
      package.json
  2. +29
    -10
      src/ai/aiProviderService.ts
  3. +225
    -0
      src/ai/anthropicProvider.ts
  4. +19
    -17
      src/ai/openaiProvider.ts
  5. +5
    -0
      src/config.ts
  6. +2
    -2
      src/constants.ts
  7. +1
    -1
      src/webviews/apps/commitDetails/commitDetails.html

+ 35
- 1
package.json 查看文件

@ -2817,6 +2817,21 @@
"scope": "window",
"order": 1
},
"gitlens.ai.experimental.provider": {
"type": "string",
"default": "openai",
"enum": [
"openai",
"anthropic"
],
"enumDescriptions": [
"OpenAI",
"Anthropic"
],
"markdownDescription": "Specifies the AI provider to use for GitLens' experimental AI features",
"scope": "window",
"order": 100
},
"gitlens.ai.experimental.openai.model": {
"type": "string",
"default": "gpt-3.5-turbo",
@ -2838,7 +2853,26 @@
],
"markdownDescription": "Specifies the OpenAI model to use for GitLens' experimental AI features",
"scope": "window",
"order": 100
"order": 101
},
"gitlens.ai.experimental.anthropic.model": {
"type": "string",
"default": "claude-v1",
"enum": [
"claude-v1",
"claude-v1-100k",
"claude-instant-v1",
"claude-instant-v1-100k"
],
"enumDescriptions": [
"Claude v1",
"Claude v1 with 100k token context",
"Claude Instant v1",
"Claude Instant v1 with 100k token context"
],
"markdownDescription": "Specifies the Anthropic model to use for GitLens' experimental AI features",
"scope": "window",
"order": 102
}
}
},

+ 29
- 10
src/ai/aiProviderService.ts 查看文件

@ -8,7 +8,9 @@ import { uncommittedStaged } from '../git/models/constants';
import type { GitRevisionReference } from '../git/models/reference';
import type { Repository } from '../git/models/repository';
import { isRepository } from '../git/models/repository';
import { configuration } from '../system/configuration';
import type { Storage } from '../system/storage';
import { AnthropicProvider } from './anthropicProvider';
import { OpenAIProvider } from './openaiProvider';
export interface AIProvider extends Disposable {
@ -20,14 +22,27 @@ export interface AIProvider extends Disposable {
}
export class AIProviderService implements Disposable {
private _provider: AIProvider;
private _provider: AIProvider | undefined;
constructor(private readonly container: Container) {
this._provider = new OpenAIProvider(container);
private get provider() {
const providerId = configuration.get('ai.experimental.provider');
if (providerId === this._provider?.id) return this._provider;
this._provider?.dispose();
if (providerId === 'anthropic') {
this._provider = new AnthropicProvider(this.container);
} else {
this._provider = new OpenAIProvider(this.container);
}
return this._provider;
}
constructor(private readonly container: Container) {}
dispose() {
this._provider.dispose();
this._provider?.dispose();
}
public async generateCommitMessage(
@ -50,15 +65,17 @@ export class AIProviderService implements Disposable {
});
if (diff?.diff == null) throw new Error('No staged changes to generate a commit message from.');
const confirmed = await confirmAIProviderToS(this._provider, this.container.storage);
const provider = this.provider;
const confirmed = await confirmAIProviderToS(provider, this.container.storage);
if (!confirmed) return undefined;
if (options?.progress != null) {
return window.withProgress(options.progress, async () =>
this._provider.generateCommitMessage(diff.diff!, { context: options?.context }),
provider.generateCommitMessage(diff.diff!, { context: options?.context }),
);
}
return this._provider.generateCommitMessage(diff.diff, { context: options?.context });
return provider.generateCommitMessage(diff.diff, { context: options?.context });
}
async explainCommit(
@ -95,7 +112,9 @@ export class AIProviderService implements Disposable {
});
if (diff?.diff == null) throw new Error('No changes found to explain.');
const confirmed = await confirmAIProviderToS(this._provider, this.container.storage);
const provider = this.provider;
const confirmed = await confirmAIProviderToS(provider, this.container.storage);
if (!confirmed) return undefined;
if (!commit.hasFullDetails()) {
@ -105,10 +124,10 @@ export class AIProviderService implements Disposable {
if (options?.progress != null) {
return window.withProgress(options.progress, async () =>
this._provider.explainChanges(commit!.message!, diff.diff!),
provider.explainChanges(commit!.message!, diff.diff!),
);
}
return this._provider.explainChanges(commit.message, diff.diff);
return provider.explainChanges(commit.message, diff.diff);
}
}

+ 225
- 0
src/ai/anthropicProvider.ts 查看文件

@ -0,0 +1,225 @@
import type { Disposable, QuickInputButton } from 'vscode';
import { env, ThemeIcon, Uri, window } from 'vscode';
import { fetch } from '@env/fetch';
import type { Container } from '../container';
import { configuration } from '../system/configuration';
import type { Storage } from '../system/storage';
import { supportedInVSCodeVersion } from '../system/utils';
import type { AIProvider } from './aiProviderService';
export class AnthropicProvider implements AIProvider {
readonly id = 'anthropic';
readonly name = 'Anthropic';
private get model(): AnthropicModels {
return configuration.get('ai.experimental.anthropic.model') || 'claude-v1';
}
constructor(private readonly container: Container) {}
dispose() {}
async generateCommitMessage(diff: string, options?: { context?: string }): Promise<string | undefined> {
const apiKey = await getApiKey(this.container.storage);
if (apiKey == null) return undefined;
const model = this.model;
const maxCodeCharacters = getMaxCharacters(model);
const code = diff.substring(0, maxCodeCharacters);
if (diff.length > maxCodeCharacters) {
void window.showWarningMessage(
`The diff of the staged changes had to be truncated to ${maxCodeCharacters} characters to fit within the Anthropic's limits.`,
);
}
let customPrompt = configuration.get('experimental.generateCommitMessagePrompt');
if (!customPrompt.endsWith('.')) {
customPrompt += '.';
}
let prompt =
"\n\nHuman: You are an AI programming assistant tasked with writing a meaningful commit message by summarizing code changes.\n- Follow the user's instructions carefully & to the letter!\n- Don't repeat yourself or make anything up!\n- Minimize any other prose.";
prompt += `\n${customPrompt}\n- Avoid phrases like "this commit", "this change", etc.`;
prompt += '\n\nAssistant: OK';
if (options?.context) {
prompt += `\n\nHuman: Use "${options.context}" to help craft the commit message.\n\nAssistant: OK`;
}
prompt += `\n\nHuman: Write a meaningful commit message for the following code changes:\n\n${code}`;
prompt += '\n\nAssistant:';
const request: AnthropicCompletionRequest = {
model: model,
prompt: prompt,
stream: false,
max_tokens_to_sample: 5000,
stop_sequences: ['\n\nHuman:'],
};
const rsp = await fetch('https://api.anthropic.com/v1/complete', {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
Client: 'anthropic-typescript/0.4.3',
'X-API-Key': apiKey,
},
method: 'POST',
body: JSON.stringify(request),
});
if (!rsp.ok) {
debugger;
throw new Error(`Unable to generate commit message: ${rsp.status}: ${rsp.statusText}`);
}
const data: AnthropicCompletionResponse = await rsp.json();
const message = data.completion.trim();
return message;
}
async explainChanges(message: string, diff: string): Promise<string | undefined> {
const apiKey = await getApiKey(this.container.storage);
if (apiKey == null) return undefined;
const model = this.model;
const maxCodeCharacters = getMaxCharacters(model);
const code = diff.substring(0, maxCodeCharacters);
if (diff.length > maxCodeCharacters) {
void window.showWarningMessage(
`The diff of the commit changes had to be truncated to ${maxCodeCharacters} characters to fit within the OpenAI's limits.`,
);
}
let prompt =
"\n\nHuman: You are an AI programming assistant tasked with providing an easy to understand but detailed explanation of a commit by summarizing the code changes while also using the commit message as additional context and framing.\nDon't make anything up!";
prompt += `\nUse the following user-provided commit message, which should provide some explanation to why these changes where made, when attempting to generate the rich explanation:\n\n${message}`;
prompt += '\n\nAssistant: OK';
prompt += `\n\nHuman: Explain the following code changes:\n\n${code}`;
prompt += '\n\nAssistant:';
const request: AnthropicCompletionRequest = {
model: model,
prompt: prompt,
stream: false,
max_tokens_to_sample: 5000,
stop_sequences: ['\n\nHuman:'],
};
const rsp = await fetch('https://api.anthropic.com/v1/complete', {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
Client: 'anthropic-typescript/0.4.3',
'X-API-Key': apiKey,
},
method: 'POST',
body: JSON.stringify(request),
});
if (!rsp.ok) {
debugger;
throw new Error(`Unable to explain commit: ${rsp.status}: ${rsp.statusText}`);
}
const data: AnthropicCompletionResponse = await rsp.json();
const summary = data.completion.trim();
return summary;
}
}
async function getApiKey(storage: Storage): Promise<string | undefined> {
let apiKey = await storage.getSecret('gitlens.anthropic.key');
if (!apiKey) {
const input = window.createInputBox();
input.ignoreFocusOut = true;
const disposables: Disposable[] = [];
try {
const infoButton: QuickInputButton = {
iconPath: new ThemeIcon(`link-external`),
tooltip: 'Open the Anthropic API Key Page',
};
apiKey = await new Promise<string | undefined>(resolve => {
disposables.push(
input.onDidHide(() => resolve(undefined)),
input.onDidChangeValue(value => {
if (value && !/sk-[a-zA-Z0-9-_]{32,}/.test(value)) {
input.validationMessage = 'Please enter a valid Anthropic API key';
return;
}
input.validationMessage = undefined;
}),
input.onDidAccept(() => {
const value = input.value.trim();
if (!value || !/sk-[a-zA-Z0-9-_]{32,}/.test(value)) {
input.validationMessage = 'Please enter a valid Anthropic API key';
return;
}
resolve(value);
}),
input.onDidTriggerButton(e => {
if (e === infoButton) {
void env.openExternal(Uri.parse('https://console.anthropic.com/account/keys'));
}
}),
);
input.password = true;
input.title = 'Connect to Anthropic';
input.placeholder = 'Please enter your Anthropic API key to use this feature';
input.prompt = supportedInVSCodeVersion('input-prompt-links')
? 'Enter your [Anthropic API Key](https://console.anthropic.com/account/keys "Get your Anthropic API key")'
: 'Enter your Anthropic API Key';
input.buttons = [infoButton];
input.show();
});
} finally {
input.dispose();
disposables.forEach(d => void d.dispose());
}
if (!apiKey) return undefined;
void storage.storeSecret('gitlens.anthropic.key', apiKey);
}
return apiKey;
}
function getMaxCharacters(model: AnthropicModels): number {
if (model === 'claude-v1-100k' || model === 'claude-instant-v1-100k') {
return 135000;
}
return 12000;
}
export type AnthropicModels = 'claude-v1' | 'claude-v1-100k' | 'claude-instant-v1' | 'claude-instant-v1-100k';
interface AnthropicCompletionRequest {
model: string;
prompt: string;
stream: boolean;
max_tokens_to_sample: number;
stop_sequences: string[];
temperature?: number;
top_k?: number;
top_p?: number;
tags?: { [key: string]: string };
}
interface AnthropicCompletionResponse {
completion: string;
stop: string | null;
stop_reason: 'stop_sequence' | 'max_tokens';
truncated: boolean;
exception: string | null;
log_id: string;
}

+ 19
- 17
src/ai/openaiProvider.ts 查看文件

@ -20,8 +20,8 @@ export class OpenAIProvider implements AIProvider {
dispose() {}
async generateCommitMessage(diff: string, options?: { context?: string }): Promise<string | undefined> {
const openaiApiKey = await getApiKey(this.container.storage);
if (openaiApiKey == null) return undefined;
const apiKey = await getApiKey(this.container.storage);
if (apiKey == null) return undefined;
const model = this.model;
const maxCodeCharacters = getMaxCharacters(model);
@ -38,7 +38,7 @@ export class OpenAIProvider implements AIProvider {
customPrompt += '.';
}
const data: OpenAIChatCompletionRequest = {
const request: OpenAIChatCompletionRequest = {
model: model,
messages: [
{
@ -54,23 +54,24 @@ export class OpenAIProvider implements AIProvider {
};
if (options?.context) {
data.messages.push({
request.messages.push({
role: 'user',
content: `Use "${options.context}" to help craft the commit message.`,
});
}
data.messages.push({
request.messages.push({
role: 'user',
content: `Write a meaningful commit message for the following code changes:\n\n${code}`,
});
const rsp = await fetch('https://api.openai.com/v1/chat/completions', {
headers: {
Authorization: `Bearer ${openaiApiKey}`,
Accept: 'application/json',
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
method: 'POST',
body: JSON.stringify(data),
body: JSON.stringify(request),
});
if (!rsp.ok) {
@ -78,14 +79,14 @@ export class OpenAIProvider implements AIProvider {
throw new Error(`Unable to generate commit message: ${rsp.status}: ${rsp.statusText}`);
}
const completion: OpenAIChatCompletionResponse = await rsp.json();
const message = completion.choices[0].message.content.trim();
const data: OpenAIChatCompletionResponse = await rsp.json();
const message = data.choices[0].message.content.trim();
return message;
}
async explainChanges(message: string, diff: string): Promise<string | undefined> {
const openaiApiKey = await getApiKey(this.container.storage);
if (openaiApiKey == null) return undefined;
const apiKey = await getApiKey(this.container.storage);
if (apiKey == null) return undefined;
const model = this.model;
const maxCodeCharacters = getMaxCharacters(model);
@ -97,13 +98,13 @@ export class OpenAIProvider implements AIProvider {
);
}
const data: OpenAIChatCompletionRequest = {
const request: OpenAIChatCompletionRequest = {
model: model,
messages: [
{
role: 'system',
content:
"You are an AI programming assistant tasked with providing a detailed explanation of a commit by summarizing the code changes while also using the commit message as additional context and framing.\n\n- Don't make anything up!",
"You are an AI programming assistant tasked with providing an easy to understand but detailed explanation of a commit by summarizing the code changes while also using the commit message as additional context and framing.\n\n- Don't make anything up!",
},
{
role: 'user',
@ -122,11 +123,12 @@ export class OpenAIProvider implements AIProvider {
const rsp = await fetch('https://api.openai.com/v1/chat/completions', {
headers: {
Authorization: `Bearer ${openaiApiKey}`,
Accept: 'application/json',
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
method: 'POST',
body: JSON.stringify(data),
body: JSON.stringify(request),
});
if (!rsp.ok) {
@ -134,8 +136,8 @@ export class OpenAIProvider implements AIProvider {
throw new Error(`Unable to explain commit: ${rsp.status}: ${rsp.statusText}`);
}
const completion: OpenAIChatCompletionResponse = await rsp.json();
const summary = completion.choices[0].message.content.trim();
const data: OpenAIChatCompletionResponse = await rsp.json();
const summary = data.choices[0].message.content.trim();
return summary;
}
}

+ 5
- 0
src/config.ts 查看文件

@ -1,3 +1,4 @@
import type { AnthropicModels } from './ai/anthropicProvider';
import type { OpenAIModels } from './ai/openaiProvider';
import type { DateTimeFormat } from './system/date';
import { LogLevel } from './system/logger.constants';
@ -5,9 +6,13 @@ import { LogLevel } from './system/logger.constants';
export interface Config {
ai: {
experimental: {
provider: 'openai' | 'anthropic';
openai: {
model?: OpenAIModels;
};
anthropic: {
model?: AnthropicModels;
};
};
};
autolinks: AutolinkReference[] | null;

+ 2
- 2
src/constants.ts 查看文件

@ -501,11 +501,11 @@ export type TelemetryEvents =
| 'subscription/changed'
| 'usage/track';
export type AIProviders = 'openai';
export type AIProviders = 'anthropic' | 'openai';
export type SecretKeys =
| `gitlens.integration.auth:${string}`
| 'gitlens.openai.key'
| `gitlens.${AIProviders}.key`
| `gitlens.plus.auth:${Environment}`;
export const enum SyncedStorageKeys {

+ 1
- 1
src/webviews/apps/commitDetails/commitDetails.html 查看文件

@ -233,7 +233,7 @@
<span slot="title">Explain (AI)</span>
<div class="pane-content">
<p>Let OpenAI assist in understanding the changes made with this commit.</p>
<p>Let AI assist in understanding the changes made with this commit.</p>
<p class="button-container">
<span class="button-group">
<button class="button button--full" type="button" data-action="explain-commit">

Loading…
取消
儲存