From 8591d06244fc936a8110fe734a3d9d0f9c53fcba Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 15 Feb 2025 19:23:15 -0800 Subject: [PATCH] tool use plugboard progress --- .../contrib/void/browser/chatThreadService.ts | 33 ++- .../contrib/void/browser/editCodeService.ts | 6 +- .../contrib/void/browser/prompt/prompts.ts | 6 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 6 +- .../contrib/void/common/llmMessageTypes.ts | 17 +- .../void/common/voidSettingsService.ts | 4 +- .../contrib/void/common/voidSettingsTypes.ts | 238 ++++++++++-------- .../electron-main/llmMessage/anthropic.ts | 5 +- .../void/electron-main/llmMessage/groq.ts | 42 ---- .../void/electron-main/llmMessage/mistral.ts | 44 ---- .../void/electron-main/llmMessage/openai.ts | 14 +- .../llmMessage/sendLLMMessage.ts | 206 ++++++++++++--- 12 files changed, 367 insertions(+), 254 deletions(-) delete mode 100644 src/vs/workbench/contrib/void/electron-main/llmMessage/groq.ts delete mode 100644 src/vs/workbench/contrib/void/electron-main/llmMessage/mistral.ts diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 0c5b761a..98165ce0 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -65,6 +65,7 @@ export type ChatMessage = role: 'tool'; name: string; // internal use params: string | null; // internal use + tool_use_id: string; // apis require this content: string | null; // summary of the tool to the LLM displayContent: string | null; // text message of result } @@ -111,10 +112,12 @@ const newThreadObject = () => { } const THREAD_VERSION_KEY = 'void.chatThreadVersion' -const THREAD_VERSION = 'v2' +const LATEST_THREAD_VERSION = 'v2' const THREAD_STORAGE_KEY = 'void.chatThreadStorage' + +type ChatMode = 'agent' | 'chat' export interface IChatThreadService { readonly _serviceBrand: undefined; @@ -134,8 +137,8 @@ export interface IChatThreadService { useFocusedStagingState(messageIdx?: number | undefined): readonly [StagingInfo, (stagingInfo: StagingInfo) => void]; - editUserMessageAndStreamResponse(userMessage: string, messageIdx: number): Promise; - addUserMessageAndStreamResponse(userMessage: string): Promise; + editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }): Promise; + addUserMessageAndStreamResponse({ userMessage, chatMode }: { userMessage: string, chatMode: ChatMode }): Promise; cancelStreaming(threadId: string): void; dismissStreamError(threadId: string): void; @@ -182,7 +185,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // always be in a thread this.openNewThread() - this._storageService.store(THREAD_VERSION_KEY, THREAD_VERSION, StorageScope.APPLICATION, StorageTarget.USER) + this._storageService.store(THREAD_VERSION_KEY, LATEST_THREAD_VERSION, StorageScope.APPLICATION, StorageTarget.USER) } @@ -272,7 +275,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { - async addUserMessageAndStreamResponse(userMessage: string, stagingOverride?: StagingInfo | null) { + async addUserMessageAndStreamResponse({ userMessage, chatMode, stagingOverride }: { userMessage: string, chatMode: ChatMode, stagingOverride?: StagingInfo | null }) { const thread = this.getCurrentThread() const threadId = thread.id @@ -293,14 +296,10 @@ class ChatThreadService extends Disposable implements IChatThreadService { // agent loop - - let shouldContinue = false do { shouldContinue = false - console.log('Q') - let res_: () => void const awaitable = new Promise((res, rej) => { res_ = res }) @@ -310,9 +309,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { logging: { loggingName: `Agent` }, messages: [ { role: 'system', content: chat_systemMessage }, - ...this.getCurrentThread().messages.map(m => ({ role: m.role, content: m.content || '(empty model output)' })), + ...this.getCurrentThread().messages.map(m => ({ ...m, content: m.content || '(empty model output)' })), ], - tools: [voidTools['read_file']], + tools: [voidTools['read_file']], // TODO!!!!! make this change on agent | chat | search onText: ({ fullText }) => { this._setStreamState(threadId, { messageSoFar: fullText }) @@ -324,13 +323,13 @@ class ChatThreadService extends Disposable implements IChatThreadService { else { for (const tool of tools) { if (!(tool.name in this._toolsService.toolFns)) { - this._addMessageToThread(threadId, { role: 'tool', name: tool.name, params: tool.args, content: `Error: This tool was not recognized, so it was not called.`, displayContent: `Error: tool not recognized.`, }) + this._addMessageToThread(threadId, { role: 'tool', name: tool.name, params: tool.args, tool_use_id: tool.tool_use_id, content: `Error: This tool was not recognized, so it was not called.`, displayContent: `Error: tool not recognized.`, }) } else { const toolName = tool.name as ToolName - const toolResult = await this._toolsService.toolFns[toolName](JSON.parse(tool.args)) - const string = this._toolsService.toolResultToString[toolName](toolResult as any) - this._addMessageToThread(threadId, { role: 'tool', name: tool.name, params: tool.args, content: string, displayContent: string, }) + const toolResult = await this._toolsService.toolFns[toolName](tool.args) + const string = this._toolsService.toolResultToString[toolName](toolResult as any) // typescript is so bad it doesn't even couple the type of ToolResult with the type of the function being called here + this._addMessageToThread(threadId, { role: 'tool', name: tool.name, params: tool.args, tool_use_id: tool.tool_use_id, content: string, displayContent: string, }) shouldContinue = true } } @@ -377,7 +376,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { } - async editUserMessageAndStreamResponse(userMessage: string, messageIdx: number) { + async editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }) { const thread = this.getCurrentThread() @@ -400,7 +399,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { }, true) // re-add the message and stream it - this.addUserMessageAndStreamResponse(userMessage, messageToReplace.staging) + this.addUserMessageAndStreamResponse({ userMessage, chatMode, stagingOverride: messageToReplace.staging }) } diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 52af46c2..36173d68 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -25,7 +25,7 @@ import * as dom from '../../../../base/browser/dom.js'; import { Widget } from '../../../../base/browser/ui/widget.js'; import { URI } from '../../../../base/common/uri.js'; import { IConsistentEditorItemService, IConsistentItemService } from './helperServices/consistentItemService.js'; -import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, fastApply_rewritewholething_userMessage, fastApply_rewritewholething_systemMessage, defaultQuickEditFimTags, searchReplace_userMessage, searchReplace_systemMessage } from './prompt/prompts.js'; +import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, rewriteCode_userMessage, rewriteCode_systemMessage, defaultQuickEditFimTags, searchReplace_userMessage, searchReplace_systemMessage } from './prompt/prompts.js'; import { mountCtrlK } from './react/out/quick-edit-tsx/index.js' import { QuickEditPropsType } from './quickEditActions.js'; @@ -1415,9 +1415,9 @@ class EditCodeService extends Disposable implements IEditCodeService { let messages: LLMChatMessage[] if (from === 'ClickApply') { - const userContent = fastApply_rewritewholething_userMessage({ originalCode, applyStr: opts.applyStr, uri }) + const userContent = rewriteCode_userMessage({ originalCode, applyStr: opts.applyStr, uri }) messages = [ - { role: 'system', content: fastApply_rewritewholething_systemMessage, }, + { role: 'system', content: rewriteCode_systemMessage, }, { role: 'user', content: userContent, } ] } diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index 45a573ae..415a0c87 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -187,7 +187,7 @@ export const chat_userMessage = async (instructions: string, selections: Staging -export const fastApply_rewritewholething_systemMessage = `\ +export const rewriteCode_systemMessage = `\ You are a coding assistant that re-writes an entire file to make a change. You are given the original file \`ORIGINAL_FILE\` and a change \`CHANGE\`. Directions: @@ -199,7 +199,7 @@ Directions: -export const fastApply_rewritewholething_userMessage = ({ originalCode, applyStr, uri }: { originalCode: string, applyStr: string, uri: URI }) => { +export const rewriteCode_userMessage = ({ originalCode, applyStr, uri }: { originalCode: string, applyStr: string, uri: URI }) => { const language = filenameToVscodeLanguage(uri.fsPath) ?? '' @@ -311,7 +311,7 @@ Directions: 4. The SEARCH/REPLACE blocks you generate will be applied immediately, and so they **MUST** produce a file that the user can run IMMEDIATELY. - Make sure you add all necessary imports. - Make sure the "final" code is complete and will not result in syntax/lint errors. -5. Follow coding convention (spaces, semilcolons, comments, etc). +5. Follow coding conventions of the user (spaces, semilcolons, comments, etc). If the user spaces or formats things a certain way, CONTINUE formatting it that way, even if you prefer otherwise. ## EXAMPLE 1 ORIGINAL_FILE diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 73118cf8..4f337a60 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -619,7 +619,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM // stream the edit const userMessage = textAreaRefState.value; - await chatThreadsService.editUserMessageAndStreamResponse(userMessage, messageIdx) + await chatThreadsService.editUserMessageAndStreamResponse({ userMessage, chatMode: 'agent', messageIdx }) } const onAbort = () => { @@ -682,7 +682,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM chatbubbleContents = } - else if (role === 'tool'){ + else if (role === 'tool') { chatbubbleContents = chatMessage.name } @@ -798,7 +798,7 @@ export const SidebarChat = () => { // send message to LLM const userMessage = textAreaRef.current?.value ?? '' - await chatThreadsService.addUserMessageAndStreamResponse(userMessage) + await chatThreadsService.addUserMessageAndStreamResponse({ userMessage, chatMode: 'agent' }) setStaging({ ...staging, selections: [], }) // clear staging textAreaFnsRef.current?.setValue('') diff --git a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts index f6ea2a2f..27ce34c1 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts @@ -22,17 +22,28 @@ export const errorDetails = (fullError: Error | null): string | null => { } export type OnText = (p: { newText: string, fullText: string }) => void -export type OnFinalMessage = (p: { fullText: string, tools: { name: string, args: string }[] }) => void +export type OnFinalMessage = (p: { fullText: string, tools: { name: string, args: string, tool_use_id: string, }[] }) => void export type OnError = (p: { message: string, fullError: Error | null }) => void export type AbortRef = { current: (() => void) | null } export type LLMChatMessage = { - role: 'system' | 'user' | 'assistant' | 'tool'; + role: 'system' | 'user'; + content: string; +} | { + role: 'tool'; + tool_use_id: string; + content: string; +} | { + role: 'assistant', + tool_calls?: { name: string, tool_use_id: string, params: string }[]; content: string; } + + export type _InternalLLMChatMessage = { - role: 'user' | 'assistant'; + role: any; + tool_use_id?: any; content: string; } diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index e44a294a..af68ea38 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -11,7 +11,7 @@ import { registerSingleton, InstantiationType } from '../../../../platform/insta import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IMetricsService } from './metricsService.js'; -import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, displayInfoOfProviderName, defaultProviderSettings, developerInfoOfRecognizedModel, modelInfoOfAutodetectedModelNames } from './voidSettingsTypes.js'; +import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, displayInfoOfProviderName, defaultProviderSettings, developerInfoOfModelName, modelInfoOfAutodetectedModelNames } from './voidSettingsTypes.js'; const STORAGE_KEY = 'void.settingsServiceStorage' @@ -334,7 +334,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { if (existingIdx !== -1) return // if exists, do nothing const newModels = [ ...models, - { ...developerInfoOfRecognizedModel(modelName), modelName, isDefault: false, isHidden: false } + { ...developerInfoOfModelName(modelName), modelName, isDefault: false, isHidden: false } ] this.setSettingOfProvider(providerName, 'models', newModels) diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 5d371b5d..da5aa81b 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -9,17 +9,25 @@ import { VoidSettingsState } from './voidSettingsService.js' // developer info used in sendLLMMessage -export type VoidModelDeveloperInfo = { +export type DeveloperInfoAtModel = { // USED: + // TODO!!! think tokens - deepseek + // TODO!!!! // UNUSED (coming soon): - recognizedModelName: RecognizedModel, // used to show user if model was auto-recognized + recognizedModelName: RecognizedModelName, // used to show user if model was auto-recognized supportsTools: boolean, // we will just do a string of tool use if it doesn't support - supportsSystemMessage: 'system' | 'developer' | false, // if null, we will just do a string of system message + supportsSystemMessage: 'developer' | 'system' | false, // if null, we will just do a string of system message supportsAutocompleteFIM: boolean, // we will just do a description of FIM if it doens't support <|fim_hole|> supportsStreaming: boolean, // (o1 does NOT) we will just dump the final result if doesn't support it - maxTokens: number, // required, DEFAULT is Infinity + maxTokens: number, // required +} + +export type DeveloperInfoAtProvider = { + separateSystemMessage?: boolean; + toolsGoInRole?: boolean; // whether to do {role:'tool'} or {role:'user' tool:...} + modelOverrides?: Partial; // any overrides for models that a provider might have (e.g. if a provider always supports tool use, even if we don't recognize the model we can set tools to true) } @@ -31,7 +39,7 @@ export type VoidModelInfo = { // <-- STATEFUL isDefault: boolean, // whether or not it's a default for its provider isHidden: boolean, // whether or not the user is hiding it (switched off) isAutodetected?: boolean, // whether the model was autodetected by polling -} & VoidModelDeveloperInfo +} & DeveloperInfoAtModel @@ -62,131 +70,155 @@ export const recognizedModels = [ ] as const +type RecognizedModelName = (typeof recognizedModels)[number] | '' - -type RecognizedModel = (typeof recognizedModels)[number] | '' - - -// const modelCapabilities: { [recognizedModel in RecognizedModel]: ({ }) => string } = { -// 'OpenAI 4o': { -// template: ({ prefix, suffix, }: { prefix: string; suffix: string; }) => `\ -// ` -// } -// } - -export function getRecognizedModel(modelName: string): RecognizedModel { +export function recognizedModelOfModelName(modelName: string): RecognizedModelName { const lower = modelName.toLowerCase(); - if (lower.includes('gpt-4o')) { + if (lower.includes('gpt-4o')) return 'OpenAI 4o'; - } - if (lower.includes('claude')) { + if (lower.includes('claude')) return 'Anthropic Claude'; - } - if (lower.includes('llama')) { + if (lower.includes('llama')) return 'Llama 3.x'; - } - if (lower.includes('qwen2.5-coder')) { + if (lower.includes('qwen2.5-coder')) return 'Alibaba Qwen2.5 Coder Instruct'; - } - if (lower.includes('mistral')) { + if (lower.includes('mistral')) return 'Mistral Codestral'; - } - // Check for "o1" or "o3" - if (/\bo1\b/.test(lower) || /\bo3\b/.test(lower)) { + if (/\bo1\b/.test(lower) || /\bo3\b/.test(lower)) // o1, o3 return 'OpenAI o1, o3'; - } - if (lower.includes('deepseek-r1') || lower.includes('deepseek-reasoner')) { + if (lower.includes('deepseek-r1') || lower.includes('deepseek-reasoner')) return 'Deepseek R1'; - } + if (lower.includes('deepseek')) + return 'Deepseek Chat' - // Fallback: return ''; } +const developerInfoAtProvider: { [providerName in ProviderName]: DeveloperInfoAtProvider } = { + 'anthropic': { + separateSystemMessage: true, + toolsGoInRole: false, + modelOverrides: { + supportsTools: true, + } + }, + 'deepseek': { + separateSystemMessage: true, + }, + 'openAI': { + separateSystemMessage: false, + toolsGoInRole: true, + }, + 'gemini': { + separateSystemMessage: true, + toolsGoInRole: false + }, + 'mistral': { + separateSystemMessage: true, + }, + 'groq': { + separateSystemMessage: true, + }, + 'ollama': { + separateSystemMessage: false, + }, + 'openRouter': { + separateSystemMessage: true, + }, + 'openAICompatible': { + separateSystemMessage: true, + }, +} +export const developerInfoOfProviderName = (providerName: ProviderName): Partial => { + return developerInfoAtProvider[providerName] ?? {} +} -export const developerInfoOfRecognizedModel = (modelName: string) => { - const devInfo: { [recognizedModel in RecognizedModel]: Omit } = { - 'OpenAI 4o': { - supportsSystemMessage: false, - supportsTools: false, - supportsAutocompleteFIM: false, - supportsStreaming: false, - maxTokens: 4096, - }, - 'Anthropic Claude': { - supportsSystemMessage: false, - supportsTools: false, - supportsAutocompleteFIM: false, - supportsStreaming: false, - maxTokens: 4096, - }, - 'Llama 3.x': { - supportsSystemMessage: false, - supportsTools: false, - supportsAutocompleteFIM: false, - supportsStreaming: false, - maxTokens: 4096, - }, - 'Deepseek Chat': { - supportsSystemMessage: false, - supportsTools: false, - supportsAutocompleteFIM: false, - supportsStreaming: false, - maxTokens: 4096, - }, +// providerName is optional, but gives some extra fallbacks if provided +const developerInfoOfRecognizedModelName: { [recognizedModel in RecognizedModelName]: Omit } = { + 'OpenAI 4o': { + supportsSystemMessage: false, + supportsTools: false, + supportsAutocompleteFIM: false, + supportsStreaming: false, + maxTokens: 4096, + }, - 'Alibaba Qwen2.5 Coder Instruct': { - supportsSystemMessage: false, - supportsTools: false, - supportsAutocompleteFIM: false, - supportsStreaming: false, - maxTokens: 4096, - }, + 'Anthropic Claude': { + supportsSystemMessage: false, + supportsTools: false, + supportsAutocompleteFIM: false, + supportsStreaming: false, + maxTokens: 4096, + }, - 'Mistral Codestral': { - supportsSystemMessage: false, - supportsTools: false, - supportsAutocompleteFIM: false, - supportsStreaming: false, - maxTokens: 4096, - }, + 'Llama 3.x': { + supportsSystemMessage: false, + supportsTools: false, + supportsAutocompleteFIM: false, + supportsStreaming: false, + maxTokens: 4096, + }, - 'OpenAI o1, o3': { - supportsSystemMessage: false, - supportsTools: false, - supportsAutocompleteFIM: false, - supportsStreaming: false, - maxTokens: 4096, - }, + 'Deepseek Chat': { + supportsSystemMessage: false, + supportsTools: false, + supportsAutocompleteFIM: false, + supportsStreaming: false, + maxTokens: 4096, + }, - 'Deepseek R1': { - supportsSystemMessage: false, - supportsTools: false, - supportsAutocompleteFIM: false, - supportsStreaming: false, - maxTokens: 4096, - }, + 'Alibaba Qwen2.5 Coder Instruct': { + supportsSystemMessage: false, + supportsTools: false, + supportsAutocompleteFIM: false, + supportsStreaming: false, + maxTokens: 4096, + }, - '': { - supportsSystemMessage: false, - supportsTools: false, - supportsAutocompleteFIM: false, - supportsStreaming: false, - maxTokens: 4096, - }, - } + 'Mistral Codestral': { + supportsSystemMessage: false, + supportsTools: false, + supportsAutocompleteFIM: false, + supportsStreaming: false, + maxTokens: 4096, + }, - const recognizedModelName = getRecognizedModel(modelName) + 'OpenAI o1, o3': { + supportsSystemMessage: false, + supportsTools: false, + supportsAutocompleteFIM: false, + supportsStreaming: false, + maxTokens: 4096, + }, + 'Deepseek R1': { + supportsSystemMessage: false, + supportsTools: false, + supportsAutocompleteFIM: false, + supportsStreaming: false, + maxTokens: 4096, + }, + + '': { + supportsSystemMessage: false, + supportsTools: false, + supportsAutocompleteFIM: false, + supportsStreaming: false, + maxTokens: 4096, + }, +} +export const developerInfoOfModelName = (modelName: string, overrides?: Partial): DeveloperInfoAtModel => { + const recognizedModelName = recognizedModelOfModelName(modelName) return { recognizedModelName: recognizedModelName, - ...devInfo[recognizedModelName], + ...developerInfoOfRecognizedModelName[recognizedModelName], + ...overrides } } @@ -202,7 +234,7 @@ export const modelInfoOfDefaultModelNames = (defaultModelNames: string[]): VoidM isDefault: true, isAutodetected: false, isHidden: defaultModelNames.length >= 10, // hide all models if there are a ton of them, and make user enable them individually - ...developerInfoOfRecognizedModel(modelName), + ...developerInfoOfModelName(modelName), })) } @@ -219,7 +251,7 @@ export const modelInfoOfAutodetectedModelNames = (defaultModelNames: string[], o isDefault: true, isAutodetected: true, isHidden: !!existingModelsMap[modelName]?.isHidden, - ...developerInfoOfRecognizedModel(modelName) + ...developerInfoOfModelName(modelName) })) } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts index aefe6c34..b443cca1 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts @@ -45,7 +45,8 @@ export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages, messages: messages, model: modelName, max_tokens: maxTokens, - tools: tools?.map(tool => toAnthropicTool(tool)) + tools: tools?.map(tool => toAnthropicTool(tool)), + tool_choice: { type: 'auto', disable_parallel_tool_use: true } // one tool use at a time }) @@ -77,7 +78,7 @@ export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages, stream.on('finalMessage', (response) => { // stringify the response's content const content = response.content.map(c => c.type === 'text' ? c.text : '').join('\n\n') - const tools = response.content.map(c => c.type === 'tool_use' ? { name: c.name, args: JSON.stringify(c.input) } : null).filter(c => !!c) + const tools = response.content.map(c => c.type === 'tool_use' ? { name: c.name, args: JSON.stringify(c.input), tool_use_id: c.id } : null).filter(c => !!c) onFinalMessage({ fullText: content, tools }) }) diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/groq.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/groq.ts deleted file mode 100644 index c6fcb290..00000000 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/groq.ts +++ /dev/null @@ -1,42 +0,0 @@ -/*-------------------------------------------------------------------------------------- - * Copyright 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. - *--------------------------------------------------------------------------------------*/ - -import Groq from 'groq-sdk'; -import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js'; - -// Groq -export const sendGroqChat: _InternalSendLLMChatMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { - let fullText = ''; - - const thisConfig = settingsOfProvider.groq - - const groq = new Groq({ - apiKey: thisConfig.apiKey, - dangerouslyAllowBrowser: true - }); - - await groq.chat.completions - .create({ - messages: messages, - model: modelName, - stream: true, - }) - .then(async response => { - _setAborter(() => response.controller.abort()) - // when receive text - for await (const chunk of response) { - const newText = chunk.choices[0]?.delta?.content || ''; - fullText += newText; - onText({ newText, fullText }); - } - - onFinalMessage({ fullText, tools: [] }); - }) - .catch(error => { - onError({ message: error + '', fullError: error }); - }) - - -}; diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/mistral.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/mistral.ts deleted file mode 100644 index ea3179ed..00000000 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/mistral.ts +++ /dev/null @@ -1,44 +0,0 @@ -/*-------------------------------------------------------------------------------------- - * Copyright 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. - *--------------------------------------------------------------------------------------*/ - -import { Mistral } from '@mistralai/mistralai'; -import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js'; - -// Mistral -export const sendMistralChat: _InternalSendLLMChatMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { - let fullText = ''; - - const thisConfig = settingsOfProvider.mistral; - - const mistral = new Mistral({ - apiKey: thisConfig.apiKey, - }) - - await mistral.chat - .stream({ - messages: messages, - model: modelName, - stream: true, - }) - .then(async response => { - // Mistral has a really nonstandard API - no interrupt and weird stream types - _setAborter(() => { console.log('Mistral does not support interrupts! Further messages will just be ignored.') }); - // when receive text - for await (const chunk of response) { - const c = chunk.data.choices[0].delta.content || '' - const newText = ( - typeof c === 'string' ? c - : c?.map(c => c.type === 'text' ? c.text : c.type).join('\n') - ) - fullText += newText; - onText({ newText, fullText }); - } - - onFinalMessage({ fullText, tools: [] }); - }) - .catch(error => { - onError({ message: error + '', fullError: error }); - }) -} diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts index 4744db62..370d411a 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts @@ -64,6 +64,18 @@ const newOpenAI = ({ settingsOfProvider, providerName }: NewParams) => { baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true }) } + else if (providerName === 'mistral') { + const thisConfig = settingsOfProvider.mistral + return new OpenAI({ + baseURL: 'https://api.mistral.ai/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, + }) + } + else if (providerName === 'groq') { + const thisConfig = settingsOfProvider.groq + return new OpenAI({ + baseURL: '"https://api.groq.com/openai/v1"', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, + }) + } else { console.error(`sendOpenAIMsg: invalid providerName: ${providerName}`) throw new Error(`providerName was invalid: ${providerName}`) @@ -167,4 +179,4 @@ export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages, on } }) -}; +} diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index 6ebdbdf6..22255292 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -10,42 +10,189 @@ import { sendAnthropicChat } from './anthropic.js'; import { sendOllamaFIM, sendOllamaChat } from './ollama.js'; import { sendOpenAIChat } from './openai.js'; import { sendGeminiChat } from './gemini.js'; -import { sendGroqChat } from './groq.js'; -import { sendMistralChat } from './mistral.js'; -import { displayInfoOfProviderName } from '../../common/voidSettingsTypes.js'; +import { developerInfoOfModelName, developerInfoOfProviderName, displayInfoOfProviderName, ProviderName, recognizedModelOfModelName } from '../../common/voidSettingsTypes.js'; + + +const cleanChatMessages = (modelName: string, providerName: ProviderName, messages: LLMChatMessage[]): { separateSystemMessageStr?: string, messages: _InternalLLMChatMessage[] } => { + const recognizedModel = recognizedModelOfModelName(modelName) + const { separateSystemMessage, toolsGoInRole, modelOverrides } = developerInfoOfProviderName(providerName) + + const { supportsSystemMessage, maxTokens, /* supportsTools, supportsAutocompleteFIM, supportsStreaming */ } = developerInfoOfModelName(recognizedModel, modelOverrides) -const cleanChatMessages = (messages: LLMChatMessage[]): _InternalLLMChatMessage[] => { // trim message content (Anthropic and other providers give an error if there is trailing whitespace) messages = messages.map(m => ({ ...m, content: m.content.trim() })) + + // 1. SYSTEM MESSAGE // find system messages and concatenate them - const systemMessage = messages + const systemMessageStr = messages .filter(msg => msg.role === 'system') .map(msg => msg.content) .join('\n') || undefined; - // remove all system messages - const noSystemMessages = messages - .filter(msg => msg.role !== 'system') as _InternalLLMChatMessage[] + let separateSystemMessageStr = undefined - // add system mesasges to first message (should be a user message) - if (systemMessage && (noSystemMessages.length !== 0)) { - const newFirstMessage = { - role: noSystemMessages[0].role, - content: ('' - + '\n' - + systemMessage - + '\n' - + '\n' - + noSystemMessages[0].content - ) + // remove all system messages + const noSystemMessages: _InternalLLMChatMessage[] = messages.filter(msg => msg.role !== 'system') + + if (systemMessageStr) { + // if supports system message + if (supportsSystemMessage) { + if (separateSystemMessage) + separateSystemMessageStr = systemMessageStr + else { + noSystemMessages.unshift({ role: supportsSystemMessage, content: systemMessageStr }) // add new first message + } + } + // if does not support system message + else { + if (supportsSystemMessage) { + if (noSystemMessages.length === 0) + noSystemMessages.push({ role: 'user', content: systemMessageStr }) + // add system mesasges to first message (should be a user message) + else { + const newFirstMessage = { + role: noSystemMessages[0].role, + content: ('' + + '\n' + + systemMessageStr + + '\n' + + '\n' + + noSystemMessages[0].content + ) + } + noSystemMessages.splice(0, 1) // delete first message + noSystemMessages.unshift(newFirstMessage) // add new first message + } + } } - noSystemMessages.splice(0, 1) // delete first message - noSystemMessages.unshift(newFirstMessage) // add new first message } - return noSystemMessages + // 2. TOOLS + + const newMessages = noSystemMessages; + + if (toolsGoInRole) { + let index = 0; + while (index < newMessages.length) { + + // merge tool with the previous assistant and the following user message + + // take prev message and add + /* +openai assistant message will have: https://platform.openai.com/docs/guides/function-calling#function-calling-steps +"tool_calls":[ +{ + "id": "call_12345xyz", + "type": "function", + "function": { + "name": "get_weather", + "arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}" + } +}] + +openai user response will be: +{ + "role": "tool", + "tool_call_id": tool_call.id, + "content": str(result) +} + +anthropic assistant message will have: https://docs.anthropic.com/en/docs/build-with-claude/tool-use#tool-use-examples +"content": [ + { + "type": "text", + "text": "I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA." + }, + { + "type": "tool_use", + "id": "toolu_01A09q90qw90lq917835lq9", + "name": "get_weather", + "input": {"location": "San Francisco, CA", "unit": "celsius"} + } + ] + +anthropic user message response will be: +"content": [ + { + "type": "tool_result", + "tool_use_id": "toolu_01A09q90qw90lq917835lq9", + "content": "15 degrees" + } +] + + +ACCORDING TO 4o: gemini: similar to openai, but function_call, and only 1 call per message (no id because only 1 message) +gemini request: { + "role": "assistant", + "content": null, + "function_call": { + "name": "get_weather", + "arguments": { + "latitude": 48.8566, + "longitude": 2.3522 + } + } +} +gemini response: +{ + "role": "assistant", + "function_response": { + "name": "get_weather", + "response": { + "temperature": "15°C", + "condition": "Cloudy" + } + } +} + + ++ anthropic + ++ openai-compat (4) + + gemini + +ollama + + +mistral: same as openai + + */ + + + if (newMessages[index].role === 'tool') { + const toolMessage = newMessages[index]; + const assistantMessage = newMessages[index - 1]; + const userMessage = newMessages[index + 1]; + + // while ((toolIndex = newMessages.findIndex((msg, idx) => idx > toolIndex && msg.role === 'tool')) !== -1) { + + // tool_use goes in assistant + if (assistantMessage?.role === 'assistant') { + assistantMessage.tool_use += `\n${toolMessage.content}`; + } + + // tool_result goes in user + if (userMessage?.role === 'user') { + + userMessage.content = `${toolMessage.content}\n${userMessage.content}`; + } + + // Remove the tool message after merging its content + newMessages.splice(index, 1); + } else { + index++; + } + } + } + + + + return { + separateSystemMessageStr, + messages: newMessages + } } @@ -68,11 +215,14 @@ export const sendLLMMessage = ({ ) => { let messagesArr: _InternalLLMChatMessage[] = [] + + // TODO!!! move this to the actual providers if (messagesType === 'chatMessages') { - messagesArr = cleanChatMessages([ + const { messages: cleanedMessages, separateSystemMessageStr } = cleanChatMessages(modelName, providerName, [ { role: 'system', content: aiInstructions }, ...messages_ ]) + messagesArr = cleanedMessages } // only captures number of messages and message "shape", no actual code, instructions, prompts, etc @@ -141,6 +291,8 @@ export const sendLLMMessage = ({ case 'openRouter': case 'deepseek': case 'openAICompatible': + case 'mistral': + case 'groq': if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - OpenAI FIM', tools: [] }) else /* */ sendOpenAIChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }); break; @@ -156,14 +308,6 @@ export const sendLLMMessage = ({ if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Gemini FIM', tools: [] }) else /* */ sendGeminiChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }); break; - case 'groq': - if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Groq FIM', tools: [] }) - else /* */ sendGroqChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }); - break; - case 'mistral': - if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Mistral FIM', tools: [] }) - else /* */ sendMistralChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }); - break; default: onError({ message: `Error: Void provider was "${providerName}", which is not recognized.`, fullError: null }) break;