diff --git a/examples/runLLMCustomTaskWithStepEvolve/createFixesForSolution.ts b/examples/runLLMCustomTaskWithStepEvolve/createFixesForSolution.ts index 186e7ed..bae5de4 100644 --- a/examples/runLLMCustomTaskWithStepEvolve/createFixesForSolution.ts +++ b/examples/runLLMCustomTaskWithStepEvolve/createFixesForSolution.ts @@ -1,8 +1,8 @@ import { z } from 'zod'; -import { createFullPromptFromSections } from '../../src/gpt/createFullPromptFromSections'; import { gptExecute } from '../../src/gpt/gptExecute'; import { GPTMode } from '../../src/gpt/types'; +import { createFullPromptFromSections } from '../../src/gpt/utils/createFullPromptFromSections'; import { Fix, SolutionWithMeta } from '../../src/stepEvolve/FitnessFunction'; import { getRandomElement } from '../../src/utils/random/getRandomElement'; import { shuffleArray } from '../../src/utils/random/shuffleArray'; diff --git a/examples/runLLMCustomTaskWithStepEvolve/fixes/fixImproveSolution.ts b/examples/runLLMCustomTaskWithStepEvolve/fixes/fixImproveSolution.ts index 35da99b..c4c00a4 100644 --- a/examples/runLLMCustomTaskWithStepEvolve/fixes/fixImproveSolution.ts +++ b/examples/runLLMCustomTaskWithStepEvolve/fixes/fixImproveSolution.ts @@ -1,8 +1,8 @@ import { z } from 'zod'; -import { createFullPromptFromSections } from '../../../src/gpt/createFullPromptFromSections'; import { gptExecute } from '../../../src/gpt/gptExecute'; import { GPTMode } from '../../../src/gpt/types'; +import { createFullPromptFromSections } from '../../../src/gpt/utils/createFullPromptFromSections'; import { SolutionWithMeta } from '../../../src/stepEvolve/FitnessFunction'; import { TaskDefinition } from '../TaskDefinition'; diff --git a/examples/runLLMCustomTaskWithStepEvolve/rateSolution.ts b/examples/runLLMCustomTaskWithStepEvolve/rateSolution.ts index f6999ce..93df54c 100644 --- a/examples/runLLMCustomTaskWithStepEvolve/rateSolution.ts +++ b/examples/runLLMCustomTaskWithStepEvolve/rateSolution.ts @@ -1,8 +1,8 @@ import { z } from 'zod'; -import { createFullPromptFromSections } from '../../src/gpt/createFullPromptFromSections'; import { gptExecute } from '../../src/gpt/gptExecute'; import { GPTMode } from '../../src/gpt/types'; +import { createFullPromptFromSections } from '../../src/gpt/utils/createFullPromptFromSections'; import { SolutionWithMeta } from '../../src/stepEvolve/FitnessFunction'; import { shuffleArray } from '../../src/utils/random/shuffleArray'; import { sum } from '../../src/utils/utils'; diff --git a/examples/runMinionTask/runMinionTask.ts b/examples/runMinionTask/runMinionTask.ts index 44f2d85..7091186 100644 --- a/examples/runMinionTask/runMinionTask.ts +++ b/examples/runMinionTask/runMinionTask.ts @@ -1,139 +1,3 @@ -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - -/* -Task: Add comments to this code -*/ - import path from 'path'; import { initCLISystems } from '../../src/CLI/setupCLISystems'; @@ -177,6 +41,6 @@ This example creates a minion task and runs it. console.log('Applying task ...'); await mutatorApplyMinionTask(task); - + getEditorManager().closeTextDocument?.(task.documentURI); console.log('Done'); })(); diff --git a/score/generateScoreTests.ts b/score/generateScoreTests.ts index 99b1f10..2d82af0 100644 --- a/score/generateScoreTests.ts +++ b/score/generateScoreTests.ts @@ -1,9 +1,9 @@ import { z } from 'zod'; -import { countTokens } from '../src/gpt/countTokens'; -import { ensureIRunThisInRange } from '../src/gpt/ensureIRunThisInRange'; import { gptExecute } from '../src/gpt/gptExecute'; import { GPTMode } from '../src/gpt/types'; +import { countTokens } from '../src/gpt/utils/countTokens'; +import { ensureIRunThisInRange } from '../src/gpt/utils/ensureIRunThisInRange'; import { MinionTask } from '../src/minionTasks/MinionTask'; import { extractExtensionNameFromPath } from '../src/utils/extractFileNameFromPath'; import { listOfTypes, ScoreTest, TestSchemas } from './types'; @@ -81,7 +81,7 @@ const prompt = async (minionTask: MinionTask, test?: boolean) => { Original code contains user's original file content ===== ORIGINAL CODE ===== - ${minionTask.originalContent} + ${minionTask.getOriginalContent} User query is a prompt for GPT and defines what should happend to code. ===== USER QUERY ===== @@ -118,7 +118,7 @@ export const generateScoreTests = async ( const maxTokens = ensureIRunThisInRange({ prompt: fullPrompt, mode, - preferedTokens: fullPromptTokens, + preferredTokens: fullPromptTokens, minTokens: fullPromptTokens, }); diff --git a/score/initTestMinionTask.ts b/score/initTestMinionTask.ts index 468d3a0..11c714b 100644 --- a/score/initTestMinionTask.ts +++ b/score/initTestMinionTask.ts @@ -3,12 +3,15 @@ import { readFile } from 'node:fs/promises'; import { existsSync } from 'fs'; import path from 'path'; -import { getEditorManager } from '../src/managers/EditorManager'; +import { + EditorPosition, + getEditorManager, +} from '../src/managers/EditorManager'; import { MinionTask } from '../src/minionTasks/MinionTask'; export interface Selection { - start: { line: number; character: number }; - end: { line: number; character: number }; + start: EditorPosition; + end: EditorPosition; selectedText: string; } diff --git a/score/prepareScoreTest.ts b/score/prepareScoreTest.ts index f4ec96e..cc21134 100644 --- a/score/prepareScoreTest.ts +++ b/score/prepareScoreTest.ts @@ -6,9 +6,9 @@ import { initCLISystems, setupCLISystemsForTest, } from '../src/CLI/setupCLISystems'; -import { countTokens } from '../src/gpt/countTokens'; import { gptExecute } from '../src/gpt/gptExecute'; import { GPTMode } from '../src/gpt/types'; +import { countTokens } from '../src/gpt/utils/countTokens'; import { generateScoreTests, GenerateScoreTestsResult, diff --git a/score/rateMinionTask.ts b/score/rateMinionTask.ts index 8ae680f..7f6b37a 100644 --- a/score/rateMinionTask.ts +++ b/score/rateMinionTask.ts @@ -1,9 +1,9 @@ import { z } from 'zod'; -import { countTokens } from '../src/gpt/countTokens'; -import { createFullPromptFromSections } from '../src/gpt/createFullPromptFromSections'; import { gptExecute } from '../src/gpt/gptExecute'; import { GPTMode } from '../src/gpt/types'; +import { countTokens } from '../src/gpt/utils/countTokens'; +import { createFullPromptFromSections } from '../src/gpt/utils/createFullPromptFromSections'; import { MinionTask } from '../src/minionTasks/MinionTask'; import { shuffleArray } from '../src/utils/random/shuffleArray'; import { sum } from '../src/utils/utils'; diff --git a/score/utils/gptAssert.ts b/score/utils/gptAssert.ts index a6d5e55..fa546c5 100644 --- a/score/utils/gptAssert.ts +++ b/score/utils/gptAssert.ts @@ -1,8 +1,8 @@ import { z } from 'zod'; -import { countTokens } from '../../src/gpt/countTokens'; import { gptExecute } from '../../src/gpt/gptExecute'; import { GPTMode } from '../../src/gpt/types'; +import { countTokens } from '../../src/gpt/utils/countTokens'; // TODO: left as reference for now, but should be removed propably export async function gptAssert({ diff --git a/src/CLI/CLIEditEntry.ts b/src/CLI/CLIEditEntry.ts index 4c7e922..69d2f75 100644 --- a/src/CLI/CLIEditEntry.ts +++ b/src/CLI/CLIEditEntry.ts @@ -1,5 +1,4 @@ import { EditorTextEdit } from '../managers/EditorManager'; - export class CLIEditEntry implements EditorTextEdit { constructor( public action: string, diff --git a/src/CLI/CLIEditorDocument.ts b/src/CLI/CLIEditorDocument.ts index 2aa3f99..9c40c16 100644 --- a/src/CLI/CLIEditorDocument.ts +++ b/src/CLI/CLIEditorDocument.ts @@ -3,6 +3,7 @@ import path from 'path'; import { EditorDocument, + EditorPosition, EditorRange, EditorUri, } from '../managers/EditorManager'; @@ -11,8 +12,7 @@ export class CLIEditorDocument implements EditorDocument { readonly languageId: string; readonly lineCount: number; readonly uri: EditorUri; - private _textLines: string[] = []; // This will store our text lines. - private _numberLines: number[] = []; // This will store our line numbers. + private textLines: string[] = []; // This will store our text lines. constructor(uri: EditorUri) { this.uri = uri; @@ -21,27 +21,21 @@ export class CLIEditorDocument implements EditorDocument { // Reading file contents synchronously for simplicity. Consider using async I/O in production code const fileContent = fs.readFileSync(fileName, 'utf8'); - this._textLines = fileContent.split('\n'); - this.lineCount = this._textLines.length; + this.textLines = fileContent.split('\n'); + this.lineCount = this.textLines.length; // Derive languageId from file extension. This is simplistic and might not always be correct. this.languageId = path.extname(fileName).slice(1); - - // Populate _numberLines assuming 1-based line numbers - this._numberLines = Array(this.lineCount) - .fill(0) - .map((_, i) => i + 1); } getText(range?: EditorRange): string { - // Return joined text from _textLines array within given range or whole text if range is not provided + // Return joined text from textLines array within given range or whole text if range is not provided return ( range - ? this._textLines.slice(range.start.line, range.end.line) - : this._textLines + ? this.textLines.slice(range.start.line, range.end.line) + : this.textLines ).join('\n'); } - lineAt(line: number): { readonly text: string; readonly lineNumber: number; @@ -52,12 +46,12 @@ export class CLIEditorDocument implements EditorDocument { // Return object with text and line number from the given line return { - text: this._textLines[line], - lineNumber: this._numberLines[line], + text: this.textLines[line], + lineNumber: line + 1, }; } - insert(start: { line: number; character: number }, text: string) { + insert(start: EditorPosition, text: string) { // First, get the existing text for the line const existingText = this.lineAt(start.line).text; @@ -68,16 +62,10 @@ export class CLIEditorDocument implements EditorDocument { existingText.slice(start.character); // Update the line with the new text - this._textLines[start.line] = newText; + this.textLines[start.line] = newText; } - replace( - range: { - start: { line: number; character: number }; - end: { line: number; character: number }; - }, - text: string, - ) { + replace(range: EditorRange, text: string) { // Here we need to replace the text from start to end within the range const startLineNumber = range.start.line; const endLineNumber = range.end.line; @@ -92,12 +80,12 @@ export class CLIEditorDocument implements EditorDocument { // If start and end line numbers are same, then it's a replacement within same line if (startLineNumber === endLineNumber) { - this._textLines[startLineNumber] = newTextStart + newTextEnd; + this.textLines[startLineNumber] = newTextStart + newTextEnd; } else { // Update start, end lines and remove lines between them - this._textLines[startLineNumber] = newTextStart; - this._textLines[endLineNumber] = newTextEnd; - this._textLines.splice( + this.textLines[startLineNumber] = newTextStart; + this.textLines[endLineNumber] = newTextEnd; + this.textLines.splice( startLineNumber + 1, endLineNumber - startLineNumber, ); @@ -106,6 +94,6 @@ export class CLIEditorDocument implements EditorDocument { save() { // Write the text back to the file synchronously for simplicity. Consider using async I/O in production code - fs.writeFileSync(this.uri.fsPath, this._textLines.join('\n'), 'utf8'); + fs.writeFileSync(this.uri.fsPath, this.textLines.join('\n'), 'utf8'); } } diff --git a/src/CLI/CLIEditorManager.ts b/src/CLI/CLIEditorManager.ts index 5cb8898..ac0ef39 100644 --- a/src/CLI/CLIEditorManager.ts +++ b/src/CLI/CLIEditorManager.ts @@ -6,13 +6,14 @@ import { } from '../managers/EditorManager'; import { CLIEditorDocument } from './CLIEditorDocument'; import { CLIWorkspaceEdit } from './CLIWorkspaceEdit'; - export class CLIEditorManager implements EditorManager { - openDocuments: EditorDocument[] = []; + private openDocuments: EditorDocument[] = []; constructor(public dryRun = false) {} - applyWorkspaceEdit(fillEdit: (edit: WorkspaceEdit) => Promise) { + applyWorkspaceEdit( + fillEdit: (edit: T) => Promise, + ) { const edit = new CLIWorkspaceEdit(); fillEdit(edit); this.applyEdit(edit); @@ -55,8 +56,6 @@ export class CLIEditorManager implements EditorManager { console.error(message); } - showInformationMessage(message: string): void {} - async openTextDocument(uri: EditorUri) { const existingDocument = this.openDocuments.find( (doc) => doc.uri.toString() === uri.toString(), @@ -71,10 +70,30 @@ export class CLIEditorManager implements EditorManager { return document; } + closeTextDocument(uri: EditorUri) { + const index = this.openDocuments.findIndex( + (doc) => doc.uri.toString() === uri.toString(), + ); + if (index !== -1) { + this.openDocuments.splice(index, 1); + } + } + + showInformationMessage(message: string): void { + console.log(message); + } + createUri(uri: string): EditorUri { return { fsPath: uri, toString: () => uri, + // scheme: 'file', + // authority: '', + // path: uri, + // query: '', + // fragment: '', + // with: () => uri as unknown as EditorUri, + // toJSON: () => uri, }; } } diff --git a/src/CLI/setupCLISystems.ts b/src/CLI/setupCLISystems.ts index cf72c57..d93c5f4 100644 --- a/src/CLI/setupCLISystems.ts +++ b/src/CLI/setupCLISystems.ts @@ -4,7 +4,7 @@ import { ServiceAccount } from 'firebase-admin'; import { readFileSync } from 'fs'; import path from 'path'; -import { setOpenAIApiKey } from '../gpt/gptExecute'; +import { setOpenAIApiKey } from '../gpt/utils/setOpenAiKey'; import { AnalyticsManager, setAnalyticsManager, @@ -26,8 +26,6 @@ export function initCLISystems() { setOpenAIApiKey(process.env.OPENAI_API_KEY); - setOpenAICacheManager(undefined); - if (process.env.NO_OPENAI_CACHE === 'true') { setOpenAICacheManager(new NoCacheOpenAICacheManager()); } else { @@ -68,6 +66,5 @@ export function initCLISystems() { } export function setupCLISystemsForTest() { - setEditorManager(undefined); setEditorManager(new CLIEditorManager()); } diff --git a/src/gpt/TokenError.ts b/src/gpt/TokenError.ts index 5df2623..e637054 100644 --- a/src/gpt/TokenError.ts +++ b/src/gpt/TokenError.ts @@ -5,11 +5,6 @@ export class TokenError extends Error { constructor(message?: string) { super(message); - // Ensuring Error is properly extended - Object.setPrototypeOf(this, TokenError.prototype); this.name = 'TokenError'; - - // Capturing stack trace, excluding constructor call from it - Error.captureStackTrace(this, this.constructor); } } diff --git a/src/gpt/gptExecute.ts b/src/gpt/gptExecute.ts index 1423dfc..263583e 100644 --- a/src/gpt/gptExecute.ts +++ b/src/gpt/gptExecute.ts @@ -1,45 +1,43 @@ -import fetch from 'node-fetch'; import { z } from 'zod'; import zodToJsonSchema from 'zod-to-json-schema'; import { getAnalyticsManager } from '../managers/AnalyticsManager'; -import { getOpenAICacheManager } from '../managers/OpenAICacheManager'; import { isZodString } from '../utils/isZodString'; -import { calculateCosts } from './calculateCosts'; -import { countTokens } from './countTokens'; -import { ensureICanRunThis } from './ensureIcanRunThis'; -import { processOpenAIResponseStream } from './processOpenAIResponseStream'; +import { getCompletions } from './openAiRequests'; import { - FAST_MODE_TOKENS, GPTExecuteRequestData, GPTExecuteRequestMessage, GPTExecuteRequestPrompt, GPTMode, GPTModel, } from './types'; +import { calculateCosts } from './utils/calculateCosts'; +import { convertResult } from './utils/convertResult'; +import { ensureICanRunThis } from './utils/ensureIcanRunThis'; +import { getCachedResults } from './utils/getCachedResults'; +import { getModelForMessages } from './utils/getModelForMessages'; +import { processOpenAIResponseStream } from './utils/processOpenAIResponseStream'; +import { getOpenAIApiKey } from './utils/setOpenAiKey'; + +interface GptExecuteParams { + fullPrompt: GPTExecuteRequestPrompt; + onChunk?: (chunk: string) => Promise; + isCancelled?: () => boolean; + maxTokens?: number; + mode: GPTMode; + temperature?: number; + controller?: AbortController; + outputSchema: OutputTypeSchema; + outputName?: string; +} -let openAIApiKey: string | undefined; -const MAX_REQUEST_ATTEMPTS = 3; +type GptExecuteResult = Promise<{ + result: z.infer; + cost: number; +}>; -export function setOpenAIApiKey(apiKey: string) { - openAIApiKey = apiKey; -} +const MAX_REQUEST_ATTEMPTS = 3; -function convertResult( - result: string, - outputSchema: OutputTypeSchema, -): z.infer { - if (isZodString(outputSchema)) { - return result; - } - const parseResult = outputSchema.safeParse(JSON.parse(result)); - if (parseResult.success) { - return parseResult.data as OutputTypeSchema; - } - console.log('RESULT', result); - console.log('SCHEMA', outputSchema); - throw new Error(`Could not parse result: ${result}`); -} export async function gptExecute({ fullPrompt, onChunk = async () => {}, @@ -50,45 +48,17 @@ export async function gptExecute({ controller = new AbortController(), outputSchema, outputName = 'output', -}: { - fullPrompt: GPTExecuteRequestPrompt; - onChunk?: (chunk: string) => Promise; - isCancelled?: () => boolean; - maxTokens?: number; - mode: GPTMode; - temperature?: number; - controller?: AbortController; - outputSchema: OutputTypeSchema; - outputName?: string; -}): Promise<{ - result: z.infer; - cost: number; -}> { - let model: GPTModel = 'gpt-4-0613'; - +}: GptExecuteParams): GptExecuteResult { + const openAIApiKey = getOpenAIApiKey(); const messages: GPTExecuteRequestMessage[] = Array.isArray(fullPrompt) ? fullPrompt : [{ role: 'user', content: fullPrompt }]; - const messagesAsString = JSON.stringify(messages); - - if (mode === GPTMode.FAST) { - model = 'gpt-3.5-turbo-16k-0613'; - - const usedTokens = countTokens(messagesAsString, mode) + maxTokens; - - if (usedTokens < FAST_MODE_TOKENS) { - model = 'gpt-3.5-turbo-0613'; - } - } + const model: GPTModel = getModelForMessages(messages, mode, maxTokens); ensureICanRunThis({ prompt: fullPrompt, mode, maxTokens }); const signal = controller.signal; - if (!openAIApiKey) { - throw new Error('OpenAI API key not found. Please set it in the settings.'); - } - const requestData: GPTExecuteRequestData = { model, messages, @@ -110,33 +80,19 @@ export async function gptExecute({ }), }; - const cachedResult = - await getOpenAICacheManager().getCachedResult(requestData); - - if (cachedResult && typeof cachedResult === 'string') { - await onChunk(cachedResult); - - return { - result: convertResult(cachedResult, outputSchema), - cost: 0, - }; - } - for (let attempt = 1; attempt <= MAX_REQUEST_ATTEMPTS; attempt++) { try { - const response = await fetch( - 'https://api.openai.com/v1/chat/completions', - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${openAIApiKey}`, - }, - body: JSON.stringify(requestData), - signal, - }, + const response = await getCompletions(openAIApiKey, requestData, signal); + const cachedResult = await getCachedResults( + requestData, + outputSchema, + onChunk, ); + if (cachedResult) { + return cachedResult; + } + const result = await processOpenAIResponseStream({ response, onChunk, diff --git a/src/gpt/openAiRequests.ts b/src/gpt/openAiRequests.ts new file mode 100644 index 0000000..aee9c71 --- /dev/null +++ b/src/gpt/openAiRequests.ts @@ -0,0 +1,27 @@ +import fetch from 'node-fetch'; + +import { GPTExecuteRequestData } from './types'; + +export const getOpenAiModels = async (openAIApiKey: string) => + await fetch('https://api.openai.com/v1/models', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${openAIApiKey}`, + }, + }); + +export const getCompletions = async ( + openAIApiKey: string, + requestData: GPTExecuteRequestData, + signal: AbortSignal, +) => + await fetch('https://api.openai.com/v1/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${openAIApiKey}`, + }, + body: JSON.stringify(requestData), + signal, + }); diff --git a/src/gpt/types.ts b/src/gpt/types.ts index 4a598f0..ed9ce06 100644 --- a/src/gpt/types.ts +++ b/src/gpt/types.ts @@ -40,7 +40,7 @@ export type ModelData = { }; export interface ModelsResponseData { - objest: string; + object: string; data: { id: string; object: string; diff --git a/src/gpt/calculateCosts.ts b/src/gpt/utils/calculateCosts.ts similarity index 97% rename from src/gpt/calculateCosts.ts rename to src/gpt/utils/calculateCosts.ts index 436b342..3ff02e9 100644 --- a/src/gpt/calculateCosts.ts +++ b/src/gpt/utils/calculateCosts.ts @@ -1,5 +1,5 @@ +import { GPTExecuteRequestData, GPTMode, GPTModel, MODEL_DATA } from '../types'; import { countTokens } from './countTokens'; -import { GPTExecuteRequestData, GPTMode, GPTModel, MODEL_DATA } from './types'; export const calculateCosts = ( model: GPTModel, diff --git a/src/gpt/utils/convertResult.ts b/src/gpt/utils/convertResult.ts new file mode 100644 index 0000000..3aca67d --- /dev/null +++ b/src/gpt/utils/convertResult.ts @@ -0,0 +1,17 @@ +import { z } from 'zod'; + +import { isZodString } from '../../utils/isZodString'; + +export function convertResult( + result: string, + outputSchema: OutputTypeSchema, +): z.infer { + if (isZodString(outputSchema)) { + return result; + } + const parseResult = outputSchema.safeParse(JSON.parse(result)); + if (parseResult.success) { + return parseResult.data as OutputTypeSchema; + } + throw new Error(`Could not parse result: ${result}`); +} diff --git a/src/gpt/countTokens.ts b/src/gpt/utils/countTokens.ts similarity index 79% rename from src/gpt/countTokens.ts rename to src/gpt/utils/countTokens.ts index fc6eda6..e06eb95 100644 --- a/src/gpt/countTokens.ts +++ b/src/gpt/utils/countTokens.ts @@ -1,5 +1,5 @@ +import { GPTMode, MODEL_DATA } from '../types'; import { getModel } from './getModel'; -import { GPTMode, MODEL_DATA } from './types'; export function countTokens(text: string, mode: GPTMode) { const model = getModel(mode); diff --git a/src/gpt/createFullPromptFromSections.ts b/src/gpt/utils/createFullPromptFromSections.ts similarity index 99% rename from src/gpt/createFullPromptFromSections.ts rename to src/gpt/utils/createFullPromptFromSections.ts index 14d1366..41f9d06 100644 --- a/src/gpt/createFullPromptFromSections.ts +++ b/src/gpt/utils/createFullPromptFromSections.ts @@ -32,7 +32,7 @@ export function createFullPromptFromSections({ return ` ${cleanedIntro} - + ${sectionPrompts} ${cleanedOutro ?? ''}`.trim(); diff --git a/src/gpt/ensureIRunThisInRange.ts b/src/gpt/utils/ensureIRunThisInRange.ts similarity index 86% rename from src/gpt/ensureIRunThisInRange.ts rename to src/gpt/utils/ensureIRunThisInRange.ts index 576b473..625206d 100644 --- a/src/gpt/ensureIRunThisInRange.ts +++ b/src/gpt/utils/ensureIRunThisInRange.ts @@ -1,12 +1,12 @@ +import { TokenError } from '../TokenError'; +import { GPTMode, MODEL_DATA } from '../types'; import { countTokens } from './countTokens'; import { getModel } from './getModel'; -import { TokenError } from './TokenError'; -import { GPTMode, MODEL_DATA } from './types'; interface EnsureICanRunThisInRangeParams { prompt: string; minTokens: number; - preferedTokens: number; + preferredTokens: number; mode: GPTMode; } /** @@ -23,11 +23,11 @@ const EXTRA_BUFFER_FOR_ENCODING_OVERHEAD = 50; export function ensureIRunThisInRange({ prompt, minTokens, - preferedTokens, + preferredTokens, mode, }: EnsureICanRunThisInRangeParams): number { const roundedMinTokens = Math.ceil(minTokens); - const roundedPreferredTokens = Math.ceil(preferedTokens); + const roundedPreferredTokens = Math.ceil(preferredTokens); const model = getModel(mode); const usedTokens = diff --git a/src/gpt/ensureIcanRunThis.ts b/src/gpt/utils/ensureIcanRunThis.ts similarity index 89% rename from src/gpt/ensureIcanRunThis.ts rename to src/gpt/utils/ensureIcanRunThis.ts index 02b2e9e..f0e1274 100644 --- a/src/gpt/ensureIcanRunThis.ts +++ b/src/gpt/utils/ensureIcanRunThis.ts @@ -1,7 +1,7 @@ +import { TokenError } from '../TokenError'; +import { GPTExecuteRequestPrompt, GPTMode, MODEL_DATA } from '../types'; import { countTokens } from './countTokens'; import { getModel } from './getModel'; -import { TokenError } from './TokenError'; -import { GPTExecuteRequestPrompt, GPTMode, MODEL_DATA } from './types'; interface EnsureICanRunThisParams { prompt: GPTExecuteRequestPrompt; diff --git a/src/gpt/extractParsedLines.ts b/src/gpt/utils/extractParsedLines.ts similarity index 98% rename from src/gpt/extractParsedLines.ts rename to src/gpt/utils/extractParsedLines.ts index 84a53b5..8f7b6ba 100644 --- a/src/gpt/extractParsedLines.ts +++ b/src/gpt/utils/extractParsedLines.ts @@ -1,4 +1,4 @@ -import { ParsedLine } from './types'; +import { ParsedLine } from '../types'; /** * This function processes the chunkBuffer string line by line, checking for lines starting with "data: " or JSON error objects. If a line starts with "data: ", it removes the prefix, trims whitespace, attempts JSON parsing, and adds the result to an array. If parsing fails, it logs an error and throws an exception. For lines not starting with "data: ", it assumes they are JSON error objects and handles them accordingly. The function repeats this process until there are no more newline characters in chunkBuffer, returning the array of ParsedLine objects and the modified chunkBuffer. diff --git a/src/gpt/utils/getCachedResults.ts b/src/gpt/utils/getCachedResults.ts new file mode 100644 index 0000000..c8cd896 --- /dev/null +++ b/src/gpt/utils/getCachedResults.ts @@ -0,0 +1,25 @@ +import { z } from 'zod'; + +import { getOpenAICacheManager } from '../../managers/OpenAICacheManager'; +import { GPTExecuteRequestData } from '../types'; +import { convertResult } from './convertResult'; + +export const getCachedResults = async ( + requestData: GPTExecuteRequestData, + outputSchema: OutputTypeSchema, + onChunk: (chunk: string) => Promise, +) => { + const cachedResult = + await getOpenAICacheManager().getCachedResult(requestData); + + if (cachedResult && typeof cachedResult === 'string') { + await onChunk(cachedResult); + + return { + result: convertResult(cachedResult, outputSchema), + cost: 0, + }; + } + + return null; +}; diff --git a/src/gpt/getMissingOpenAIModels.ts b/src/gpt/utils/getMissingOpenAIModels.ts similarity index 69% rename from src/gpt/getMissingOpenAIModels.ts rename to src/gpt/utils/getMissingOpenAIModels.ts index faf48a3..fbb19d8 100644 --- a/src/gpt/getMissingOpenAIModels.ts +++ b/src/gpt/utils/getMissingOpenAIModels.ts @@ -1,6 +1,5 @@ -import fetch from 'node-fetch'; - -import { GPTModel, MODEL_DATA, ModelsResponseData } from './types'; +import { getOpenAiModels } from '../openAiRequests'; +import { GPTModel, MODEL_DATA, ModelsResponseData } from '../types'; /** * Function to check the availability of all models in OpenAI. @@ -11,13 +10,7 @@ export async function getMissingOpenAIModels( const missingModels: GPTModel[] = Object.keys(MODEL_DATA) as GPTModel[]; try { - const response = await fetch('https://api.openai.com/v1/models', { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${openAIApiKey}`, - }, - }); + const response = await getOpenAiModels(openAIApiKey); const responseData = (await response.json()) as ModelsResponseData; if (!responseData || !responseData.data) { diff --git a/src/gpt/getModel.ts b/src/gpt/utils/getModel.ts similarity index 73% rename from src/gpt/getModel.ts rename to src/gpt/utils/getModel.ts index 3f9c3b2..df328f4 100644 --- a/src/gpt/getModel.ts +++ b/src/gpt/utils/getModel.ts @@ -1,4 +1,4 @@ -import { GPTMode, GPTModel } from './types'; +import { GPTMode, GPTModel } from '../types'; export function getModel(mode: GPTMode): GPTModel { return mode === GPTMode.FAST ? 'gpt-3.5-turbo-16k-0613' : 'gpt-4-0613'; diff --git a/src/gpt/utils/getModelForMessages.ts b/src/gpt/utils/getModelForMessages.ts new file mode 100644 index 0000000..4fe0fa4 --- /dev/null +++ b/src/gpt/utils/getModelForMessages.ts @@ -0,0 +1,25 @@ +import { ChatCompletionRequestMessage } from 'openai'; + +import { FAST_MODE_TOKENS, GPTMode, GPTModel } from '../types'; +import { countTokens } from './countTokens'; + +export function getModelForMessages( + messages: ChatCompletionRequestMessage[], + mode: GPTMode, + maxTokens: number, +): GPTModel { + const messagesAsString = JSON.stringify(messages); + let model: GPTModel = 'gpt-4-0613'; + + if (mode === GPTMode.FAST) { + model = 'gpt-3.5-turbo-16k-0613'; + + const usedTokens = countTokens(messagesAsString, mode) + maxTokens; + + if (usedTokens < FAST_MODE_TOKENS) { + model = 'gpt-3.5-turbo-0613'; + } + } + + return model; +} diff --git a/src/gpt/processOpenAIResponseStream.ts b/src/gpt/utils/processOpenAIResponseStream.ts similarity index 97% rename from src/gpt/processOpenAIResponseStream.ts rename to src/gpt/utils/processOpenAIResponseStream.ts index c46e92d..019980f 100644 --- a/src/gpt/processOpenAIResponseStream.ts +++ b/src/gpt/utils/processOpenAIResponseStream.ts @@ -1,7 +1,7 @@ import AsyncLock from 'async-lock'; import { Response } from 'node-fetch'; -import { CANCELED_STAGE_NAME } from '../tasks/stageNames'; +import { CANCELED_STAGE_NAME } from '../../tasks/stageNames'; import { extractParsedLines } from './extractParsedLines'; const openAILock = new AsyncLock(); diff --git a/src/gpt/utils/setOpenAiKey.ts b/src/gpt/utils/setOpenAiKey.ts new file mode 100644 index 0000000..3df29eb --- /dev/null +++ b/src/gpt/utils/setOpenAiKey.ts @@ -0,0 +1,13 @@ +let openAIApiKey: string | undefined; + +export function setOpenAIApiKey(apiKey: string) { + openAIApiKey = apiKey; +} + +export function getOpenAIApiKey() { + if (!openAIApiKey) { + throw new Error('OpenAI API key not found. Please set it in the settings.'); + } + + return openAIApiKey; +} diff --git a/src/managers/EditorManager.ts b/src/managers/EditorManager.ts index 3b8cb38..81989ef 100644 --- a/src/managers/EditorManager.ts +++ b/src/managers/EditorManager.ts @@ -2,6 +2,20 @@ export type EditorPosition = { readonly line: number; readonly character: number; }; +export type EditorTextEdit = { + action: string; + startLine: number; + startCharacter: number; + text: string; + endLine?: number; + endCharacter?: number; +}; + +export interface WorkspaceEdit { + replace(uri: EditorUri, range: EditorRange, newText: string): void; + insert(uri: EditorUri, position: EditorPosition, newText: string): void; + getEntries(): [EditorUri, EditorTextEdit[]][]; +} export type EditorRange = { readonly start: EditorPosition; @@ -24,20 +38,13 @@ export type EditorDocument = { }; }; -// TODO: replace this object type -export type EditorTextEdit = object; - -export interface WorkspaceEdit { - replace(uri: EditorUri, range: EditorRange, newText: string): void; - insert(uri: EditorUri, position: EditorPosition, newText: string): void; - - getEntries(): [EditorUri, EditorTextEdit[]][]; -} - export interface EditorManager { - applyWorkspaceEdit(fillEdit: (edit: WorkspaceEdit) => Promise): unknown; - showInformationMessage(message: string): unknown; + applyWorkspaceEdit( + fillEdit: (edit: T) => Promise, + ): void; + showInformationMessage(message: string): void; openTextDocument(uri: EditorUri): Promise; + closeTextDocument?(uri: EditorUri): void; showErrorMessage(message: string): void; createUri(uri: string): EditorUri; } diff --git a/src/minionTasks/MinionTask.ts b/src/minionTasks/MinionTask.ts index c3e4f41..afc7b42 100644 --- a/src/minionTasks/MinionTask.ts +++ b/src/minionTasks/MinionTask.ts @@ -32,7 +32,7 @@ export class MinionTask implements TaskContext { readonly selection: EditorRange; readonly selectedText: string; - private _originalContent: string; + private originalContent: string; readonly id: string; @@ -74,12 +74,12 @@ export class MinionTask implements TaskContext { return true; } - get originalContent(): string { - return this._originalContent; + get getOriginalContent(): string { + return this.originalContent; } - set originalContent(value: string) { - this._originalContent = value; + set setOriginalContent(value: string) { + this.originalContent = value; getOriginalContentProvider().reportChange(this.originalContentURI); } @@ -94,7 +94,7 @@ export class MinionTask implements TaskContext { modificationProcedure: string; inlineMessage: string; - aplicationStatus?: ApplicationStatus; + applicationStatus?: ApplicationStatus; relevantKnowledge?: WorkspaceFilesKnowledge[]; constructor({ @@ -152,7 +152,7 @@ export class MinionTask implements TaskContext { this.userQuery = userQuery; this.selection = selection; this.selectedText = selectedText; - this._originalContent = originalContent; + this.originalContent = originalContent; this.contentAfterApply = finalContent; this.contentWhenDismissed = contentWhenDismissed; this.startTime = startTime; @@ -166,7 +166,7 @@ export class MinionTask implements TaskContext { this.relevantKnowledgeIds = relevantKnowledgeIds; this.logContent = logContent; this.totalCost = totalCost; - this.aplicationStatus = aplicationStatus; + this.applicationStatus = aplicationStatus; this.relevantKnowledge = relevantKnowledge; } diff --git a/src/minionTasks/SerializedMinionTask.ts b/src/minionTasks/SerializedMinionTask.ts index 4b2e0b0..06cf539 100644 --- a/src/minionTasks/SerializedMinionTask.ts +++ b/src/minionTasks/SerializedMinionTask.ts @@ -46,7 +46,7 @@ export function serializeMinionTask( endCharacter: minionTask.selection.end.character, }, selectedText: minionTask.selectedText, - originalContent: minionTask.originalContent, + originalContent: minionTask.getOriginalContent, finalContent: minionTask.contentAfterApply, startTime: minionTask.startTime, shortName: minionTask.shortName, @@ -57,7 +57,7 @@ export function serializeMinionTask( strategy: minionTask.strategyId === '' ? null : minionTask.strategyId, logContent: minionTask.logContent, contentWhenDismissed: minionTask.contentWhenDismissed, - aplicationStatus: minionTask.aplicationStatus, + aplicationStatus: minionTask.applicationStatus, relevantKnowledge: minionTask.relevantKnowledge, }; } diff --git a/src/minionTasks/advancedCodeChangeStrategy.ts b/src/minionTasks/advancedCodeChangeStrategy.ts index a0df247..677a5b2 100644 --- a/src/minionTasks/advancedCodeChangeStrategy.ts +++ b/src/minionTasks/advancedCodeChangeStrategy.ts @@ -1,43 +1,24 @@ -import { writeFile } from 'node:fs/promises'; - -import { format as dtFormat } from 'date-and-time'; -import fs from 'fs'; -import path from 'path'; - import { generateScoreTests } from '../../score/generateScoreTests'; -import { initMinionTask, Selection } from '../../score/initTestMinionTask'; -import { ScoreTestType } from '../../score/types'; -import { createFitnessAndNextSolutionsFunction } from '../stepEvolve/createFitnessAndNextSolutionsFunction'; -import { createNewSolutionFix } from '../stepEvolve/createNewSolutionFix'; -import { createSolutionWithMetaWithFitness } from '../stepEvolve/createSolutionWithMetaWithFitness'; -import { SolutionWithMeta } from '../stepEvolve/FitnessFunction'; import { stepEvolve } from '../stepEvolve/stepEvolve'; import { mutateAppendToLog } from '../tasks/logs/mutators/mutateAppendToLog'; import { mutateEndStage } from '../tasks/mutators/mutateEndStage'; import { mutateStartStage } from '../tasks/mutators/mutateStartStage'; +import { generateInitialSolutions } from './generateInitialSolutions'; import { MinionTask } from './MinionTask'; +import { onFinalSolution } from './observers/onFinalSolution'; +import { onInitialSolutions } from './observers/onInitialSolutions'; +import { onProgressMade } from './observers/onProgressMade'; +import { MinionTaskSolution, ParsedCriteriaDefinition } from './types'; const ITERATIONS = 3; const MAX_STALE_ITERATIONS = 3; const THRESHOLD = 120; -const BRANCHING = 3; const FULL_PROGRESS = 1; const PROGRESS_FOR_PRE_STAGES = 0.2; const PROGRESS_FOR_STRATEGY_STAGES = FULL_PROGRESS - PROGRESS_FOR_PRE_STAGES; const PROGRESS_PER_PRE_STAGE = PROGRESS_FOR_PRE_STAGES / ITERATIONS; -export interface MinionTaskSolution { - resultingCode: string; - modificationProcedure: string; - modificationDescription: string; - originalCode?: string; -} -interface ParsedCriteriaDefinition { - items: ScoreTestType[]; -} -type MinionTaskSolutionWithMeta = SolutionWithMeta; - export const advancedCodeChangeStrategy = async ( task: MinionTask, test?: boolean, @@ -61,47 +42,18 @@ export const advancedCodeChangeStrategy = async ( criteriaDefinition ? JSON.parse(criteriaDefinition.result) : {} ) as ParsedCriteriaDefinition; - const selectionData: Selection = { - start: task.selection.start, - end: task.selection.end, - selectedText: task.selectedText, - }; - - const initialSolutionsPromises = []; - - for (let i = 0; i < ITERATIONS; i++) { - mutateStartStage({ + const { initialSolutions, costs: initialSolutionsCosts } = + await generateInitialSolutions( task, - name: 'Preparing solutions...', - progressIncrement: PROGRESS_PER_PRE_STAGE, - }); - if (task.stopped) return; - const { execution: tempMinionTask } = await initMinionTask( - task.userQuery, - task.documentURI.fsPath, - selectionData, - ); - tempMinionTask.relevantKnowledge = task.relevantKnowledge; - initialSolutionsPromises.push( - createSolutionWithMetaWithFitness({ - solution: await createNewSolutionFix(tempMinionTask), - createdWith: 'initial', - fitnessAndNextSolutionsFunction: createFitnessAndNextSolutionsFunction({ - task: tempMinionTask, - maxBranching: BRANCHING, - criteriaDefinition: parsedCriteriaDefinition, - }), - }), + ITERATIONS, + PROGRESS_PER_PRE_STAGE, + parsedCriteriaDefinition, ); - costs += tempMinionTask.totalCost; - mutateEndStage(task); + if (initialSolutions?.length) { + costs += initialSolutionsCosts; } - if (task.stopped) return; - - const initialSolutions = await Promise.all(initialSolutionsPromises); - const finalSolution = await stepEvolve({ task, initialSolutions, @@ -110,79 +62,9 @@ export const advancedCodeChangeStrategy = async ( maxStaleIterations: MAX_STALE_ITERATIONS, observers: [ { - onInitialSolutions: async (solutionsWithMeta, iteration) => { - for (const solutionWithMeta of solutionsWithMeta) { - mutateAppendToLog( - task, - `Initial solution is: ${solutionWithMeta.solution} ${solutionWithMeta.totalFitness} (${solutionWithMeta.createdWith})` + - `.`, - ); - } - const logsPath = path.resolve(__dirname, 'logs'); - - if (!fs.existsSync(logsPath)) { - fs.mkdirSync(logsPath, { recursive: true }); - } - writeFile( - path.join( - __dirname, - 'logs', - `${iteration}-${dtFormat( - new Date(), - 'YYYY-MM-DD_HH-mm-ss', - )}.json`, - ), - JSON.stringify({ iteration, solutionsWithMeta }, null, 2), - 'utf8', - ); - }, - onProgressMade: async ( - oldSolutionsWithMeta: MinionTaskSolutionWithMeta[], - accepted: MinionTaskSolutionWithMeta[], - rejected: MinionTaskSolutionWithMeta[], - newSolutions: MinionTaskSolutionWithMeta[], - iteration: number, - ) => { - writeFile( - path.join(__dirname, 'logs', `${iteration}.json`), - JSON.stringify( - { iteration, accepted, rejected, newSolutions }, - null, - 2, - ), - 'utf8', - ); - mutateAppendToLog( - task, - `Solutions ${oldSolutionsWithMeta - .map((s) => s.solution) - .join(', ')}`, - ); - - for (const solutionWithMeta of accepted) { - mutateAppendToLog( - task, - `New best ${iteration}: ${solutionWithMeta.solution} ${solutionWithMeta.totalFitness} (${solutionWithMeta.createdWith}).`, - ); - } - }, - onFinalSolution: async (solutionWithMeta, iteration) => { - const { - totalFitness, - solution: { resultingCode }, - iteration: solutionIteration, - } = solutionWithMeta; - - mutateAppendToLog(task, 'Final solution is:'); - mutateAppendToLog(task, '```'); - mutateAppendToLog(task, resultingCode); - mutateAppendToLog(task, '```'); - mutateAppendToLog(task, `Fitness: ${totalFitness}`); - mutateAppendToLog( - task, - `Iteration: ${iteration} (Best solution found in iteration: ${solutionIteration})`, - ); - }, + onInitialSolutions: onInitialSolutions(task), + onProgressMade: onProgressMade(task), + onFinalSolution: onFinalSolution(task), }, ], }); diff --git a/src/minionTasks/applyModificationProcedure.ts b/src/minionTasks/applyModificationProcedure.ts index 9483971..9744173 100644 --- a/src/minionTasks/applyModificationProcedure.ts +++ b/src/minionTasks/applyModificationProcedure.ts @@ -1,5 +1,6 @@ import { getCommentForLanguage } from '../utils/code/comments'; -import { fuzzyReplaceTextInner } from '../utils/code/fuzzyReplaceText'; +import { fuzzyReplaceTextInner } from '../utils/code/fuzzyReplaceTextInner'; +import { sleep } from '../utils/sleep'; type CommandSegment = { name: string; @@ -148,10 +149,8 @@ export async function applyModificationProcedure( } } - for await (const line of lines) { - await new Promise((resolve) => { - setTimeout(resolve, 1); - }); + for (const line of lines) { + await sleep(1); const possibilities: CommandSegment[] = inCommand ? inCommand.followedBy || [] @@ -175,7 +174,7 @@ export async function applyModificationProcedure( followedBy.name.startsWith('END_') && followedBy.execute, ); - if (findEnd && findEnd.execute) { + if (findEnd?.execute) { currentCode = await findEnd.execute( currentCode, languageId, diff --git a/src/minionTasks/createChooseStrategyPrompt.ts b/src/minionTasks/createChooseStrategyPrompt.ts index 81e72db..ba203bb 100644 --- a/src/minionTasks/createChooseStrategyPrompt.ts +++ b/src/minionTasks/createChooseStrategyPrompt.ts @@ -1,4 +1,4 @@ -import { createFullPromptFromSections } from '../gpt/createFullPromptFromSections'; +import { createFullPromptFromSections } from '../gpt/utils/createFullPromptFromSections'; import { MinionTask } from './MinionTask'; export async function createChooseStrategyPrompt(task: MinionTask) { @@ -7,7 +7,7 @@ export async function createChooseStrategyPrompt(task: MinionTask) { return createFullPromptFromSections({ intro: ` You are an expert senior software architect, with 10 years of experience, experience in numerous projects and up to date knowledge and an IQ of 200. -Your collegue asked you to help him with some code, the task is provided below in TASK section. +Your colleague asked you to help him with some code, the task is provided below in TASK section. To choose the most accurate files take a look on imports in CODE section in the first place. @@ -20,10 +20,11 @@ Your job is to choose strategy and best materials for handling the TASK, so tomo task.selection.start.character + 1 } in the file)`]: task.selectedText, [`FILE CONTEXT (Language: ${document.languageId})`]: - task.originalContent, + task.getOriginalContent, } : { - [`CODE (Language: ${document.languageId})`]: task.originalContent, + [`CODE (Language: ${document.languageId})`]: + task.getOriginalContent, }), [`TASK (applies to CODE)`]: task.userQuery, }, diff --git a/src/minionTasks/createModificationProcedure.ts b/src/minionTasks/createModificationProcedure.ts index 84972b6..8d57727 100644 --- a/src/minionTasks/createModificationProcedure.ts +++ b/src/minionTasks/createModificationProcedure.ts @@ -1,10 +1,10 @@ import { z } from 'zod'; import { DEBUG_PROMPTS } from '../const'; -import { countTokens } from '../gpt/countTokens'; -import { ensureIRunThisInRange } from '../gpt/ensureIRunThisInRange'; import { gptExecute } from '../gpt/gptExecute'; import { GPTMode, QUALITY_MODE_TOKENS } from '../gpt/types'; +import { countTokens } from '../gpt/utils/countTokens'; +import { ensureIRunThisInRange } from '../gpt/utils/ensureIRunThisInRange'; import { WorkspaceFilesKnowledge } from './generateDescriptionForWorkspaceFiles'; import { createPrompt } from './prompts/createModificationProcedurePrompt'; import { trimKnowledge } from './utils/trimKnowledge'; @@ -46,7 +46,7 @@ export async function createModificationProcedure( let maxTokens = ensureIRunThisInRange({ prompt: promptWithContext, mode, - preferedTokens: fullPromptTokens, + preferredTokens: fullPromptTokens, minTokens, }); diff --git a/src/minionTasks/generateDescriptionForWorkspaceFiles.ts b/src/minionTasks/generateDescriptionForWorkspaceFiles.ts index 5dd3d13..a8a0bed 100644 --- a/src/minionTasks/generateDescriptionForWorkspaceFiles.ts +++ b/src/minionTasks/generateDescriptionForWorkspaceFiles.ts @@ -1,4 +1,4 @@ -import { countTokens } from '../gpt/countTokens'; +import { countTokens } from '../gpt/utils/countTokens'; import { GPTMode } from '../gpt/types'; import { mutateCreateFileDescription } from './mutators/mutateCreateFileDescription'; diff --git a/src/minionTasks/generateInitialSolutions.ts b/src/minionTasks/generateInitialSolutions.ts new file mode 100644 index 0000000..0438332 --- /dev/null +++ b/src/minionTasks/generateInitialSolutions.ts @@ -0,0 +1,65 @@ +import { initMinionTask } from '../../score/initTestMinionTask'; +import { Selection } from '../../score/initTestMinionTask'; +import { createFitnessAndNextSolutionsFunction } from '../stepEvolve/createFitnessAndNextSolutionsFunction'; +import { createNewSolutionFix } from '../stepEvolve/createNewSolutionFix'; +import { createSolutionWithMetaWithFitness } from '../stepEvolve/createSolutionWithMetaWithFitness'; +import { mutateEndStage } from '../tasks/mutators/mutateEndStage'; +import { mutateStartStage } from '../tasks/mutators/mutateStartStage'; +import { MinionTask } from './MinionTask'; +import { ParsedCriteriaDefinition } from './types'; + +const BRANCHING = 3; + +interface InitialSolutionsResult { + initialSolutions: any[]; + costs: number; +} + +export const generateInitialSolutions = async ( + task: MinionTask, + iterations: number, + progressIncrement: number, + criteriaDefinition: ParsedCriteriaDefinition, +): Promise => { + const initialSolutionsPromises = []; + let costs = 0; + + const selectionData: Selection = { + start: task.selection.start, + end: task.selection.end, + selectedText: task.selectedText, + }; + + for (let i = 0; i < iterations; i++) { + mutateStartStage({ + task, + name: 'Preparing solutions...', + progressIncrement, + }); + if (task.stopped) break; + const { execution: tempMinionTask } = await initMinionTask( + task.userQuery, + task.documentURI.fsPath, + selectionData, + ); + tempMinionTask.relevantKnowledge = task.relevantKnowledge; + initialSolutionsPromises.push( + createSolutionWithMetaWithFitness({ + solution: await createNewSolutionFix(tempMinionTask), + createdWith: 'initial', + fitnessAndNextSolutionsFunction: createFitnessAndNextSolutionsFunction({ + task: tempMinionTask, + maxBranching: BRANCHING, + criteriaDefinition, + }), + }), + ); + + costs += tempMinionTask.totalCost; + mutateEndStage(task); + } + + const initialSolutions = await Promise.all(initialSolutionsPromises); + + return { initialSolutions, costs }; +}; diff --git a/src/minionTasks/mutateExecuteMinionTaskStages.ts b/src/minionTasks/mutateExecuteMinionTaskStages.ts index c8499bd..cf9e077 100644 --- a/src/minionTasks/mutateExecuteMinionTaskStages.ts +++ b/src/minionTasks/mutateExecuteMinionTaskStages.ts @@ -28,8 +28,10 @@ export async function mutateExecuteMinionTaskStages( const workspaceFilesKnowledge = getExternalData ? await getExternalData() : []; + const knowledge = [...workspaceFilesKnowledge, ...minionsKnowledge]; mutateEndStage(task); + mutateStartStage({ task, name: 'Understanding ...', progressIncrement: 0.3 }); const { strategy, relevantKnowledge } = await taskChooseKnowledgeAndStrategy({ task, @@ -43,8 +45,10 @@ export async function mutateExecuteMinionTaskStages( task.relevantKnowledgeIds = relevantKnowledge.map( (knowledge) => knowledge.id, ); + task.relevantKnowledge = relevantKnowledge as WorkspaceFilesKnowledge[]; mutateEndStage(task); + switch (task.strategyId) { case 'AnswerQuestion': mutateStartStage({ diff --git a/src/minionTasks/mutators/mutateApplyFallback.ts b/src/minionTasks/mutators/mutateApplyFallback.ts index dc2884e..b9b3d15 100644 --- a/src/minionTasks/mutators/mutateApplyFallback.ts +++ b/src/minionTasks/mutators/mutateApplyFallback.ts @@ -20,16 +20,17 @@ ${minionTask.modificationDescription} mutateAppendToLogNoNewline(minionTask, LOG_PLAIN_COMMENT_MARKER); - minionTask.originalContent = document.getText(); - minionTask.aplicationStatus = ApplicationStatus.APPLIED_AS_FALLBACK; + minionTask.setOriginalContent = document.getText(); + minionTask.applicationStatus = ApplicationStatus.APPLIED_AS_FALLBACK; - await getEditorManager().applyWorkspaceEdit(async (edit) => { + getEditorManager().applyWorkspaceEdit(async (edit) => { edit.insert( minionTask.documentURI, { line: 0, character: 0 }, `${decomposedString}\n`, ); }); + getEditorManager().showInformationMessage( `Modification applied successfully.`, ); diff --git a/src/minionTasks/mutators/mutateApplyMinionTask.ts b/src/minionTasks/mutators/mutateApplyMinionTask.ts index 5d5f48f..0f9a0c1 100644 --- a/src/minionTasks/mutators/mutateApplyMinionTask.ts +++ b/src/minionTasks/mutators/mutateApplyMinionTask.ts @@ -17,7 +17,7 @@ export async function mutatorApplyMinionTask(minionTask: MinionTask) { if (minionTask.executionStage !== FINISHED_STAGE_NAME) { getEditorManager().showErrorMessage(`Cannot apply unfinished task.`); - minionTask.aplicationStatus = ApplicationStatus.NOT_APPLIED; + minionTask.applicationStatus = ApplicationStatus.NOT_APPLIED; return; } @@ -48,18 +48,18 @@ export async function mutatorApplyMinionTask(minionTask: MinionTask) { return; } - minionTask.originalContent = currentDocContent; + minionTask.setOriginalContent = currentDocContent; - const preprocessedContent = minionTask.originalContent; + const preprocessedContent = minionTask.getOriginalContent; const modifiedContent = await applyModificationProcedure( preprocessedContent, minionTask.modificationProcedure, document.languageId, ); - minionTask.aplicationStatus = ApplicationStatus.APPLIED; + minionTask.applicationStatus = ApplicationStatus.APPLIED; - await getEditorManager().applyWorkspaceEdit(async (edit) => { + getEditorManager().applyWorkspaceEdit(async (edit) => { edit.replace( document.uri, { diff --git a/src/minionTasks/mutators/mutateCreateAnswer.ts b/src/minionTasks/mutators/mutateCreateAnswer.ts index fccfa38..a340ae5 100644 --- a/src/minionTasks/mutators/mutateCreateAnswer.ts +++ b/src/minionTasks/mutators/mutateCreateAnswer.ts @@ -1,9 +1,9 @@ import { z } from 'zod'; -import { countTokens } from '../../gpt/countTokens'; -import { ensureIRunThisInRange } from '../../gpt/ensureIRunThisInRange'; import { gptExecute } from '../../gpt/gptExecute'; import { GPTMode, QUALITY_MODE_TOKENS } from '../../gpt/types'; +import { countTokens } from '../../gpt/utils/countTokens'; +import { ensureIRunThisInRange } from '../../gpt/utils/ensureIRunThisInRange'; import { mutateAppendToLog } from '../../tasks/logs/mutators/mutateAppendToLog'; import { mutateAppendToLogNoNewline } from '../../tasks/logs/mutators/mutateAppendToLogNoNewline'; import { mutateReportSmallProgress } from '../../tasks/mutators/mutateReportSmallProgress'; @@ -23,7 +23,7 @@ export async function mutateCreateAnswer(task: MinionTask) { const document = await task.document(); const userQuery = task.userQuery; const selectedText = task.selectedText; - const fullFileContents = task.originalContent; + const fullFileContents = task.getOriginalContent; const isCancelled = () => { return task.stopped; }; @@ -48,7 +48,7 @@ export async function mutateCreateAnswer(task: MinionTask) { let maxTokens = ensureIRunThisInRange({ prompt: promptWithContext, mode, - preferedTokens: fullPromptTokens, + preferredTokens: fullPromptTokens, minTokens, }); diff --git a/src/minionTasks/mutators/mutateCreateFileDescription.ts b/src/minionTasks/mutators/mutateCreateFileDescription.ts index 4dc6abf..4fcefaa 100644 --- a/src/minionTasks/mutators/mutateCreateFileDescription.ts +++ b/src/minionTasks/mutators/mutateCreateFileDescription.ts @@ -1,10 +1,10 @@ import { z } from 'zod'; -import { countTokens } from '../../gpt/countTokens'; -import { createFullPromptFromSections } from '../../gpt/createFullPromptFromSections'; -import { ensureIRunThisInRange } from '../../gpt/ensureIRunThisInRange'; import { gptExecute } from '../../gpt/gptExecute'; import { GPTMode } from '../../gpt/types'; +import { countTokens } from '../../gpt/utils/countTokens'; +import { createFullPromptFromSections } from '../../gpt/utils/createFullPromptFromSections'; +import { ensureIRunThisInRange } from '../../gpt/utils/ensureIRunThisInRange'; export interface WorkspaceFileData { path: string; @@ -33,7 +33,7 @@ export async function mutateCreateFileDescription(fileData: WorkspaceFileData) { const maxTokens = ensureIRunThisInRange({ prompt: promptWithContext, mode, - preferedTokens: fullPromptTokens, + preferredTokens: fullPromptTokens, minTokens, }); diff --git a/src/minionTasks/mutators/mutateCreateModification.ts b/src/minionTasks/mutators/mutateCreateModification.ts index 7930173..7102406 100644 --- a/src/minionTasks/mutators/mutateCreateModification.ts +++ b/src/minionTasks/mutators/mutateCreateModification.ts @@ -1,9 +1,9 @@ import { z } from 'zod'; -import { countTokens } from '../../gpt/countTokens'; -import { ensureIRunThisInRange } from '../../gpt/ensureIRunThisInRange'; import { gptExecute } from '../../gpt/gptExecute'; import { GPTMode, QUALITY_MODE_TOKENS } from '../../gpt/types'; +import { countTokens } from '../../gpt/utils/countTokens'; +import { ensureIRunThisInRange } from '../../gpt/utils/ensureIRunThisInRange'; import { mutateAppendToLogNoNewline } from '../../tasks/logs/mutators/mutateAppendToLogNoNewline'; import { mutateReportSmallProgress } from '../../tasks/mutators/mutateReportSmallProgress'; import { MinionTask } from '../MinionTask'; @@ -21,7 +21,7 @@ export async function mutateCreateModification(task: MinionTask) { const document = await task.document(); const userQuery = task.userQuery; const selectedText = task.selectedText; - const fullFileContents = task.originalContent; + const fullFileContents = task.getOriginalContent; const isCancelled = () => { return task.stopped; }; @@ -47,7 +47,7 @@ export async function mutateCreateModification(task: MinionTask) { let maxTokens = ensureIRunThisInRange({ prompt: promptWithContext, mode, - preferedTokens: fullPromptTokens, + preferredTokens: fullPromptTokens, minTokens, }); diff --git a/src/minionTasks/mutators/mutateCreateModificationProcedure.ts b/src/minionTasks/mutators/mutateCreateModificationProcedure.ts index 28d02f8..0c104d9 100644 --- a/src/minionTasks/mutators/mutateCreateModificationProcedure.ts +++ b/src/minionTasks/mutators/mutateCreateModificationProcedure.ts @@ -9,7 +9,7 @@ import { MinionTask } from '../MinionTask'; export async function mutateCreateModificationProcedure(task: MinionTask) { const { strategyId, - originalContent, + getOriginalContent: originalContent, modificationDescription, baseName, relevantKnowledge, diff --git a/src/minionTasks/mutators/mutateExtractRelevantCode.ts b/src/minionTasks/mutators/mutateExtractRelevantCode.ts index 44e76a3..5ba60c1 100644 --- a/src/minionTasks/mutators/mutateExtractRelevantCode.ts +++ b/src/minionTasks/mutators/mutateExtractRelevantCode.ts @@ -1,10 +1,10 @@ import { z } from 'zod'; import { DEBUG_PROMPTS, DEBUG_RESPONSES } from '../../const'; -import { countTokens } from '../../gpt/countTokens'; -import { ensureICanRunThis } from '../../gpt/ensureIcanRunThis'; import { gptExecute } from '../../gpt/gptExecute'; import { GPTMode } from '../../gpt/types'; +import { countTokens } from '../../gpt/utils/countTokens'; +import { ensureICanRunThis } from '../../gpt/utils/ensureIcanRunThis'; import { EditorDocument, EditorPosition } from '../../managers/EditorManager'; import { mutateAppendSectionToLog } from '../../tasks/logs/mutators/mutateAppendSectionToLog'; import { mutateAppendToLog } from '../../tasks/logs/mutators/mutateAppendToLog'; @@ -56,7 +56,7 @@ export async function stageExtractRelevantCode(task: MinionTask) { const userQuery = task.userQuery; const selectionPosition = task.selection.start; const selectedText = task.selectedText; - const fullFileContents = task.originalContent; + const fullFileContents = task.getOriginalContent; const promptWithContext = extractRelevantCodePrompt({ userQuery, diff --git a/src/minionTasks/mutators/mutateStageStarting.ts b/src/minionTasks/mutators/mutateStageStarting.ts index 31e3960..6fe6112 100644 --- a/src/minionTasks/mutators/mutateStageStarting.ts +++ b/src/minionTasks/mutators/mutateStageStarting.ts @@ -7,7 +7,7 @@ import { MinionTask } from '../MinionTask'; export async function mutateStageStarting(task: MinionTask) { const document = await getEditorManager().openTextDocument(task.documentURI); - task.originalContent = document.getText(); + task.setOriginalContent = document.getText(); mutateClearLog(task); mutateAppendToLog(task, `Id: ${task.id}`); diff --git a/src/minionTasks/observers/onFinalSolution.ts b/src/minionTasks/observers/onFinalSolution.ts new file mode 100644 index 0000000..1d2622e --- /dev/null +++ b/src/minionTasks/observers/onFinalSolution.ts @@ -0,0 +1,27 @@ +import { SolutionWithMeta } from '../../stepEvolve/FitnessFunction'; +import { mutateAppendToLog } from '../../tasks/logs/mutators/mutateAppendToLog'; +import { MinionTask } from '../MinionTask'; +import { MinionTaskSolution } from '../types'; + +export const onFinalSolution = + (task: MinionTask) => + async ( + solutionWithMeta: SolutionWithMeta, + iteration: number, + ) => { + const { + totalFitness, + solution: { resultingCode }, + iteration: solutionIteration, + } = solutionWithMeta; + + mutateAppendToLog(task, 'Final solution is:'); + mutateAppendToLog(task, '```'); + mutateAppendToLog(task, resultingCode); + mutateAppendToLog(task, '```'); + mutateAppendToLog(task, `Fitness: ${totalFitness}`); + mutateAppendToLog( + task, + `Iteration: ${iteration} (Best solution found in iteration: ${solutionIteration})`, + ); + }; diff --git a/src/minionTasks/observers/onInitialSolutions.ts b/src/minionTasks/observers/onInitialSolutions.ts new file mode 100644 index 0000000..f6f08a8 --- /dev/null +++ b/src/minionTasks/observers/onInitialSolutions.ts @@ -0,0 +1,39 @@ +import { writeFile } from 'node:fs/promises'; + +import { format as dtFormat } from 'date-and-time'; +import fs from 'fs'; +import path from 'path'; + +import { SolutionWithMeta } from '../../stepEvolve/FitnessFunction'; +import { mutateAppendToLog } from '../../tasks/logs/mutators/mutateAppendToLog'; +import { MinionTask } from '../MinionTask'; +import { MinionTaskSolution } from '../types'; + +export const onInitialSolutions = + (task: MinionTask) => + async ( + solutionsWithMeta: SolutionWithMeta[], + iteration: number, + ) => { + for (const solutionWithMeta of solutionsWithMeta) { + mutateAppendToLog( + task, + `Initial solution is: ${solutionWithMeta.solution} ${solutionWithMeta.totalFitness} (${solutionWithMeta.createdWith})` + + `.`, + ); + } + const logsPath = path.resolve(__dirname, 'logs'); + + if (!fs.existsSync(logsPath)) { + fs.mkdirSync(logsPath, { recursive: true }); + } + await writeFile( + path.join( + __dirname, + 'logs', + `${iteration}-${dtFormat(new Date(), 'YYYY-MM-DD_HH-mm-ss')}.json`, + ), + JSON.stringify({ iteration, solutionsWithMeta }, null, 2), + 'utf8', + ); + }; diff --git a/src/minionTasks/observers/onProgressMade.ts b/src/minionTasks/observers/onProgressMade.ts new file mode 100644 index 0000000..ca696ea --- /dev/null +++ b/src/minionTasks/observers/onProgressMade.ts @@ -0,0 +1,33 @@ +import { writeFile } from 'node:fs/promises'; +import path from 'node:path'; + +import { mutateAppendToLog } from '../../tasks/logs/mutators/mutateAppendToLog'; +import { MinionTask } from '../MinionTask'; +import { MinionTaskSolutionWithMeta } from '../types'; + +export const onProgressMade = + (task: MinionTask) => + async ( + oldSolutionsWithMeta: MinionTaskSolutionWithMeta[], + accepted: MinionTaskSolutionWithMeta[], + rejected: MinionTaskSolutionWithMeta[], + newSolutions: MinionTaskSolutionWithMeta[], + iteration: number, + ) => { + await writeFile( + path.join(__dirname, 'logs', `${iteration}.json`), + JSON.stringify({ iteration, accepted, rejected, newSolutions }, null, 2), + 'utf8', + ); + mutateAppendToLog( + task, + `Solutions ${oldSolutionsWithMeta.map((s) => s.solution).join(', ')}`, + ); + + for (const solutionWithMeta of accepted) { + mutateAppendToLog( + task, + `New best ${iteration}: ${solutionWithMeta.solution} ${solutionWithMeta.totalFitness} (${solutionWithMeta.createdWith}).`, + ); + } + }; diff --git a/src/minionTasks/prompts/createAnswerPrompt.ts b/src/minionTasks/prompts/createAnswerPrompt.ts index b86c350..30d9ac4 100644 --- a/src/minionTasks/prompts/createAnswerPrompt.ts +++ b/src/minionTasks/prompts/createAnswerPrompt.ts @@ -1,4 +1,4 @@ -import { createFullPromptFromSections } from '../../gpt/createFullPromptFromSections'; +import { createFullPromptFromSections } from '../../gpt/utils/createFullPromptFromSections'; import { EditorDocument, EditorPosition } from '../../managers/EditorManager'; import { PromptKnowledge } from '../utils/trimKnowledge'; import { knowledgeHelperPrompt } from './knowledgeHelperPrompt'; diff --git a/src/minionTasks/prompts/createModificationProcedurePrompt.ts b/src/minionTasks/prompts/createModificationProcedurePrompt.ts index a2e7ebb..7b11a7f 100644 --- a/src/minionTasks/prompts/createModificationProcedurePrompt.ts +++ b/src/minionTasks/prompts/createModificationProcedurePrompt.ts @@ -1,4 +1,4 @@ -import { createFullPromptFromSections } from '../../gpt/createFullPromptFromSections'; +import { createFullPromptFromSections } from '../../gpt/utils/createFullPromptFromSections'; import { PromptKnowledge } from '../utils/trimKnowledge'; import { AVAILABLE_COMMANDS } from './codeModificationCommandsPrompt'; import { knowledgeHelperPrompt } from './knowledgeHelperPrompt'; diff --git a/src/minionTasks/prompts/createModificationPrompt.ts b/src/minionTasks/prompts/createModificationPrompt.ts index 196335f..f6be401 100644 --- a/src/minionTasks/prompts/createModificationPrompt.ts +++ b/src/minionTasks/prompts/createModificationPrompt.ts @@ -1,4 +1,4 @@ -import { createFullPromptFromSections } from '../../gpt/createFullPromptFromSections'; +import { createFullPromptFromSections } from '../../gpt/utils/createFullPromptFromSections'; import { EditorDocument, EditorPosition } from '../../managers/EditorManager'; import { PromptKnowledge } from '../utils/trimKnowledge'; import { knowledgeHelperPrompt } from './knowledgeHelperPrompt'; diff --git a/src/minionTasks/types.ts b/src/minionTasks/types.ts new file mode 100644 index 0000000..3730dda --- /dev/null +++ b/src/minionTasks/types.ts @@ -0,0 +1,13 @@ +import { ScoreTestType } from '../../score/types'; +import { SolutionWithMeta } from '../stepEvolve/FitnessFunction'; + +export interface MinionTaskSolution { + resultingCode: string; + modificationProcedure: string; + modificationDescription: string; + originalCode?: string; +} +export interface ParsedCriteriaDefinition { + items: ScoreTestType[]; +} +export type MinionTaskSolutionWithMeta = SolutionWithMeta; diff --git a/src/minionTasks/utils/trimKnowledge.ts b/src/minionTasks/utils/trimKnowledge.ts index f4674a1..5088ff3 100644 --- a/src/minionTasks/utils/trimKnowledge.ts +++ b/src/minionTasks/utils/trimKnowledge.ts @@ -1,7 +1,7 @@ -import { countTokens } from '../../gpt/countTokens'; -import { ensureIRunThisInRange } from '../../gpt/ensureIRunThisInRange'; -import { getModel } from '../../gpt/getModel'; import { GPTMode, MODEL_DATA } from '../../gpt/types'; +import { countTokens } from '../../gpt/utils/countTokens'; +import { ensureIRunThisInRange } from '../../gpt/utils/ensureIRunThisInRange'; +import { getModel } from '../../gpt/utils/getModel'; import { WorkspaceFilesKnowledge } from '../generateDescriptionForWorkspaceFiles'; export type PromptKnowledge = { knowledge?: WorkspaceFilesKnowledge[] }; @@ -55,7 +55,7 @@ export const trimKnowledge = ({ const newMaxTokens = ensureIRunThisInRange({ prompt: newPrompt, mode, - preferedTokens: fullPromptTokens, + preferredTokens: fullPromptTokens, minTokens, }); diff --git a/src/stepEvolve/createFitnessAndNextSolutionsFunction.ts b/src/stepEvolve/createFitnessAndNextSolutionsFunction.ts index aefd5d3..5187245 100644 --- a/src/stepEvolve/createFitnessAndNextSolutionsFunction.ts +++ b/src/stepEvolve/createFitnessAndNextSolutionsFunction.ts @@ -5,8 +5,8 @@ import { FitnessAndNextSolutionsFunction, SolutionWithMeta, } from '../../src/stepEvolve/FitnessFunction'; -import { MinionTaskSolution } from '../minionTasks/advancedCodeChangeStrategy'; import { MinionTask } from '../minionTasks/MinionTask'; +import { MinionTaskSolution } from '../minionTasks/types'; import { createFixesForSolution } from './createFixesForSolution'; export function createFitnessAndNextSolutionsFunction({ diff --git a/src/stepEvolve/createFixesForSolution.ts b/src/stepEvolve/createFixesForSolution.ts index 4015d9d..ec135ba 100644 --- a/src/stepEvolve/createFixesForSolution.ts +++ b/src/stepEvolve/createFixesForSolution.ts @@ -1,15 +1,15 @@ import { z } from 'zod'; import { CriteriaRatings, MAX_POINTS } from '../../score/rateMinionTask'; -import { createFullPromptFromSections } from '../../src/gpt/createFullPromptFromSections'; import { gptExecute } from '../../src/gpt/gptExecute'; import { GPTMode } from '../../src/gpt/types'; import { Fix, SolutionWithMeta } from '../../src/stepEvolve/FitnessFunction'; import { getRandomElement } from '../../src/utils/random/getRandomElement'; import { shuffleArray } from '../../src/utils/random/shuffleArray'; -import { countTokens } from '../gpt/countTokens'; -import { MinionTaskSolution } from '../minionTasks/advancedCodeChangeStrategy'; +import { countTokens } from '../gpt/utils/countTokens'; +import { createFullPromptFromSections } from '../gpt/utils/createFullPromptFromSections'; import { MinionTask } from '../minionTasks/MinionTask'; +import { MinionTaskSolution } from '../minionTasks/types'; import { improveSolutionFix, ImproveSolutionFixResult, @@ -37,7 +37,7 @@ export async function createFixesForSolution( 'brilliant software engineer with a high IQ, specializing in compiler optimization to enhance code efficiency', 'data scientist of exceptional intelligence, focused on developing advanced machine learning algorithms for code improvement suggestions', 'ingenious systems architect possessing a high IQ, dedicated to optimizing code for scalability and peak performance', - 'harp-minded security researcher with a high IQ, adept at identifying and rectifying code vulnerabilities for enhanced code quality', + 'sharp-minded security researcher with a high IQ, adept at identifying and rectifying code vulnerabilities for enhanced code quality', ])}. provide suggestions on how to improve the SOLUTION that full fill USER_QUERY request based on MODIFICATION_DESCRIPTION in order to maximize the judging CRITERIA.`, sections: { MODIFICATION_DESCRIPTION: modificationDescription, diff --git a/src/stepEvolve/createNewSolutionFix.ts b/src/stepEvolve/createNewSolutionFix.ts index 21b5f6c..68613ba 100644 --- a/src/stepEvolve/createNewSolutionFix.ts +++ b/src/stepEvolve/createNewSolutionFix.ts @@ -1,8 +1,8 @@ -import { MinionTaskSolution } from '../minionTasks/advancedCodeChangeStrategy'; import { applyModificationProcedure } from '../minionTasks/applyModificationProcedure'; import { MinionTask } from '../minionTasks/MinionTask'; import { mutateCreateModification } from '../minionTasks/mutators/mutateCreateModification'; import { mutateCreateModificationProcedure } from '../minionTasks/mutators/mutateCreateModificationProcedure'; +import { MinionTaskSolution } from '../minionTasks/types'; import { mutateStopExecution } from '../tasks/mutators/mutateStopExecution'; const MAX_ATTEMPTS_NUMBER = 4; @@ -22,7 +22,7 @@ export const createNewSolutionFix = async ( await mutateCreateModificationProcedure(task); mutateStopExecution(task); modifiedContent = await applyModificationProcedure( - task.originalContent, + task.getOriginalContent, task.modificationProcedure, 'ts', ); @@ -36,6 +36,6 @@ export const createNewSolutionFix = async ( resultingCode: modifiedContent, modificationProcedure: task.modificationProcedure, modificationDescription: task.modificationDescription, - originalCode: task.originalContent, + originalCode: task.getOriginalContent, }; }; diff --git a/src/stepEvolve/fixImproveSolution.ts b/src/stepEvolve/fixImproveSolution.ts index 8ef899f..8a38271 100644 --- a/src/stepEvolve/fixImproveSolution.ts +++ b/src/stepEvolve/fixImproveSolution.ts @@ -1,12 +1,12 @@ import { z } from 'zod'; -import { countTokens } from '../gpt/countTokens'; -import { createFullPromptFromSections } from '../gpt/createFullPromptFromSections'; -import { ensureIRunThisInRange } from '../gpt/ensureIRunThisInRange'; import { gptExecute } from '../gpt/gptExecute'; import { GPTMode } from '../gpt/types'; -import { MinionTaskSolution } from '../minionTasks/advancedCodeChangeStrategy'; +import { countTokens } from '../gpt/utils/countTokens'; +import { createFullPromptFromSections } from '../gpt/utils/createFullPromptFromSections'; +import { ensureIRunThisInRange } from '../gpt/utils/ensureIRunThisInRange'; import { MinionTask } from '../minionTasks/MinionTask'; +import { MinionTaskSolution } from '../minionTasks/types'; import { SolutionWithMeta } from './FitnessFunction'; const EXTRA_TOKENS = 200; @@ -34,7 +34,7 @@ export function improveSolutionFix({ const solution = solutionWithMeta.solution.resultingCode; const fullPrompt = createFullPromptFromSections({ intro: - 'Improve the following SOLUTION and MODIFICATION_PROCEDURE that fullfill USER_QUERY request based on MODIFICATION_DESCRIPTION, use SUGGESTIONS as guidance. Do not output any section markers or additional sections in your response, just the new improved solution, improved MODIFICATION_PROCEDURE.', + 'Improve the following SOLUTION and MODIFICATION_PROCEDURE that fullfil USER_QUERY request based on MODIFICATION_DESCRIPTION, use SUGGESTIONS as guidance. Do not output any section markers or additional sections in your response, just the new improved solution, improved MODIFICATION_PROCEDURE.', sections: { MODIFICATION_DESCRIPTION: modificationDescription, MODIFICATION_PROCEDURE: modificationProcedure, @@ -51,7 +51,7 @@ export function improveSolutionFix({ const maxTokens = ensureIRunThisInRange({ prompt: fullPrompt, mode, - preferedTokens: fullPromptTokens, + preferredTokens: fullPromptTokens, minTokens, }); diff --git a/src/stepEvolve/stepEvolve.ts b/src/stepEvolve/stepEvolve.ts index 121bbfb..603597a 100644 --- a/src/stepEvolve/stepEvolve.ts +++ b/src/stepEvolve/stepEvolve.ts @@ -1,5 +1,6 @@ import { MinionTask } from '../minionTasks/MinionTask'; import { getRandomElement } from '../utils/random/getRandomElement'; +import { sleep } from '../utils/sleep'; import { type SolutionWithMeta } from './FitnessFunction'; import { FitnessObserver } from './FitnessObserver'; @@ -90,9 +91,7 @@ export async function stepEvolve({ ), ), ); - await new Promise((r) => { - setTimeout(r, 0); - }); + await sleep(0); } await Promise.all( diff --git a/src/strategyAndKnowledge/mutators/mutateCreateSimpleAnswer.ts b/src/strategyAndKnowledge/mutators/mutateCreateSimpleAnswer.ts index 20b6ed6..73b831a 100644 --- a/src/strategyAndKnowledge/mutators/mutateCreateSimpleAnswer.ts +++ b/src/strategyAndKnowledge/mutators/mutateCreateSimpleAnswer.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { createFullPromptFromSections } from '../../gpt/createFullPromptFromSections'; +import { createFullPromptFromSections } from '../../gpt/utils/createFullPromptFromSections'; import { GPTMode } from '../../gpt/types'; import { mutateAppendSectionToLog } from '../../tasks/logs/mutators/mutateAppendSectionToLog'; import { mutateAppendToLogNoNewline } from '../../tasks/logs/mutators/mutateAppendToLogNoNewline'; diff --git a/src/strategyAndKnowledge/mutators/taskChooseKnowledgeAndStrategy.ts b/src/strategyAndKnowledge/mutators/taskChooseKnowledgeAndStrategy.ts index 32c2224..a816474 100644 --- a/src/strategyAndKnowledge/mutators/taskChooseKnowledgeAndStrategy.ts +++ b/src/strategyAndKnowledge/mutators/taskChooseKnowledgeAndStrategy.ts @@ -1,10 +1,10 @@ import { z } from 'zod'; import { DEBUG_PROMPTS } from '../../const'; -import { countTokens } from '../../gpt/countTokens'; -import { ensureIRunThisInRange } from '../../gpt/ensureIRunThisInRange'; -import { getModel } from '../../gpt/getModel'; import { GPTExecuteRequestMessage, GPTMode, MODEL_DATA } from '../../gpt/types'; +import { countTokens } from '../../gpt/utils/countTokens'; +import { ensureIRunThisInRange } from '../../gpt/utils/ensureIRunThisInRange'; +import { getModel } from '../../gpt/utils/getModel'; import { mutateAppendSectionToLog } from '../../tasks/logs/mutators/mutateAppendSectionToLog'; import { mutateAppendToLog } from '../../tasks/logs/mutators/mutateAppendToLog'; import { taskGPTExecute } from '../../tasks/mutators/taskGPTExecute'; @@ -108,7 +108,7 @@ export async function taskChooseKnowledgeAndStrategy({ const maxTokens = ensureIRunThisInRange({ prompt, mode, - preferedTokens: fullPromptTokens, + preferredTokens: fullPromptTokens, minTokens, }); const KnowledgeIdsEnum = z.enum([ diff --git a/src/strategyAndKnowledge/mutators/taskChooseStrategy.ts b/src/strategyAndKnowledge/mutators/taskChooseStrategy.ts index 97affef..c150ca4 100644 --- a/src/strategyAndKnowledge/mutators/taskChooseStrategy.ts +++ b/src/strategyAndKnowledge/mutators/taskChooseStrategy.ts @@ -7,7 +7,7 @@ import { mutateAppendToLog } from '../../tasks/logs/mutators/mutateAppendToLog'; import { taskGPTExecute } from '../../tasks/mutators/taskGPTExecute'; import { shuffleArray } from '../../utils/random/shuffleArray'; import { Strategy } from '../Strategy'; -import { countTokens } from '../../gpt/countTokens'; +import { countTokens } from '../../gpt/utils/countTokens'; export async function taskChooseStrategy(task: TC, strategies: Strategy[], taskToPrompt: (task: TC) => Promise) { const promptWithContext = ` diff --git a/src/tasks/mutators/mutateRunTaskStages.ts b/src/tasks/mutators/mutateRunTaskStages.ts index e255685..86c0d48 100644 --- a/src/tasks/mutators/mutateRunTaskStages.ts +++ b/src/tasks/mutators/mutateRunTaskStages.ts @@ -6,7 +6,7 @@ import { TaskContext } from '../TaskContext'; import { TaskCanceledError } from '../utils/TaskCanceled'; import { mutateStopExecution } from './mutateStopExecution'; -export function mutateRunTaskStages( +export async function mutateRunTaskStages( task: TC, execute: ( task: TC, @@ -16,40 +16,40 @@ export function mutateRunTaskStages( getExternalData?: () => Promise, test?: boolean, ) { - return new Promise((resolve, reject) => { - if (task.stopped) { - return; - } + if (task.stopped) { + return; + } - task.onSuccess = resolve; - task.onErrorOrCancel = reject; + task.progress = 0; - try { - task.progress = 0; - execute(task, getExternalData, test); - mutateStopExecution(task); - } catch (error) { - if (!(error instanceof TaskCanceledError)) { - getEditorManager().showErrorMessage(`Error in execution: ${error}`); - console.error('Error in execution', error); - } + try { + await execute(task, getExternalData, test); + await mutateStopExecution(task); + task.onSuccess?.(); + } catch (error) { + if (!(error instanceof TaskCanceledError)) { + getEditorManager().showErrorMessage(`Error in execution: ${error}`); + console.error('Error in execution', error); + } - mutateStopExecution( - task, - error instanceof Error ? `Error: ${error.message}` : String(error), - ); - } finally { - const executionTime = Date.now() - task.startTime; - const formattedExecutionTime = - calculateAndFormatExecutionTime(executionTime); + await mutateStopExecution( + task, + error instanceof Error ? `Error: ${error.message}` : String(error), + ); + task.onErrorOrCancel?.( + error instanceof Error ? error.message : String(error), + ); + } finally { + const executionTime = Date.now() - task.startTime; + const formattedExecutionTime = + calculateAndFormatExecutionTime(executionTime); - mutateAppendToLog(task, `Total Cost: ~${task.totalCost.toFixed(2)}$`); - mutateAppendToLog( - task, - `${task.executionStage} (Execution Time: ${formattedExecutionTime})`, - ); + mutateAppendToLog(task, `Total Cost: ~${task.totalCost.toFixed(2)}$`); + mutateAppendToLog( + task, + `${task.executionStage} (Execution Time: ${formattedExecutionTime})`, + ); - task.progress = 1; - } - }); + task.progress = 1; + } } diff --git a/src/tasks/mutators/taskGPTExecute.ts b/src/tasks/mutators/taskGPTExecute.ts index 805b1d2..b96bd07 100644 --- a/src/tasks/mutators/taskGPTExecute.ts +++ b/src/tasks/mutators/taskGPTExecute.ts @@ -1,9 +1,9 @@ import { z } from 'zod'; import { DEBUG_RESPONSES } from '../../const'; -import { ensureICanRunThis } from '../../gpt/ensureIcanRunThis'; import { gptExecute } from '../../gpt/gptExecute'; import { GPTExecuteRequestPrompt, GPTMode } from '../../gpt/types'; +import { ensureICanRunThis } from '../../gpt/utils/ensureIcanRunThis'; import { mutateAppendToLog } from '../logs/mutators/mutateAppendToLog'; import { mutateAppendToLogNoNewline } from '../logs/mutators/mutateAppendToLogNoNewline'; import { TaskContext } from '../TaskContext'; diff --git a/src/utils/MultiSet.ts b/src/utils/MultiSet.ts deleted file mode 100644 index 459781c..0000000 --- a/src/utils/MultiSet.ts +++ /dev/null @@ -1,47 +0,0 @@ -export class MultiSet { - private _backing: { [key: string]: number } = {}; - private _array: string[] = []; - - constructor(values: string[]) { - this.add(...values); - } - - add(...values: string[]) { - for (const value of values) { - if (this._backing[value] > 0) { - this._backing[value] = 1 + this._backing[value]; - } else { - this._backing[value] = 1; - } - this._array.push(value); - } - } - - delete(value: string) { - if (this.get(value) > 0) { - this._backing[value] = this.get(value) - 1; - - const idx = this._array.indexOf(value); - if (idx === -1) throw new Error('Internal error'); - this._array.splice(idx, 1); - } - } - - entries() { - return Object.entries(this._backing); - } - - expand() { - return this._array; - } - - get(value: string): number { - const v = this._backing[value]; - // eslint-disable-next-line eqeqeq - if (v != null && v > 0) { - return v; - } - - return 0; - } -} diff --git a/src/utils/code/coreSimilarityFunction.ts b/src/utils/code/coreSimilarityFunction.ts new file mode 100644 index 0000000..2e03d24 --- /dev/null +++ b/src/utils/code/coreSimilarityFunction.ts @@ -0,0 +1,50 @@ +import { + codeStringSimilarity, + levenshteinDistanceSimilarity, +} from '../string/stringUtils'; +import { exactLinesSimilarityAndMap } from './exactLinesSimilarityAndMap'; +import { ignoreLeadingAndTrailingWhiteSpaceSimilarityFunction } from './ignoreLeadingAndTrailingWhiteSpaceSimilarityFunction'; +import { normalizeIndent } from './normalizeIndent'; +import { stripAllComments } from './stripAllComments'; + +export const coreSimilarityFunction = ( + original: string[], + replacement: string[], +) => { + if (original.join('\n') === replacement.join('\n')) { + return 1; + } + + const similarityWithWsDistance = exactLinesSimilarityAndMap( + original, + replacement, + (a, b) => + ignoreLeadingAndTrailingWhiteSpaceSimilarityFunction( + a, + b, + codeStringSimilarity, + ), + ).similarity; + + const similarityNotIgnoringWhitespace = exactLinesSimilarityAndMap( + normalizeIndent(stripAllComments(original)), + normalizeIndent(stripAllComments(replacement)), + levenshteinDistanceSimilarity, + ).similarity; + + const core = Math.max( + similarityWithWsDistance, + similarityNotIgnoringWhitespace, + ); + + const similarity = + 0.6 * core + + 0.2 * similarityWithWsDistance + + 0.2 * similarityNotIgnoringWhitespace; + + if (isNaN(similarity)) { + throw new Error('similarity is NaN'); + } + + return similarity; +}; diff --git a/src/utils/code/exactLinesSimilarityAndMap.ts b/src/utils/code/exactLinesSimilarityAndMap.ts new file mode 100644 index 0000000..a4e59d7 --- /dev/null +++ b/src/utils/code/exactLinesSimilarityAndMap.ts @@ -0,0 +1,188 @@ +import { SingleLineSimilarityFunction } from './fuzzyReplaceText'; + +export function exactLinesSimilarityAndMap( + original: string[], + find: string[], + lineSimilarityFunction: SingleLineSimilarityFunction, + mapFindLine: (original: string | undefined, findLine: string) => string = ( + original, + findLine, + ) => findLine, +): { similarity: number; mappedFind: string[] } { + const mappedFind: string[] = []; + let originalLine = 0; + let findLine = 0; + + let originalSimilarityLines = 0; + + function lineSkippedValue(line: string) { + const baseSkippedValue = 0.02; + const scalableSkippedValue = 0.98; + const skipScaling = 1 - 1 / (1 + line.trim().length); + + return baseSkippedValue + scalableSkippedValue * skipScaling; + } + + const options = [ + { + condition: () => originalLine < original.length && findLine < find.length, + similarity: () => + lineSimilarityFunction(original[originalLine], find[findLine]), + skippedOriginalLines: () => 0, + skippedFindLines: () => 0, + apply: () => { + mappedFind.push(mapFindLine(original[originalLine], find[findLine])); + originalLine++; + findLine++; + originalSimilarityLines++; + }, + }, + (() => { + const skippedOriginalLines = 1; + + return { + condition: () => + originalLine + skippedOriginalLines < original.length && + findLine < find.length, + similarity: () => + lineSimilarityFunction( + original[originalLine + skippedOriginalLines], + find[findLine], + ), + skippedOriginalLines: () => skippedOriginalLines, + skippedFindLines: () => 0, + apply: () => { + mappedFind.push( + mapFindLine( + original[originalLine + skippedOriginalLines], + find[findLine], + ), + ); + + originalLine++; + findLine++; + originalSimilarityLines++; + + originalLine += skippedOriginalLines; + }, + }; + })(), + (() => { + const skippedFindLines = 1; + + return { + condition: () => + originalLine < original.length && + findLine + skippedFindLines < find.length, + similarity: () => + lineSimilarityFunction( + original[originalLine], + find[findLine + skippedFindLines], + ), + skippedOriginalLines: () => 0, + skippedFindLines: () => skippedFindLines, + apply: () => { + for (let i = 0; i < skippedFindLines; i++) { + mappedFind.push(mapFindLine(undefined, find[findLine + i])); + } + mappedFind.push( + mapFindLine( + original[originalLine], + find[findLine + skippedFindLines], + ), + ); + + originalLine++; + findLine++; + originalSimilarityLines++; + + findLine += skippedFindLines; + }, + }; + })(), + { + condition: () => + originalLine < original.length && findLine >= find.length, + similarity: () => 0, + skippedOriginalLines: () => 1, + skippedFindLines: () => 0, + apply: () => { + originalLine++; + }, + }, + { + condition: () => + originalLine >= original.length && findLine < find.length, + similarity: () => 0, + skippedOriginalLines: () => 0, + skippedFindLines: () => 1, + apply: () => { + mappedFind.push(mapFindLine(undefined, find[findLine])); + findLine++; + }, + }, + ]; + + let similaritySum = 0; + let linesSkipped = 0; + + // eslint-disable-next-line no-constant-condition + while (true) { + let bestOption; + let bestSimilarity = Number.MIN_SAFE_INTEGER; + + for (const option of options) { + if (option.condition()) { + const similarity = option.similarity(); + + if (isNaN(similarity)) { + throw new Error('similarity is NaN'); + } + + if (similarity > bestSimilarity) { + bestSimilarity = similarity; + bestOption = option; + } + } + } + + if (bestOption === undefined) { + break; + } + + for ( + let orgIndex = 0; + orgIndex < bestOption.skippedOriginalLines(); + orgIndex++ + ) { + linesSkipped += lineSkippedValue(original[originalLine + orgIndex]); + } + + for ( + let findIndex = 0; + findIndex < bestOption.skippedFindLines(); + findIndex++ + ) { + linesSkipped += lineSkippedValue(find[findLine + findIndex]); + } + + bestOption.apply(); + similaritySum += bestSimilarity; + } + + if (original.length === 0 && find.length === 0) { + return { similarity: 1, mappedFind }; + } + + if (original.length === 0 && find.length !== 0) { + return { similarity: 0, mappedFind }; + } + + const averageSimilarity = + originalSimilarityLines === 0 ? 1 : similaritySum / originalSimilarityLines; + const noSkipRatio = + (3 * (1 - linesSkipped / original.length) + 1 * (1 / (1 + linesSkipped))) / + 4; + + return { similarity: averageSimilarity * noSkipRatio, mappedFind }; +} diff --git a/src/utils/code/findIndentationDifference.ts b/src/utils/code/findIndentationDifference.ts new file mode 100644 index 0000000..5430cb5 --- /dev/null +++ b/src/utils/code/findIndentationDifference.ts @@ -0,0 +1,37 @@ +import { fuzzyGetIndentationDifference } from './fuzzyGetIndentationDifference'; +import { checkIndentation } from './regexUtils'; + +/** + * Try to guess indentation from the current slice and replaceTextLines + */ +export function findIndentationDifference( + currentSlice: string[], + replaceTextLines: string[], + similarityFunction: (a: string, b: string) => number, +) { + const indentationDifferences: number[] = []; + + for ( + let i = 0; + i < Math.min(currentSlice.length, replaceTextLines.length); + i++ + ) { + const replaceLine = replaceTextLines[i]; + const replaceIndentation = checkIndentation(replaceLine); + const currentLine = currentSlice[i]; + + const currentIndentation = checkIndentation(currentLine); + fuzzyGetIndentationDifference(currentLine, replaceLine, similarityFunction); + indentationDifferences.push(currentIndentation - replaceIndentation); + } + + const resultLines: string[] = []; + + for (let i = 0; i < replaceTextLines.length; i++) { + const indentation = ' '.repeat(Math.abs(indentationDifferences[0])); + resultLines.push(indentation); + } + resultLines.sort((a, b) => b.length - a.length); + + return resultLines; +} diff --git a/src/utils/code/fuzzyFindText.ts b/src/utils/code/fuzzyFindText.ts new file mode 100644 index 0000000..65ca64b --- /dev/null +++ b/src/utils/code/fuzzyFindText.ts @@ -0,0 +1,71 @@ +import { sleep } from '../sleep'; +import { coreSimilarityFunction } from './coreSimilarityFunction'; + +const DEFAULT_SIMILARITY_THRESHOLD = 0.75; + +export async function fuzzyFindText({ + currentCode, + findText, + similarityFunction = coreSimilarityFunction, + lineNumTolerance = Math.ceil(findText.split('\n').length * 0.05), + similarityThreshold = DEFAULT_SIMILARITY_THRESHOLD, +}: { + currentCode: string; + findText: string; + similarityFunction?: (original: string[], replacement: string[]) => number; + lineNumTolerance?: number; + similarityThreshold?: number; +}): Promise<{ + lineStartIndex: number; + lineEndIndex: number; + confidence: number; +}> { + const currentCodeLines = currentCode.split('\n'); + const findTextLines = findText.split('\n'); + + // Step 3: Iterate through the currentCodeLines with a nested loop to find the highest similarity between the lines in the currentCode and the findText. + let maxSimilarity = -1; + let maxSimilarityLineStartIndex = -1; + let maxSimilarityLineEndIndex = -1; + + const minLinesToReplace = Math.max( + 0, + findTextLines.length - lineNumTolerance, + ); + + for ( + let start = 0; + start < currentCodeLines.length - minLinesToReplace; + start++ + ) { + let maxLinesToReplace = minLinesToReplace + 3; // This will get enlarged + let lastSimilarity = 0; + for ( + let end = start + minLinesToReplace; + end <= Math.min(currentCodeLines.length, start + maxLinesToReplace); + end++ + ) { + const currentSlice = currentCodeLines.slice(start, end); + const similarity = similarityFunction(currentSlice, findTextLines); + + if (similarity > lastSimilarity) { + maxLinesToReplace += 1; + } + lastSimilarity = similarity; + + if (similarity > maxSimilarity && similarity >= similarityThreshold) { + maxSimilarity = similarity; + maxSimilarityLineStartIndex = start; + maxSimilarityLineEndIndex = end; + } + } + + await sleep(1); + } + + return { + lineStartIndex: maxSimilarityLineStartIndex, + lineEndIndex: maxSimilarityLineEndIndex, + confidence: maxSimilarity, + }; +} diff --git a/src/utils/code/fuzzyGetIndentationDifference.ts b/src/utils/code/fuzzyGetIndentationDifference.ts new file mode 100644 index 0000000..2e6de55 --- /dev/null +++ b/src/utils/code/fuzzyGetIndentationDifference.ts @@ -0,0 +1,26 @@ +import { checkStartWhiteSpace } from './regexUtils'; + +export function getIndentationDifference( + currentLine: string, + replaceTextLine: string, +) { + const currentIndent = checkStartWhiteSpace(currentLine); + const replaceTextIndent = checkStartWhiteSpace(replaceTextLine); + const indentDifference = currentIndent.slice( + 0, + currentIndent.length - replaceTextIndent.length, + ); + + return indentDifference; +} + +export function fuzzyGetIndentationDifference( + currentLine: string, + replaceTextLine: string, + similarityFunction: (a: string, b: string) => number, +) { + return { + confidence: similarityFunction(currentLine.trim(), replaceTextLine.trim()), + indent: getIndentationDifference(currentLine, replaceTextLine), + }; +} diff --git a/src/utils/code/fuzzyReplaceText.ts b/src/utils/code/fuzzyReplaceText.ts index 93a08fa..f6e39fc 100644 --- a/src/utils/code/fuzzyReplaceText.ts +++ b/src/utils/code/fuzzyReplaceText.ts @@ -1,11 +1,5 @@ -import { - applyIndent, - codeStringSimilarity, - equalsStringSimilarity, - levenshteinDistanceSimilarity, - removeIndent, -} from '../string/stringUtils'; -import { stripAllComments } from './stripAllComments'; +import { coreSimilarityFunction } from './coreSimilarityFunction'; +import { fuzzyReplaceTextInner } from './fuzzyReplaceTextInner'; export type SingleLineSimilarityFunction = ( original: string, @@ -18,522 +12,6 @@ export type MultiLineSimilarityFunction = ( const DEFAULT_SIMILARITY_THRESHOLD = 0.75; -function fuzzyGetIndentationDifference( - currentLine: string, - replaceTextLine: string, - similarityFunction: (a: string, b: string) => number, -) { - return { - confidence: similarityFunction(currentLine.trim(), replaceTextLine.trim()), - indent: getIndentationDifference(currentLine, replaceTextLine), - }; -} - -function getIndentationDifference( - currentLine: string, - replaceTextLine: string, -) { - const currentIndent = currentLine.match(/(^\s*)/)?.[1] || ''; - const replaceTextIndent = replaceTextLine.match(/(^\s*)/)?.[1] || ''; - const indentDifference = currentIndent.slice( - 0, - currentIndent.length - replaceTextIndent.length, - ); - - return indentDifference; -} - -function ignoreLeadingAndTrailingWhiteSpaceSimilariryunction( - currentLine: string, - replaceTextLine: string, - contentSimilarityFunction: (a: string, b: string) => number, -) { - const currentPrefix = currentLine.match(/(^\s*)/)?.[1] || ''; - const replaceTextPrefix = replaceTextLine.match(/(^\s*)/)?.[1] || ''; - - const currentPostfix = currentLine.match(/(\s*$)/)?.[1] || ''; - const replaceTextPostfix = replaceTextLine.match(/(\s*$)/)?.[1] || ''; - - const CONTENT_WEIGTH = 0.9; - const PREFIX_WEIGTH = 0.05; - const POSTFIX_WEIGTH = 0.05; - - return ( - contentSimilarityFunction(currentLine.trim(), replaceTextLine.trim()) * - CONTENT_WEIGTH + - equalsStringSimilarity(currentPrefix, replaceTextPrefix) * PREFIX_WEIGTH + - equalsStringSimilarity(currentPostfix, replaceTextPostfix) * POSTFIX_WEIGTH - ); -} - -function normalizeIndent(slice: string[]) { - if (slice.length === 0) { - return slice; - } - - const sliceNoIndent = removeIndent(slice); - - //Normalized form has the first line not copied in full - sliceNoIndent[0] = sliceNoIndent[0].trimStart(); - - return sliceNoIndent; -} -/** - * Try to guess identation from the current slice and replaceTextLines - */ -function findIndentationDifference( - currentSlice: string[], - replaceTextLines: string[], - similarityFunction: (a: string, b: string) => number, -) { - const indentationDifferences: number[] = []; - - for ( - let i = 0; - i < Math.min(currentSlice.length, replaceTextLines.length); - i++ - ) { - const replaceLine = replaceTextLines[i]; - const replaceIndentation = replaceLine.match(/^\s*/)?.[0].length || 0; - const currentLine = currentSlice[i]; - - const currentIndentation = currentLine.match(/^\s*/)?.[0].length || 0; - fuzzyGetIndentationDifference(currentLine, replaceLine, similarityFunction); - indentationDifferences.push(currentIndentation - replaceIndentation); - } - - const resultLines: string[] = []; - - for (let i = 0; i < replaceTextLines.length; i++) { - const indentation = ' '.repeat(Math.abs(indentationDifferences[0])); - resultLines.push(indentation); - } - resultLines.sort((a, b) => b.length - a.length); - - return resultLines; -} - -export function exactLinesSimilarityAndMap( - original: string[], - find: string[], - lineSimilarityFunction: SingleLineSimilarityFunction, - mapFindLine: (original: string | undefined, findLine: string) => string = ( - original, - findLine, - ) => findLine, -): { similiarity: number; mappedFind: string[] } { - const mappedFind: string[] = []; - let originalLine = 0; - let findLine = 0; - - let originalSimilarityLines = 0; - - function lineSkippedValue(line: string) { - const baseSkippedValue = 0.02; - const scalableSkippedValue = 0.98; - const skipScaling = 1 - 1 / (1 + line.trim().length); - - return baseSkippedValue + scalableSkippedValue * skipScaling; - } - - const options = [ - { - condition: () => originalLine < original.length && findLine < find.length, - simiarity: () => - lineSimilarityFunction(original[originalLine], find[findLine]), - skippedOriginalLines: () => 0, - skippedFindLines: () => 0, - apply: () => { - mappedFind.push(mapFindLine(original[originalLine], find[findLine])); - originalLine++; - findLine++; - originalSimilarityLines++; - }, - }, - (() => { - const skippedOriginalLines = 1; - - return { - condition: () => - originalLine + skippedOriginalLines < original.length && - findLine < find.length, - simiarity: () => - lineSimilarityFunction( - original[originalLine + skippedOriginalLines], - find[findLine], - ), - skippedOriginalLines: () => skippedOriginalLines, - skippedFindLines: () => 0, - apply: () => { - mappedFind.push( - mapFindLine( - original[originalLine + skippedOriginalLines], - find[findLine], - ), - ); - - originalLine++; - findLine++; - originalSimilarityLines++; - - originalLine += skippedOriginalLines; - }, - }; - })(), - (() => { - const skippedFindLines = 1; - - return { - condition: () => - originalLine < original.length && - findLine + skippedFindLines < find.length, - simiarity: () => - lineSimilarityFunction( - original[originalLine], - find[findLine + skippedFindLines], - ), - skippedOriginalLines: () => 0, - skippedFindLines: () => skippedFindLines, - apply: () => { - for (let i = 0; i < skippedFindLines; i++) { - mappedFind.push(mapFindLine(undefined, find[findLine + i])); - } - mappedFind.push( - mapFindLine( - original[originalLine], - find[findLine + skippedFindLines], - ), - ); - - originalLine++; - findLine++; - originalSimilarityLines++; - - findLine += skippedFindLines; - }, - }; - })(), - { - condition: () => - originalLine < original.length && findLine >= find.length, - simiarity: () => 0, - skippedOriginalLines: () => 1, - skippedFindLines: () => 0, - apply: () => { - originalLine++; - }, - }, - { - condition: () => - originalLine >= original.length && findLine < find.length, - simiarity: () => 0, - skippedOriginalLines: () => 0, - skippedFindLines: () => 1, - apply: () => { - mappedFind.push(mapFindLine(undefined, find[findLine])); - findLine++; - }, - }, - ]; - - let similaritySum = 0; - let linesSkipped = 0; - - // eslint-disable-next-line no-constant-condition - while (true) { - let bestOption; - let bestSimialrity = Number.MIN_SAFE_INTEGER; - - for (const option of options) { - if (option.condition()) { - const similarity = option.simiarity(); - - if (isNaN(similarity)) { - throw new Error('similarity is NaN'); - } - - if (similarity > bestSimialrity) { - bestSimialrity = similarity; - bestOption = option; - } - } - } - - if (bestOption === undefined) { - break; - } - - for ( - let orgIndex = 0; - orgIndex < bestOption.skippedOriginalLines(); - orgIndex++ - ) { - linesSkipped += lineSkippedValue(original[originalLine + orgIndex]); - } - - for ( - let findIndex = 0; - findIndex < bestOption.skippedFindLines(); - findIndex++ - ) { - linesSkipped += lineSkippedValue(find[findLine + findIndex]); - } - - bestOption.apply(); - similaritySum += bestSimialrity; - } - - if (original.length === 0 && find.length === 0) { - return { similiarity: 1, mappedFind }; - } - - if (original.length === 0 && find.length !== 0) { - return { similiarity: 0, mappedFind }; - } - - const averageSimilarity = - originalSimilarityLines === 0 ? 1 : similaritySum / originalSimilarityLines; - const noSkipRatio = - (3 * (1 - linesSkipped / original.length) + 1 * (1 / (1 + linesSkipped))) / - 4; - - return { similiarity: averageSimilarity * noSkipRatio, mappedFind }; -} - -export const coreSimilarityFunction = ( - original: string[], - replacement: string[], -) => { - if (original.join('\n') === replacement.join('\n')) { - return 1; - } - - const similartyWithWsDistance = exactLinesSimilarityAndMap( - original, - replacement, - (a, b) => - ignoreLeadingAndTrailingWhiteSpaceSimilariryunction( - a, - b, - codeStringSimilarity, - ), - ).similiarity; - - const similarityNotIgnoringWhitespace = exactLinesSimilarityAndMap( - normalizeIndent(stripAllComments(original)), - normalizeIndent(stripAllComments(replacement)), - levenshteinDistanceSimilarity, - ).similiarity; - - const core = Math.max( - similartyWithWsDistance, - similarityNotIgnoringWhitespace, - ); - - const similarity = - 0.6 * core + - 0.2 * similartyWithWsDistance + - 0.2 * similarityNotIgnoringWhitespace; - - if (isNaN(similarity)) { - throw new Error('similarity is NaN'); - } - - { - // Just for testing - - const similartyWithWsDistance = exactLinesSimilarityAndMap( - original, - replacement, - (a, b) => - ignoreLeadingAndTrailingWhiteSpaceSimilariryunction( - a, - b, - codeStringSimilarity, - ), - ).similiarity; - - const similarityNotIgnoringWhitespace = exactLinesSimilarityAndMap( - normalizeIndent(stripAllComments(original)), - normalizeIndent(stripAllComments(replacement)), - levenshteinDistanceSimilarity, - ).similiarity; - } - - return similarity; -}; - -export async function fuzzyFindText({ - currentCode, - findText, - similarityFunction = coreSimilarityFunction, - lineNumTolerance = Math.ceil(findText.split('\n').length * 0.05), - similarityThreshold = DEFAULT_SIMILARITY_THRESHOLD, -}: { - currentCode: string; - findText: string; - similarityFunction?: (original: string[], replacement: string[]) => number; - lineNumTolerance?: number; - similarityThreshold?: number; -}): Promise<{ - lineStartIndex: number; - lineEndIndex: number; - confidence: number; -}> { - const currentCodeLines = currentCode.split('\n'); - const findTextLines = findText.split('\n'); - - // Step 3: Iterate through the currentCodeLines with a nested loop to find the highest similarity between the lines in the currentCode and the findText. - let maxSimilarity = -1; - let maxSimilarityLineStartIndex = -1; - let maxSimilarityLineEndIndex = -1; - - const minLinesToReplace = Math.max( - 0, - findTextLines.length - lineNumTolerance, - ); - - for ( - let start = 0; - start < currentCodeLines.length - minLinesToReplace; - start++ - ) { - let maxLinesToReplace = minLinesToReplace + 3; // This will get enlarged - let lastSimilarity = 0; - for ( - let end = start + minLinesToReplace; - end <= Math.min(currentCodeLines.length, start + maxLinesToReplace); - end++ - ) { - const currentSlice = currentCodeLines.slice(start, end); - const similarity = similarityFunction(currentSlice, findTextLines); - - if (similarity > lastSimilarity) { - maxLinesToReplace += 1; - } - lastSimilarity = similarity; - - if (similarity > maxSimilarity && similarity >= similarityThreshold) { - //console.log(`sim: ${similarity} start: ${start} end: ${end} minLinesToReplace: ${minLinesToReplace} maxLinesToReplace: ${maxLinesToReplace}`); - - maxSimilarity = similarity; - maxSimilarityLineStartIndex = start; - maxSimilarityLineEndIndex = end; - } - } - - await new Promise((resolve) => { - setTimeout(resolve, 1); - }); - } - - return { - lineStartIndex: maxSimilarityLineStartIndex, - lineEndIndex: maxSimilarityLineEndIndex, - confidence: maxSimilarity, - }; -} - -export async function fuzzyReplaceTextInner({ - currentCode, - findText, - withText, - similarityFunction = coreSimilarityFunction, - similarityThreshold = DEFAULT_SIMILARITY_THRESHOLD, -}: { - currentCode: string; - findText: string; - withText: string; - similarityFunction?: MultiLineSimilarityFunction; - similarityThreshold?: number; - lineNumTolerance?: number; -}) { - const { - lineStartIndex: startIndex, - lineEndIndex: endIndex, - confidence, - } = await fuzzyFindText({ - currentCode, - findText, - similarityFunction, - similarityThreshold, - }); - - if (confidence >= similarityThreshold) { - const currentCodeLines = currentCode.split('\n'); - - const currentSlice = currentCodeLines.slice(startIndex, endIndex); - const findTextLines = findText.split('\n'); - const withTextLines = withText.split('\n'); - - function mapFindWithIndent( - originalLine: string | undefined, - searchLine: string, - ) { - if (originalLine === undefined) { - return lastIndent + searchLine; - } - const indentDiff = getIndentationDifference(originalLine, searchLine); - lastIndent = indentDiff; - - return indentDiff + searchLine; - } - - let lastIndent = ''; - - const indentAdjustedFindLines = exactLinesSimilarityAndMap( - currentSlice, - findTextLines, - (a, b) => levenshteinDistanceSimilarity(a, b), - mapFindWithIndent, - ).mappedFind; - - lastIndent = ''; - - //split the withTextLines into a segment containing the segment up to first non empty first line and a segment containing the rest - const withTextUpToFirstNonEmptyLine = withTextLines.slice(0, 1); - const withTextRest = withTextLines.slice(1); - - const indentAdjustedFindLinesUpToFirstNonEmptyLine = - indentAdjustedFindLines.slice(0, 1); - const indentAdjustedFindLinesRest = indentAdjustedFindLines.slice(1); - - const indentAdjustedWithTextupToFirstNonEmptyLine = - exactLinesSimilarityAndMap( - indentAdjustedFindLinesUpToFirstNonEmptyLine, - withTextUpToFirstNonEmptyLine, - (a, b) => levenshteinDistanceSimilarity(a, b), - mapFindWithIndent, - ).mappedFind; - - const overalIndentDifference = - findIndentationDifference( - currentSlice, - withTextLines, - equalsStringSimilarity, - ) || ''; - const indentAdjustedWithTextRest = applyIndent( - withTextRest, - overalIndentDifference, - ); - const indentAdjustedWithLines = [ - ...indentAdjustedWithTextupToFirstNonEmptyLine, - ...indentAdjustedWithTextRest, - ]; - - const adjustedWithText = indentAdjustedWithLines.join('\n'); - - const preChange = currentCodeLines.slice(0, startIndex).join('\n'); - const postChange = currentCodeLines.slice(endIndex).join('\n'); - - return [ - preChange + (preChange ? '\n' : ''), - adjustedWithText, - (postChange ? '\n' : '') + postChange, - ]; - } -} - export async function fuzzyReplaceText({ currentCode, findText, diff --git a/src/utils/code/fuzzyReplaceTextInner.ts b/src/utils/code/fuzzyReplaceTextInner.ts new file mode 100644 index 0000000..2301c9c --- /dev/null +++ b/src/utils/code/fuzzyReplaceTextInner.ts @@ -0,0 +1,113 @@ +import { + applyIndent, + equalsStringSimilarity, + levenshteinDistanceSimilarity, +} from '../string/stringUtils'; +import { coreSimilarityFunction } from './coreSimilarityFunction'; +import { exactLinesSimilarityAndMap } from './exactLinesSimilarityAndMap'; +import { findIndentationDifference } from './findIndentationDifference'; +import { fuzzyFindText } from './fuzzyFindText'; +import { getIndentationDifference } from './fuzzyGetIndentationDifference'; +import { MultiLineSimilarityFunction } from './fuzzyReplaceText'; + +const DEFAULT_SIMILARITY_THRESHOLD = 0.75; + +export async function fuzzyReplaceTextInner({ + currentCode, + findText, + withText, + similarityFunction = coreSimilarityFunction, + similarityThreshold = DEFAULT_SIMILARITY_THRESHOLD, +}: { + currentCode: string; + findText: string; + withText: string; + similarityFunction?: MultiLineSimilarityFunction; + similarityThreshold?: number; + lineNumTolerance?: number; +}) { + const { + lineStartIndex: startIndex, + lineEndIndex: endIndex, + confidence, + } = await fuzzyFindText({ + currentCode, + findText, + similarityFunction, + similarityThreshold, + }); + + if (confidence >= similarityThreshold) { + const currentCodeLines = currentCode.split('\n'); + + const currentSlice = currentCodeLines.slice(startIndex, endIndex); + const findTextLines = findText.split('\n'); + const withTextLines = withText.split('\n'); + + function mapFindWithIndent( + originalLine: string | undefined, + searchLine: string, + ) { + if (originalLine === undefined) { + return lastIndent + searchLine; + } + const indentDiff = getIndentationDifference(originalLine, searchLine); + lastIndent = indentDiff; + + return indentDiff + searchLine; + } + + let lastIndent = ''; + + const indentAdjustedFindLines = exactLinesSimilarityAndMap( + currentSlice, + findTextLines, + (a, b) => levenshteinDistanceSimilarity(a, b), + mapFindWithIndent, + ).mappedFind; + + lastIndent = ''; + + //split the withTextLines into a segment containing the segment up to first non empty first line and a segment containing the rest + const withTextUpToFirstNonEmptyLine = withTextLines.slice(0, 1); + const withTextRest = withTextLines.slice(1); + + const indentAdjustedFindLinesUpToFirstNonEmptyLine = + indentAdjustedFindLines.slice(0, 1); + const indentAdjustedFindLinesRest = indentAdjustedFindLines.slice(1); + + const indentAdjustedWithTextualToFirstNonEmptyLine = + exactLinesSimilarityAndMap( + indentAdjustedFindLinesUpToFirstNonEmptyLine, + withTextUpToFirstNonEmptyLine, + (a, b) => levenshteinDistanceSimilarity(a, b), + mapFindWithIndent, + ).mappedFind; + + const overallIndentDifference = + findIndentationDifference( + currentSlice, + withTextLines, + equalsStringSimilarity, + ) || ''; + const indentAdjustedWithTextRest = applyIndent( + withTextRest, + overallIndentDifference, + ); + const indentAdjustedWithLines = [ + ...indentAdjustedWithTextualToFirstNonEmptyLine, + ...indentAdjustedWithTextRest, + ]; + + const adjustedWithText = indentAdjustedWithLines.join('\n'); + + const preChange = currentCodeLines.slice(0, startIndex).join('\n'); + const postChange = currentCodeLines.slice(endIndex).join('\n'); + + return [ + preChange + (preChange ? '\n' : ''), + adjustedWithText, + (postChange ? '\n' : '') + postChange, + ]; + } +} diff --git a/src/utils/code/ignoreLeadingAndTrailingWhiteSpaceSimilarityFunction.ts b/src/utils/code/ignoreLeadingAndTrailingWhiteSpaceSimilarityFunction.ts new file mode 100644 index 0000000..ce19abd --- /dev/null +++ b/src/utils/code/ignoreLeadingAndTrailingWhiteSpaceSimilarityFunction.ts @@ -0,0 +1,25 @@ +import { equalsStringSimilarity } from '../string/stringUtils'; +import { checkEndWhiteSpace, checkStartWhiteSpace } from './regexUtils'; + +export function ignoreLeadingAndTrailingWhiteSpaceSimilarityFunction( + currentLine: string, + replaceTextLine: string, + contentSimilarityFunction: (a: string, b: string) => number, +) { + const currentPrefix = checkStartWhiteSpace(currentLine); + const replaceTextPrefix = checkStartWhiteSpace(replaceTextLine); + + const currentPostfix = checkEndWhiteSpace(currentLine); + const replaceTextPostfix = checkEndWhiteSpace(replaceTextLine); + + const CONTENT_WEIGHT = 0.9; + const PREFIX_WEIGHT = 0.05; + const POSTFIX_WEIGHT = 0.05; + + return ( + contentSimilarityFunction(currentLine.trim(), replaceTextLine.trim()) * + CONTENT_WEIGHT + + equalsStringSimilarity(currentPrefix, replaceTextPrefix) * PREFIX_WEIGHT + + equalsStringSimilarity(currentPostfix, replaceTextPostfix) * POSTFIX_WEIGHT + ); +} diff --git a/src/utils/code/normalizeIndent.ts b/src/utils/code/normalizeIndent.ts new file mode 100644 index 0000000..992b7c7 --- /dev/null +++ b/src/utils/code/normalizeIndent.ts @@ -0,0 +1,14 @@ +import { removeIndent } from '../string/stringUtils'; + +export function normalizeIndent(slice: string[]) { + if (slice.length === 0) { + return slice; + } + + const sliceNoIndent = removeIndent(slice); + + //Normalized form has the first line not copied in full + sliceNoIndent[0] = sliceNoIndent[0].trimStart(); + + return sliceNoIndent; +} diff --git a/src/utils/code/regexUtils.ts b/src/utils/code/regexUtils.ts new file mode 100644 index 0000000..131bd80 --- /dev/null +++ b/src/utils/code/regexUtils.ts @@ -0,0 +1,6 @@ +export const checkStartWhiteSpace = (text: string) => + text.match(/^\s*/)?.[0] || ''; +export const checkEndWhiteSpace = (text: string) => + text.match(/(\s*$)/)?.[1] || ''; +export const checkIndentation = (text: string) => + text.match(/^\s*/)?.[0].length || 0; diff --git a/src/utils/random/getRandomInt.ts b/src/utils/random/getRandomInt.ts index a886a7e..ef8ddd2 100644 --- a/src/utils/random/getRandomInt.ts +++ b/src/utils/random/getRandomInt.ts @@ -1,6 +1,6 @@ -export function getRandomInt(_min: number, _max: number, random = Math.random) { - const min = Math.ceil(_min); - const max = Math.floor(_max); +export function getRandomInt(min: number, max: number, random = Math.random) { + const ceiledMin = Math.ceil(min); + const flooredMax = Math.floor(max); - return Math.floor(random() * (max - min + 1)) + min; + return Math.floor(random() * (flooredMax - ceiledMin + 1)) + ceiledMin; } diff --git a/src/utils/random/uniqueWeightedRandomElements.ts b/src/utils/random/uniqueWeightedRandomElements.ts index 92a6023..bf30cb3 100644 --- a/src/utils/random/uniqueWeightedRandomElements.ts +++ b/src/utils/random/uniqueWeightedRandomElements.ts @@ -4,19 +4,19 @@ import { weightedRandomElement } from './weightedRandomElement'; export function uniqueWeightedRandomElements( options: T[], attr: KeyOfType, - _count: number, + count: number, random = Math.random, ): T[] { const ret: T[] = []; - const count = Math.max(0, _count); + const maximumCount = Math.max(0, count); let remainingOptions = options.slice(); - while (ret.length < count && remainingOptions.length > 0) { + while (ret.length < maximumCount && remainingOptions.length > 0) { ret.push(weightedRandomElement(remainingOptions, attr, random)); remainingOptions = options.filter((o) => !ret.includes(o)); } - if (ret.length !== count) { + if (ret.length !== maximumCount) { throw new Error( 'Something wrong with the options, unable to generate viable result', ); diff --git a/src/utils/random/weightedRandomElements.ts b/src/utils/random/weightedRandomElements.ts index 31c6633..31d4fc1 100644 --- a/src/utils/random/weightedRandomElements.ts +++ b/src/utils/random/weightedRandomElements.ts @@ -4,17 +4,17 @@ import { weightedRandomElement } from './weightedRandomElement'; export function weightedRandomElements( options: T[], attr: KeyOfType, - _count: number, + count: number, random = Math.random, ): T[] { const ret: T[] = []; - const count = Math.max(0, _count); + const maximumCount = Math.max(0, count); - while (ret.length < count && options.length > 0) { + while (ret.length < maximumCount && options.length > 0) { ret.push(weightedRandomElement(options, attr, random)); } - if (ret.length !== count) { + if (ret.length !== maximumCount) { throw new Error( 'Something wrong with the options, unable to generate viable result', ); diff --git a/src/utils/sleep.ts b/src/utils/sleep.ts new file mode 100644 index 0000000..e45ce8f --- /dev/null +++ b/src/utils/sleep.ts @@ -0,0 +1,4 @@ +export const sleep = async (ms: number) => + await new Promise((resolve) => { + setTimeout(resolve, ms); + });