tool calls via plaintext initial draft

This commit is contained in:
Andrew Pareles 2025-04-07 22:25:07 -07:00
parent 8f8fa8548d
commit 1c5adb96d3
11 changed files with 118 additions and 147 deletions

View file

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

View file

@ -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}]

View file

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

View file

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

View file

@ -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 = {

View file

@ -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) ========================================================

View file

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

View file

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

View file

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

View file

@ -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 = ''

View file

@ -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') {