mirror of
https://github.com/voideditor/void
synced 2026-05-24 09:58:23 +00:00
tool calls via plaintext initial draft
This commit is contained in:
parent
8f8fa8548d
commit
1c5adb96d3
11 changed files with 118 additions and 147 deletions
|
|
@ -12,7 +12,7 @@ import { URI } from '../../../../base/common/uri.js';
|
|||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
|
||||
import { chat_userMessageContent, chat_systemMessage, } from '../common/prompt/prompts.js';
|
||||
import { getErrorMessage, LLMChatMessage, ToolCallType } from '../common/sendLLMMessageTypes.js';
|
||||
import { getErrorMessage, LLMChatMessage, ParsedToolCallObj, ParsedToolParamsObj } from '../common/sendLLMMessageTypes.js';
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { generateUuid } from '../../../../base/common/uuid.js';
|
||||
import { FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js';
|
||||
|
|
@ -67,14 +67,18 @@ const toLLMChatMessages = (chatMessages: ChatMessage[]): LLMChatMessage[] => {
|
|||
// merge tools into user message
|
||||
|
||||
for (const c of chatMessages) {
|
||||
if (c.role === 'user') {
|
||||
llmChatMessages.push({ role: c.role, content: c.content })
|
||||
}
|
||||
else if (c.role === 'assistant')
|
||||
if (c.role === 'assistant')
|
||||
llmChatMessages.push({ role: c.role, content: c.content, anthropicReasoning: c.anthropicReasoning })
|
||||
else if (c.role === 'tool')
|
||||
llmChatMessages.push({ role: c.role, id: c.id, name: c.name, params: c.paramsStr, content: c.content })
|
||||
else if (c.role === 'decorative_canceled_tool') { // pass
|
||||
// merge all tool/user messages into one big user message
|
||||
else if (c.role === 'user' || c.role === 'tool') {
|
||||
if (llmChatMessages.length === 0 || llmChatMessages[llmChatMessages.length - 1].role !== 'user') {
|
||||
llmChatMessages.push({ role: 'user', content: c.content })
|
||||
}
|
||||
else {
|
||||
llmChatMessages[llmChatMessages.length - 1].content += '\n\n' + c.content
|
||||
}
|
||||
}
|
||||
else if (c.role === 'interrupted_streaming_tool') { // pass
|
||||
}
|
||||
else if (c.role === 'checkpoint') { // pass
|
||||
}
|
||||
|
|
@ -144,8 +148,7 @@ export type ThreadStreamState = {
|
|||
streamingToken?: string;
|
||||
messageSoFar?: string;
|
||||
reasoningSoFar?: string;
|
||||
toolNameSoFar?: string;
|
||||
toolParamsSoFar?: string;
|
||||
toolCallSoFar?: ParsedToolCallObj;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -473,8 +476,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
role: 'tool',
|
||||
type: 'running_now',
|
||||
name: lastMsg.name,
|
||||
paramsStr: lastMsg.paramsStr,
|
||||
id: lastMsg.id,
|
||||
params: lastMsg.params,
|
||||
content: '(value not received yet...)', // this typically shouldn't ever get read
|
||||
result: null
|
||||
|
|
@ -497,29 +498,13 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
}
|
||||
else return
|
||||
|
||||
const { name, paramsStr, id } = lastMsg
|
||||
const { name } = lastMsg
|
||||
|
||||
const errorMessage = this.errMsgs.rejected
|
||||
this._updateLatestToolTo(threadId, { role: 'tool', type: 'rejected', params: params, name: name, paramsStr: paramsStr, id, content: errorMessage, result: null })
|
||||
this._updateLatestToolTo(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null })
|
||||
this._setStreamState(threadId, {}, 'set')
|
||||
}
|
||||
|
||||
// private _rejectLatestStreamingTool(threadId: string) {
|
||||
// const thread = this.state.allThreads[threadId]
|
||||
// if (!thread) return // should never happen
|
||||
|
||||
// const lastMessage = thread.messages[thread.messages.length - 1]
|
||||
// if (lastMessage.role !== 'tool') return
|
||||
// const { name, paramsStr, id, result } = lastMessage
|
||||
// if (result.type !== 'running_now') return
|
||||
// const { params } = result
|
||||
|
||||
// const errorMessage = this.errMsgs.rejected
|
||||
// this._swapOutLatestStreamingToolWithResult(threadId, { role: 'tool', name: name, paramsStr: paramsStr, id, content: errorMessage, result: { type: 'rejected', params: params }, })
|
||||
// this._setStreamState(threadId, {}, 'set')
|
||||
|
||||
// }
|
||||
|
||||
stopRunning(threadId: string) {
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return // should never happen
|
||||
|
|
@ -536,16 +521,16 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
// abort the stream first so it doesn't change any state
|
||||
const messageSoFar = this.streamState[threadId]?.messageSoFar ?? ''
|
||||
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
|
||||
const toolInProgress = this.streamState[threadId]?.toolNameSoFar
|
||||
console.log('toolInProgress', toolInProgress)
|
||||
const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar
|
||||
console.log('toolInProgress', toolCallSoFar)
|
||||
|
||||
const llmCancelToken = this.streamState[threadId]?.streamingToken
|
||||
if (llmCancelToken !== undefined) { this._llmMessageService.abort(llmCancelToken) }
|
||||
|
||||
this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
|
||||
|
||||
if (toolInProgress) {
|
||||
this._addMessageToThread(threadId, { role: 'decorative_canceled_tool', name: toolInProgress })
|
||||
if (toolCallSoFar) {
|
||||
this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -616,26 +601,24 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
// returns true when the tool call is waiting for user approval
|
||||
const handleToolCall = async (
|
||||
tool: ToolCallType,
|
||||
opts?: { preapproved: true, toolParams: ToolCallParams[ToolName] },
|
||||
toolName: ToolName,
|
||||
opts: { preapproved: true, validatedParams: ToolCallParams[ToolName] } | { preapproved: false, unvalidatedToolParams: ParsedToolParamsObj },
|
||||
): Promise<{ awaitingUserApproval?: boolean, interrupted?: boolean }> => {
|
||||
const toolName: ToolName = tool.name
|
||||
const toolParamsStr = tool.paramsStr
|
||||
const toolId = tool.id
|
||||
|
||||
// compute these below
|
||||
let toolParams: ToolCallParams[ToolName]
|
||||
let toolResult: ToolResultType[typeof toolName]
|
||||
let toolResultStr: string
|
||||
|
||||
if (!opts?.preapproved) { // skip this if pre-approved
|
||||
if (!opts.preapproved) { // skip this if pre-approved
|
||||
// 1. validate tool params
|
||||
try {
|
||||
const params = await this._toolsService.validateParams[toolName](toolParamsStr)
|
||||
|
||||
const params = await this._toolsService.validateParams[toolName](opts.unvalidatedToolParams)
|
||||
toolParams = params
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error)
|
||||
this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', params: null, result: null, name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, })
|
||||
this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', params: null, result: null, name: toolName, content: errorMessage, })
|
||||
return {}
|
||||
}
|
||||
// once validated, add checkpoint for edit
|
||||
|
|
@ -646,14 +629,14 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
if (requiresApproval) {
|
||||
const autoApprove = this._settingsService.state.globalSettings.autoApprove
|
||||
// add a tool_request because we use it for UI if a tool is loading (this should be improved in the future)
|
||||
this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(never)', result: null, name: toolName, paramsStr: toolParamsStr, params: toolParams, id: toolId })
|
||||
this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(never)', result: null, name: toolName, params: toolParams })
|
||||
if (!autoApprove) {
|
||||
return { awaitingUserApproval: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
toolParams = opts.toolParams
|
||||
toolParams = opts.validatedParams
|
||||
}
|
||||
|
||||
// 3. call the tool
|
||||
|
|
@ -674,7 +657,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
return { interrupted: true }
|
||||
}
|
||||
const errorMessage = getErrorMessage(error)
|
||||
this._updateLatestToolTo(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, })
|
||||
this._updateLatestToolTo(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, })
|
||||
return {}
|
||||
}
|
||||
|
||||
|
|
@ -683,12 +666,12 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any)
|
||||
} catch (error) {
|
||||
const errorMessage = this.errMsgs.errWhenStringifying(error)
|
||||
this._updateLatestToolTo(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, })
|
||||
this._updateLatestToolTo(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, })
|
||||
return {}
|
||||
}
|
||||
|
||||
// 5. add to history and keep going
|
||||
this._updateLatestToolTo(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, paramsStr: toolParamsStr, id: toolId, content: toolResultStr, })
|
||||
this._updateLatestToolTo(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, })
|
||||
|
||||
return {}
|
||||
};
|
||||
|
|
@ -706,7 +689,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
// before enter loop, call tool
|
||||
if (callThisToolFirst) {
|
||||
const { interrupted } = await handleToolCall(callThisToolFirst, { preapproved: true, toolParams: callThisToolFirst.params })
|
||||
const { interrupted } = await handleToolCall(callThisToolFirst.name, { preapproved: true, validatedParams: callThisToolFirst.params })
|
||||
if (interrupted) return
|
||||
}
|
||||
|
||||
|
|
@ -717,27 +700,28 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
isRunningWhenEnd = undefined
|
||||
nMessagesSent += 1
|
||||
|
||||
let resMessageIsDonePromise: (toolCalls?: ToolCallType[] | undefined) => void // resolves when user approves this tool use (or if tool doesn't require approval)
|
||||
const messageIsDonePromise = new Promise<ToolCallType[] | undefined>((res, rej) => { resMessageIsDonePromise = res })
|
||||
let resMessageIsDonePromise: (toolCall?: ParsedToolCallObj | undefined) => void // resolves when user approves this tool use (or if tool doesn't require approval)
|
||||
const messageIsDonePromise = new Promise<ParsedToolCallObj | undefined>((res, rej) => { resMessageIsDonePromise = res })
|
||||
|
||||
// send llm message
|
||||
this._setStreamState(threadId, { isRunning: 'LLM' }, 'merge')
|
||||
const messages = await getLatestMessages()
|
||||
const llmCancelToken = this._llmMessageService.sendLLMMessage({
|
||||
messagesType: 'chatMessages',
|
||||
chatMode,
|
||||
messages,
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } },
|
||||
onText: ({ fullText, fullReasoning, fullToolName, fullToolParams }) => {
|
||||
this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning, toolNameSoFar: fullToolName, toolParamsSoFar: fullToolParams }, 'merge')
|
||||
onText: ({ fullText, fullReasoning, toolCall }) => {
|
||||
this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall }, 'merge')
|
||||
},
|
||||
onFinalMessage: async ({ fullText, toolCalls, fullReasoning, anthropicReasoning }) => {
|
||||
onFinalMessage: async ({ fullText, toolCall, fullReasoning, anthropicReasoning }) => {
|
||||
this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning, anthropicReasoning })
|
||||
// added to history and no longer streaming this, so clear messages so far and streamingToken (but do not stop isRunning)
|
||||
this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolNameSoFar: undefined, toolParamsSoFar: undefined }, 'merge')
|
||||
this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge')
|
||||
// resolve with tool calls
|
||||
resMessageIsDonePromise(toolCalls)
|
||||
resMessageIsDonePromise(toolCall)
|
||||
},
|
||||
onError: (error) => {
|
||||
const messageSoFar = this.streamState[threadId]?.messageSoFar ?? ''
|
||||
|
|
@ -763,14 +747,14 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
break
|
||||
}
|
||||
this._setStreamState(threadId, { streamingToken: llmCancelToken }, 'merge') // new stream token for the new message
|
||||
const toolCalls = await messageIsDonePromise // wait for message to complete
|
||||
const toolCall = await messageIsDonePromise // wait for message to complete
|
||||
if (aborted) { return }
|
||||
this._setStreamState(threadId, { streamingToken: undefined }, 'merge') // streaming message is done
|
||||
|
||||
// call tool if there is one
|
||||
const tool: ToolCallType | undefined = toolCalls?.[0]
|
||||
const tool: ParsedToolCallObj | undefined = toolCall
|
||||
if (tool) {
|
||||
const { awaitingUserApproval, interrupted } = await handleToolCall(tool)
|
||||
const { awaitingUserApproval, interrupted } = await handleToolCall(tool.name, { preapproved: false, unvalidatedToolParams: tool.rawParams })
|
||||
|
||||
// stop if interrupted. we don't have to do this for llmMessage because we have a stream token for it and onAbort gets called, but we don't have the equivalent for tools.
|
||||
// just detect tool interruption which is the same as chat interruption right now
|
||||
|
|
|
|||
|
|
@ -1400,6 +1400,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
messages,
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
chatMode: null, // not chat
|
||||
onText: (params) => {
|
||||
const { fullText: fullText_ } = params
|
||||
const newText_ = fullText_.substring(fullTextSoFar.length, Infinity)
|
||||
|
|
@ -1617,6 +1618,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
messages,
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
chatMode: null, // not chat
|
||||
onText: (params) => {
|
||||
const { fullText } = params
|
||||
// blocks are [done done done ... {writingFinal|writingOriginal}]
|
||||
|
|
|
|||
|
|
@ -1921,7 +1921,7 @@ const ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, mes
|
|||
return null
|
||||
}
|
||||
|
||||
else if (role === 'decorative_canceled_tool') {
|
||||
else if (role === 'interrupted_streaming_tool') {
|
||||
return <div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
|
||||
<CanceledTool toolName={chatMessage.name} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { IVoidCommandBarService } from './voidCommandBarService.js'
|
|||
import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree1Deep } from './directoryStrService.js'
|
||||
import { IMarkerService } from '../../../../platform/markers/common/markers.js'
|
||||
import { timeout } from '../../../../base/common/async.js'
|
||||
import { ParsedToolParamsObj } from '../common/sendLLMMessageTypes.js'
|
||||
|
||||
|
||||
// tool use for AI
|
||||
|
|
@ -23,7 +24,7 @@ import { timeout } from '../../../../base/common/async.js'
|
|||
|
||||
|
||||
|
||||
type ValidateParams = { [T in ToolName]: (p: string) => Promise<ToolCallParams[T]> }
|
||||
type ValidateParams = { [T in ToolName]: (p: ParsedToolParamsObj) => Promise<ToolCallParams[T]> }
|
||||
type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T], interruptTool?: () => void }> }
|
||||
type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: Awaited<ToolResultType[T]>) => string }
|
||||
|
||||
|
|
@ -38,25 +39,6 @@ export const TERMINAL_TIMEOUT_TIME = 15
|
|||
export const TERMINAL_BG_WAIT_TIME = 1
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const validateJSON = (s: string): { [s: string]: unknown } => {
|
||||
try {
|
||||
const o = JSON.parse(s)
|
||||
if (typeof o !== 'object') throw new Error()
|
||||
|
||||
if ('result' in o) { // openrouter sometimes wraps the result with { 'result': ... }
|
||||
return o.result
|
||||
}
|
||||
|
||||
return o
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error(`Invalid LLM output format: Tool parameter was not a string of a valid JSON: "${s}".`)
|
||||
}
|
||||
}
|
||||
|
||||
const isFalsy = (u: unknown) => {
|
||||
return !u || u === 'null' || u === 'undefined'
|
||||
}
|
||||
|
|
@ -172,9 +154,8 @@ export class ToolsService implements IToolsService {
|
|||
const queryBuilder = instantiationService.createInstance(QueryBuilder);
|
||||
|
||||
this.validateParams = {
|
||||
read_file: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
const { uri: uriStr, startLine: startLineUnknown, endLine: endLineUnknown, pageNumber: pageNumberUnknown } = o
|
||||
read_file: async (params: ParsedToolParamsObj) => {
|
||||
const { uri: uriStr, startLine: startLineUnknown, endLine: endLineUnknown, pageNumber: pageNumberUnknown } = params
|
||||
|
||||
const uri = validateURI(uriStr)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
|
|
@ -184,27 +165,24 @@ export class ToolsService implements IToolsService {
|
|||
|
||||
return { uri, startLine, endLine, pageNumber }
|
||||
},
|
||||
ls_dir: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
const { uri: uriStr, pageNumber: pageNumberUnknown } = o
|
||||
ls_dir: async (params: ParsedToolParamsObj) => {
|
||||
const { uri: uriStr, pageNumber: pageNumberUnknown } = params
|
||||
|
||||
const uri = validateURI(uriStr)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
return { rootURI: uri, pageNumber }
|
||||
},
|
||||
get_dir_structure: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
const { uri: uriStr, } = o
|
||||
get_dir_structure: async (params: ParsedToolParamsObj) => {
|
||||
const { uri: uriStr, } = params
|
||||
const uri = validateURI(uriStr)
|
||||
return { rootURI: uri }
|
||||
},
|
||||
search_pathnames_only: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
search_pathnames_only: async (params: ParsedToolParamsObj) => {
|
||||
const {
|
||||
query: queryUnknown,
|
||||
include: includeUnknown,
|
||||
pageNumber: pageNumberUnknown
|
||||
} = o
|
||||
} = params
|
||||
|
||||
const queryStr = validateStr('query', queryUnknown)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
|
|
@ -213,14 +191,13 @@ export class ToolsService implements IToolsService {
|
|||
return { queryStr, include, pageNumber }
|
||||
|
||||
},
|
||||
search_files: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
search_files: async (params: ParsedToolParamsObj) => {
|
||||
const {
|
||||
query: queryUnknown,
|
||||
searchInFolder: searchInFolderUnknown,
|
||||
isRegex: isRegexUnknown,
|
||||
pageNumber: pageNumberUnknown
|
||||
} = o
|
||||
} = params
|
||||
|
||||
const queryStr = validateStr('query', queryUnknown)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
|
|
@ -233,18 +210,16 @@ export class ToolsService implements IToolsService {
|
|||
|
||||
// ---
|
||||
|
||||
create_file_or_folder: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
const { uri: uriUnknown } = o
|
||||
create_file_or_folder: async (params: ParsedToolParamsObj) => {
|
||||
const { uri: uriUnknown } = params
|
||||
const uri = validateURI(uriUnknown)
|
||||
const uriStr = validateStr('uri', uriUnknown)
|
||||
const isFolder = checkIfIsFolder(uriStr)
|
||||
return { uri, isFolder }
|
||||
},
|
||||
|
||||
delete_file_or_folder: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
const { uri: uriUnknown, params: paramsStr } = o
|
||||
delete_file_or_folder: async (params: ParsedToolParamsObj) => {
|
||||
const { uri: uriUnknown, params: paramsStr } = params
|
||||
const uri = validateURI(uriUnknown)
|
||||
const isRecursive = validateRecursiveParamStr(paramsStr)
|
||||
const uriStr = validateStr('uri', uriUnknown)
|
||||
|
|
@ -252,17 +227,15 @@ export class ToolsService implements IToolsService {
|
|||
return { uri, isRecursive, isFolder }
|
||||
},
|
||||
|
||||
edit_file: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
const { uri: uriStr, changeDescription: changeDescriptionUnknown } = o
|
||||
edit_file: async (params: ParsedToolParamsObj) => {
|
||||
const { uri: uriStr, changeDescription: changeDescriptionUnknown } = params
|
||||
const uri = validateURI(uriStr)
|
||||
const changeDescription = validateStr('changeDescription', changeDescriptionUnknown)
|
||||
return { uri, changeDescription }
|
||||
},
|
||||
|
||||
run_terminal_command: async (s: string) => {
|
||||
const o = validateJSON(s)
|
||||
const { command: commandUnknown, terminalId: terminalIdUnknown, waitForCompletion: waitForCompletionUnknown } = o
|
||||
run_terminal_command: async (params: ParsedToolParamsObj) => {
|
||||
const { command: commandUnknown, terminalId: terminalIdUnknown, waitForCompletion: waitForCompletionUnknown } = params
|
||||
const command = validateStr('command', commandUnknown)
|
||||
const proposedTerminalId = validateProposedTerminalId(terminalIdUnknown)
|
||||
const waitForCompletion = validateBoolean(waitForCompletionUnknown, { default: true })
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ import { ToolName, ToolCallParams, ToolResultType } from './toolsServiceTypes.js
|
|||
|
||||
export type ToolMessage<T extends ToolName> = {
|
||||
role: 'tool';
|
||||
paramsStr: string; // internal use
|
||||
id: string; // apis require this tool use id
|
||||
content: string; // give this result to LLM (string of value)
|
||||
} & (
|
||||
// in order of events:
|
||||
|
|
@ -27,18 +25,10 @@ export type ToolMessage<T extends ToolName> = {
|
|||
) // user rejected
|
||||
|
||||
export type DecorativeCanceledTool = {
|
||||
role: 'decorative_canceled_tool';
|
||||
role: 'interrupted_streaming_tool';
|
||||
name: string;
|
||||
}
|
||||
|
||||
// export type ToolRequestApproval<T extends ToolName> = {
|
||||
// role: 'tool_request';
|
||||
// name: T; // internal use
|
||||
// params: ToolCallParams[T]; // internal use
|
||||
// paramsStr: string; // internal use - this is what the LLM outputted, not necessarily JSON.stringify(params)
|
||||
// id: string; // proposed tool's id
|
||||
// }
|
||||
|
||||
|
||||
// checkpoints
|
||||
export type CheckpointEntry = {
|
||||
|
|
|
|||
|
|
@ -212,15 +212,17 @@ Available tools:
|
|||
${availableToolsStr(tools)}
|
||||
|
||||
Tool calling details: ${''/* We expect tools to come at the end - not a hard limit, but that's just how we process them, and the flow makes more sense that way. */}
|
||||
- Tool calling is optional.
|
||||
- To call a tool, just write its name followed by any parameters in XML format. For example:
|
||||
<tool_name>
|
||||
<parameter1>value1</parameter1>
|
||||
<parameter2>value2</parameter2>
|
||||
</tool_name>
|
||||
- You must write all tool calls at the END of your response. The beginning of your response should be your normal response followed by tool calls at the END.
|
||||
- You are allowed to call multiple tools by specifying them consecutively. However, there should be NO text or writing between tool calls or after them.
|
||||
- Tool that you call will be executed immediately, and you will have access to the results in your next response.`
|
||||
- You must write your tool call at the END of your response. The beginning of your response should be your normal response followed by the tool call at the END.
|
||||
- You are only allowed to output one tool call per response.
|
||||
- The tool call will be executed immediately, and you will have access to the results in your next response.`
|
||||
}
|
||||
// - You are allowed to call multiple tools by specifying them consecutively. However, there should be NO text or writing between tool calls or after them.
|
||||
|
||||
|
||||
// ======================================================== chat (normal, gather, agent) ========================================================
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ToolName } from './toolsServiceTypes.js'
|
||||
import { ModelSelection, ModelSelectionOptions, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
|
||||
import { ChatMode, ModelSelection, ModelSelectionOptions, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
|
||||
|
||||
|
||||
export const errorDetails = (fullError: Error | null): string | null => {
|
||||
|
|
@ -40,16 +40,21 @@ export type LLMChatMessage = {
|
|||
}
|
||||
|
||||
|
||||
export type ToolCallType = {
|
||||
name: ToolName;
|
||||
paramsStr: string;
|
||||
id: string;
|
||||
export type ParsedToolParamsObj = {
|
||||
[paramName: string]: string;
|
||||
}
|
||||
export type ParsedToolCallObj = {
|
||||
name: ToolName;
|
||||
rawParams: ParsedToolParamsObj;
|
||||
doneParams: string[];
|
||||
isDone: boolean;
|
||||
};
|
||||
|
||||
|
||||
export type AnthropicReasoning = ({ type: 'thinking'; thinking: any; signature: string; } | { type: 'redacted_thinking', data: any })
|
||||
|
||||
export type OnText = (p: { fullText: string; fullReasoning: string; }) => void
|
||||
export type OnFinalMessage = (p: { fullText: string; fullReasoning: string; toolCalls?: ToolCallType[]; anthropicReasoning: AnthropicReasoning[] | null }) => void // id is tool_use_id
|
||||
export type OnText = (p: { fullText: string; fullReasoning: string; toolCall?: ParsedToolCallObj }) => void
|
||||
export type OnFinalMessage = (p: { fullText: string; fullReasoning: string; toolCall?: ParsedToolCallObj; anthropicReasoning: AnthropicReasoning[] | null }) => void // id is tool_use_id
|
||||
export type OnError = (p: { message: string; fullError: Error | null }) => void
|
||||
export type OnAbort = () => void
|
||||
export type AbortRef = { current: (() => void) | null }
|
||||
|
|
@ -64,9 +69,11 @@ export type LLMFIMMessage = {
|
|||
type SendLLMType = {
|
||||
messagesType: 'chatMessages';
|
||||
messages: LLMChatMessage[];
|
||||
chatMode: ChatMode | null;
|
||||
} | {
|
||||
messagesType: 'FIMMessage';
|
||||
messages: LLMFIMMessage;
|
||||
chatMode?: undefined;
|
||||
}
|
||||
|
||||
// service types
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], opt
|
|||
|
||||
export const modelFilterOfFeatureName: { [featureName in FeatureName]: { filter: (o: ModelSelection, opts: { chatMode: ChatMode }) => boolean; emptyMessage: null | { message: string, priority: 'always' | 'fallback' } } } = {
|
||||
'Autocomplete': { filter: (o) => getModelCapabilities(o.providerName, o.modelName).supportsFIM, emptyMessage: { message: 'No models support FIM', priority: 'always' } },
|
||||
'Chat': { filter: (o, { chatMode }) => chatMode === 'normal' ? true : !!getModelCapabilities(o.providerName, o.modelName).supportsTools, emptyMessage: { message: 'No models support tool use', priority: 'fallback' } },
|
||||
'Chat': { filter: o => true, emptyMessage: null, },
|
||||
'Ctrl+K': { filter: o => true, emptyMessage: null, },
|
||||
'Apply': { filter: o => true, emptyMessage: null, },
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { endsWithAnyPrefixOf } from '../../common/helpers/extractCodeFromResult.js'
|
||||
import { InternalToolInfo } from '../../common/prompt/prompts.js'
|
||||
import { OnText } from '../../common/sendLLMMessageTypes.js'
|
||||
import { OnText, ParsedToolCallObj } from '../../common/sendLLMMessageTypes.js'
|
||||
import sax from 'sax'
|
||||
import { ToolName } from '../../common/toolsServiceTypes.js'
|
||||
|
||||
|
||||
// =========================================== reasoning ===========================================
|
||||
// =============== reasoning ===============
|
||||
|
||||
// could simplify this - this assumes we can never add a tag without committing it to the user's screen, but that's not true
|
||||
export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string, string]): OnText => {
|
||||
|
|
@ -115,19 +116,19 @@ export const extractReasoningOnFinalMessage = (fullText_: string, thinkTags: [st
|
|||
}
|
||||
|
||||
|
||||
// =========================================== tools ===========================================
|
||||
// =============== tools ===============
|
||||
|
||||
type ToolsState = {
|
||||
level: 'normal',
|
||||
} | {
|
||||
level: 'tool',
|
||||
toolName: string,
|
||||
currentToolCall: ToolCall,
|
||||
currentToolCall: ParsedToolCallObj,
|
||||
} | {
|
||||
level: 'param',
|
||||
toolName: string,
|
||||
paramName: string,
|
||||
currentToolCall: ToolCall,
|
||||
currentToolCall: ParsedToolCallObj,
|
||||
}
|
||||
|
||||
export const extractToolsOnTextWrapper = (onText: OnText, availableTools: InternalToolInfo[]) => {
|
||||
|
|
@ -137,16 +138,20 @@ export const extractToolsOnTextWrapper = (onText: OnText, availableTools: Intern
|
|||
// detect <availableTools[0]></availableTools[0]>, etc
|
||||
let fullText = '';
|
||||
let trueFullText = ''
|
||||
const currentToolCalls: ToolCall[] = []; // the answer
|
||||
const currentToolCalls: ParsedToolCallObj[] = []; // the answer
|
||||
|
||||
let state: ToolsState = { level: 'normal' }
|
||||
|
||||
|
||||
const getRawNewText = () => {
|
||||
return trueFullText.substring(parser.startTagPosition, parser.position + 1)
|
||||
}
|
||||
const parser = sax.parser(false);
|
||||
|
||||
|
||||
// when see open tag <tagName>
|
||||
parser.onopentag = (node) => {
|
||||
const rawNewText = trueFullText.substring(parser.startTagPosition, parser.position)
|
||||
const rawNewText = getRawNewText()
|
||||
console.log('raw new text a', rawNewText)
|
||||
console.log('OPEN!', node.name)
|
||||
const tagName = node.name;
|
||||
|
|
@ -155,7 +160,7 @@ export const extractToolsOnTextWrapper = (onText: OnText, availableTools: Intern
|
|||
state = {
|
||||
level: 'tool',
|
||||
toolName: tagName,
|
||||
currentToolCall: { name: tagName, parameters: {} }
|
||||
currentToolCall: { name: tagName as ToolName, rawParams: {}, doneParams: [], isDone: false }
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
|
@ -190,12 +195,12 @@ export const extractToolsOnTextWrapper = (onText: OnText, availableTools: Intern
|
|||
// ignore all text in a tool, all text should go in the param tags inside it
|
||||
}
|
||||
else if (state.level === 'param') {
|
||||
state.currentToolCall.parameters[state.currentToolCall.name] += text
|
||||
state.currentToolCall.rawParams[state.currentToolCall.name] += text
|
||||
}
|
||||
}
|
||||
|
||||
parser.onclosetag = (tagName) => {
|
||||
const rawNewText = trueFullText.substring(parser.startTagPosition, parser.position)
|
||||
const rawNewText = getRawNewText()
|
||||
console.log('raw new text b', rawNewText)
|
||||
console.log('CLOSE!', tagName)
|
||||
if (state.level === 'normal') {
|
||||
|
|
@ -203,6 +208,7 @@ export const extractToolsOnTextWrapper = (onText: OnText, availableTools: Intern
|
|||
}
|
||||
else if (state.level === 'tool') {
|
||||
if (tagName === state.toolName) { // closed the tool
|
||||
state.currentToolCall.isDone = true
|
||||
currentToolCalls.push(state.currentToolCall)
|
||||
state = {
|
||||
level: 'normal',
|
||||
|
|
@ -214,6 +220,7 @@ export const extractToolsOnTextWrapper = (onText: OnText, availableTools: Intern
|
|||
}
|
||||
else if (state.level === 'param') {
|
||||
if (tagName === state.paramName) { // closed the param
|
||||
state.currentToolCall.doneParams.push(state.paramName)
|
||||
state = {
|
||||
level: 'tool',
|
||||
toolName: state.toolName,
|
||||
|
|
@ -226,15 +233,14 @@ export const extractToolsOnTextWrapper = (onText: OnText, availableTools: Intern
|
|||
|
||||
const newOnText: OnText = (params) => {
|
||||
const newText = params.fullText.substring(fullText.length);
|
||||
console.log('newText', newText)
|
||||
console.log('newText', state.level, newText)
|
||||
trueFullText = params.fullText
|
||||
parser.write(newText)
|
||||
|
||||
console.log('state',)
|
||||
onText({
|
||||
...params,
|
||||
fullText,
|
||||
toolCalls: currentToolCalls.length > 0 ? [...currentToolCalls] : undefined
|
||||
toolCall: currentToolCalls.length > 0 ? currentToolCalls[0] : undefined
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -8,10 +8,11 @@ import { Ollama } from 'ollama';
|
|||
import OpenAI, { ClientOptions } from 'openai';
|
||||
|
||||
import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/sendLLMMessageTypes.js';
|
||||
import { defaultProviderSettings, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
|
||||
import { ChatMode, defaultProviderSettings, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
|
||||
import { prepareFIMMessage, prepareMessages } from './preprocessLLMMessages.js';
|
||||
import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities } from '../../common/modelCapabilities.js';
|
||||
import { extractReasoningOnFinalMessage, extractReasoningOnTextWrapper, extractToolsOnTextWrapper } from './extractGrammar.js';
|
||||
import { availableTools } from '../../common/prompt/prompts.js';
|
||||
|
||||
|
||||
type InternalCommonMessageParams = {
|
||||
|
|
@ -26,7 +27,7 @@ type InternalCommonMessageParams = {
|
|||
_setAborter: (aborter: () => void) => void;
|
||||
}
|
||||
|
||||
type SendChatParams_Internal = InternalCommonMessageParams & { messages: LLMChatMessage[]; }
|
||||
type SendChatParams_Internal = InternalCommonMessageParams & { messages: LLMChatMessage[]; chatMode: ChatMode | null; }
|
||||
type SendFIMParams_Internal = InternalCommonMessageParams & { messages: LLMFIMMessage; }
|
||||
export type ListParams_Internal<ModelResponse> = ModelListParams<ModelResponse>
|
||||
|
||||
|
|
@ -123,7 +124,7 @@ const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError
|
|||
|
||||
|
||||
|
||||
const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, aiInstructions }: SendChatParams_Internal) => {
|
||||
const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, aiInstructions, chatMode }: SendChatParams_Internal) => {
|
||||
const {
|
||||
modelName,
|
||||
supportsSystemMessage,
|
||||
|
|
@ -159,8 +160,13 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
|
|||
onText = extractReasoningOnTextWrapper(onText, openSourceThinkTags)
|
||||
}
|
||||
|
||||
if ()
|
||||
onText = extractToolsOnTextWrapper(onText,)
|
||||
// manually parse out tool results
|
||||
if (chatMode) {
|
||||
const tools = availableTools(chatMode)
|
||||
if (tools) {
|
||||
onText = extractToolsOnTextWrapper(onText, tools)
|
||||
}
|
||||
}
|
||||
|
||||
let fullReasoningSoFar = ''
|
||||
let fullTextSoFar = ''
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export const sendLLMMessage = ({
|
|||
settingsOfProvider,
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
chatMode,
|
||||
}: SendLLMMessageParams,
|
||||
|
||||
metricsService: IMetricsService
|
||||
|
|
@ -107,7 +108,7 @@ export const sendLLMMessage = ({
|
|||
}
|
||||
const { sendFIM, sendChat } = implementation
|
||||
if (messagesType === 'chatMessages') {
|
||||
sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, aiInstructions })
|
||||
sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, aiInstructions, chatMode })
|
||||
return
|
||||
}
|
||||
if (messagesType === 'FIMMessage') {
|
||||
|
|
|
|||
Loading…
Reference in a new issue