From cff42ad749a01efc6312121114b7a76baea7ac6e Mon Sep 17 00:00:00 2001 From: hitesh-1997 Date: Mon, 4 Nov 2024 23:57:41 +0530 Subject: [PATCH] add default config for the autoedits --- lib/shared/src/configuration.ts | 6 +- vscode/src/autoedits/autoedits-provider.ts | 211 ++++++++++-------- vscode/src/autoedits/prompt-provider.ts | 15 +- .../src/autoedits/providers/cody-gateway.ts | 81 +++++++ vscode/src/autoedits/providers/deepseek.ts | 69 ------ vscode/src/autoedits/providers/fireworks.ts | 15 +- vscode/src/autoedits/providers/openai.ts | 12 +- vscode/src/configuration.test.ts | 9 +- vscode/src/configuration.ts | 6 +- vscode/src/main.ts | 7 +- vscode/src/testutils/mocks.ts | 3 +- 11 files changed, 247 insertions(+), 187 deletions(-) create mode 100644 vscode/src/autoedits/providers/cody-gateway.ts delete mode 100644 vscode/src/autoedits/providers/deepseek.ts diff --git a/lib/shared/src/configuration.ts b/lib/shared/src/configuration.ts index 59ea261f23fa..cb01c43b470b 100644 --- a/lib/shared/src/configuration.ts +++ b/lib/shared/src/configuration.ts @@ -32,8 +32,9 @@ export interface AutoEditsTokenLimit { } export interface AutoEditsModelConfig { - provider: string + provider: 'openai' | 'fireworks' | 'cody-gateway-fastpath-chat' model: string + url: string apiKey: string tokenLimit: AutoEditsTokenLimit } @@ -80,7 +81,8 @@ interface RawClientConfiguration { experimentalTracing: boolean experimentalSupercompletions: boolean experimentalAutoeditsRendererTesting: boolean - experimentalAutoedits: AutoEditsModelConfig | undefined + experimentalAutoeditsConfigOverride: AutoEditsModelConfig | undefined + experimentalAutoeditsEnabled: boolean experimentalCommitMessage: boolean experimentalNoodle: boolean experimentalMinionAnthropicKey: string | undefined diff --git a/vscode/src/autoedits/autoedits-provider.ts b/vscode/src/autoedits/autoedits-provider.ts index d137c3bbc9ad..01ff3d262d08 100644 --- a/vscode/src/autoedits/autoedits-provider.ts +++ b/vscode/src/autoedits/autoedits-provider.ts @@ -1,8 +1,15 @@ -import { type AutoEditsTokenLimit, type DocumentContext, tokensToChars } from '@sourcegraph/cody-shared' +import { + type AutoEditsModelConfig, + type AutoEditsTokenLimit, + currentResolvedConfig, + dotcomTokenToGatewayToken, + tokensToChars, +} from '@sourcegraph/cody-shared' import { Observable } from 'observable-fns' import * as vscode from 'vscode' import { ContextMixer } from '../completions/context/context-mixer' import { DefaultContextStrategyFactory } from '../completions/context/context-strategy' +import { RetrieverIdentifier } from '../completions/context/utils' import { getCurrentDocContext } from '../completions/get-current-doc-context' import { lines } from '../completions/text-processing' import { getConfiguration } from '../configuration' @@ -10,7 +17,7 @@ import { getLineLevelDiff } from './diff-utils' import { autoeditsLogger } from './logger' import type { PromptProvider } from './prompt-provider' import type { CodeToReplaceData } from './prompt-utils' -import { DeepSeekPromptProvider } from './providers/deepseek' +import { CodyGatewayPromptProvider } from './providers/cody-gateway' import { FireworksPromptProvider } from './providers/fireworks' import { OpenAIPromptProvider } from './providers/openai' import { AutoEditsRendererManager } from './renderer' @@ -33,6 +40,15 @@ export interface AutoeditsPrediction { prediction: string } +interface ProviderConfig { + experimentalAutoeditsConfigOverride: AutoEditsModelConfig | undefined + providerName: AutoEditsModelConfig['provider'] + provider: PromptProvider + model: string + url: string + tokenLimit: AutoEditsTokenLimit +} + /** * Provides inline completions and auto-edits functionality. */ @@ -41,11 +57,7 @@ export class AutoeditsProvider implements vscode.InlineCompletionItemProvider, v private readonly contextMixer: ContextMixer private readonly rendererManager: AutoEditsRendererManager private readonly debounceIntervalMs: number - - private autoEditsTokenLimit?: AutoEditsTokenLimit - private provider?: PromptProvider - private model?: string - private apiKey?: string + private readonly config: ProviderConfig constructor() { this.contextMixer = new ContextMixer({ @@ -56,21 +68,36 @@ export class AutoeditsProvider implements vscode.InlineCompletionItemProvider, v }) this.rendererManager = new AutoEditsRendererManager() this.debounceIntervalMs = DEFAULT_DEBOUNCE_INTERVAL_MS - - this.initializeFromConfig() + this.config = this.initializeConfig() this.registerCommands() } - private initializeFromConfig(): void { - const config = getConfiguration().experimentalAutoedits - if (!config) { - autoeditsLogger.logDebug('Config', 'No Configuration found in the settings') - return + private initializeConfig(): ProviderConfig { + const userConfig = getConfiguration().experimentalAutoeditsConfigOverride + const baseConfig = userConfig ?? this.getDefaultConfig() + + return { + experimentalAutoeditsConfigOverride: userConfig, + providerName: baseConfig.provider, + provider: this.createPromptProvider(baseConfig.provider), + model: baseConfig.model, + url: baseConfig.url, + tokenLimit: baseConfig.tokenLimit, + } + } + + private createPromptProvider(providerName: AutoEditsModelConfig['provider']): PromptProvider { + switch (providerName) { + case 'openai': + return new OpenAIPromptProvider() + case 'fireworks': + return new FireworksPromptProvider() + case 'cody-gateway-fastpath-chat': + return new CodyGatewayPromptProvider() + default: + autoeditsLogger.logDebug('Config', `Provider ${providerName} not supported`) + throw new Error(`Provider ${providerName} not supported`) } - this.initializePromptProvider(config.provider) - this.autoEditsTokenLimit = config.tokenLimit as AutoEditsTokenLimit - this.model = config.model - this.apiKey = config.apiKey } private registerCommands(): void { @@ -201,11 +228,8 @@ export class AutoeditsProvider implements vscode.InlineCompletionItemProvider, v ): boolean { const currentFileLines = lines(currentFileText) const predictedFileLines = lines(predictedFileText) - const { modifiedLines, removedLines, addedLines } = getLineLevelDiff( - currentFileLines, - predictedFileLines - ) - if (modifiedLines.length > 0 || removedLines.length > 0 || addedLines.length === 0) { + const { addedLines } = getLineLevelDiff(currentFileLines, predictedFileLines) + if (addedLines.length === 0) { return false } addedLines.sort() @@ -221,51 +245,9 @@ export class AutoeditsProvider implements vscode.InlineCompletionItemProvider, v return false } - private logDebugData( - isPrefixMatch: boolean, - isSuffixMatch: boolean, - prediction: string, - prefix: string, - suffix: string - ): void { - const debugData = { - isPrefixMatch, - isSuffixMatch, - prediction, - prefix, - suffix, - } - autoeditsLogger.logDebug( - 'InlineCompletions', - 'Data Debug:\n', - JSON.stringify(debugData, null, 2) - ) - } - - private initializePromptProvider(provider: string): void { - switch (provider) { - case 'openai': - this.provider = new OpenAIPromptProvider() - break - case 'deepseek': - this.provider = new DeepSeekPromptProvider() - break - case 'fireworks': - this.provider = new FireworksPromptProvider() - break - default: - autoeditsLogger.logDebug('Config', `Provider ${provider} not supported`) - this.provider = undefined - } - } - public async predictAutoeditAtDocAndPosition( options: AutoEditsProviderOptions ): Promise { - if (!this.isConfigValid()) { - return null - } - const start = Date.now() const prediction = await this.generatePrediction(options) @@ -286,35 +268,37 @@ export class AutoeditsProvider implements vscode.InlineCompletionItemProvider, v return prediction } - private isConfigValid(): boolean { - if (!this.provider || !this.autoEditsTokenLimit || !this.model || !this.apiKey) { - autoeditsLogger.logDebug('Config', 'No Provider or Token Limit found in the settings') - return false - } - return true - } - private async generatePrediction( options: AutoEditsProviderOptions ): Promise { - const docContext = this.getDocContext(options.document, options.position) + const docContext = getCurrentDocContext({ + document: options.document, + position: options.position, + maxPrefixLength: tokensToChars(this.config.tokenLimit.prefixTokens), + maxSuffixLength: tokensToChars(this.config.tokenLimit.suffixTokens), + }) const { context } = await this.contextMixer.getContext({ document: options.document, position: options.position, docContext, - maxChars: 100000, + maxChars: 32_000, }) - const { codeToReplace, promptResponse: prompt } = this.provider!.getPrompt( + const { codeToReplace, promptResponse: prompt } = this.config.provider.getPrompt( docContext, options.document, options.position, context, - this.autoEditsTokenLimit! + this.config.tokenLimit ) - - const response = await this.provider!.getModelResponse(this.model!, this.apiKey!, prompt) - const postProcessedResponse = this.provider!.postProcessResponse(codeToReplace, response) + const apiKey = await this.getApiKey() + const response = await this.config.provider.getModelResponse( + this.config.url, + this.config.model, + apiKey, + prompt + ) + const postProcessedResponse = this.config.provider.postProcessResponse(codeToReplace, response) return { codeToReplaceData: codeToReplace, @@ -322,13 +306,66 @@ export class AutoeditsProvider implements vscode.InlineCompletionItemProvider, v } } - private getDocContext(document: vscode.TextDocument, position: vscode.Position): DocumentContext { - return getCurrentDocContext({ - document, - position, - maxPrefixLength: tokensToChars(this.autoEditsTokenLimit?.prefixTokens ?? 0), - maxSuffixLength: tokensToChars(this.autoEditsTokenLimit?.suffixTokens ?? 0), - }) + private logDebugData( + isPrefixMatch: boolean, + isSuffixMatch: boolean, + prediction: string, + prefix: string, + suffix: string + ): void { + const debugData = { + isPrefixMatch, + isSuffixMatch, + prediction, + prefix, + suffix, + } + autoeditsLogger.logDebug( + 'InlineCompletions', + 'Data Debug:\n', + JSON.stringify(debugData, null, 2) + ) + } + + private getDefaultConfig(): Omit { + const defaultTokenLimit: AutoEditsTokenLimit = { + prefixTokens: 2500, + suffixTokens: 2500, + maxPrefixLinesInArea: 11, + maxSuffixLinesInArea: 4, + codeToRewritePrefixLines: 1, + codeToRewriteSuffixLines: 2, + contextSpecificTokenLimit: { + [RetrieverIdentifier.RecentEditsRetriever]: 1500, + [RetrieverIdentifier.JaccardSimilarityRetriever]: 0, + [RetrieverIdentifier.RecentCopyRetriever]: 500, + [RetrieverIdentifier.DiagnosticsRetriever]: 500, + [RetrieverIdentifier.RecentViewPortRetriever]: 2500, + }, + } + return { + provider: 'cody-gateway-fastpath-chat', + model: 'cody-model-auto-edits-fireworks-default', + url: 'https://cody-gateway.sourcegraph.com//v1/completions/fireworks', + tokenLimit: defaultTokenLimit, + } + } + + private async getApiKey(): Promise { + if (this.config.providerName === 'cody-gateway-fastpath-chat') { + const config = await currentResolvedConfig() + const fastPathAccessToken = dotcomTokenToGatewayToken(config.auth.accessToken) + if (!fastPathAccessToken) { + autoeditsLogger.logError('Autoedits', 'FastPath access token is not available') + throw new Error('FastPath access token is not available') + } + return fastPathAccessToken + } + if (this.config.experimentalAutoeditsConfigOverride?.apiKey) { + return this.config.experimentalAutoeditsConfigOverride.apiKey + } + autoeditsLogger.logError('Autoedits', 'No api key provided in the config override') + throw new Error('No api key provided in the config override') } public dispose(): void { diff --git a/vscode/src/autoedits/prompt-provider.ts b/vscode/src/autoedits/prompt-provider.ts index fd9d2988e880..487bd40aad92 100644 --- a/vscode/src/autoedits/prompt-provider.ts +++ b/vscode/src/autoedits/prompt-provider.ts @@ -5,16 +5,15 @@ import type { DocumentContext, } from '../../../lib/shared/src/completions/types' import type * as utils from './prompt-utils' -export type CompletionsPrompt = PromptString + export type ChatPrompt = { role: 'system' | 'user' | 'assistant' content: PromptString }[] -export type PromptProviderResponse = CompletionsPrompt | ChatPrompt export interface PromptResponseData { codeToReplace: utils.CodeToReplaceData - promptResponse: PromptProviderResponse + promptResponse: ChatPrompt } export interface PromptProvider { @@ -28,15 +27,21 @@ export interface PromptProvider { postProcessResponse(codeToReplace: utils.CodeToReplaceData, completion: string | null): string - getModelResponse(model: string, apiKey: string, prompt: PromptProviderResponse): Promise + getModelResponse(url: string, model: string, apiKey: string, prompt: ChatPrompt): Promise } -export async function getModelResponse(url: string, body: string, apiKey: string): Promise { +export async function getModelResponse( + url: string, + body: string, + apiKey: string, + customHeaders: Record = {} +): Promise { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}`, + ...customHeaders, }, body: body, }) diff --git a/vscode/src/autoedits/providers/cody-gateway.ts b/vscode/src/autoedits/providers/cody-gateway.ts new file mode 100644 index 000000000000..8dcd5943b4ce --- /dev/null +++ b/vscode/src/autoedits/providers/cody-gateway.ts @@ -0,0 +1,81 @@ +import type { AutoEditsTokenLimit } from '@sourcegraph/cody-shared' +import type * as vscode from 'vscode' +import type { + AutocompleteContextSnippet, + DocumentContext, +} from '../../../../lib/shared/src/completions/types' +import { autoeditsLogger } from '../logger' +import type { ChatPrompt, PromptProvider, PromptResponseData } from '../prompt-provider' +import { getModelResponse } from '../prompt-provider' +import { type CodeToReplaceData, SYSTEM_PROMPT, getBaseUserPrompt } from '../prompt-utils' +import * as utils from '../utils' + +export class CodyGatewayPromptProvider implements PromptProvider { + getPrompt( + docContext: DocumentContext, + document: vscode.TextDocument, + position: vscode.Position, + context: AutocompleteContextSnippet[], + tokenBudget: AutoEditsTokenLimit + ): PromptResponseData { + const { codeToReplace, promptResponse: userPrompt } = getBaseUserPrompt( + docContext, + document, + position, + context, + tokenBudget + ) + const prompt: ChatPrompt = [ + { + role: 'system', + content: SYSTEM_PROMPT, + }, + { + role: 'user', + content: userPrompt, + }, + ] + return { + codeToReplace: codeToReplace, + promptResponse: prompt, + } + } + + postProcessResponse(codeToReplace: CodeToReplaceData, response: string): string { + // todo (hitesh): The finetuned model is messing up the identation of the first line. + // todo: correct it manully for now, by checking the first line of the code to rewrite and adding the same indentation to the first line of the completion + const fixedIndentationResponse = utils.fixFirstLineIndentation( + codeToReplace.codeToRewrite, + response + ) + return fixedIndentationResponse + } + + async getModelResponse( + url: string, + model: string, + apiKey: string, + prompt: ChatPrompt + ): Promise { + try { + const headers = { + 'X-Sourcegraph-Feature': 'chat_completions', + } + const body = { + stream: false, + model: model, + messages: prompt, + temperature: 0.2, + max_tokens: 256, + response_format: { + type: 'text', + }, + } + const response = await getModelResponse(url, JSON.stringify(body), apiKey, headers) + return response.choices[0].message.content + } catch (error) { + autoeditsLogger.logDebug('AutoEdits', 'Error calling Cody Gateway:', error) + throw error + } + } +} diff --git a/vscode/src/autoedits/providers/deepseek.ts b/vscode/src/autoedits/providers/deepseek.ts deleted file mode 100644 index 1384e481ff59..000000000000 --- a/vscode/src/autoedits/providers/deepseek.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { type AutoEditsTokenLimit, type PromptString, ps } from '@sourcegraph/cody-shared' -import type * as vscode from 'vscode' -import type { - AutocompleteContextSnippet, - DocumentContext, -} from '../../../../lib/shared/src/completions/types' -import { autoeditsLogger } from '../logger' -import type { PromptProvider, PromptProviderResponse, PromptResponseData } from '../prompt-provider' -import { getModelResponse } from '../prompt-provider' -import { type CodeToReplaceData, SYSTEM_PROMPT, getBaseUserPrompt } from '../prompt-utils' - -export class DeepSeekPromptProvider implements PromptProvider { - private readonly bosToken: PromptString = ps`<|begin▁of▁sentence|>` - private readonly userToken: PromptString = ps`User: ` - private readonly assistantToken: PromptString = ps`Assistant: ` - - getPrompt( - docContext: DocumentContext, - document: vscode.TextDocument, - position: vscode.Position, - context: AutocompleteContextSnippet[], - tokenBudget: AutoEditsTokenLimit - ): PromptResponseData { - const { codeToReplace, promptResponse: userPrompt } = getBaseUserPrompt( - docContext, - document, - position, - context, - tokenBudget - ) - const prompt = ps`${this.bosToken}${SYSTEM_PROMPT} - -${this.userToken}${userPrompt} - -${this.assistantToken}` - - return { - codeToReplace: codeToReplace, - promptResponse: prompt, - } - } - - postProcessResponse(codeToReplace: CodeToReplaceData, response: string): string { - return response - } - - async getModelResponse( - model: string, - apiKey: string, - prompt: PromptProviderResponse - ): Promise { - try { - const response = await getModelResponse( - 'https://api.fireworks.ai/inference/v1/completions', - JSON.stringify({ - model: model, - prompt: prompt.toString(), - temperature: 0.5, - max_tokens: 256, - }), - apiKey - ) - return response.choices[0].text - } catch (error) { - autoeditsLogger.logDebug('AutoEdits', 'Error calling Fireworks API:', error) - throw error - } - } -} diff --git a/vscode/src/autoedits/providers/fireworks.ts b/vscode/src/autoedits/providers/fireworks.ts index 795a1a57a982..b9422eadfc6f 100644 --- a/vscode/src/autoedits/providers/fireworks.ts +++ b/vscode/src/autoedits/providers/fireworks.ts @@ -5,12 +5,7 @@ import type { DocumentContext, } from '../../../../lib/shared/src/completions/types' import { autoeditsLogger } from '../logger' -import type { - ChatPrompt, - PromptProvider, - PromptProviderResponse, - PromptResponseData, -} from '../prompt-provider' +import type { ChatPrompt, PromptProvider, PromptResponseData } from '../prompt-provider' import { getModelResponse } from '../prompt-provider' import { type CodeToReplaceData, SYSTEM_PROMPT, getBaseUserPrompt } from '../prompt-utils' import * as utils from '../utils' @@ -57,14 +52,14 @@ export class FireworksPromptProvider implements PromptProvider { } async getModelResponse( + url: string, model: string, apiKey: string, - prompt: PromptProviderResponse + prompt: ChatPrompt ): Promise { try { const response = await getModelResponse( - // 'https://sourcegraph-905e5804.direct.fireworks.ai/v1/chat/completions', - 'https://api.fireworks.ai/inference/v1/chat/completions', + url, JSON.stringify({ model: model, messages: prompt, @@ -78,7 +73,7 @@ export class FireworksPromptProvider implements PromptProvider { ) return response.choices[0].message.content } catch (error) { - autoeditsLogger.logDebug('AutoEdits', 'Error calling OpenAI API:', error) + autoeditsLogger.logDebug('AutoEdits', 'Error calling Fireworks API:', error) throw error } } diff --git a/vscode/src/autoedits/providers/openai.ts b/vscode/src/autoedits/providers/openai.ts index b66e119ed3a7..769ef5b56d1e 100644 --- a/vscode/src/autoedits/providers/openai.ts +++ b/vscode/src/autoedits/providers/openai.ts @@ -5,12 +5,7 @@ import type { DocumentContext, } from '../../../../lib/shared/src/completions/types' import { autoeditsLogger } from '../logger' -import type { - ChatPrompt, - PromptProvider, - PromptProviderResponse, - PromptResponseData, -} from '../prompt-provider' +import type { ChatPrompt, PromptProvider, PromptResponseData } from '../prompt-provider' import { getModelResponse } from '../prompt-provider' import { type CodeToReplaceData, SYSTEM_PROMPT, getBaseUserPrompt } from '../prompt-utils' @@ -50,13 +45,14 @@ export class OpenAIPromptProvider implements PromptProvider { } async getModelResponse( + url: string, model: string, apiKey: string, - prompt: PromptProviderResponse + prompt: ChatPrompt ): Promise { try { const response = await getModelResponse( - 'https://api.openai.com/v1/chat/completions', + url, JSON.stringify({ model: model, messages: prompt, diff --git a/vscode/src/configuration.test.ts b/vscode/src/configuration.test.ts index 2707f5d0f22d..b7e40e19d927 100644 --- a/vscode/src/configuration.test.ts +++ b/vscode/src/configuration.test.ts @@ -95,10 +95,12 @@ describe('getConfiguration', () => { return false case 'cody.experimental.supercompletions': return false - case 'cody.experimental.autoedit': - return undefined + case 'cody.experimental.autoedits.enabled': + return false case 'cody.experimental.autoedits-renderer-testing': return false + case 'cody.experimental.autoedits.config.override': + return undefined case 'cody.experimental.noodle': return false case 'cody.experimental.minion.anthropicKey': @@ -158,8 +160,9 @@ describe('getConfiguration', () => { }, commandCodeLenses: true, experimentalSupercompletions: false, + experimentalAutoeditsEnabled: false, + experimentalAutoeditsConfigOverride: undefined, experimentalAutoeditsRendererTesting: false, - experimentalAutoedits: undefined, experimentalMinionAnthropicKey: undefined, experimentalTracing: true, experimentalCommitMessage: true, diff --git a/vscode/src/configuration.ts b/vscode/src/configuration.ts index fe07c566e95c..8a45ed9050f3 100644 --- a/vscode/src/configuration.ts +++ b/vscode/src/configuration.ts @@ -112,7 +112,11 @@ export function getConfiguration( experimentalTracing: getHiddenSetting('experimental.tracing', false), experimentalSupercompletions: getHiddenSetting('experimental.supercompletions', false), - experimentalAutoedits: getHiddenSetting('experimental.autoedit', undefined), + experimentalAutoeditsEnabled: getHiddenSetting('experimental.autoedits.enabled', false), + experimentalAutoeditsConfigOverride: getHiddenSetting( + 'experimental.autoedits.config.override', + undefined + ), experimentalAutoeditsRendererTesting: getHiddenSetting( 'experimental.autoedits-renderer-testing', false diff --git a/vscode/src/main.ts b/vscode/src/main.ts index 48dd49bcbfd4..4679c4169220 100644 --- a/vscode/src/main.ts +++ b/vscode/src/main.ts @@ -719,7 +719,12 @@ async function tryRegisterTutorial( function registerAutoEdits(disposables: vscode.Disposable[]): void { disposables.push( enableFeature( - ({ configuration }) => configuration.experimentalAutoedits !== undefined, + ({ configuration }) => { + return ( + configuration.experimentalAutoeditsEnabled !== undefined && + configuration.autocomplete === false + ) + }, () => { const provider = new AutoeditsProvider() vscode.languages.registerInlineCompletionItemProvider( diff --git a/vscode/src/testutils/mocks.ts b/vscode/src/testutils/mocks.ts index 42b2ab05779a..fd80fa3115ba 100644 --- a/vscode/src/testutils/mocks.ts +++ b/vscode/src/testutils/mocks.ts @@ -887,8 +887,9 @@ export const DEFAULT_VSCODE_SETTINGS = { }, commandCodeLenses: false, experimentalSupercompletions: false, - experimentalAutoedits: undefined, + experimentalAutoeditsEnabled: false, experimentalAutoeditsRendererTesting: false, + experimentalAutoeditsConfigOverride: undefined, experimentalMinionAnthropicKey: undefined, experimentalTracing: false, experimentalCommitMessage: true,