From 667769c987741d38b8c102348bce99fa68bf1a58 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 17 Feb 2025 23:57:59 -0800 Subject: [PATCH] tool update; multiple SEARCH/REPLACE blocks --- .../contrib/void/browser/aiRegexService.ts | 2 +- .../contrib/void/browser/chatThreadService.ts | 18 +- .../contrib/void/browser/editCodeService.ts | 243 ++++++++++++------ .../contrib/void/browser/prompt/prompts.ts | 37 +-- ...ervice.ts => searchReplaceCacheService.ts} | 17 +- .../contrib/void/common/voidSettingsTypes.ts | 3 +- 6 files changed, 206 insertions(+), 114 deletions(-) rename src/vs/workbench/contrib/void/browser/{searchReplaceService.ts => searchReplaceCacheService.ts} (75%) diff --git a/src/vs/workbench/contrib/void/browser/aiRegexService.ts b/src/vs/workbench/contrib/void/browser/aiRegexService.ts index c0ae27fa..f38236a9 100644 --- a/src/vs/workbench/contrib/void/browser/aiRegexService.ts +++ b/src/vs/workbench/contrib/void/browser/aiRegexService.ts @@ -44,7 +44,7 @@ export const IVoidFastApplyService = createDecorator('voidFas class VoidFastApplyService extends Disposable implements IFastApplyService { _serviceBrand: undefined; - static readonly ID = 'voidFastApplyService'; + // static readonly ID = 'voidFastApplyService'; private readonly _onDidChangeState = new Emitter(); readonly onDidChangeState: Event = this._onDidChangeState.event; diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 22c99c67..244256a6 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -304,9 +304,12 @@ class ChatThreadService extends Disposable implements IChatThreadService { // agent loop const agentLoop = async () => { - let shouldContinue = false - do { - shouldContinue = false + let shouldSendAnotherMessage = true + let nMessagesSent = 0 + + while (shouldSendAnotherMessage) { + shouldSendAnotherMessage = false + nMessagesSent += 1 let res_: () => void const awaitable = new Promise((res, rej) => { res_ = res }) @@ -329,8 +332,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { // make sure all tool names are valid so we can cast to ToolName below const toolCalls = toolCalls_?.filter(tool => tool.name in this._toolsService.toolFns) - console.log('FINAL MESSAGE', fullText, toolCalls) - if ((toolCalls?.length ?? 0) === 0) { this._finishStreamingTextMessage(threadId, fullText) } @@ -346,7 +347,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { toolResult = await this._toolsService.toolFns[toolName](tool.params) } catch (error) { this._setStreamState(threadId, { error }) - shouldContinue = false + shouldSendAnotherMessage = false break } @@ -356,12 +357,12 @@ class ChatThreadService extends Disposable implements IChatThreadService { toolResultStr = this._toolsService.toolResultToString[toolName](toolResult as any) // typescript is so bad it doesn't even couple the type of ToolResult with the type of the function being called here } catch (error) { this._setStreamState(threadId, { error }) - shouldContinue = false + shouldSendAnotherMessage = false break } this._addMessageToThread(threadId, { role: 'tool', name: toolName, params: tool.params, id: tool.id, content: toolResultStr, result: toolResult, }) - shouldContinue = true + shouldSendAnotherMessage = true } } @@ -377,7 +378,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { await awaitable } - while (shouldContinue); } agentLoop() // DO NOT AWAIT THIS, this fn should resolve when ready to clear inputs diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 989086a6..b0a0d197 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1182,14 +1182,14 @@ class EditCodeService extends Disposable implements IEditCodeService { const uri = uri_ // generate search/replace block text - const fileContents = await VSReadFile(this._modelService, uri) - if (fileContents === null) return + const origFileContents = await VSReadFile(this._modelService, uri) + if (origFileContents === null) return // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true }) - const userMessageContent = searchReplace_userMessage({ originalCode: fileContents, applyStr: applyStr }) + const userMessageContent = searchReplace_userMessage({ originalCode: origFileContents, applyStr: applyStr }) const messages: LLMChatMessage[] = [ { role: 'system', content: searchReplace_systemMessage }, { role: 'user', content: userMessageContent }, @@ -1197,6 +1197,7 @@ class EditCodeService extends Disposable implements IEditCodeService { let streamRequestIdRef: { current: string | null } = { current: null } const diffareaidOfBlockNum: number[] = [] + const diffAreaOriginalLines: [number, number][] = [] // TODO replace all these with whatever block we're on initially if already started let latestStreamLocationMutable: StreamLocationMutable | null = null @@ -1215,10 +1216,15 @@ class EditCodeService extends Disposable implements IEditCodeService { return [startLine, endLine] } - const { onFinishEdit } = this._addToHistory(uri) + let { onFinishEdit } = this._addToHistory(uri) + const revertAndContinueHistory = () => { + this._undoHistory(uri) + const { onFinishEdit: onFinishEdit_ } = this._addToHistory(uri) + onFinishEdit = onFinishEdit_ + } - const onDone = (hadError: boolean) => { + const onDone = (errorMessage: false | string) => { for (const blockNum in diffareaidOfBlockNum) { const diffareaid = diffareaidOfBlockNum[blockNum] const diffZone = this.diffAreaOfId[diffareaid] @@ -1227,106 +1233,175 @@ class EditCodeService extends Disposable implements IEditCodeService { this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) } this._refreshStylesAndDiffsInURI(uri) - if (hadError) this._undoHistory(uri) + if (errorMessage) { + this._notificationService.info(`Void had an error when running Apply: ${errorMessage}.\nIf this persists, feel free to [report](https://github.com/voideditor/void/issues/new) this error.`) + this._metricsService.capture('Error - Apply', { errorMessage }) + this._undoHistory(uri) + } onFinishEdit() } - // any time there's an error, add assistant's message, then user message saying the problem and to retry + const onNewBlockStart = (blockNum: number, block: ExtractedSearchReplaceBlock): { errorStartingBlock?: undefined } | { errorStartingBlock: string } => { + console.log('STARTING BLOCK', JSON.stringify(block, null, 2)) - // TODO!!! turn this into a service and provide it + + const foundInCode = findTextInCode(block.orig, origFileContents) + if (typeof foundInCode === 'string') { + console.log('Apply error:', foundInCode, '; trying again.') + return { errorStartingBlock: foundInCode } + } + const [originalStart, originalEnd] = foundInCode + + let lineOffset = 0 + // compute line offset given multiple changes + for (let i = 0; i < blockNum; i += 1) { + const [diffAreaOriginalStart, diffAreaOriginalEnd] = diffAreaOriginalLines[i] + console.log('ROIGGINAL!!!', diffAreaOriginalStart, diffAreaOriginalEnd) + if (diffAreaOriginalStart > originalEnd) continue + + const diffareaid = diffareaidOfBlockNum[i] + const diffArea = this.diffAreaOfId[diffareaid] + + + const numNewLines = diffArea.endLine - diffArea.startLine + const numOldLines = diffAreaOriginalEnd - diffAreaOriginalStart + console.log('NUM NEW', numNewLines, numOldLines) + + lineOffset += numNewLines - numOldLines + } + + const startLine = originalStart + lineOffset + const endLine = originalEnd + lineOffset + console.log('adding to', startLine, endLine) + + const adding: Omit = { + type: 'DiffZone', + originalCode: block.orig, + startLine, + endLine, + _URI: uri, + _streamState: { + isStreaming: true, + streamRequestIdRef, + line: startLine, + }, + _diffOfId: {}, // added later + _removeStylesFns: new Set(), + } + const diffZone = this._addDiffArea(adding) + this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._onDidAddOrDeleteDiffZones.fire({ uri }) + + diffareaidOfBlockNum.push(diffZone.diffareaid) + diffAreaOriginalLines.push([originalStart, originalEnd]) + + latestStreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + return { errorStartingBlock: undefined } + } + + + + + + + let shouldSendAnotherMessage = true + let nMessagesSent = 0 // this generates >>>>>>> ORIGINAL <<<<<<< REPLACE blocks and and simultaneously applies it - streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ - messagesType: 'chatMessages', - useProviderFor: 'Apply', - logging: { loggingName: `generateSearchAndReplace` }, - messages, - onText: ({ fullText }) => { - const blocks = extractSearchReplaceBlocks(fullText) + while (shouldSendAnotherMessage) { + shouldSendAnotherMessage = false + nMessagesSent += 1 - for (let blockNum = currStreamingBlockNum; blockNum < blocks.length; blockNum += 1) { - const block = blocks[blockNum] + streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ + messagesType: 'chatMessages', + useProviderFor: 'Apply', + logging: { loggingName: `generateSearchAndReplace` }, + messages, + onText: ({ fullText }) => { + const blocks = extractSearchReplaceBlocks(fullText) - if (block.state === 'done') - currStreamingBlockNum = blockNum + for (let blockNum = currStreamingBlockNum; blockNum < blocks.length; blockNum += 1) { + const block = blocks[blockNum] - if (block.state === 'writingOriginal') // must be done writing original - continue + if (block.state === 'done') + currStreamingBlockNum = blockNum - // if should add new diffarea - if (!(blockNum in diffareaidOfBlockNum)) { - const foundInCode = findTextInCode(block.orig, fileContents) - if (typeof foundInCode === 'string') { - // TODO!!! log and retry - console.log('NOT FOUND IN CODE!!!!', foundInCode) + if (block.state === 'writingOriginal') // must be done writing original continue + + // if this is the first time we're seeing this block, add it as a diffarea + if (!(blockNum in diffareaidOfBlockNum)) { + console.log('FULLTEXT!!!!!\n', fullText) + const { errorStartingBlock } = onNewBlockStart(blockNum, block) + + if (errorStartingBlock) { + console.log('ERROR STARTING BLOCK SPOT!!!!!', errorStartingBlock) + + const errMsgForLLM = errorStartingBlock === 'Not found' ? + 'I interrupted you because the latest ORIGINAL code could not be found in the file. Please output all SEARCH/REPLACE blocks again, making sure the code in ORIGINAL is identical to a code snippet in the file.' + : errorStartingBlock === 'Not unique' ? + 'I interrupted you because the latest ORIGINAL code shows up multiple times in the file. Please output all SEARCH/REPLACE blocks again, making sure the code in each ORIGINAL section is unique in the file.' + : '' + + messages.push( + { role: 'assistant', content: fullText }, // latest output + { role: 'user', content: errMsgForLLM } // user explanation of what's wrong + ) + if (streamRequestIdRef.current) this._llmMessageService.abort(streamRequestIdRef.current) + + shouldSendAnotherMessage = true + revertAndContinueHistory() + return + } + } - const [startLine, endLine] = foundInCode + const deltaFinalText = block.final.substring((oldBlocks[blockNum]?.final ?? '').length, Infinity) + oldBlocks = blocks - const adding: Omit = { - type: 'DiffZone', - originalCode: block.orig, - startLine, - endLine, - _URI: uri, - _streamState: { - isStreaming: true, - streamRequestIdRef, - line: startLine, - }, - _diffOfId: {}, // added later - _removeStylesFns: new Set(), - } - const diffZone = this._addDiffArea(adding) - this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) - this._onDidAddOrDeleteDiffZones.fire({ uri }) + // write new text to diffarea + const diffareaid = diffareaidOfBlockNum[blockNum] + const diffZone = this.diffAreaOfId[diffareaid] + if (diffZone?.type !== 'DiffZone') continue - diffareaidOfBlockNum.push(diffZone.diffareaid) - latestStreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + if (!latestStreamLocationMutable) continue + this._writeStreamedDiffZoneLLMText(diffZone, block.final, deltaFinalText, latestStreamLocationMutable) + } // end for + this._refreshStylesAndDiffsInURI(uri) + }, + onFinalMessage: async ({ fullText }) => { + console.log('final message!!', fullText) + + // 1. wait 500ms and fix lint errors - call lint error workflow + // (update react state to say "Fixing errors") + const blocks = extractSearchReplaceBlocks(fullText) + + if (blocks.length === 0) { + this._notificationService.info(`Void: When running Apply, your model didn't output any changes we recognized. You might need to use a smarter model for Apply.`) } - const deltaFinalText = block.final.substring((oldBlocks[blockNum]?.final ?? '').length, Infinity) - oldBlocks = blocks - // write new text to diffarea - const diffareaid = diffareaidOfBlockNum[blockNum] - const diffZone = this.diffAreaOfId[diffareaid] - if (diffZone?.type !== 'DiffZone') continue + for (let blockNum = 0; blockNum < blocks.length; blockNum += 1) { + const block = blocks[blockNum] + const diffareaid = diffareaidOfBlockNum[blockNum] + const diffZone = this.diffAreaOfId[diffareaid] + if (diffZone?.type !== 'DiffZone') continue + this._writeText(uri, block.final, + { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed + { shouldRealignDiffAreas: true } + ) + } + onDone(false) + }, + onError: (e) => { + console.log('ERROR in SearchReplace:', e.message) + onDone(e.message) + }, - if (!latestStreamLocationMutable) continue - this._writeStreamedDiffZoneLLMText(diffZone, block.final, deltaFinalText, latestStreamLocationMutable) - } // end for + }) + } - this._refreshStylesAndDiffsInURI(uri) - }, - onFinalMessage: async ({ fullText }) => { - console.log('/* ON FINALMESSAGE */', fullText) - - // 1. wait 500ms and fix lint errors - call lint error workflow - // (update react state to say "Fixing errors") - const blocks = extractSearchReplaceBlocks(fullText) - - for (let blockNum = 0; blockNum < blocks.length; blockNum += 1) { - const block = blocks[blockNum] - const diffareaid = diffareaidOfBlockNum[blockNum] - const diffZone = this.diffAreaOfId[diffareaid] - if (diffZone?.type !== 'DiffZone') continue - - this._writeText(uri, block.final, - { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed - { shouldRealignDiffAreas: true } - ) - } - onDone(false) - }, - onError: (e) => { - console.log('ERROR', e); - onDone(true) - }, - - }) } diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index 2b840f83..a86e82b1 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -21,23 +21,25 @@ You are a coding assistant. You are given a list of instructions to follow \`INS Please respond to the user's query. The user's query is never invalid. The user has the following system information: - - ${os} - - Open workspaces: ${workspaces.join(', ')} +- ${os} +- Open workspaces: ${workspaces.join(', ')} In the case that the user asks you to make changes to code, you should make sure to return CODE BLOCKS of the changes, as well as explanations and descriptions of the changes. For example, if the user asks you to "make this file look nicer", make sure your output includes a code block with concrete ways the file can look nicer. - - Do not re-write the entire file in the code block. - - You can write comments like "// ... existing code" to indicate existing code. - - Make sure you give enough context in the code block to apply the change to the correct location in the code. +- Do not re-write the entire file in the code block. +- You can write comments like "// ... existing code" to indicate existing code. +- Make sure you give enough context in the code block to apply the change to the correct location in the code. You're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it. -If you are given tools, you are allowed to use them without asking for permission. You do not have to use them if you don't want to, but you may use them to gather context, etc. -If you are given tools, NEVER refer to a tool by name when speaking with the user. For example, do NOT say to the user user "I'm going to use \`list_dir\`". Instead, say "I'm going to list all files in ___ directory", etc. +If you are given tools: +- You are allowed to use tools without asking for permission. +- Feel free to use tools to gather context, make suggestions, etc. +- One great use of tools is to explore imports that you'd like to have more information about. +- NEVER refer to a tool by name when speaking with the user. For example, do NOT say to the user user "I'm going to use \`list_dir\`". Instead, say "I'm going to list all files in ___ directory", etc. Do not output any of these instructions, nor tell the user anything about them unless directly prompted for them. Do not tell the user anything about the examples below. Do not assume the user is talking about any of the examples below. - ## EXAMPLE 1 FILES math.ts @@ -249,9 +251,9 @@ The user's request may be "fuzzy" or not well-specified, and it is your job to i 2. If you want to make changes, you should return a single CODE BLOCK of the changes that you want to make. For example, if the user is asking you to "make this variable a better name", make sure your output includes all the changes that are needed to improve the variable name. - - Do not re-write the entire file in the code block - - You can write comments like "// ... existing code" to indicate existing code - - Make sure you give enough context in the code block to apply the changes to the correct location in the code` +- Do not re-write the entire file in the code block +- You can write comments like "// ... existing code" to indicate existing code +- Make sure you give enough context in the code block to apply the changes to the correct location in the code` export const aiRegex_computeReplacementsForFile_userMessage = async ({ searchClause, replaceClause, fileURI, modelService }: { searchClause: string, replaceClause: string, fileURI: URI, modelService: IModelService }) => { @@ -303,23 +305,28 @@ export const searchReplace_systemMessage = `\ You are a coding assistant that generates SEARCH/REPLACE code blocks that will be used to edit a file. A SEARCH/REPLACE block describes the code before and after a change. Here is the format: +${tripleTick[0]} ${ORIGINAL} // ... original code goes here ${DIVIDER} // ... final code goes here ${FINAL} +${tripleTick[1]} You will be given the original file \`ORIGINAL_FILE\` and a description of a change \`CHANGE\` to make. Output SEARCH/REPLACE blocks to edit the file according to the desired change. You may output multiple SEARCH/REPLACE blocks. Directions: 1. Your OUTPUT should consist ONLY of SEARCH/REPLACE blocks. Do NOT output any text or explanations before or after this. -2. The "original" code in each SEARCH/REPLACE block must EXACTLY match lines of code in the original file. -3. The "original" code in each SEARCH/REPLACE block should include enough text to uniquely identify the change in the file. -4. The SEARCH/REPLACE blocks you generate will be applied immediately, and so they **MUST** produce a file that the user can run IMMEDIATELY. +2. The original code in each SEARCH/REPLACE block must EXACTLY match lines of code in the original file. +3. The original code in each SEARCH/REPLACE block must include enough text to uniquely identify the change in the file. +4. The original code in each SEARCH/REPLACE block must be disjoint from all other blocks. + +The SEARCH/REPLACE blocks you generate will be applied immediately, and so they **MUST** produce a file that the user can run IMMEDIATELY. - Make sure you add all necessary imports. - Make sure the "final" code is complete and will not result in syntax/lint errors. -5. Follow coding conventions of the user (spaces, semilcolons, comments, etc). If the user spaces or formats things a certain way, CONTINUE formatting it that way, even if you prefer otherwise. + +Follow coding conventions of the user (spaces, semilcolons, comments, etc). If the user spaces or formats things a certain way, CONTINUE formatting it that way, even if you prefer otherwise. ## EXAMPLE 1 ORIGINAL_FILE diff --git a/src/vs/workbench/contrib/void/browser/searchReplaceService.ts b/src/vs/workbench/contrib/void/browser/searchReplaceCacheService.ts similarity index 75% rename from src/vs/workbench/contrib/void/browser/searchReplaceService.ts rename to src/vs/workbench/contrib/void/browser/searchReplaceCacheService.ts index 9bf958c4..e7a9448e 100644 --- a/src/vs/workbench/contrib/void/browser/searchReplaceService.ts +++ b/src/vs/workbench/contrib/void/browser/searchReplaceCacheService.ts @@ -8,6 +8,7 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { ILLMMessageService } from '../common/llmMessageService.js'; +import { ServiceSendLLMMessageParams } from '../common/llmMessageTypes.js'; @@ -15,12 +16,10 @@ export interface ISearchReplaceService { readonly _serviceBrand: undefined; } -export const ISearchReplaceService = createDecorator('SearchReplaceService'); +export const ISearchReplaceService = createDecorator('SearchReplaceCacheService'); class SearchReplaceService extends Disposable implements ISearchReplaceService { _serviceBrand: undefined; - static readonly ID = 'SearchReplaceService'; - private readonly _onDidChangeState = new Emitter(); readonly onDidChangeState: Event = this._onDidChangeState.event; @@ -28,8 +27,18 @@ class SearchReplaceService extends Disposable implements ISearchReplaceService { @ILLMMessageService private readonly llmMessageService: ILLMMessageService, ) { super() - // this.llmMessageService.sendLLMMessage({}) + } + send(params: Omit & { onText: (p: { newText: string, fullText: string }) => { retry: boolean } }) { + this.llmMessageService.sendLLMMessage({ + ...params as ServiceSendLLMMessageParams, + onText: (p) => { + const { retry } = params.onText(p) + if (retry) { + + } + } + }) } } diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 14371a22..06e708fd 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -552,7 +552,8 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName : '(never)', subTextMd: providerName === 'ollama' ? 'If you would like to change this endpoint, please read more about [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).' : - undefined, + providerName === 'vLLM' ? 'If you would like to change this endpoint, please read more about [Endpoints here](https://docs.vllm.ai/en/latest/getting_started/quickstart.html#openai-compatible-server).' : + undefined, } } else if (settingName === '_didFillInProviderSettings') {