tools are getting close

This commit is contained in:
Andrew Pareles 2025-03-03 20:13:26 -08:00
parent fcbad101a4
commit 1257e54ebe
6 changed files with 289 additions and 164 deletions

View file

@ -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)
},
})

View file

@ -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 = <T extends ToolName,>({ toolName, errorMessage }: { toolName: T, errorMessage: string }) => {
return <ToolResult
toolName={toolName}
actionParam={errorMessage}
/>
actionParam={'Error'}
>
<ErrorDisplay
message={errorMessage}
fullError={null}
onDismiss={null}
/>
</ToolResult>
}
@ -616,7 +622,7 @@ const toolResultToComponent: ToolResultToComponent = {
const commandService = accessor.get('ICommandService')
if (message.result.type === 'error') return <ToolError toolName='read_file' errorMessage={message.result.value} />
const { value } = message.result
const { value, params } = message.result
return (
<ToolResult
toolName='read_file'
@ -625,10 +631,10 @@ const toolResultToComponent: ToolResultToComponent = {
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
<div
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
onClick={() => { commandService.executeCommand('vscode.open', value.uri, { preview: true }) }}
onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }}
>
<svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg>
{getBasename(value.uri.fsPath)}
{getBasename(params.uri.fsPath)}
</div>
{value.hasNextPage && (<div className="italic">AI can scroll for more content...</div>)}
</div>
@ -643,11 +649,11 @@ const toolResultToComponent: ToolResultToComponent = {
// message.result.itemsRemaining = 400
if (message.result.type === 'error') return <ToolError toolName='list_dir' errorMessage={message.result.value} />
const { value } = message.result
const { value, params } = message.result
return (
<ToolResult
toolName='list_dir'
actionParam={`${getBasename(value.rootURI.fsPath)}/`}
actionParam={`${getBasename(params.rootURI.fsPath)}/`}
actionNumResults={value.children?.length}
>
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
@ -674,11 +680,11 @@ const toolResultToComponent: ToolResultToComponent = {
const commandService = accessor.get('ICommandService')
if (message.result.type === 'error') return <ToolError toolName='pathname_search' errorMessage={message.result.value} />
const { value } = message.result
const { value, params } = message.result
return (
<ToolResult
toolName='pathname_search'
actionParam={`"${value.queryStr}"`}
actionParam={`"${params.queryStr}"`}
actionNumResults={value.uris.length}
>
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
@ -701,11 +707,11 @@ const toolResultToComponent: ToolResultToComponent = {
const commandService = accessor.get('ICommandService')
if (message.result.type === 'error') return <ToolError toolName='search' errorMessage={message.result.value} />
const { value } = message.result
const { value, params } = message.result
return (
<ToolResult
toolName='search'
actionParam={`"${value.queryStr}"`}
actionParam={`"${params.queryStr}"`}
actionNumResults={value.uris.length}
>
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
@ -732,20 +738,20 @@ const toolResultToComponent: ToolResultToComponent = {
if (message.result.type === 'error') return <ToolError toolName='create_uri' errorMessage={message.result.value} />
const { value } = message.result
const { params } = message.result
return (
<ToolResult
toolName='create_uri'
actionParam={getBasename(value.uri.fsPath)}
onClick={() => { commandService.executeCommand('vscode.open', value.uri, { preview: true }) }}
actionParam={getBasename(params.uri.fsPath)}
onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }}
>
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
<div
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
onClick={() => { commandService.executeCommand('vscode.open', value.uri, { preview: true }) }}
onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }}
>
<svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg>
{value.uri.fsPath.split('/').pop()}
{params.uri.fsPath.split('/').pop()}
</div>
</div>
</ToolResult>
@ -757,20 +763,20 @@ const toolResultToComponent: ToolResultToComponent = {
if (message.result.type === 'error') return <ToolError toolName='delete_uri' errorMessage={message.result.value} />
const { value } = message.result
const { params } = message.result
return (
<ToolResult
toolName='delete_uri'
actionParam={getBasename(value.uri.fsPath) + ' (deleted)'}
onClick={() => { commandService.executeCommand('vscode.open', value.uri, { preview: true }) }}
actionParam={getBasename(params.uri.fsPath) + ' (deleted)'}
onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }}
>
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
<div
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
onClick={() => { commandService.executeCommand('vscode.open', value.uri, { preview: true }) }}
onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }}
>
<svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg>
{value.uri.fsPath.split('/').pop()}
{params.uri.fsPath.split('/').pop()}
</div>
</div>
</ToolResult>
@ -782,20 +788,20 @@ const toolResultToComponent: ToolResultToComponent = {
if (message.result.type === 'error') return <ToolError toolName='edit' errorMessage={message.result.value} />
const { value } = message.result
const { params } = message.result
return (
<ToolResult
toolName='edit'
actionParam={getBasename(value.uri.fsPath)}
onClick={() => { commandService.executeCommand('vscode.open', value.uri, { preview: true }) }}
actionParam={getBasename(params.uri.fsPath)}
onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }}
>
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
<div
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
onClick={() => { commandService.executeCommand('vscode.open', value.uri, { preview: true }) }}
onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }}
>
<svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg>
{value.uri.fsPath.split('/').pop()}
{params.uri.fsPath.split('/').pop()}
</div>
</div>
</ToolResult>
@ -807,16 +813,16 @@ const toolResultToComponent: ToolResultToComponent = {
if (message.result.type === 'error') return <ToolError toolName='terminal_command' errorMessage={message.result.value} />
const { value } = message.result
const { params } = message.result
return (
<ToolResult
toolName='terminal_command'
actionParam={value.command}
actionParam={params.command}
>
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
<div
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
// TODO!!! open terminal
// TODO!!! open terminal
>
<svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg>
@ -1026,7 +1032,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM
chatbubbleContents = <ToolComponent message={chatMessage} />
console.log('tool result:', chatMessage.name, chatMessage.params, chatMessage.result)
console.log('tool result:', chatMessage.name, chatMessage.paramsStr, chatMessage.result)
}

View file

@ -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<T extends ToolName> = {
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<T extends ToolName> = {
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<ToolName>
| ToolRequestApproval<ToolName>
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<void>((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<void>((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_()
}

View file

@ -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.`)
}

View file

@ -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<T extends ToolName> = keyof typeof voidTools[T]['params']
export type ToolParamsObj<T extends ToolName> = { [paramName in ToolParamNames<T>]: 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<ToolName>(['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<ToolCallReturnType[T]> }
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<ToolCallParams[T]> }
export type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<ToolResultType[T]> }
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<ToolCallReturnType['list_dir']> => {
): Promise<ToolResultType['list_dir']> => {
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<IToolsService>('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.`
},
}

View file

@ -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_<PROVIDER>
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