diff --git a/gitbutler-ui/src/lib/backend/aiService.test.ts b/gitbutler-ui/src/lib/backend/aiService.test.ts index 0aba2960c..93d13f5b7 100644 --- a/gitbutler-ui/src/lib/backend/aiService.test.ts +++ b/gitbutler-ui/src/lib/backend/aiService.test.ts @@ -20,11 +20,15 @@ const defaultGitConfig = Object.freeze({ class DummyGitConfigService implements GitConfigService { constructor(private config: { [index: string]: string | undefined }) {} - async get(key: string): Promise { - return (this.config[key] || null) as T | null; + async get(key: string): Promise { + return (this.config[key] || undefined) as T | undefined; } - async set(key: string, value: T): Promise { + async getWithDefault(key: string): Promise { + return this.config[key] as T; + } + + async set(key: string, value: T): Promise { return (this.config[key] = value); } } @@ -42,14 +46,14 @@ class DummyAIClient implements AIClient { const examplePatch = ` @@ -52,7 +52,8 @@ - + export enum AnthropicModelName { Opus = 'claude-3-opus-20240229', - Sonnet = 'claude-3-sonnet-20240229' + Sonnet = 'claude-3-sonnet-20240229', + Haiku = 'claude-3-haiku-20240307' } - + export const AI_SERVICE_CONTEXT = Symbol(); `; diff --git a/gitbutler-ui/src/lib/backend/aiService.ts b/gitbutler-ui/src/lib/backend/aiService.ts index b01f33d12..7240eff73 100644 --- a/gitbutler-ui/src/lib/backend/aiService.ts +++ b/gitbutler-ui/src/lib/backend/aiService.ts @@ -56,6 +56,30 @@ export enum AnthropicModelName { Haiku = 'claude-3-haiku-20240307' } +export enum ConfigKeys { + ModelProvider = 'gitbutler.aiModelProvider', + OpenAIKeyOption = 'gitbutler.aiOpenAIKeyOption', + OpenAIModelName = 'gitbutler.aiOpenAIModelName', + OpenAIKey = 'gitbutler.aiOpenAIKey', + AnthropicKeyOption = 'gitbutler.aiAnthropicKeyOption', + AnthropicModelName = 'gitbutler.aiAnthropicModelName', + AnthropicKey = 'gitbutler.aiAnthropicKey' +} + +type SummarizeCommitOpts = { + diff: string; + useEmojiStyle?: boolean; + useBriefStyle?: boolean; + commitTemplate?: string; + userToken?: string; +}; + +type SummarizeBranchOpts = { + diff: string; + branchTemplate?: string; + userToken?: string; +}; + export class AIService { constructor( private gitConfig: GitConfigService, @@ -66,13 +90,18 @@ export class AIService { // Firstly, if the user has opted to use the GB API and isn't logged in, it will return undefined // Secondly, if the user has opted to bring their own key but hasn't provided one, it will return undefined async buildClient(userToken?: string): Promise { - const modelKind = - (await this.gitConfig.get('gitbutler.aiModelProvider')) || ModelKind.OpenAI; - const openAIKeyOption = - (await this.gitConfig.get('gitbutler.aiOpenAIKeyOption')) || KeyOption.ButlerAPI; - const anthropicKeyOption = - (await this.gitConfig.get('gitbutler.aiAnthropicKeyOption')) || - KeyOption.ButlerAPI; + const modelKind = await this.gitConfig.getWithDefault( + ConfigKeys.ModelProvider, + ModelKind.OpenAI + ); + const openAIKeyOption = await this.gitConfig.getWithDefault( + ConfigKeys.OpenAIKeyOption, + KeyOption.ButlerAPI + ); + const anthropicKeyOption = await this.gitConfig.getWithDefault( + ConfigKeys.AnthropicKeyOption, + KeyOption.ButlerAPI + ); if ( (modelKind == ModelKind.OpenAI && openAIKeyOption == KeyOption.ButlerAPI) || @@ -80,24 +109,22 @@ export class AIService { ) { if (!userToken) { toasts.error("When using GitButler's API to summarize code, you must be logged in"); - return; } - return new ButlerAIClient(this.cloud, userToken, ModelKind.OpenAI); } if (modelKind == ModelKind.OpenAI) { - const openAIModelName = - (await this.gitConfig.get('gitbutler.aiOpenAIModelName')) || - OpenAIModelName.GPT35Turbo; - const openAIKey = await this.gitConfig.get('gitbutler.aiOpenAIKey'); + const openAIModelName = await this.gitConfig.getWithDefault( + ConfigKeys.OpenAIModelName, + OpenAIModelName.GPT35Turbo + ); + const openAIKey = await this.gitConfig.get(ConfigKeys.OpenAIKey); if (!openAIKey) { toasts.error( 'When using OpenAI in a bring your own key configuration, you must provide a valid token' ); - return; } @@ -105,17 +132,16 @@ export class AIService { return new OpenAIClient(openAIModelName, openAI); } if (modelKind == ModelKind.Anthropic) { - const anthropicModelName = - (await this.gitConfig.get('gitbutler.aiAnthropicModelName')) || - AnthropicModelName.Haiku; - const anthropicKey = await this.gitConfig.get('gitbutler.aiAnthropicKey'); + const anthropicModelName = await this.gitConfig.getWithDefault( + ConfigKeys.AnthropicModelName, + AnthropicModelName.Haiku + ); + const anthropicKey = await this.gitConfig.get(ConfigKeys.AnthropicKey); - // TODO: Provide feedback to user if (!anthropicKey) { toasts.error( 'When using Anthropic in a bring your own key configuration, you must provide a valid token' ); - return; } @@ -129,31 +155,21 @@ export class AIService { useBriefStyle = false, commitTemplate = defaultCommitTemplate, userToken - }: { - diff: string; - useEmojiStyle?: boolean; - useBriefStyle?: boolean; - commitTemplate?: string; - userToken?: string; - }) { + }: SummarizeCommitOpts) { const aiClient = await this.buildClient(userToken); if (!aiClient) return; let prompt = commitTemplate.replaceAll('%{diff}', diff.slice(0, diffLengthLimit)); - if (useBriefStyle) { - prompt = prompt.replaceAll( - '%{brief_style}', - 'The commit message must be only one sentence and as short as possible.' - ); - } else { - prompt = prompt.replaceAll('%{brief_style}', ''); - } - if (useEmojiStyle) { - prompt = prompt.replaceAll('%{emoji_style}', 'Make use of GitMoji in the title prefix.'); - } else { - prompt = prompt.replaceAll('%{emoji_style}', "Don't use any emoji."); - } + const briefPart = useBriefStyle + ? 'The commit message must be only one sentence and as short as possible.' + : ''; + prompt = prompt.replaceAll('%{brief_style}', briefPart); + + const emojiPart = useEmojiStyle + ? 'Make use of GitMoji in the title prefix.' + : "Don't use any emoji."; + prompt = prompt.replaceAll('%{emoji_style}', emojiPart); let message = await aiClient.evaluate(prompt); @@ -161,9 +177,8 @@ export class AIService { message = message.split('\n')[0]; } - const firstNewLine = message.indexOf('\n'); - const summary = firstNewLine > -1 ? message.slice(0, firstNewLine).trim() : message; - const description = firstNewLine > -1 ? message.slice(firstNewLine + 1).trim() : ''; + const parts = message.split(/\n+(.*?)\w*/s); + const [summary, description] = [parts[0] || '', parts[1] || '']; return description.length > 0 ? `${summary}\n\n${description}` : summary; } @@ -172,20 +187,12 @@ export class AIService { diff, branchTemplate = defaultBranchTemplate, userToken = undefined - }: { - diff: string; - branchTemplate?: string; - userToken?: string; - }) { + }: SummarizeBranchOpts) { const aiClient = await this.buildClient(userToken); if (!aiClient) return; const prompt = branchTemplate.replaceAll('%{diff}', diff.slice(0, diffLengthLimit)); - - let message = await aiClient.evaluate(prompt); - - message = message.replaceAll(' ', '-'); - message = message.replaceAll('\n', '-'); - return message; + const message = await aiClient.evaluate(prompt); + return message.replaceAll(' ', '-').replaceAll('\n', '-'); } } diff --git a/gitbutler-ui/src/lib/backend/gitConfigService.ts b/gitbutler-ui/src/lib/backend/gitConfigService.ts index 11bd8da11..3864011a0 100644 --- a/gitbutler-ui/src/lib/backend/gitConfigService.ts +++ b/gitbutler-ui/src/lib/backend/gitConfigService.ts @@ -1,11 +1,16 @@ import { invoke } from '@tauri-apps/api/tauri'; export class GitConfigService { - get(key: string): Promise { - return invoke('git_get_global_config', { key }); + async get(key: string): Promise { + return await invoke('git_get_global_config', { key }); } - set(key: string, value: T) { - return invoke('git_set_global_config', { key, value }); + async getWithDefault(key: string, defaultValue: T): Promise { + const value = await invoke('git_get_global_config', { key }); + return value || defaultValue; + } + + async set(key: string, value: T) { + return invoke('git_set_global_config', { key, value }); } } diff --git a/gitbutler-ui/src/lib/components/AISettings.svelte b/gitbutler-ui/src/lib/components/AISettings.svelte index 038f2006d..32a4d97c8 100644 --- a/gitbutler-ui/src/lib/components/AISettings.svelte +++ b/gitbutler-ui/src/lib/components/AISettings.svelte @@ -4,6 +4,7 @@ import TextBox from './TextBox.svelte'; import { AnthropicModelName, + ConfigKeys, KeyOption, ModelKind, OpenAIModelName @@ -17,36 +18,48 @@ const gitConfigService = getContextByClass(GitConfigService); let modelKind: ModelKind; - $: gitConfigService.set('gitbutler.aiModelProvider', modelKind); let openAIKeyOption: KeyOption; - $: gitConfigService.set('gitbutler.aiOpenAIKeyOption', openAIKeyOption); let anthropicKeyOption: KeyOption; - $: gitConfigService.set('gitbutler.aiAnthropicKeyOption', anthropicKeyOption); let openAIKey: string | undefined; - $: if (openAIKey) gitConfigService.set('gitbutler.aiOpenAIKey', openAIKey); let openAIModelName: OpenAIModelName; - $: gitConfigService.set('gitbutler.aiOpenAIModelName', openAIModelName); let anthropicKey: string | undefined; - $: if (anthropicKey) gitConfigService.set('gitbutler.aiAnthropicKey', anthropicKey); let anthropicModelName: AnthropicModelName; + + $: gitConfigService.set('gitbutler.aiModelProvider', modelKind); + + $: gitConfigService.set('gitbutler.aiOpenAIKeyOption', openAIKeyOption); + $: gitConfigService.set('gitbutler.aiOpenAIModelName', openAIModelName); + $: if (openAIKey) gitConfigService.set('gitbutler.aiOpenAIKey', openAIKey); + + $: gitConfigService.set('gitbutler.aiAnthropicKeyOption', anthropicKeyOption); $: gitConfigService.set('gitbutler.aiAnthropicModelName', anthropicModelName); + $: if (anthropicKey) gitConfigService.set('gitbutler.aiAnthropicKey', anthropicKey); onMount(async () => { - modelKind = - (await gitConfigService.get('gitbutler.aiModelProvider')) || ModelKind.OpenAI; - openAIKeyOption = - (await gitConfigService.get('gitbutler.aiOpenAIKeyOption')) || KeyOption.ButlerAPI; - anthropicKeyOption = - (await gitConfigService.get('gitbutler.aiAnthropicKeyOption')) || - KeyOption.ButlerAPI; - openAIModelName = - (await gitConfigService.get('gitbutler.aiOpenAIModelName')) || - OpenAIModelName.GPT35Turbo; - openAIKey = (await gitConfigService.get('gitbutler.aiOpenAIKey')) || undefined; - anthropicModelName = - (await gitConfigService.get('gitbutler.aiAnthropicModelName')) || - AnthropicModelName.Haiku; - anthropicKey = (await gitConfigService.get('gitbutler.aiAnthropicKey')) || undefined; + modelKind = await gitConfigService.getWithDefault( + ConfigKeys.ModelProvider, + ModelKind.OpenAI + ); + + openAIKeyOption = await gitConfigService.getWithDefault( + ConfigKeys.OpenAIKeyOption, + KeyOption.ButlerAPI + ); + openAIModelName = await gitConfigService.getWithDefault( + ConfigKeys.OpenAIModelName, + OpenAIModelName.GPT35Turbo + ); + openAIKey = await gitConfigService.get(ConfigKeys.OpenAIKey); + + anthropicKeyOption = await gitConfigService.getWithDefault( + ConfigKeys.AnthropicKeyOption, + KeyOption.ButlerAPI + ); + anthropicModelName = await gitConfigService.getWithDefault( + ConfigKeys.AnthropicModelName, + AnthropicModelName.Haiku + ); + anthropicKey = await gitConfigService.get(ConfigKeys.AnthropicKey); }); $: if (form) form.modelKind.value = modelKind;