mirror of
https://github.com/voideditor/void
synced 2026-05-24 09:58:23 +00:00
tools are getting close
This commit is contained in:
parent
fcbad101a4
commit
1257e54ebe
6 changed files with 289 additions and 164 deletions
|
|
@ -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)
|
||||
},
|
||||
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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_()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.`
|
||||
},
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue