From 1257e54ebe2ebd03cdf3ff04558953080cc8b903 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 3 Mar 2025 20:13:26 -0800 Subject: [PATCH] tools are getting close --- .../contrib/void/browser/editCodeService.ts | 20 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 68 ++--- .../contrib/void/common/chatThreadService.ts | 82 +++++- .../contrib/void/common/llmMessageTypes.ts | 8 +- .../contrib/void/common/toolsService.ts | 256 +++++++++++------- .../void/electron-main/llmMessage/MODELS.ts | 19 +- 6 files changed, 289 insertions(+), 164 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 9c4e79e4..f8b7dd07 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -39,7 +39,7 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ILLMMessageService } from '../common/llmMessageService.js'; -import { LLMChatMessage, OnError, errorDetails } from '../common/llmMessageTypes.js'; +import { LLMChatMessage, OnError, OnFinalMessage, OnText, errorDetails } from '../common/llmMessageTypes.js'; import { IMetricsService } from '../common/metricsService.js'; import { IVoidFileService } from '../common/voidFileService.js'; @@ -133,6 +133,10 @@ export type StartApplyingOpts = { type: 'searchReplace' | 'rewrite'; applyStr: string; uri: 'current' | URI; + + onText?: OnText; + onFinalMessage?: OnFinalMessage; + onError?: OnError; } @@ -1450,7 +1454,7 @@ class EditCodeService extends Disposable implements IEditCodeService { private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }) { - const { applyStr, uri: givenURI } = opts + const { applyStr, uri: givenURI, onText: onText_, onFinalMessage: onFinalMessage_, onError: onError_, } = opts let uri: URI if (givenURI === 'current') { @@ -1583,7 +1587,8 @@ class EditCodeService extends Disposable implements IEditCodeService { useProviderFor: 'Apply', logging: { loggingName: `generateSearchAndReplace` }, messages, - onText: ({ fullText }) => { + onText: (params) => { + const { fullText } = params // blocks are [done done done ... {writingFinal|writingOriginal}] // ^ // currStreamingBlockNum @@ -1678,8 +1683,11 @@ class EditCodeService extends Disposable implements IEditCodeService { } // end for this._refreshStylesAndDiffsInURI(uri) + + onText_?.(params) }, - onFinalMessage: async ({ fullText }) => { + onFinalMessage: async (params) => { + const { fullText } = params console.log('final message!!', fullText) // 1. wait 500ms and fix lint errors - call lint error workflow @@ -1715,11 +1723,15 @@ class EditCodeService extends Disposable implements IEditCodeService { } onDone() + + onFinalMessage_?.(params) }, onError: (e) => { this._notifyError(e) onDone() this._undoHistory(uri) + + onError_?.(e) }, }) diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 05cf7fc0..e89ddbb0 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -25,7 +25,7 @@ import { ChevronRight, Pencil, X } from 'lucide-react'; import { FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'; import { WarningBox } from '../void-settings-tsx/WarningBox.js'; -import { ToolCallReturnType, ToolName } from '../../../../common/toolsService.js'; +import { ToolResultType, ToolName } from '../../../../common/toolsService.js'; @@ -604,8 +604,14 @@ const ToolResult = ({ const ToolError = ({ toolName, errorMessage }: { toolName: T, errorMessage: string }) => { return + actionParam={'Error'} + > + + } @@ -616,7 +622,7 @@ const toolResultToComponent: ToolResultToComponent = { const commandService = accessor.get('ICommandService') if (message.result.type === 'error') return - const { value } = message.result + const { value, params } = message.result return (
{ commandService.executeCommand('vscode.open', value.uri, { preview: true }) }} + onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} > - {getBasename(value.uri.fsPath)} + {getBasename(params.uri.fsPath)}
{value.hasNextPage && (
AI can scroll for more content...
)} @@ -643,11 +649,11 @@ const toolResultToComponent: ToolResultToComponent = { // message.result.itemsRemaining = 400 if (message.result.type === 'error') return - const { value } = message.result + const { value, params } = message.result return (
@@ -674,11 +680,11 @@ const toolResultToComponent: ToolResultToComponent = { const commandService = accessor.get('ICommandService') if (message.result.type === 'error') return - const { value } = message.result + const { value, params } = message.result return (
@@ -701,11 +707,11 @@ const toolResultToComponent: ToolResultToComponent = { const commandService = accessor.get('ICommandService') if (message.result.type === 'error') return - const { value } = message.result + const { value, params } = message.result return (
@@ -732,20 +738,20 @@ const toolResultToComponent: ToolResultToComponent = { if (message.result.type === 'error') return - const { value } = message.result + const { params } = message.result return ( { commandService.executeCommand('vscode.open', value.uri, { preview: true }) }} + actionParam={getBasename(params.uri.fsPath)} + onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} >
{ commandService.executeCommand('vscode.open', value.uri, { preview: true }) }} + onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} > - {value.uri.fsPath.split('/').pop()} + {params.uri.fsPath.split('/').pop()}
@@ -757,20 +763,20 @@ const toolResultToComponent: ToolResultToComponent = { if (message.result.type === 'error') return - const { value } = message.result + const { params } = message.result return ( { commandService.executeCommand('vscode.open', value.uri, { preview: true }) }} + actionParam={getBasename(params.uri.fsPath) + ' (deleted)'} + onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} >
{ commandService.executeCommand('vscode.open', value.uri, { preview: true }) }} + onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} > - {value.uri.fsPath.split('/').pop()} + {params.uri.fsPath.split('/').pop()}
@@ -782,20 +788,20 @@ const toolResultToComponent: ToolResultToComponent = { if (message.result.type === 'error') return - const { value } = message.result + const { params } = message.result return ( { commandService.executeCommand('vscode.open', value.uri, { preview: true }) }} + actionParam={getBasename(params.uri.fsPath)} + onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} >
{ commandService.executeCommand('vscode.open', value.uri, { preview: true }) }} + onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} > - {value.uri.fsPath.split('/').pop()} + {params.uri.fsPath.split('/').pop()}
@@ -807,16 +813,16 @@ const toolResultToComponent: ToolResultToComponent = { if (message.result.type === 'error') return - const { value } = message.result + const { params } = message.result return (
@@ -1026,7 +1032,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM chatbubbleContents = - console.log('tool result:', chatMessage.name, chatMessage.params, chatMessage.result) + console.log('tool result:', chatMessage.name, chatMessage.paramsStr, chatMessage.result) } diff --git a/src/vs/workbench/contrib/void/common/chatThreadService.ts b/src/vs/workbench/contrib/void/common/chatThreadService.ts index dc16b52d..32afe5e8 100644 --- a/src/vs/workbench/contrib/void/common/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/common/chatThreadService.ts @@ -13,7 +13,7 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { ILLMMessageService } from './llmMessageService.js'; import { chat_userMessageContent, chat_systemMessage, chat_userMessageContentWithAllFilesToo, chat_selectionsString } from '../browser/prompt/prompts.js'; -import { InternalToolInfo, IToolsService, ToolCallReturnType, ToolName, voidTools } from './toolsService.js'; +import { InternalToolInfo, IToolsService, ToolCallParams, ToolResultType, ToolName, toolNamesThatRequireApproval, voidTools } from './toolsService.js'; import { toLLMChatMessage, ToolCallType } from './llmMessageTypes.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { IVoidFileService } from './voidFileService.js'; @@ -58,11 +58,18 @@ export type StagingSelectionItem = CodeSelection | FileSelection export type ToolMessage = { role: 'tool'; name: T; // internal use - params: string; // internal use + paramsStr: string; // internal use id: string; // apis require this tool use id content: string; // give this result to LLM - result: { type: 'success'; value: ToolCallReturnType[T], } | { type: 'error'; value: string }; // give this result to user + result: { type: 'success'; params: ToolCallParams[T]; value: ToolResultType[T], } | { type: 'error'; value: string }; // give this result to user } +export type ToolRequestApproval = { + role: 'tool_request'; + name: T; // internal use + params: ToolCallParams[T]; // internal use + voidToolId: string; // internal id Void uses +} + // WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors. export type ChatMessage = { @@ -80,6 +87,7 @@ export type ChatMessage = reasoning: string | null; // reasoning from the LLM, used for step-by-step thinking } | ToolMessage + | ToolRequestApproval type UserMessageType = ChatMessage & { role: 'user' } type UserMessageState = UserMessageType['state'] @@ -316,6 +324,18 @@ class ChatThreadService extends Disposable implements IChatThreadService { } + private resRejOfToolAwaitingApproval: { [toolId: string]: { res: () => void, rej: () => void } } = {} + approveTool(toolId: string) { + const resRej = this.resRejOfToolAwaitingApproval[toolId] + resRej?.res() + delete this.resRejOfToolAwaitingApproval[toolId] + } + rejectTool(toolId: string) { + const resRej = this.resRejOfToolAwaitingApproval[toolId] + resRej?.rej() + delete this.resRejOfToolAwaitingApproval[toolId] + } + async addUserMessageAndStreamResponse({ userMessage, chatMode, chatSelections }: { userMessage: string, chatMode: ChatMode, chatSelections?: { prevSelns?: StagingSelectionItem[], currSelns?: StagingSelectionItem[] } }) { @@ -357,7 +377,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const awaitable = new Promise((res, rej) => { res_ = res }) // replace last userMessage with userMessageFullContent (which contains all the files too) - const messages_ = this.getCurrentThread().messages.map(m => (toLLMChatMessage(m))) + const messages_ = this.getCurrentThread().messages.map(m => (toLLMChatMessage(m))).filter(m => !!m) const lastUserMsgIdx = findLastIndex(messages_, m => m.role === 'user') let messages = messages_ if (lastUserMsgIdx !== -1) { // should never be -1 @@ -398,31 +418,63 @@ class ChatThreadService extends Disposable implements IChatThreadService { } const toolName = tool.name - // 1. - let toolResultVal: ToolCallReturnType[typeof toolName] + // 1. validate tool params + let toolParams: ToolCallParams[typeof toolName] try { - const val = await this._toolsService.toolFns[toolName](tool.params) - toolResultVal = val + const params = await this._toolsService.validateParams[toolName](tool.paramsStr) + toolParams = params } catch (error) { const errorMessage = getErrorMessage(error) - this._addMessageToThread(threadId, { role: 'tool', name: toolName, params: tool.params, id: tool.id, content: errorMessage, result: { type: 'error', value: errorMessage }, }) + this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', value: errorMessage }, }) shouldSendAnotherMessage = true res_() return } - // 2. - let toolResultStr: string + // 2. if tool requires approval, await the approval + if (toolNamesThatRequireApproval.has(toolName)) { + const voidToolId = generateUuid() + const toolApprovalPromise = new Promise((res, rej) => { this.resRejOfToolAwaitingApproval[voidToolId] = { res, rej } }) + this._addMessageToThread(threadId, { role: 'tool_request', name: toolName, params: toolParams, voidToolId: voidToolId }) + try { + await toolApprovalPromise + // accepted tool + } + catch (e) { + const errorMessage = 'Tool call was rejected by the user.' + this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', value: errorMessage }, }) + shouldSendAnotherMessage = false + res_() + return + } + } + + // 3. call the tool + let toolResult: ToolResultType[typeof toolName] try { - toolResultStr = this._toolsService.toolResultToString[toolName](toolResultVal as any) // typescript is so bad it doesn't even couple the type of ToolResult with the type of the function being called here + toolResult = this._toolsService.callTool[toolName](toolParams 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) { - // treat as irrecoverable error - this._setStreamState(threadId, { error }) + const errorMessage = getErrorMessage(error) + this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', value: errorMessage }, }) + shouldSendAnotherMessage = true res_() return } - this._addMessageToThread(threadId, { role: 'tool', name: toolName, params: tool.params, id: tool.id, content: toolResultStr, result: { type: 'success', value: toolResultVal }, }) + // 4. stringify the result to give the LLM + let toolResultStr: string + try { + toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any) + } catch (error) { + const errorMessage = `Tool call succeeded, but there was an error stringifying the output.\n${getErrorMessage(error)}` + this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', value: errorMessage }, }) + shouldSendAnotherMessage = true + res_() + return + } + + // 5. add to history + this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: toolResultStr, result: { type: 'success', params: toolParams, value: toolResult }, }) shouldSendAnotherMessage = true res_() } diff --git a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts index 4257e616..b38e7030 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts @@ -45,7 +45,7 @@ export type LLMChatMessage = { export type ToolCallType = { name: ToolName; - params: string; + paramsStr: string; id: string; } @@ -56,14 +56,16 @@ export type OnError = (p: { message: string, fullError: Error | null }) => void export type AbortRef = { current: (() => void) | null } -export const toLLMChatMessage = (c: ChatMessage): LLMChatMessage => { +export const toLLMChatMessage = (c: ChatMessage): LLMChatMessage | null => { if (c.role === 'user') { return { role: c.role, content: c.content || '(empty message)' } } else if (c.role === 'assistant') return { role: c.role, content: c.content || '(empty message)' } else if (c.role === 'tool') - return { role: c.role, id: c.id, name: c.name, params: c.params, content: c.content || '(empty output)' } + return { role: c.role, id: c.id, name: c.name, params: c.paramsStr, content: c.content || '(empty output)' } + else if (c.role === 'tool_request') + return null else { throw new Error(`Role ${(c as any).role} not recognized.`) } diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index cf6052dd..4e0d080a 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -9,7 +9,6 @@ import { ISearchService } from '../../../../workbench/services/search/common/sea import { IEditCodeService } from '../browser/editCodeService.js' import { editToolDesc_toolDescription } from '../browser/prompt/prompts.js' import { IVoidFileService } from './voidFileService.js' -import { ITerminalToolService } from '../browser/terminalToolService.js' // tool use for AI @@ -129,20 +128,7 @@ export const isAToolName = (toolName: string): toolName is ToolName => { } -export type ToolParamNames = keyof typeof voidTools[T]['params'] -export type ToolParamsObj = { [paramName in ToolParamNames]: unknown } - -export type ToolCallReturnType = { - 'read_file': { uri: URI, fileContents: string, hasNextPage: boolean }, - 'list_dir': { rootURI: URI, children: DirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number }, - 'pathname_search': { queryStr: string, uris: URI[], hasNextPage: boolean }, - 'search': { queryStr: string, uris: URI[], hasNextPage: boolean }, - // --- - 'edit': { uri: URI, changeDescription: string }, - 'create_uri': { uri: URI }, - 'delete_uri': { uri: URI }, - 'terminal_command': { command: string }, -} +export const toolNamesThatRequireApproval = new Set(['create_uri', 'delete_uri', 'edit', 'terminal_command'] satisfies ToolName[]) type DirectoryItem = { uri: URI; @@ -151,8 +137,39 @@ type DirectoryItem = { isSymbolicLink: boolean; } -export type ToolFns = { [T in ToolName]: (p: string) => Promise } -export type ToolResultToString = { [T in ToolName]: (result: ToolCallReturnType[T]) => string } + +export type ToolCallParams = { + 'read_file': { uri: URI, pageNumber: number }, + 'list_dir': { rootURI: URI, pageNumber: number }, + 'pathname_search': { queryStr: string, pageNumber: number }, + 'search': { queryStr: string, pageNumber: number }, + // --- + 'edit': { uri: URI, changeDescription: string }, + 'create_uri': { uri: URI }, + 'delete_uri': { uri: URI, isRecursive: boolean }, + 'terminal_command': { command: string }, +} + + +export type ToolResultType = { + 'read_file': { fileContents: string, hasNextPage: boolean }, + 'list_dir': { children: DirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number }, + 'pathname_search': { uris: URI[], hasNextPage: boolean }, + 'search': { uris: URI[], hasNextPage: boolean }, + // --- + 'edit': {}, + 'create_uri': {}, + 'delete_uri': {}, + 'terminal_command': {}, +} + + + +export type ValidateParams = { [T in ToolName]: (p: string) => Promise } +export type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise } +export type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: ToolResultType[T]) => string } + + // pagination info @@ -165,10 +182,10 @@ const computeDirectoryResult = async ( fileService: IFileService, rootURI: URI, pageNumber: number = 1 -): Promise => { +): Promise => { const stat = await fileService.resolve(rootURI, { resolveMetadata: false }); if (!stat.isDirectory) { - return { rootURI, children: null, hasNextPage: false, hasPrevPage: false, itemsRemaining: 0 }; + return { children: null, hasNextPage: false, hasPrevPage: false, itemsRemaining: 0 }; } const originalChildrenLength = stat.children?.length ?? 0; @@ -188,7 +205,6 @@ const computeDirectoryResult = async ( const itemsRemaining = Math.max(0, originalChildrenLength - (toChildIdx + 1)); return { - rootURI, children, hasNextPage, hasPrevPage, @@ -196,16 +212,16 @@ const computeDirectoryResult = async ( }; }; -const directoryResultToString = (result: ToolCallReturnType['list_dir']): string => { +const directoryResultToString = (params: ToolCallParams['list_dir'], result: ToolResultType['list_dir']): string => { if (!result.children) { - return `Error: ${result.rootURI} is not a directory`; + return `Error: ${params.rootURI} is not a directory`; } let output = ''; const entries = result.children; if (!result.hasPrevPage) { - output += `${result.rootURI}\n`; + output += `${params.rootURI}\n`; } for (let i = 0; i < entries.length; i++) { @@ -270,8 +286,9 @@ const validateRecursiveParamStr = (paramsUnknown: unknown) => { export interface IToolsService { readonly _serviceBrand: undefined; - toolFns: ToolFns; - toolResultToString: ToolResultToString; + validateParams: ValidateParams; + callTool: CallTool; + stringOfResult: ToolResultToString; } export const IToolsService = createDecorator('ToolsService'); @@ -280,8 +297,9 @@ export class ToolsService implements IToolsService { readonly _serviceBrand: undefined; - public toolFns: ToolFns; - public toolResultToString: ToolResultToString; + public validateParams: ValidateParams; + public callTool: CallTool; + public stringOfResult: ToolResultToString; constructor( @@ -291,12 +309,12 @@ export class ToolsService implements IToolsService { @IInstantiationService instantiationService: IInstantiationService, @IVoidFileService voidFileService: IVoidFileService, @IEditCodeService editCodeService: IEditCodeService, - @ITerminalToolService private readonly terminalToolService: ITerminalToolService, + // @ITerminalToolService private readonly terminalToolService: ITerminalToolService, ) { const queryBuilder = instantiationService.createInstance(QueryBuilder); - this.toolFns = { + this.validateParams = { read_file: async (params: string) => { console.log('read_file') @@ -306,18 +324,7 @@ export class ToolsService implements IToolsService { const uri = validateURI(uriStr) const pageNumber = validatePageNum(pageNumberUnknown) - const readFileContents = await voidFileService.readFile(uri) - - const fromIdx = MAX_FILE_CHARS_PAGE * (pageNumber - 1) - const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1 - const fileContents = readFileContents.slice(fromIdx, toIdx + 1) || '(empty)' // paginate - const hasNextPage = (readFileContents.length - 1) - toIdx >= 1 - - - console.log('read_file result:', fileContents) - - - return { uri, fileContents, hasNextPage } + return { uri, pageNumber } }, list_dir: async (params: string) => { const o = validateJSON(params) @@ -325,9 +332,7 @@ export class ToolsService implements IToolsService { const uri = validateURI(uriStr) const pageNumber = validatePageNum(pageNumberUnknown) - - const dirResult = await computeDirectoryResult(fileService, uri, pageNumber) - return dirResult + return { rootURI: uri, pageNumber } }, pathname_search: async (params: string) => { const o = validateJSON(params) @@ -336,6 +341,74 @@ export class ToolsService implements IToolsService { const queryStr = validateStr('query', queryUnknown) const pageNumber = validatePageNum(pageNumberUnknown) + return { queryStr, pageNumber } + + }, + search: async (params: string) => { + const o = validateJSON(params) + const { query: queryUnknown, pageNumber: pageNumberUnknown } = o + + const queryStr = validateStr('query', queryUnknown) + const pageNumber = validatePageNum(pageNumberUnknown) + + return { queryStr, pageNumber } + }, + + // --- + + create_uri: async (params: string) => { + const o = validateJSON(params) + const { uri: uriStr } = o + const uri = validateURI(uriStr) + return { uri } + }, + + delete_uri: async (params: string) => { + const o = validateJSON(params) + const { uri: uriStr, params: paramsStr } = o + const uri = validateURI(uriStr) + const isRecursive = validateRecursiveParamStr(paramsStr) + return { uri, isRecursive } + }, + + edit: async (params: string) => { + const o = validateJSON(params) + const { uri: uriStr, changeDescription: changeDescriptionUnknown } = o + const uri = validateURI(uriStr) + const changeDescription = validateStr('changeDescription', changeDescriptionUnknown) + + + return { uri, changeDescription } + }, + + terminal_command: async (s: string) => { + const o = validateJSON(s) + const { command: commandUnknown } = o + const command = validateStr('command', commandUnknown) + return { command } + }, + + } + + + this.callTool = { + read_file: async ({ uri, pageNumber }) => { + const readFileContents = await voidFileService.readFile(uri) + + const fromIdx = MAX_FILE_CHARS_PAGE * (pageNumber - 1) + const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1 + const fileContents = readFileContents.slice(fromIdx, toIdx + 1) || '(empty)' // paginate + const hasNextPage = (readFileContents.length - 1) - toIdx >= 1 + console.log('read_file result:', fileContents) + return { fileContents, hasNextPage } + }, + + list_dir: async ({ rootURI, pageNumber }) => { + const dirResult = await computeDirectoryResult(fileService, rootURI, pageNumber) + return dirResult + }, + + pathname_search: async ({ queryStr, pageNumber }) => { const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), { filePattern: queryStr, }) const data = await searchService.fileSearch(query, CancellationToken.None) @@ -346,15 +419,10 @@ export class ToolsService implements IToolsService { .map(({ resource, results }) => resource) const hasNextPage = (data.results.length - 1) - toIdx >= 1 - return { queryStr, uris, hasNextPage } + return { uris, hasNextPage } }, - search: async (params: string) => { - const o = validateJSON(params) - const { query: queryUnknown, pageNumber: pageNumberUnknown } = o - - const queryStr = validateStr('query', queryUnknown) - const pageNumber = validatePageNum(pageNumberUnknown) + search: async ({ queryStr, pageNumber }) => { const query = queryBuilder.text({ pattern: queryStr, }, workspaceContextService.getWorkspace().folders.map(f => f.uri)) const data = await searchService.textSearch(query, CancellationToken.None) @@ -370,83 +438,67 @@ export class ToolsService implements IToolsService { // --- - create_uri: async (params: string) => { - const o = validateJSON(params) - const { uri: uriStr } = o - const uri = validateURI(uriStr) + create_uri: async ({ uri }) => { await fileService.createFile(uri) - return { uri } + return {} }, - delete_uri: async (params: string) => { - const o = validateJSON(params) - const { uri: uriStr, params: paramsStr } = o - const uri = validateURI(uriStr) - const isRecursive = validateRecursiveParamStr(paramsStr) + delete_uri: async ({ uri, isRecursive }) => { await fileService.del(uri, { recursive: isRecursive }) - return { uri } + return {} }, - edit: async (params: string) => { - const o = validateJSON(params) - const { uri: uriStr, changeDescription: changeDescriptionUnknown } = o - const uri = validateURI(uriStr) - const changeDescription = validateStr('changeDescription', changeDescriptionUnknown) - - const applyId = editCodeService.startApplying({ uri, applyStr: changeDescription, from: 'ClickApply', type: 'rewrite' }) - - // // TODO!!! - - // await // await apply done before moving on - - return { uri, changeDescription } + edit: async ({ uri, changeDescription }) => { + const p = new Promise((res, rej) => { + editCodeService.startApplying({ + uri, + applyStr: changeDescription, + from: 'ClickApply', + type: 'rewrite', + onFinalMessage: (p) => { res(p) }, + onError: (e) => { throw new Error(e.message) }, + }) + }) + await p + return {} }, - - terminal_command: async (s: string) => { - const o = validateJSON(s) - const { command: commandUnknown } = o - const command = validateStr('command', commandUnknown) - + terminal_command: async ({ command }) => { // TODO!!!! // await // Await user confirmation and then command execution before resolving - - - return { command } + return {} }, - - - } + const nextPageStr = (hasNextPage: boolean) => hasNextPage ? '\n\n(more on next page...)' : '' // given to the LLM after the call - this.toolResultToString = { - read_file: (result) => { - return nextPageStr(result.hasNextPage) + this.stringOfResult = { + read_file: (params, result) => { + return result.fileContents + nextPageStr(result.hasNextPage) }, - list_dir: (result) => { - const dirTreeStr = directoryResultToString(result) + list_dir: (params, result) => { + const dirTreeStr = directoryResultToString(params, result) return dirTreeStr + nextPageStr(result.hasNextPage) }, - pathname_search: (result) => { + pathname_search: (params, result) => { return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage) }, - search: (result) => { + search: (params, result) => { return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage) }, // --- - create_uri: (result) => { - return `URI ${result.uri.fsPath} successfully created.` + create_uri: (params, result) => { + return `URI ${params.uri.fsPath} successfully created.` }, - delete_uri: (result) => { - return `URI ${result.uri.fsPath} successfully deleted.` + delete_uri: (params, result) => { + return `URI ${params.uri.fsPath} successfully deleted.` }, - edit: (result) => { - return `Change successfully made ${result.uri.fsPath} successfully deleted.` + edit: (params, result) => { + return `Change successfully made ${params.uri.fsPath} successfully deleted.` }, - terminal_command: (result) => { - return `Terminal command "${result.command}" successfully executed.` + terminal_command: (params, result) => { + return `Terminal command "${params.command}" successfully executed.` }, } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts index c618a0da..ccad5462 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts @@ -10,7 +10,7 @@ import OpenAI, { ClientOptions } from 'openai'; import { Model as OpenAIModel } from 'openai/resources/models.js'; import { extractReasoningOnFinalMessage, extractReasoningOnTextWrapper } from '../../browser/helpers/extractCodeFromResult.js'; import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/llmMessageTypes.js'; -import { InternalToolInfo, isAToolName } from '../../common/toolsService.js'; +import { InternalToolInfo, isAToolName, ToolName } from '../../common/toolsService.js'; import { defaultProviderSettings, displayInfoOfProviderName, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; import { prepareFIMMessage, prepareMessages } from './preprocessLLMMessages.js'; @@ -582,12 +582,13 @@ const toOpenAICompatibleTool = (toolInfo: InternalToolInfo) => { } satisfies OpenAI.Chat.Completions.ChatCompletionTool } -type ToolCallOfIndex = { [index: string]: { name: string, params: string, id: string } } +type ToolCallOfIndex = { [index: string]: { name: string, paramsStr: string, id: string } } // type used to stream tool calls as they come in +type ToolCallsFrom_ReturnType = { name: ToolName, id: string, paramsStr: string }[] // return type of toolCallsFrom_ -const toolCallsFrom_OpenAICompat = (toolCallOfIndex: ToolCallOfIndex) => { +const toolCallsFrom_OpenAICompat = (toolCallOfIndex: ToolCallOfIndex): ToolCallsFrom_ReturnType => { return Object.keys(toolCallOfIndex).map(index => { const tool = toolCallOfIndex[index] - return isAToolName(tool.name) ? { name: tool.name, id: tool.id, params: tool.params } : null + return isAToolName(tool.name) ? { name: tool.name, id: tool.id, paramsStr: tool.paramsStr } : null }).filter(t => !!t) } @@ -719,9 +720,9 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage // tool call for (const tool of chunk.choices[0]?.delta?.tool_calls ?? []) { const index = tool.index - if (!toolCallOfIndex[index]) toolCallOfIndex[index] = { name: '', params: '', id: '' } + if (!toolCallOfIndex[index]) toolCallOfIndex[index] = { name: '', paramsStr: '', id: '' } toolCallOfIndex[index].name += tool.function?.name ?? '' - toolCallOfIndex[index].params += tool.function?.arguments ?? ''; + toolCallOfIndex[index].paramsStr += tool.function?.arguments ?? ''; toolCallOfIndex[index].id = tool.id ?? '' } // message @@ -804,11 +805,11 @@ const toAnthropicTool = (toolInfo: InternalToolInfo) => { } satisfies Anthropic.Messages.Tool } -const toolCallsFromAnthropicContent = (content: Anthropic.Messages.ContentBlock[]) => { +const toolCallsFrom_AnthropicContent = (content: Anthropic.Messages.ContentBlock[]): ToolCallsFrom_ReturnType => { return content.map(c => { if (c.type !== 'tool_use') return null if (!isAToolName(c.name)) return null - return c.type === 'tool_use' ? { name: c.name, params: JSON.stringify(c.input), id: c.id } : null + return c.type === 'tool_use' ? { name: c.name, paramsStr: JSON.stringify(c.input), id: c.id } : null }).filter(t => !!t) } @@ -842,7 +843,7 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM // when we get the final message on this stream (or when error/fail) stream.on('finalMessage', (response) => { const content = response.content.map(c => c.type === 'text' ? c.text : '').join('\n\n') - const toolCalls = toolCallsFromAnthropicContent(response.content) + const toolCalls = toolCallsFrom_AnthropicContent(response.content) onFinalMessage({ fullText: content, toolCalls }) }) // on error