From 4e197f46590dfe42958f0298996be13968c4e412 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Tue, 8 Apr 2025 02:04:08 -0700 Subject: [PATCH] debug --- .../contrib/void/browser/chatThreadService.ts | 8 +- .../contrib/void/browser/toolsService.ts | 1 - .../contrib/void/common/prompt/prompts.ts | 17 ++- .../void/common/sendLLMMessageTypes.ts | 2 +- .../llmMessage/extractGrammar.ts | 120 ++++++++++++------ .../void/electron-main/llmMessage/sax.ts | 8 +- .../llmMessage/sendLLMMessage.impl.ts | 21 +-- 7 files changed, 113 insertions(+), 64 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 5d79d68a..f2b28552 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -586,6 +586,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const runningTerminalIds = this._terminalToolService.listTerminalIds() const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, runningTerminalIds, chatMode }) + // console.log('SYSTEM MESSAGE', systemMessage) // all messages so far in the chat history (including tools) const messages: LLMChatMessage[] = [ { role: 'system', content: systemMessage, }, @@ -613,6 +614,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { if (!opts.preapproved) { // skip this if pre-approved // 1. validate tool params try { + console.log('VALIDATING PARAMS!!!', opts.unvalidatedToolParams) const params = await this._toolsService.validateParams[toolName](opts.unvalidatedToolParams) toolParams = params @@ -716,12 +718,10 @@ class ChatThreadService extends Disposable implements IChatThreadService { onText: ({ fullText, fullReasoning, toolCall }) => { this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall }, 'merge') }, - onFinalMessage: async ({ fullText, toolCall, fullReasoning, anthropicReasoning }) => { + onFinalMessage: async ({ fullText, fullReasoning, toolCall, 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, toolCallSoFar: undefined }, 'merge') - // resolve with tool calls - resMessageIsDonePromise(toolCall) + resMessageIsDonePromise(toolCall) // resolve with tool calls }, onError: (error) => { const messageSoFar = this.streamState[threadId]?.messageSoFar ?? '' diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index d7823531..e088d556 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -157,7 +157,6 @@ export class ToolsService implements IToolsService { this.validateParams = { read_file: async (params: ParsedToolParamsObj) => { const { uri: uriStr, startLine: startLineUnknown, endLine: endLineUnknown, pageNumber: pageNumberUnknown } = params - const uri = validateURI(uriStr) const pageNumber = validatePageNum(pageNumberUnknown) diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 61ee6524..6e71724b 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -188,9 +188,11 @@ export const availableTools = (chatMode: ChatMode) => { const availableToolsStr = (tools: InternalToolInfo[]) => { return `${tools.map((t, i) => { - const params = Object.keys(t.params).map(paramName => `<${paramName}>\n${t.params[paramName].description}\n`).join('\n') + const params = Object.keys(t.params).map(paramName => ` <${paramName}>\n${t.params[paramName].description}\n `).join('\n') return `\ -${i}. ${t.name}: ${t.description} +${i}. ${t.name} +Description: ${t.description} +Format: <${t.name}>${!params ? '' : `\n${params}`} ` }).join('\n\n')}` @@ -225,11 +227,16 @@ Tool calling details: ${''/* We expect tools to come at the end - not a hard li - Tool calling is optional. - To call a tool, just write its name followed by any parameters in XML format. For example: - value1 - value2 + +value1 + + +value2 + -- 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 must write your tool call at the END of your response. The beginning of your response should be normal text, explanations, etc (if you decide to write anything), followed by the tool call at the END. - You are only allowed to output one tool call per response. +- You may omit optional parameters. - 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. diff --git a/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts index 49fe38fc..de65b21d 100644 --- a/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts @@ -41,7 +41,7 @@ export type LLMChatMessage = { export type ParsedToolParamsObj = { - [paramName: string]: string; + [paramName: string]: string | undefined; } export type RawToolCallObj = { name: ToolName; diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts index 33ecc5b2..8fb20d3d 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts @@ -1,14 +1,21 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + import { endsWithAnyPrefixOf } from '../../common/helpers/extractCodeFromResult.js' import { availableTools, InternalToolInfo, ToolName } from '../../common/prompt/prompts.js' -import { OnText, RawToolCallObj } from '../../common/sendLLMMessageTypes.js' +import { OnFinalMessage, OnText, RawToolCallObj } from '../../common/sendLLMMessageTypes.js' import { ChatMode } from '../../common/voidSettingsTypes.js' -import sax from 'sax' +import { createSaxParser } from './sax.js' // =============== 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 => { +export const extractReasoningOnTextWrapper = ( + onText: OnText, onFinalMessage: OnFinalMessage, thinkTags: [string, string] +): { newOnText: OnText, newOnFinalMessage: OnFinalMessage } => { let latestAddIdx = 0 // exclusive index in fullText_ let foundTag1 = false let foundTag2 = false @@ -100,19 +107,26 @@ export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) } - return newOnText -} + const getOnFinalMessageParams = () => { + const fullText_ = fullTextSoFar + const tag1Idx = fullText_.indexOf(thinkTags[0]) + const tag2Idx = fullText_.indexOf(thinkTags[1]) + if (tag1Idx === -1) return { fullText: fullText_, fullReasoning: '' } // never started reasoning + if (tag2Idx === -1) return { fullText: '', fullReasoning: fullText_ } // never stopped reasoning -export const extractReasoningOnFinalMessage = (fullText_: string, thinkTags: [string, string]): { fullText: string, fullReasoning: string } => { - const tag1Idx = fullText_.indexOf(thinkTags[0]) - const tag2Idx = fullText_.indexOf(thinkTags[1]) - if (tag1Idx === -1) return { fullText: fullText_, fullReasoning: '' } // never started reasoning - if (tag2Idx === -1) return { fullText: '', fullReasoning: fullText_ } // never stopped reasoning + const fullReasoning = fullText_.substring(tag1Idx + thinkTags[0].length, tag2Idx) + const fullText = fullText_.substring(0, tag1Idx) + fullText_.substring(tag2Idx + thinkTags[1].length, Infinity) - const fullReasoning = fullText_.substring(tag1Idx + thinkTags[0].length, tag2Idx) - const fullText = fullText_.substring(0, tag1Idx) + fullText_.substring(tag2Idx + thinkTags[1].length, Infinity) - return { fullText, fullReasoning } + return { fullText, fullReasoning } + } + + const newOnFinalMessage: OnFinalMessage = (params) => { + const { fullText, fullReasoning } = getOnFinalMessageParams() + onFinalMessage({ ...params, fullText, fullReasoning }) + } + + return { newOnText, newOnFinalMessage } } @@ -131,9 +145,11 @@ type ToolsState = { currentToolCall: RawToolCallObj, } -export const extractToolsOnTextWrapper = (onText: OnText, chatMode: ChatMode) => { +export const extractToolsOnTextWrapper = ( + onText: OnText, onFinalMessage: OnFinalMessage, chatMode: ChatMode +): { newOnText: OnText, newOnFinalMessage: OnFinalMessage } => { const tools = availableTools(chatMode) - if (!tools) return onText + if (!tools) return { newOnText: onText, newOnFinalMessage: onFinalMessage } const toolOfToolName: { [toolName: string]: InternalToolInfo | undefined } = {} for (const t of tools) { toolOfToolName[t.name] = t } @@ -149,17 +165,15 @@ export const extractToolsOnTextWrapper = (onText: OnText, chatMode: ChatMode) => const getRawNewText = () => { return trueFullText.substring(parser.startTagPosition, parser.position + 1) } - const parser = sax.parser(false, { - lowercase: true, - }); - + const parser = createSaxParser({ lowercase: true }) // when see open tag parser.onopentag = (node) => { const rawNewText = getRawNewText() - console.log('raw new text a', rawNewText) - console.log('OPEN!', node.name) const tagName = node.name; + console.log('OPENING', tagName) + console.log('state0:', state.level, { toolName: (state as any).toolName, paramName: (state as any).paramName }) + if (state.level === 'normal') { if (tagName in toolOfToolName) { // valid toolName state = { @@ -170,6 +184,8 @@ export const extractToolsOnTextWrapper = (onText: OnText, chatMode: ChatMode) => } else { fullText += rawNewText // count as plaintext + console.log('adding raw a', rawNewText) + } } else if (state.level === 'tool') { @@ -185,31 +201,25 @@ export const extractToolsOnTextWrapper = (onText: OnText, chatMode: ChatMode) => // would normally be rawNewText, but we ignore all text inside tools } } - else if (state.level === 'param') { + else if (state.level === 'param') { // cannot double nest fullText += rawNewText // count as plaintext - } - }; + console.log('adding raw b', rawNewText) - parser.ontext = (text) => { - console.log('TEXT!', JSON.stringify(text)) - if (state.level === 'normal') { - fullText += text } - // start param - else if (state.level === 'tool') { - // ignore all text in a tool, all text should go in the param tags inside it - } - else if (state.level === 'param') { - state.currentToolCall.rawParams[state.currentToolCall.name] += text - } - } + + console.log('state1:', state.level, { toolName: (state as any).toolName, paramName: (state as any).paramName }) + + }; parser.onclosetag = (tagName) => { const rawNewText = getRawNewText() - console.log('raw new text b', rawNewText) - console.log('CLOSE!', tagName) + console.log('CLOSING', tagName) + console.log('state0:', state.level, { toolName: (state as any).toolName, paramName: (state as any).paramName }) + + if (state.level === 'normal') { fullText += rawNewText + console.log('adding raw A', rawNewText) } else if (state.level === 'tool') { if (tagName === state.toolName) { // closed the tool @@ -221,6 +231,7 @@ export const extractToolsOnTextWrapper = (onText: OnText, chatMode: ChatMode) => } else { // add as text fullText += rawNewText + console.log('adding raw B', rawNewText) } } else if (state.level === 'param') { @@ -234,21 +245,40 @@ export const extractToolsOnTextWrapper = (onText: OnText, chatMode: ChatMode) => } else { fullText += rawNewText + console.log('adding raw C', rawNewText) + } } + console.log('state1:', state.level, { toolName: (state as any).toolName, paramName: (state as any).paramName }) + }; + + parser.ontext = (text) => { + if (state.level === 'normal') { + fullText += text + } + // start param + else if (state.level === 'tool') { + // ignore all text in a tool, all text should go in the param tags inside it + } + else if (state.level === 'param') { + if (!(state.paramName in state.currentToolCall.rawParams)) state.currentToolCall.rawParams[state.paramName] = '' + state.currentToolCall.rawParams[state.paramName] += text + } + } + + + let prevFullTextLen = 0 const newOnText: OnText = (params) => { const newText = params.fullText.substring(prevFullTextLen) prevFullTextLen = params.fullText.length trueFullText = params.fullText - console.log('newText', newText.length) parser.write(newText) - console.log('calling ontext...') onText({ ...params, fullText, @@ -256,7 +286,15 @@ export const extractToolsOnTextWrapper = (onText: OnText, chatMode: ChatMode) => }); }; - return newOnText; + + const newOnFinalMessage: OnFinalMessage = (params) => { + console.log('final message!!!', trueFullText) + console.log('----- returning ----\n', fullText) + console.log('----- tools ----\n', JSON.stringify(currentToolCalls, null, 2)) + onFinalMessage({ ...params, fullText, toolCall: currentToolCalls[0] }) + } + + return { newOnText, newOnFinalMessage }; } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sax.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sax.ts index 9fce5a2c..e27e0753 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sax.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sax.ts @@ -63,7 +63,7 @@ export function createSaxParser(options: SaxParserOptions = {}): SaxParser { if (cursor < buffer.length && this.ontext) { this.ontext(buffer.substring(cursor)); } - // Clear the buffer once all content is processed. + // Clear the buffer since all content is processed. buffer = ''; break; } @@ -123,7 +123,11 @@ export function createSaxParser(options: SaxParserOptions = {}): SaxParser { // Move the cursor past the current tag. cursor = gtIndex + 1; } - }, + + // Remove any content already processed from the buffer. + buffer = buffer.slice(cursor); + } + }; return parser; diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts index c8af7ff4..3f49d017 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts @@ -11,7 +11,7 @@ import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, On 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 { extractReasoningOnTextWrapper, extractToolsOnTextWrapper } from './extractGrammar.js'; type InternalCommonMessageParams = { @@ -156,12 +156,16 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage const { needsManualParse: needsManualReasoningParse, nameOfFieldInDelta: nameOfReasoningFieldInDelta } = providerReasoningIOSettings?.output ?? {} const manuallyParseReasoning = needsManualReasoningParse && canIOReasoning && openSourceThinkTags if (manuallyParseReasoning) { - onText = extractReasoningOnTextWrapper(onText, openSourceThinkTags) + const { newOnText, newOnFinalMessage } = extractReasoningOnTextWrapper(onText, onFinalMessage, openSourceThinkTags) + onText = newOnText + onFinalMessage = newOnFinalMessage } // manually parse out tool results if (chatMode) { - onText = extractToolsOnTextWrapper(onText, chatMode) + const { newOnText, newOnFinalMessage } = extractToolsOnTextWrapper(onText, onFinalMessage, chatMode) + onText = newOnText + onFinalMessage = newOnFinalMessage } let fullReasoningSoFar = '' @@ -192,12 +196,7 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage onError({ message: 'Void: Response from model was empty.', fullError: null }) } else { - if (manuallyParseReasoning) { - const { fullText, fullReasoning } = extractReasoningOnFinalMessage(fullTextSoFar, openSourceThinkTags) - onFinalMessage({ fullText, fullReasoning, anthropicReasoning: null }); - } else { - onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null }); - } + onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null }); } }) // when error/fail - this catches errors of both .create() and .then(for await) @@ -282,7 +281,9 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM // manually parse out tool results if (chatMode) { - onText = extractToolsOnTextWrapper(onText, chatMode) + const { newOnText, newOnFinalMessage } = extractToolsOnTextWrapper(onText, onFinalMessage, chatMode) + onText = newOnText + onFinalMessage = newOnFinalMessage } // when receive text