tools should work!

This commit is contained in:
Andrew Pareles 2025-02-16 02:53:56 -08:00
parent 131493b5e1
commit 7244d433dd
9 changed files with 322 additions and 206 deletions

View file

@ -14,7 +14,7 @@ import { IRange } from '../../../../editor/common/core/range.js';
import { ILLMMessageService } from '../common/llmMessageService.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { chat_userMessage, chat_systemMessage } from './prompt/prompts.js';
import { IToolsService, ToolName, voidTools } from '../common/toolsService.js';
import { InternalToolInfo, IToolsService, ToolName, voidTools } from '../common/toolsService.js';
import { toLLMChatMessage } from '../common/llmMessageTypes.js';
// one of the square items that indicates a selection in a chat bubble (NOT a file, a Selection of text)
@ -66,10 +66,10 @@ export type ChatMessage =
| {
role: 'tool';
name: string; // internal use
params: string | null; // internal use
params: string; // internal use
id: string; // apis require this tool use id
content: string | null; // summary of the tool to the LLM
displayContent: string | null; // text message of result
content: string; // result
displayContent: string; // text message of result
}
// a 'thread' means a chat message history
@ -296,6 +296,10 @@ class ChatThreadService extends Disposable implements IChatThreadService {
this._setStreamState(threadId, { error: undefined })
const tools: InternalToolInfo[] | undefined = (
chatMode === 'chat' ? undefined
: chatMode === 'agent' ? Object.keys(voidTools).map(toolName => voidTools[toolName as ToolName])
: undefined)
// agent loop
const agentLoop = async () => {
@ -316,8 +320,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
...this.getCurrentThread().messages.map(m => (toLLMChatMessage(m))),
],
// TODO!!!!! make this change on 'agent' | 'chat'
tools: Object.keys(voidTools).map(toolName => voidTools[toolName as ToolName]),
tools: tools,
onText: ({ fullText }) => {
this._setStreamState(threadId, { messageSoFar: fullText })

View file

@ -1261,6 +1261,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
if (!(blockNum in diffareaidOfBlockNum)) {
const foundInCode = findTextInCode(block.orig, fileContents)
if (typeof foundInCode === 'string') {
// TODO!!! log and retry
console.log('NOT FOUND IN CODE!!!!', foundInCode)
continue
}
@ -1305,7 +1306,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
this._refreshStylesAndDiffsInURI(uri)
},
onFinalMessage: async ({ fullText }) => {
console.log('/* ONFIN */', fullText)
console.log('/* ON FINALMESSAGE */', fullText)
// 1. wait 500ms and fix lint errors - call lint error workflow
// (update react state to say "Fixing errors")
@ -1325,8 +1326,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
onDone(false)
},
onError: (e) => {
console.log('/* ERRRRRR */')
console.log('ERROR', e);
onDone(true)
},

View file

@ -385,7 +385,7 @@ export const AIInstructionsBox = () => {
return <VoidInputBox2
className='min-h-[81px] p-3 rounded-sm'
initValue={voidSettingsState.globalSettings.aiInstructions}
placeholder={`Do not change my indentation or delete my comments. When writing TS or JS, do not add ;'s. Respond to all queries in French. `}
placeholder={`Do not change my indentation or delete my comments. When writing TS or JS, do not add ;'s. Write new code using Rust if possible. `}
multiline
onChangeText={(newText) => {
voidSettingsService.setGlobalSetting('aiInstructions', newText)

View file

@ -32,12 +32,13 @@ export type LLMChatMessage = {
content: string;
} | {
role: 'assistant',
tool_calls?: { name: string, id: string, params: string }[];
content: string;
} | {
role: 'tool';
content: string; // result
name: string;
params: string;
id: string;
content: string;
}
export const toLLMChatMessage = (c: ChatMessage): LLMChatMessage => {
@ -45,21 +46,15 @@ export const toLLMChatMessage = (c: ChatMessage): LLMChatMessage => {
return { role: c.role, content: c.content ?? '(empty)' }
}
else if (c.role === 'assistant')
return { role: c.role, tool_calls: c.tool_calls, content: c.content ?? '(empty model output)' }
return { role: c.role, content: c.content ?? '(empty model output)' }
else if (c.role === 'tool')
return { role: c.role, id: c.id, content: c.content ?? '(empty output)' }
return { role: c.role, id: c.id, name: c.name, params: c.params, content: c.content ?? '(empty output)' }
else {
throw 1
}
}
export type _InternalLLMChatMessage = {
role: any;
id?: any;
content: string;
}
type _InternalSendFIMMessage = {
prefix: string;
suffix: string;
@ -115,6 +110,8 @@ export type EventLLMMessageOnErrorParams = Parameters<OnError>[0] & { requestId:
export type _InternalSendLLMChatMessageFnType = (
params: {
aiInstructions: string;
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: OnError;

View file

@ -1,177 +0,0 @@
import { _InternalLLMChatMessage, LLMChatMessage } from '../../common/llmMessageTypes.js';
import { DeveloperInfoAtModel, developerInfoOfModelName, developerInfoOfProviderName, ProviderName } from '../../common/voidSettingsTypes.js';
// no matter whether the model supports a system message or not (or what format it supports), add it in some way
// also take into account tools if the model doesn't support tool use
export const addSystemMessageAndToolSupport = (modelName: string, providerName: ProviderName, messages_: LLMChatMessage[], { separateSystemMessage }: { separateSystemMessage: boolean }): { separateSystemMessageStr?: string, messages: _InternalLLMChatMessage[], devInfo: DeveloperInfoAtModel } => {
const messages: _InternalLLMChatMessage[] = messages_.map(m => ({ ...m, content: m.content.trim(), }))
const { overrideSettingsForAllModels } = developerInfoOfProviderName(providerName)
const devInfo = developerInfoOfModelName(modelName, overrideSettingsForAllModels)
const { supportsSystemMessage } = devInfo
// 1. SYSTEM MESSAGE
// find system messages and concatenate them
let systemMessageStr = messages
.filter(msg => msg.role === 'system')
.map(msg => msg.content)
.join('\n') || undefined;
let separateSystemMessageStr: string | undefined = undefined
// remove all system messages
const newMessages: _InternalLLMChatMessage[] = messages.filter(msg => msg.role !== 'system')
// if (!supportsTools) {
// if (!systemMessageStr) systemMessageStr = ''
// systemMessageStr += '' // TODO!!! add tool use system message here
// }
if (systemMessageStr) {
// if supports system message
if (supportsSystemMessage) {
if (separateSystemMessage)
separateSystemMessageStr = systemMessageStr
else {
newMessages.unshift({ role: supportsSystemMessage, content: systemMessageStr }) // add new first message
}
}
// if does not support system message
else {
if (supportsSystemMessage) {
if (newMessages.length === 0)
newMessages.push({ role: 'user', content: systemMessageStr })
// add system mesasges to first message (should be a user message)
else {
const newFirstMessage = {
role: newMessages[0].role,
content: (''
+ '<SYSTEM_MESSAGE>\n'
+ systemMessageStr
+ '\n'
+ '</SYSTEM_MESSAGE>\n'
+ newMessages[0].content
)
}
newMessages.splice(0, 1) // delete first message
newMessages.unshift(newFirstMessage) // add new first message
}
}
}
}
return {
separateSystemMessageStr,
messages: newMessages,
devInfo,
}
}
// const { maxTokens, supportsTools, supportsAutocompleteFIM, supportsStreaming, } = developerInfoOfModelName(recognizedModel)
// 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": "<thinking>I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA.</thinking>"
},
{
"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
*/

View file

@ -7,7 +7,7 @@ import Anthropic from '@anthropic-ai/sdk';
import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js';
import { anthropicMaxPossibleTokens } from '../../common/voidSettingsTypes.js';
import { InternalToolInfo } from '../../common/toolsService.js';
import { addSystemMessageAndToolSupport } from './addSupport.js';
import { addSystemMessageAndToolSupport } from './processMessages.js';
@ -29,7 +29,7 @@ export const toAnthropicTool = (toolInfo: InternalToolInfo) => {
export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages: messages_, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, tools: tools_ }) => {
export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages: messages_, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, aiInstructions, tools: tools_ }) => {
const thisConfig = settingsOfProvider.anthropic
@ -39,7 +39,7 @@ export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages:
return
}
const { messages, separateSystemMessageStr, devInfo } = addSystemMessageAndToolSupport(modelName, providerName, messages_, { separateSystemMessage: true })
const { messages, separateSystemMessageStr, devInfo } = addSystemMessageAndToolSupport(modelName, providerName, messages_, aiInstructions, { separateSystemMessage: true })
const anthropic = new Anthropic({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true });

View file

@ -7,7 +7,7 @@ import OpenAI from 'openai';
import { _InternalModelListFnType, _InternalSendLLMFIMMessageFnType, _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js';
import { Model } from 'openai/resources/models.js';
import { InternalToolInfo } from '../../common/toolsService.js';
import { addSystemMessageAndToolSupport } from './addSupport.js';
import { addSystemMessageAndToolSupport } from './processMessages.js';
// import { parseMaxTokensStr } from './util.js';
@ -144,12 +144,12 @@ export const sendOpenAIFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onTe
// OpenAI, OpenRouter, OpenAICompatible
export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools: tools_ }) => {
export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools: tools_ }) => {
let fullText = ''
const toolCallOfIndex: { [index: string]: { name: string, args: string, id: string } } = {}
const { messages, devInfo } = addSystemMessageAndToolSupport(modelName, providerName, messages_, { separateSystemMessage: false })
const { messages, devInfo } = addSystemMessageAndToolSupport(modelName, providerName, messages_, aiInstructions, { separateSystemMessage: false })
const tools = devInfo?.supportsTools && (tools_?.length ?? 0) !== 0 ? tools_?.map(tool => toOpenAITool(tool)) : undefined

View file

@ -0,0 +1,294 @@
import { LLMChatMessage } from '../../common/llmMessageTypes.js';
import { DeveloperInfoAtModel, developerInfoOfModelName, developerInfoOfProviderName, ProviderName } from '../../common/voidSettingsTypes.js';
import { deepClone } from '../../../../../base/common/objects.js';
// no matter whether the model supports a system message or not (or what format it supports), add it in some way
// also take into account tools if the model doesn't support tool use
export const addSystemMessageAndToolSupport = (modelName: string, providerName: ProviderName, messages_: LLMChatMessage[], aiInstructions: string, { separateSystemMessage }: { separateSystemMessage: boolean }): { separateSystemMessageStr?: string, messages: any[], devInfo: DeveloperInfoAtModel } => {
const messages = deepClone(messages_).map(m => ({ ...m, content: m.content.trim(), }))
const { overrideSettingsForAllModels } = developerInfoOfProviderName(providerName)
const devInfo = developerInfoOfModelName(modelName, overrideSettingsForAllModels)
const { supportsSystemMessage, supportsTools } = devInfo
// 1. SYSTEM MESSAGE
// find system messages and concatenate them
let systemMessageStr = messages
.filter(msg => msg.role === 'system')
.map(msg => msg.content)
.join('\n') || undefined;
if (aiInstructions)
systemMessageStr = `${(systemMessageStr ? `${systemMessageStr}\n\n` : '')}GUIDELINES\n${aiInstructions}`
let separateSystemMessageStr: string | undefined = undefined
// remove all system messages
const newMessages: (LLMChatMessage | { role: 'developer', content: string })[] = messages.filter(msg => msg.role !== 'system')
// if (!supportsTools) {
// if (!systemMessageStr) systemMessageStr = ''
// systemMessageStr += '' // TODO!!! add tool use system message here
// }
if (systemMessageStr) {
// if supports system message
if (supportsSystemMessage) {
if (separateSystemMessage)
separateSystemMessageStr = systemMessageStr
else {
newMessages.unshift({ role: supportsSystemMessage, content: systemMessageStr }) // add new first message
}
}
// if does not support system message
else {
if (supportsSystemMessage) {
if (newMessages.length === 0)
newMessages.push({ role: 'user', content: systemMessageStr })
// add system mesasges to first message (should be a user message)
else {
const newFirstMessage = {
role: 'user',
content: (''
+ '<SYSTEM_MESSAGE>\n'
+ systemMessageStr
+ '\n'
+ '</SYSTEM_MESSAGE>\n'
+ newMessages[0].content
)
} as const
newMessages.splice(0, 1) // delete first message
newMessages.unshift(newFirstMessage) // add new first message
}
}
}
}
// 2. MAKE TOOLS FORMAT CORRECT in messages
let finalMessages: any[]
if (!supportsTools) {
// do nothing
finalMessages = newMessages
}
// anthropic assistant message will have: https://docs.anthropic.com/en/docs/build-with-claude/tool-use#tool-use-examples
// "content": [
// {
// "type": "text",
// "text": "<thinking>I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA.</thinking>"
// },
// {
// "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"
// }
// ]
else if (providerName === 'anthropic') { // convert role:'tool' to anthropic's type
const newMessagesTools: (
Exclude<typeof newMessages[0], { role: 'assistant' | 'user' }> | {
role: 'assistant',
content: string | ({
type: 'text';
text: string;
} | {
type: 'tool_use';
name: string;
input: string;
id: string;
})[]
} | {
role: 'user',
content: string | ({
type: 'text';
text: string;
} | {
type: 'tool_response';
tool_use_id: string;
content: string;
})[]
}
)[] = newMessages;
for (let i = 0; i < newMessagesTools.length; i += 1) {
const currMsg = newMessagesTools[i]
if (currMsg.role !== 'tool') continue
const prevMsg = 0 <= i - 1 && i - 1 <= newMessagesTools.length ? newMessagesTools[i - 1] : undefined
const nextMsg = 0 <= i + 1 && i + 1 <= newMessagesTools.length ? newMessagesTools[i + 1] : undefined
if (prevMsg?.role === 'assistant') {
if (typeof prevMsg.content === 'string') prevMsg.content = [{ type: 'text', text: typeof prevMsg.content }]
prevMsg.content.push({ type: 'tool_use', name: currMsg.name, input: currMsg.params, id: currMsg.id })
}
if (nextMsg?.role === 'user') {
if (typeof nextMsg.content === 'string') nextMsg.content = [{ type: 'text', text: typeof nextMsg.content }]
nextMsg.content.push({ type: 'tool_response', tool_use_id: currMsg.id, content: currMsg.content })
}
}
finalMessages = newMessagesTools
}
// openai assistant message will have: https://platform.openai.com/docs/guides/function-calling#function-calling-steps
// "tool_calls":[
// {
// "type": "function",
// "id": "call_12345xyz",
// "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)
// }
// treat all other providers like openai tool message for now
else {
const newMessagesTools: (
Exclude<typeof newMessages[0], { role: 'assistant' | 'tool' }> | {
role: 'assistant',
content: string;
tool_calls?: {
type: 'function';
id: string;
function: {
name: string;
arguments: string;
}
}[]
} | {
role: 'tool',
id: string; // old val
tool_call_id: string; // new val
content: string;
}
)[] = [];
for (let i = 0; i < newMessages.length; i += 1) {
const currMsg = newMessages[i]
if (currMsg.role !== 'tool') {
newMessagesTools.push(currMsg)
continue
}
// edit previous assistant message to have called the tool
const prevMsg = 0 <= i - 1 && i - 1 <= newMessagesTools.length ? newMessagesTools[i - 1] : undefined
if (prevMsg?.role === 'assistant') {
prevMsg.tool_calls = [{
type: 'function',
id: currMsg.id,
function: {
name: currMsg.name,
arguments: currMsg.params
}
}]
}
// add the tool
newMessagesTools.push({
role: 'tool',
id: currMsg.id,
content: currMsg.content,
tool_call_id: currMsg.id,
})
}
finalMessages = newMessagesTools
}
// 3. CROP MESSAGES SO EVERYTHING FITS IN CONTEXT
// TODO!!!
return {
separateSystemMessageStr,
messages: finalMessages,
devInfo,
}
}
/*
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
*/

View file

@ -3,7 +3,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { SendLLMMessageParams, OnText, OnFinalMessage, OnError, _InternalLLMChatMessage } from '../../common/llmMessageTypes.js';
import { SendLLMMessageParams, OnText, OnFinalMessage, OnError } from '../../common/llmMessageTypes.js';
import { IMetricsService } from '../../common/metricsService.js';
import { displayInfoOfProviderName } from '../../common/voidSettingsTypes.js';
@ -104,11 +104,11 @@ export const sendLLMMessage = ({
case 'groq':
case 'gemini':
if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - OpenAI FIM', tools: [] })
else /* */ sendOpenAIChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools });
else /* */ sendOpenAIChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools });
break;
case 'anthropic':
if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Anthropic FIM', tools: [] })
else /* */ sendAnthropicChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools });
else /* */ sendAnthropicChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools });
break;
default:
onError({ message: `Error: Void provider was "${providerName}", which is not recognized.`, fullError: null })