mirror of
https://github.com/voideditor/void
synced 2026-05-24 09:58:23 +00:00
tools
This commit is contained in:
parent
05d8b3a982
commit
bc6150aeac
11 changed files with 97 additions and 61 deletions
|
|
@ -42,6 +42,7 @@ import { ILLMMessageService } from '../common/llmMessageService.js';
|
|||
import { LLMChatMessage, _InternalLLMChatMessage, errorDetails } from '../common/llmMessageTypes.js';
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { VSReadFile } from './helpers/readFile.js';
|
||||
import { voidTools } from '../common/toolsService.js';
|
||||
|
||||
const configOfBG = (color: Color) => {
|
||||
return { dark: color, light: color, hcDark: color, hcLight: color, }
|
||||
|
|
@ -65,7 +66,6 @@ registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true);
|
|||
|
||||
|
||||
|
||||
|
||||
const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number => {
|
||||
|
||||
const model = editor.getModel();
|
||||
|
|
@ -1138,6 +1138,44 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
async startAgent(queryStr: string) {
|
||||
// agent loop
|
||||
const messages: LLMChatMessage[] = []
|
||||
|
||||
while (true) {
|
||||
await new Promise((res, rej) => {
|
||||
this._llmMessageService.sendLLMMessage({
|
||||
messagesType: 'chatMessages',
|
||||
tools: [voidTools['read_file']],
|
||||
useProviderFor: 'Apply',
|
||||
logging: { loggingName: `Agent` },
|
||||
messages,
|
||||
onText: ({ fullText }) => {
|
||||
|
||||
},
|
||||
onFinalMessage: async ({ fullText, tools }) => {
|
||||
res(tools)
|
||||
},
|
||||
onError: (e) => {
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
stopAgent() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public startApplying(opts: StartApplyingOpts) {
|
||||
|
||||
if (opts.type === 'rewrite') {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export const errorDetails = (fullError: Error | null): string | null => {
|
|||
}
|
||||
|
||||
export type OnText = (p: { newText: string, fullText: string }) => void
|
||||
export type OnFinalMessage = (p: { fullText: string }) => void
|
||||
export type OnFinalMessage = (p: { fullText: string, tools: { name: string, args: string }[] }) => void
|
||||
export type OnError = (p: { message: string, fullError: Error | null }) => void
|
||||
export type AbortRef = { current: (() => void) | null }
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ const paginationHelper = {
|
|||
param: { pageNumber: { type: 'number', description: 'The page number (optional, default is 1).' }, }
|
||||
} as const
|
||||
|
||||
export const contextTools = {
|
||||
export const voidTools: { [name: string]: InternalToolInfo } = {
|
||||
read_file: {
|
||||
name: 'read_file',
|
||||
description: 'Returns file contents of a given URI.',
|
||||
|
|
@ -73,12 +73,11 @@ export const contextTools = {
|
|||
// description: 'Searches files semantically for the given string query.',
|
||||
// // RAG
|
||||
// },
|
||||
|
||||
}
|
||||
|
||||
export type ContextToolName = keyof typeof contextTools
|
||||
type ContextToolParamNames<T extends ContextToolName> = keyof typeof contextTools[T]['params']
|
||||
type ContextToolParams<T extends ContextToolName> = { [paramName in ContextToolParamNames<T>]: unknown }
|
||||
export type ToolName = keyof typeof voidTools
|
||||
type ToolParamNames<T extends ToolName> = keyof typeof voidTools[T]['params']
|
||||
type ToolParamsObj<T extends ToolName> = { [paramName in ToolParamNames<T>]: unknown }
|
||||
|
||||
|
||||
|
||||
|
|
@ -121,7 +120,7 @@ export class ToolService implements IToolService {
|
|||
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
public contextToolCallFns
|
||||
public toolFns
|
||||
|
||||
constructor(
|
||||
@IFileService fileService: IFileService,
|
||||
|
|
@ -133,32 +132,32 @@ export class ToolService implements IToolService {
|
|||
|
||||
const queryBuilder = instantiationService.createInstance(QueryBuilder);
|
||||
|
||||
this.contextToolCallFns = {
|
||||
read_file: async ({ uri: uriStr }: ContextToolParams<'read_file'>) => {
|
||||
this.toolFns = {
|
||||
read_file: async ({ uri: uriStr }: ToolParamsObj<'read_file'>) => {
|
||||
const uri = validateURI(uriStr)
|
||||
const fileContents = await VSReadFileRaw(fileService, uri)
|
||||
return fileContents ?? '(could not read file)'
|
||||
},
|
||||
list_dir: async ({ uri: uriStr }: ContextToolParams<'list_dir'>) => {
|
||||
list_dir: async ({ uri: uriStr }: ToolParamsObj<'list_dir'>) => {
|
||||
const uri = validateURI(uriStr)
|
||||
// TODO!!!! check to make sure in workspace
|
||||
// TODO check to make sure is not gitignored
|
||||
const treeStr = await generateDirectoryTreeMd(fileService, uri)
|
||||
return treeStr
|
||||
},
|
||||
pathname_search: async ({ query: queryStr }: ContextToolParams<'pathname_search'>) => {
|
||||
pathname_search: async ({ query: queryStr }: ToolParamsObj<'pathname_search'>) => {
|
||||
if (typeof queryStr !== 'string') return '(Error: query was not a string)'
|
||||
const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), { filePattern: queryStr, });
|
||||
const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), { filePattern: queryStr, })
|
||||
|
||||
const data = await searchService.fileSearch(query, CancellationToken.None);
|
||||
const data = await searchService.fileSearch(query, CancellationToken.None)
|
||||
const URIs = data.results.map(({ resource, results }) => resource.fsPath)
|
||||
return URIs
|
||||
},
|
||||
search: async ({ query: queryStr }: ContextToolParams<'search'>) => {
|
||||
search: async ({ query: queryStr }: ToolParamsObj<'search'>) => {
|
||||
if (typeof queryStr !== 'string') return '(Error: query was not a string)'
|
||||
const query = queryBuilder.text({ pattern: queryStr, }, workspaceContextService.getWorkspace().folders.map(f => f.uri));
|
||||
const query = queryBuilder.text({ pattern: queryStr, }, workspaceContextService.getWorkspace().folders.map(f => f.uri))
|
||||
|
||||
const data = await searchService.textSearch(query, CancellationToken.None);
|
||||
const data = await searchService.textSearch(query, CancellationToken.None)
|
||||
const URIs = data.results.map(({ resource, results }) => resource)
|
||||
return URIs
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,7 +6,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 { InternalToolInfo, voidTools } from '../../common/toolsService.js';
|
||||
|
||||
|
||||
|
||||
|
|
@ -45,7 +45,7 @@ export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages,
|
|||
messages: messages,
|
||||
model: modelName,
|
||||
max_tokens: maxTokens,
|
||||
// tools: [toAnthropicTool(contextTools.list_dir)]
|
||||
tools: [toAnthropicTool(voidTools.list_dir)]
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -55,33 +55,31 @@ export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages,
|
|||
})
|
||||
|
||||
|
||||
// can do tool use streaming
|
||||
const toolCallOfIndex: { [index: string]: { name: string, args: string } } = {}
|
||||
stream.on('streamEvent', e => {
|
||||
if (e.type === 'content_block_start') {
|
||||
if (e.content_block.type !== 'tool_use') return
|
||||
const index = e.index
|
||||
if (!toolCallOfIndex[index]) toolCallOfIndex[index] = { name: '', args: '' }
|
||||
toolCallOfIndex[index].name += e.content_block.name ?? ''
|
||||
toolCallOfIndex[index].args += e.content_block.input ?? ''
|
||||
}
|
||||
else if (e.type === 'content_block_delta') {
|
||||
if (e.delta.type !== 'input_json_delta') return
|
||||
toolCallOfIndex[e.index].args += e.delta.partial_json
|
||||
}
|
||||
// TODO!!!!!
|
||||
// onText({})
|
||||
})
|
||||
// // can do tool use streaming
|
||||
// const toolCallOfIndex: { [index: string]: { name: string, args: string } } = {}
|
||||
// stream.on('streamEvent', e => {
|
||||
// if (e.type === 'content_block_start') {
|
||||
// if (e.content_block.type !== 'tool_use') return
|
||||
// const index = e.index
|
||||
// if (!toolCallOfIndex[index]) toolCallOfIndex[index] = { name: '', args: '' }
|
||||
// toolCallOfIndex[index].name += e.content_block.name ?? ''
|
||||
// toolCallOfIndex[index].args += e.content_block.input ?? ''
|
||||
// }
|
||||
// else if (e.type === 'content_block_delta') {
|
||||
// if (e.delta.type !== 'input_json_delta') return
|
||||
// toolCallOfIndex[e.index].args += e.delta.partial_json
|
||||
// }
|
||||
// // TODO!!!!!
|
||||
// // onText({})
|
||||
// })
|
||||
|
||||
// when we get the final message on this stream (or when error/fail)
|
||||
stream.on('finalMessage', (response) => {
|
||||
// stringify the response's content
|
||||
const content = response.content.map(c => c.type === 'text' ? c.text : '').join('\n')
|
||||
const tools = response.content.map(c => c.type === 'tool_use' ? { name: c.name, input: c.input } : null).filter(c => !!c)
|
||||
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)
|
||||
|
||||
console.log("TOOLS!!!!", typeof tools[0]?.input, JSON.stringify(tools, null, 2))
|
||||
|
||||
onFinalMessage({ fullText: content, })
|
||||
onFinalMessage({ fullText: content, tools })
|
||||
})
|
||||
|
||||
stream.on('error', (error) => {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export const sendGeminiChat: _InternalSendLLMChatMessageFnType = async ({ messag
|
|||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
onFinalMessage({ fullText });
|
||||
onFinalMessage({ fullText, tools: [] });
|
||||
})
|
||||
.catch((error) => {
|
||||
onError({ message: error + '', fullError: error })
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export const sendGroqChat: _InternalSendLLMChatMessageFnType = async ({ messages
|
|||
onText({ newText, fullText });
|
||||
}
|
||||
|
||||
onFinalMessage({ fullText });
|
||||
onFinalMessage({ fullText, tools: [] });
|
||||
})
|
||||
.catch(error => {
|
||||
onError({ message: error + '', fullError: error });
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export const sendMistralChat: _InternalSendLLMChatMessageFnType = async ({ messa
|
|||
onText({ newText, fullText });
|
||||
}
|
||||
|
||||
onFinalMessage({ fullText });
|
||||
onFinalMessage({ fullText, tools: [] });
|
||||
})
|
||||
.catch(error => {
|
||||
onError({ message: error + '', fullError: error });
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ export const sendOllamaFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onTe
|
|||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
onFinalMessage({ fullText });
|
||||
onFinalMessage({ fullText, tools: [] });
|
||||
})
|
||||
// when error/fail
|
||||
.catch((error) => {
|
||||
|
|
@ -100,14 +100,13 @@ export const sendOllamaChat: _InternalSendLLMChatMessageFnType = ({ messages, on
|
|||
for await (const chunk of stream) {
|
||||
const newText = chunk.message.content;
|
||||
|
||||
|
||||
|
||||
// chunk.message.tool_calls[0].function.arguments
|
||||
|
||||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
onFinalMessage({ fullText });
|
||||
|
||||
onFinalMessage({ fullText, tools: [] });
|
||||
|
||||
})
|
||||
// when error/fail
|
||||
|
|
|
|||
|
|
@ -111,30 +111,32 @@ export const sendOpenAIFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onTe
|
|||
|
||||
// openai.completions has a FIM parameter called `suffix`, but it's deprecated and only works for ~GPT 3 era models
|
||||
|
||||
onFinalMessage({ fullText: 'TODO' })
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// OpenAI, OpenRouter, OpenAICompatible
|
||||
export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) => {
|
||||
export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }) => {
|
||||
|
||||
let fullText = ''
|
||||
const toolCallOfIndex: { [index: string]: { name: string, args: string } } = {}
|
||||
|
||||
|
||||
const openai: OpenAI = newOpenAI({ providerName, settingsOfProvider })
|
||||
const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
|
||||
model: modelName,
|
||||
messages: messages,
|
||||
stream: true,
|
||||
// tools: Object.keys(contextTools).map(name => toOpenAITool(name, contextTools[name as ContextToolName])),
|
||||
tools: tools?.map(tool => toOpenAITool(tool)),
|
||||
}
|
||||
|
||||
openai.chat.completions
|
||||
.create(options)
|
||||
.then(async response => {
|
||||
_setAborter(() => response.controller.abort())
|
||||
|
||||
// when receive text
|
||||
for await (const chunk of response) {
|
||||
|
||||
|
|
@ -153,7 +155,7 @@ export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages, on
|
|||
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
onFinalMessage({ fullText });
|
||||
onFinalMessage({ fullText, tools: Object.keys(toolCallOfIndex).map(index => toolCallOfIndex[index]) });
|
||||
})
|
||||
// when error/fail - this catches errors of both .create() and .then(for await)
|
||||
.catch(error => {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { IMetricsService } from '../../common/metricsService.js';
|
|||
|
||||
import { sendAnthropicChat } from './anthropic.js';
|
||||
import { sendOllamaFIM, sendOllamaChat } from './ollama.js';
|
||||
import { sendOpenAIChat, sendOpenAIFIM } from './openai.js';
|
||||
import { sendOpenAIChat } from './openai.js';
|
||||
import { sendGeminiChat } from './gemini.js';
|
||||
import { sendGroqChat } from './groq.js';
|
||||
import { sendMistralChat } from './mistral.js';
|
||||
|
|
@ -107,10 +107,10 @@ export const sendLLMMessage = ({
|
|||
_fullTextSoFar = fullText
|
||||
}
|
||||
|
||||
const onFinalMessage: OnFinalMessage = ({ fullText }) => {
|
||||
const onFinalMessage: OnFinalMessage = ({ fullText, tools }) => {
|
||||
if (_didAbort) return
|
||||
captureLLMEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
|
||||
onFinalMessage_({ fullText })
|
||||
onFinalMessage_({ fullText, tools })
|
||||
}
|
||||
|
||||
const onError: OnError = ({ message: error, fullError }) => {
|
||||
|
|
@ -141,7 +141,7 @@ export const sendLLMMessage = ({
|
|||
case 'openRouter':
|
||||
case 'deepseek':
|
||||
case 'openAICompatible':
|
||||
if (messagesType === 'FIMMessage') sendOpenAIFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - OpenAI FIM', tools: [] })
|
||||
else /* */ sendOpenAIChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools });
|
||||
break;
|
||||
case 'ollama':
|
||||
|
|
@ -149,19 +149,19 @@ export const sendLLMMessage = ({
|
|||
else /* */ sendOllamaChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools });
|
||||
break;
|
||||
case 'anthropic':
|
||||
if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Anthropic FIM' })
|
||||
if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Anthropic FIM', tools: [] })
|
||||
else /* */ sendAnthropicChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools });
|
||||
break;
|
||||
case 'gemini':
|
||||
if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Gemini FIM' })
|
||||
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' })
|
||||
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' })
|
||||
if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Mistral FIM', tools: [] })
|
||||
else /* */ sendMistralChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools });
|
||||
break;
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -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 }) => { this._onFinalMessage_llm.fire({ requestId, fullText }); },
|
||||
onFinalMessage: ({ fullText, tools }) => { this._onFinalMessage_llm.fire({ requestId, fullText, tools }); },
|
||||
onError: ({ message: error, fullError }) => { console.log('sendLLM: firing err'); this._onError_llm.fire({ requestId, message: error, fullError }); },
|
||||
abortRef: this._abortRefOfRequestId_llm[requestId],
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue