mirror of
https://github.com/voideditor/void
synced 2026-05-24 01:48:25 +00:00
debug
This commit is contained in:
parent
39bf2283cc
commit
4e197f4659
7 changed files with 113 additions and 64 deletions
|
|
@ -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 ?? ''
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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</${paramName}>`).join('\n')
|
||||
const params = Object.keys(t.params).map(paramName => ` <${paramName}>\n${t.params[paramName].description}\n </${paramName}>`).join('\n')
|
||||
return `\
|
||||
${i}. ${t.name}: ${t.description}
|
||||
${i}. ${t.name}
|
||||
Description: ${t.description}
|
||||
Format:
|
||||
<${t.name}>${!params ? '' : `\n${params}`}
|
||||
</${t.name}>`
|
||||
}).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:
|
||||
<tool_name>
|
||||
<parameter1>value1</parameter1>
|
||||
<parameter2>value2</parameter2>
|
||||
<parameter1>
|
||||
value1
|
||||
</parameter1>
|
||||
<parameter2>
|
||||
value2
|
||||
</parameter2>
|
||||
</tool_name>
|
||||
- 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.
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export type LLMChatMessage = {
|
|||
|
||||
|
||||
export type ParsedToolParamsObj = {
|
||||
[paramName: string]: string;
|
||||
[paramName: string]: string | undefined;
|
||||
}
|
||||
export type RawToolCallObj = {
|
||||
name: ToolName;
|
||||
|
|
|
|||
|
|
@ -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 <tagName>
|
||||
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 };
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue