anthropic tool use fix

This commit is contained in:
Andrew Pareles 2025-02-16 20:50:07 -08:00
parent 2bc3d67e39
commit 432a1766af
8 changed files with 65 additions and 37 deletions

View file

@ -46,24 +46,25 @@ const defaultStaging: StagingInfo = { isBeingEdited: false, selections: [] }
// WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors.
export type ChatMessage =
| {
role: 'system';
content: string;
displayContent?: undefined;
} | {
role: 'user';
content: string | null; // content sent to the llm - allowed to be '', will be replaced with (empty)
displayContent: string | null; // content displayed to user - allowed to be '', will be ignored
selections: StagingSelectionItem[] | null; // the user's selection
staging: StagingInfo | null
}
| {
} | {
role: 'assistant';
tool_calls?: { name: string, id: string, params: string }[];
tool_calls?: {
name: string,
id: string,
params: string
}[];
content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty)
displayContent: string | null; // content displayed to user (this is the same as content for now) - allowed to be '', will be ignored
}
| {
role: 'system';
content: string;
displayContent?: undefined;
}
| {
} | {
role: 'tool';
name: string; // internal use
params: string; // internal use
@ -325,7 +326,8 @@ class ChatThreadService extends Disposable implements IChatThreadService {
onText: ({ fullText }) => {
this._setStreamState(threadId, { messageSoFar: fullText })
},
onFinalMessage: async ({ fullText, tools }) => {
onFinalMessage: async ({ fullText, toolCalls: tools }) => {
console.log('FINAL MESSAGE', fullText, tools)
if ((tools?.length ?? 0) === 0) {
this._finishStreamingTextMessage(threadId, fullText)

View file

@ -22,10 +22,6 @@ export const errorDetails = (fullError: Error | null): string | null => {
return null
}
export type OnText = (p: { newText: string, fullText: string }) => void
export type OnFinalMessage = (p: { fullText: string, tools?: { name: string, params: string, id: string, }[] }) => void // id is tool_use_id
export type OnError = (p: { message: string, fullError: Error | null }) => void
export type AbortRef = { current: (() => void) | null }
export type LLMChatMessage = {
role: 'system' | 'user';
@ -41,6 +37,19 @@ export type LLMChatMessage = {
id: string;
}
export type LLMToolCallType = {
name: string;
params: string;
id: string;
}
export type OnText = (p: { newText: string, fullText: string }) => void
export type OnFinalMessage = (p: { fullText: string, toolCalls?: LLMToolCallType[] }) => void // id is tool_use_id
export type OnError = (p: { message: string, fullError: Error | null }) => void
export type AbortRef = { current: (() => void) | null }
export const toLLMChatMessage = (c: ChatMessage): LLMChatMessage => {
if (c.role === 'system' || c.role === 'user') {
return { role: c.role, content: c.content ?? '(empty)' }

View file

@ -142,7 +142,7 @@ export class ToolsService implements IToolsService {
const queryBuilder = instantiationService.createInstance(QueryBuilder);
const parseObj = <T extends ToolName,>(s: string): { [s: string]: unknown } | null => {
const parseObj = (s: string): { [s: string]: unknown } | null => {
try {
const o = JSON.parse(s)
return o

View file

@ -7,7 +7,7 @@ import Anthropic from '@anthropic-ai/sdk';
import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js';
import { anthropicMaxPossibleTokens, developerInfoOfModelName, developerInfoOfProviderName } from '../../common/voidSettingsTypes.js';
import { InternalToolInfo } from '../../common/toolsService.js';
import { addSystemMessageAndToolSupport } from './processMessages.js';
import { addSystemMessageAndToolSupport } from './preprocessLLMMessages.js';
@ -86,9 +86,9 @@ 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), tool_use_id: c.id } : null).filter(c => !!c)
const tools = response.content.map(c => c.type === 'tool_use' ? { name: c.name, params: JSON.stringify(c.input), id: c.id } : null).filter(c => !!c)
onFinalMessage({ fullText: content, tools: [] })
onFinalMessage({ fullText: content, toolCalls: tools })
})
stream.on('error', (error) => {

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 './processMessages.js';
import { addSystemMessageAndToolSupport } from './preprocessLLMMessages.js';
import { developerInfoOfModelName, developerInfoOfProviderName } from '../../common/voidSettingsTypes.js';
// import { parseMaxTokensStr } from './util.js';
@ -192,7 +192,12 @@ export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages: me
onText({ newText, fullText });
}
onFinalMessage({ fullText, tools: Object.keys(toolCallOfIndex).map(index => toolCallOfIndex[index]) });
onFinalMessage({
fullText, toolCalls: Object.keys(toolCallOfIndex).map(index => {
const tool = toolCallOfIndex[index]
return { name: tool.name, id: tool.id, params: tool.params }
})
});
})
// when error/fail - this catches errors of both .create() and .then(for await)
.catch(error => {

View file

@ -5,7 +5,14 @@ import { developerInfoOfModelName, developerInfoOfProviderName, ProviderName } f
import { deepClone } from '../../../../../base/common/objects.js';
export const parseObject = (args: unknown) => {
if (typeof args === 'object')
return args
if (typeof args === 'string')
try { return JSON.parse(args) }
catch (e) { return { args } }
return {}
}
// 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
@ -118,7 +125,7 @@ export const addSystemMessageAndToolSupport = (modelName: string, providerName:
} | {
type: 'tool_use';
name: string;
input: string;
input: Record<string, any>;
id: string;
})[]
} | {
@ -127,7 +134,7 @@ export const addSystemMessageAndToolSupport = (modelName: string, providerName:
type: 'text';
text: string;
} | {
type: 'tool_response';
type: 'tool_result';
tool_use_id: string;
content: string;
})[]
@ -141,17 +148,22 @@ export const addSystemMessageAndToolSupport = (modelName: string, providerName:
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 (typeof prevMsg.content === 'string') prevMsg.content = [{ type: 'text', text: prevMsg.content }]
prevMsg.content.push({ type: 'tool_use', id: currMsg.id, name: currMsg.name, input: parseObject(currMsg.params) })
}
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 })
// turn each tool into a user message with tool results at the end
newMessagesTools[i] = {
role: 'user',
content: [
...[{ type: 'tool_result', tool_use_id: currMsg.id, content: currMsg.content }] as const,
...currMsg.content ? [{ type: 'text', text: currMsg.content }] as const : [],
]
}
}
finalMessages = newMessagesTools
}
@ -212,7 +224,7 @@ export const addSystemMessageAndToolSupport = (modelName: string, providerName:
id: currMsg.id,
function: {
name: currMsg.name,
arguments: currMsg.params
arguments: JSON.stringify(currMsg.params)
}
}]
}
@ -236,7 +248,7 @@ export const addSystemMessageAndToolSupport = (modelName: string, providerName:
console.log('SYSMG', separateSystemMessage)
console.log('FINAL MESSAGES', finalMessages)
console.log('FINAL MESSAGES', JSON.stringify(finalMessages, null, 2))
return {

View file

@ -62,10 +62,10 @@ export const sendLLMMessage = ({
_fullTextSoFar = fullText
}
const onFinalMessage: OnFinalMessage = ({ fullText, tools }) => {
const onFinalMessage: OnFinalMessage = ({ fullText, toolCalls: tools }) => {
if (_didAbort) return
captureLLMEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
onFinalMessage_({ fullText, tools })
onFinalMessage_({ fullText, toolCalls: tools })
}
const onError: OnError = ({ message: error, fullError }) => {
@ -103,11 +103,11 @@ export const sendLLMMessage = ({
case 'ollama':
case 'groq':
case 'gemini':
if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - OpenAI FIM', tools: [] })
if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - OpenAI FIM', toolCalls: [] })
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: [] })
if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Anthropic FIM', toolCalls: [] })
else /* */ sendAnthropicChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools });
break;
default:

View file

@ -100,7 +100,7 @@ export class LLMMessageChannel implements IServerChannel {
const mainThreadParams: SendLLMMessageParams = {
...params,
onText: ({ newText, fullText }) => { this._onText_llm.fire({ requestId, newText, fullText }); },
onFinalMessage: ({ fullText, tools }) => { this._onFinalMessage_llm.fire({ requestId, fullText, tools }); },
onFinalMessage: ({ fullText, toolCalls: tools }) => { this._onFinalMessage_llm.fire({ requestId, fullText, toolCalls: tools }); },
onError: ({ message: error, fullError }) => { console.log('sendLLM: firing err'); this._onError_llm.fire({ requestId, message: error, fullError }); },
abortRef: this._abortRefOfRequestId_llm[requestId],
}