diff --git a/docs/src/tools/message.md b/docs/src/tools/message.md index 1b4c14a4e..6bff27fc3 100644 --- a/docs/src/tools/message.md +++ b/docs/src/tools/message.md @@ -247,10 +247,14 @@ useMessage({ 用于接入模型返回的 `tool_calls`:在请求前注入 `tools` 列表,在请求完成后解析 `tool_calls`、执行 `callTool`、追加 tool 消息并自动发起下一轮请求。支持取消/失败时补充或标记 tool 消息、下一轮是否排除 tool 消息等。**需显式添加到 `plugins` 数组才会生效**。 +`toolPlugin` 也是 message 插件体系中的工具聚合入口。除自身的 `getTools` 外,具备工具能力的插件可以通过 `ToolProvider` 协议暴露 `provideTools(context)`,让 `toolPlugin` 在 `onBeforeRequest` 阶段统一收集并写入最终发送给模型的 `requestBody.tools`。这适合让能力型插件按自己的状态提供工具,例如 skill 文件工具、运行时工具或业务上下文相关工具。 + +工具来源会写入工具调用上下文的 `toolSource` 字段,便于在 `callTool`、`onToolCallStart`、`onToolCallEnd` 中做日志、分流或调试。`toolPlugin.getTools` 提供的工具来源为 `{ type: 'toolPlugin' }`;其他插件通过 `ToolProvider.provideTools` 提供的工具来源为 `{ type: 'toolProvider', pluginName?: string }`;无法识别来源时为 `{ type: 'unknown' }`。 + | 参数 | 类型 | 必填 | 默认值 | 说明 | | ----------------------------- | ---------------------------------------------------------------------------------------------------------------- | ---- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `getTools` | `() => Promise` | 是 | - | 返回当前轮次要传给 API 的工具列表(OpenAI 格式)。 | -| `callTool` | `(toolCall, context) => Promise> \| AsyncGenerator>` | 是 | - | 执行单个工具调用,返回结果字符串或可流式返回的对象,结果会合并到对应 tool 消息的 `content`。 | +| `getTools` | `(context: BasePluginContext) => MaybePromise` | 是 | - | 返回当前轮次要传给 API 的工具列表。可以返回普通 OpenAI tool schema,也可以返回带执行函数的 runtime tool。 | +| `callTool` | `(toolCall, context) => MaybeStreamableResult>` | 是 | - | 执行单个工具调用,返回结果字符串或可流式返回的对象,结果会合并到对应 tool 消息的 `content`。可通过 `context.toolSource` 判断工具来源。 | | `beforeCallTools` | `(toolCalls, context) => Promise` | 否 | - | 在真正执行工具前调用,可用于统一校验、鉴权、埋点。新字段为 `context.assistantMessage`;`context.currentMessage` 继续保留,但已弃用。 | | `onToolCallStart` | `(toolCall, context) => void` | 否 | - | 单个工具开始执行时触发。此时对应的 tool 消息已经创建并追加到 `messages` 中;`context` 额外包含 `assistantMessage`、`primaryMessage`(兼容字段)和 `toolMessage`。 | | `onToolCallEnd` | `(toolCall, context) => void` | 否 | - | 单个工具执行结束时触发。`context.status` 为 `'success' \| 'failed' \| 'cancelled'`,并额外包含 `assistantMessage`、`primaryMessage`(兼容字段)和 `toolMessage`,失败或取消时可能有 `context.error`。 | @@ -263,9 +267,33 @@ useMessage({ | 回调 | 额外上下文字段 | 说明 | | ----------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `beforeCallTools` | `assistantMessage`、`currentMessage`(已弃用) | 在 `BasePluginContext` 基础上额外包含当前这条带 `tool_calls` 的 assistant 消息。推荐使用 `assistantMessage`;`currentMessage` 为兼容旧代码保留。 | -| `callTool` | `assistantMessage`、`currentMessage`(已弃用)、`toolMessage` | 在 `BasePluginContext` 基础上额外包含当前这条带 `tool_calls` 的 assistant 消息,以及当前工具对应的 `toolMessage`。推荐使用 `assistantMessage`;`currentMessage` 为兼容旧代码保留。 | -| `onToolCallStart` | `assistantMessage`、`primaryMessage`(兼容字段)、`toolMessage` | 在 `BasePluginContext` 基础上额外包含触发当前工具调用的 assistant 消息和当前 tool 消息。推荐使用 `assistantMessage`;`primaryMessage` 为兼容旧代码保留。 | -| `onToolCallEnd` | `assistantMessage`、`primaryMessage`(兼容字段)、`toolMessage`、`status`、`error?` | 在 `BasePluginContext` 基础上额外包含 assistant 消息、当前 tool 消息和执行状态;当工具执行失败或被取消时,还可能包含 `error`。推荐使用 `assistantMessage`;`primaryMessage` 为兼容旧代码保留。 | +| `callTool` | `assistantMessage`、`currentMessage`(已弃用)、`toolMessage`、`toolSource` | 在 `BasePluginContext` 基础上额外包含当前这条带 `tool_calls` 的 assistant 消息、当前工具对应的 `toolMessage` 和工具来源。推荐使用 `assistantMessage`;`currentMessage` 为兼容旧代码保留。 | +| `onToolCallStart` | `assistantMessage`、`primaryMessage`(兼容字段)、`toolMessage`、`toolSource` | 在 `BasePluginContext` 基础上额外包含触发当前工具调用的 assistant 消息、当前 tool 消息和工具来源。推荐使用 `assistantMessage`;`primaryMessage` 为兼容旧代码保留。 | +| `onToolCallEnd` | `assistantMessage`、`primaryMessage`(兼容字段)、`toolMessage`、`toolSource`、`status`、`error?` | 在 `BasePluginContext` 基础上额外包含 assistant 消息、当前 tool 消息、工具来源和执行状态;当工具执行失败或被取消时,还可能包含 `error`。推荐使用 `assistantMessage`;`primaryMessage` 为兼容旧代码保留。 | + +`toolSource` 类型: + +```typescript +type ToolSource = + | { type: 'toolPlugin' } + | { type: 'toolProvider'; pluginName?: string } + | { type: 'unknown' } +``` + +`ToolProviderItem` 表示可提供给模型的工具项,可以是普通 OpenAI function tool schema,也可以是带本地执行函数的 runtime tool: + +```typescript +type ToolProviderItem = ChatCompletionFunctionTool | RuntimeTool +type AsyncStreamableResult = Promise | AsyncGenerator | Promise> +type MaybeStreamableResult = T | AsyncStreamableResult + +interface RuntimeTool { + tool: ChatCompletionFunctionTool + handler: (toolCall, context) => MaybeStreamableResult> +} +``` + +`ToolProvider` 是供插件扩展使用的高级协议。对于使用 `toolPlugin` 的业务代码,通常只需要通过 `getTools` 和 `callTool` 接入工具;当插件本身需要按内部状态向模型暴露工具时,再实现 `provideTools(context)`。 ##### 基础示例 @@ -292,8 +320,9 @@ useMessage({ }, }, ], - callTool: async (toolCall) => { + callTool: async (toolCall, context) => { const args = JSON.parse(toolCall.function?.arguments || '{}') + console.log('Tool source:', context.toolSource) return `Weather of ${args.city}: Sunny.` }, onToolCallEnd: (toolCall, { status }) => console.log('Tool end:', status), diff --git a/packages/kit/src/message/core/engine.ts b/packages/kit/src/message/core/engine.ts index fcb467014..362d27c38 100644 --- a/packages/kit/src/message/core/engine.ts +++ b/packages/kit/src/message/core/engine.ts @@ -1,4 +1,4 @@ -import { ChatCompletion, ChatCompletionChunk } from 'openai/resources/index' +import { ChatCompletion, ChatCompletionChunk } from 'openai/resources' import { lengthPlugin, thinkingPlugin } from '../plugins' import { BasePluginContext, @@ -156,6 +156,7 @@ export const createMessageEngine = ( mutate, abortSignal, currentTurn: runtime.currentTurn, + plugins, customContext: runtime.customContext, setRequestState, setCustomContext, diff --git a/packages/kit/src/message/plugins/index.ts b/packages/kit/src/message/plugins/index.ts index b7efeef2c..6f69df37e 100644 --- a/packages/kit/src/message/plugins/index.ts +++ b/packages/kit/src/message/plugins/index.ts @@ -1,3 +1,4 @@ export { lengthPlugin } from './lengthPlugin' export { thinkingPlugin } from './thinkingPlugin' export { toolPlugin } from './toolPlugin' +export type { RuntimeTool, ToolCallContext, ToolProvider, ToolProviderItem, ToolSource } from './toolPlugin' diff --git a/packages/kit/src/message/plugins/toolPlugin.ts b/packages/kit/src/message/plugins/toolPlugin.ts index 0748c1715..2aff3fb6a 100644 --- a/packages/kit/src/message/plugins/toolPlugin.ts +++ b/packages/kit/src/message/plugins/toolPlugin.ts @@ -1,5 +1,10 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { ChatCompletionMessageToolCall, ChatCompletionTool } from 'openai/resources/index' +import { + ChatCompletionFunctionTool, + ChatCompletionMessageFunctionToolCall, + ChatCompletionMessageToolCall, +} from 'openai/resources' +import type { MaybePromise, MaybeStreamableResult } from '../../types' import type { BasePluginContext, ChatMessage, MessageEnginePlugin, MutateMessageStateFn } from '../types' import { combineDeltaData, normalizeToAsyncGenerator } from '../utils' @@ -8,9 +13,29 @@ type AssistantMessageWithState = ChatMessage< { toolCall?: Record> } > -type ToolCallContext = BasePluginContext & { +export type ToolSource = { type: 'toolPlugin' } | { type: 'toolProvider'; pluginName?: string } | { type: 'unknown' } + +export type ToolCallContext = BasePluginContext & { assistantMessage: AssistantMessageWithState toolMessage: ChatMessage + /** + * 当前工具的来源。 + */ + toolSource: ToolSource +} + +export interface RuntimeTool { + tool: ChatCompletionFunctionTool + handler: ( + toolCall: ChatCompletionMessageFunctionToolCall, + context: ToolCallContext, + ) => MaybeStreamableResult> +} + +export type ToolProviderItem = ChatCompletionFunctionTool | RuntimeTool + +export interface ToolProvider { + provideTools: (context: BasePluginContext) => MaybePromise } /** @@ -99,9 +124,9 @@ function fillMissingToolMessages({ export const toolPlugin = ( options: MessageEnginePlugin & { /** - * 获取工具列表的函数。会在请求大模型前调用。 + * 获取本轮可用工具。可以返回普通 tool schema,也可以返回带执行函数的 runtime tool。 */ - getTools: () => Promise + getTools: (context: BasePluginContext) => MaybePromise /** * 在处理包含 tool_calls 的响应前调用。 */ @@ -115,7 +140,7 @@ export const toolPlugin = ( callTool: ( toolCall: ChatCompletionMessageToolCall, context: ToolCallContext, - ) => Promise> | AsyncGenerator> + ) => MaybeStreamableResult> /** * 工具调用开始时的回调函数。 * 触发时机:工具消息已创建并追加后,调用 callTool 之前触发。 @@ -197,6 +222,86 @@ export const toolPlugin = ( onToolCallEnd?.(...args) } + const isFunctionToolCall = ( + toolCall: ChatCompletionMessageToolCall, + ): toolCall is ChatCompletionMessageFunctionToolCall => { + return toolCall.type === 'function' && 'function' in toolCall + } + + const isRuntimeTool = (tool: ToolProviderItem): tool is RuntimeTool => { + return Boolean(tool && typeof tool === 'object' && 'tool' in tool && 'handler' in tool) + } + + const getToolProvider = (plugin: MessageEnginePlugin): ToolProvider | undefined => { + const toolProvider = plugin as Partial + return typeof toolProvider.provideTools === 'function' ? (toolProvider as ToolProvider) : undefined + } + + const isPluginDisabled = (plugin: MessageEnginePlugin, context: BasePluginContext) => { + return typeof plugin.disabled === 'function' ? plugin.disabled(context) : Boolean(plugin.disabled) + } + + const resolveTools = async (context: BasePluginContext, existingTools: ChatCompletionFunctionTool[] = []) => { + const providedToolItems: Array<{ item: ToolProviderItem; source: ToolSource }> = [] + + for (const plugin of context.plugins) { + const toolProvider = getToolProvider(plugin) + if (!isPluginDisabled(plugin, context) && toolProvider) { + providedToolItems.push( + ...(await toolProvider.provideTools(context)).map((item) => ({ + item, + source: { + type: 'toolProvider' as const, + pluginName: plugin.name, + }, + })), + ) + } + } + + const toolItems = [ + ...providedToolItems, + ...(await getTools(context)).map((item) => ({ + item, + source: { type: 'toolPlugin' as const }, + })), + ] + const tools: ChatCompletionFunctionTool[] = [] + const runtimeToolMap = new Map() + const toolSourceMap = new Map() + const seenToolNames = new Set() + + const registerToolName = (tool: ChatCompletionFunctionTool) => { + const toolName = tool.function.name + + if (seenToolNames.has(toolName)) { + throw new Error( + `Duplicate tool name "${toolName}" detected. Tool names must be unique because tool calls are routed by function.name.`, + ) + } + + seenToolNames.add(toolName) + } + + existingTools.forEach(registerToolName) + + for (const { item: toolItem, source } of toolItems) { + const tool = isRuntimeTool(toolItem) ? toolItem.tool : toolItem + + registerToolName(tool) + toolSourceMap.set(tool.function.name, source) + + if (isRuntimeTool(toolItem)) { + tools.push(toolItem.tool) + runtimeToolMap.set(toolItem.tool.function.name, toolItem) + } else { + tools.push(toolItem) + } + } + + return { tools, runtimeToolMap, toolSourceMap } + } + return { name: 'tool', ...restOptions, @@ -213,9 +318,10 @@ export const toolPlugin = ( onBeforeRequest: async (context) => { const { requestBody } = context - const tools = await getTools() + const existingTools = Array.isArray(requestBody.tools) ? requestBody.tools : [] + const { tools } = await resolveTools(context, existingTools) if (tools && tools.length > 0) { - requestBody.tools = tools + requestBody.tools = existingTools.length ? [...existingTools, ...tools] : tools } return restOptions.onBeforeRequest?.(context) @@ -242,6 +348,8 @@ export const toolPlugin = ( assistantMessage: currentMessage as AssistantMessageWithState, }) + const { runtimeToolMap, toolSourceMap } = await resolveTools(context) + const toolCallPromises = currentMessage.tool_calls.map(async (toolCall) => { const now = Math.floor(Date.now() / 1000) let hasMeaningfulResult = false @@ -257,15 +365,25 @@ export const toolPlugin = ( appendMessage(toolMessage) - const contextWithToolMessage = { + const functionToolCall = isFunctionToolCall(toolCall) ? toolCall : undefined + const toolSource = functionToolCall + ? (toolSourceMap.get(functionToolCall.function.name) ?? { type: 'unknown' as const }) + : { type: 'unknown' as const } + + const contextWithToolMessage: ToolCallContext = { ...context, assistantMessage: currentMessage as AssistantMessageWithState, toolMessage, + toolSource, } toolCallStart(toolCall, contextWithToolMessage) try { - const result = callTool(toolCall, contextWithToolMessage) + const runtimeTool = functionToolCall ? runtimeToolMap.get(functionToolCall.function.name) : undefined + const result = + runtimeTool && functionToolCall + ? runtimeTool.handler(functionToolCall, contextWithToolMessage) + : callTool(toolCall, contextWithToolMessage) // 将 Promise 或异步迭代器统一转换为异步生成器 const iterator = normalizeToAsyncGenerator(result) diff --git a/packages/kit/src/message/test/mockResponseProvider.ts b/packages/kit/src/message/test/mockResponseProvider.ts index 7394521e3..3c6c56229 100644 --- a/packages/kit/src/message/test/mockResponseProvider.ts +++ b/packages/kit/src/message/test/mockResponseProvider.ts @@ -1,4 +1,4 @@ -import type { ChatCompletionChunk } from 'openai/resources/index' +import type { ChatCompletionChunk } from 'openai/resources' import type { ResponseProvider } from '../types' import { AbortError } from '../utils' diff --git a/packages/kit/src/message/test/toolPlugin.test.ts b/packages/kit/src/message/test/toolPlugin.test.ts new file mode 100644 index 000000000..fa73465bc --- /dev/null +++ b/packages/kit/src/message/test/toolPlugin.test.ts @@ -0,0 +1,293 @@ +import type { ChatCompletion } from 'openai/resources' +import { describe, expect, it, vi } from 'vitest' +import { createNativeMessageAdapter } from '../adapters/native' +import { createMessageEngine } from '../core/engine' +import { lengthPlugin, thinkingPlugin, toolPlugin, type RuntimeTool, type ToolProvider } from '../plugins' +import type { CreateMessageEngineOptions, MessageEnginePlugin, ResponseProvider } from '../types' + +const silentDefaultPlugins = [thinkingPlugin({ disabled: true }), lengthPlugin({ disabled: true })] + +const createTestMessageEngine = (options: CreateMessageEngineOptions) => + createMessageEngine(createNativeMessageAdapter(), options) + +describe('toolPlugin', () => { + it('injects and executes runtime tools before falling back to callTool', async () => { + const runtimeCall = vi.fn(() => ({ result: 'runtime-result' })) + const fallbackCall = vi.fn() + const runtimeTool: RuntimeTool = { + tool: { + type: 'function', + function: { + name: 'runtime_lookup', + description: 'Runtime lookup', + parameters: { + type: 'object', + properties: { + query: { type: 'string' }, + }, + required: ['query'], + }, + }, + }, + handler: runtimeCall, + } + const responseProvider = vi.fn(async (requestBody) => { + const hasToolResult = requestBody.messages.some((message) => message.role === 'tool') + + if (!hasToolResult) { + expect(requestBody.tools?.map((tool) => tool.function.name)).toEqual(['runtime_lookup']) + return { + id: 'tool-call', + object: 'chat.completion', + created: Math.floor(Date.now() / 1000), + model: 'mock', + choices: [ + { + index: 0, + message: { + role: 'assistant', + content: '', + tool_calls: [ + { + id: 'call-1', + type: 'function', + function: { + name: 'runtime_lookup', + arguments: JSON.stringify({ query: 'vue' }), + }, + }, + ], + }, + finish_reason: 'tool_calls', + }, + ], + } as ChatCompletion + } + + expect(requestBody.messages.at(-1)).toMatchObject({ + role: 'tool', + tool_call_id: 'call-1', + content: JSON.stringify({ result: 'runtime-result' }), + }) + return { + id: 'final-answer', + object: 'chat.completion', + created: Math.floor(Date.now() / 1000), + model: 'mock', + choices: [ + { + index: 0, + message: { + role: 'assistant', + content: 'done', + }, + finish_reason: 'stop', + }, + ], + } as ChatCompletion + }) + + const engine = createTestMessageEngine({ + plugins: [ + ...silentDefaultPlugins, + toolPlugin({ + getTools: async () => [runtimeTool], + callTool: fallbackCall, + }), + ], + responseProvider, + }) + + await engine.sendMessage('lookup vue') + + expect(runtimeCall).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'call-1', + function: expect.objectContaining({ name: 'runtime_lookup' }), + }), + expect.objectContaining({ + toolMessage: expect.objectContaining({ role: 'tool' }), + toolSource: { type: 'toolPlugin' }, + }), + ) + expect(fallbackCall).not.toHaveBeenCalled() + expect(responseProvider).toHaveBeenCalledTimes(2) + expect(engine.getState().messages.at(-1)).toMatchObject({ + role: 'assistant', + content: 'done', + }) + }) + + it('throws when tool names are duplicated', async () => { + const runtimeTool: RuntimeTool = { + tool: { + type: 'function', + function: { + name: 'duplicate_tool', + description: 'Runtime duplicate', + }, + }, + handler: () => 'runtime', + } + const engine = createTestMessageEngine({ + plugins: [ + ...silentDefaultPlugins, + toolPlugin({ + getTools: async () => [ + { + type: 'function', + function: { + name: 'duplicate_tool', + description: 'Schema duplicate', + }, + }, + runtimeTool, + ], + callTool: async () => 'fallback', + }), + ], + responseProvider: async () => { + throw new Error('responseProvider should not be called') + }, + }) + + await expect(engine.sendMessage('trigger duplicate tools')).rejects.toThrow( + 'Duplicate tool name "duplicate_tool" detected.', + ) + }) + + it('throws when provided tools conflict with existing request tools', async () => { + const engine = createTestMessageEngine({ + plugins: [ + ...silentDefaultPlugins, + { + name: 'existing-tools', + onBeforeRequest: (context) => { + context.requestBody.tools = [ + { + type: 'function', + function: { + name: 'duplicate_tool', + description: 'Existing request tool', + }, + }, + ] + }, + }, + toolPlugin({ + getTools: async () => [ + { + type: 'function', + function: { + name: 'duplicate_tool', + description: 'Provided tool', + }, + }, + ], + callTool: async () => 'fallback', + }), + ], + responseProvider: async () => { + throw new Error('responseProvider should not be called') + }, + }) + + await expect(engine.sendMessage('trigger duplicate existing tool')).rejects.toThrow( + 'Duplicate tool name "duplicate_tool" detected.', + ) + }) + + it('loads tools provided by other plugins and passes provider source to fallback tool calls', async () => { + const fallbackCall = vi.fn(async () => 'provider result') + const responseProvider = vi.fn(async (requestBody) => { + const hasToolResult = requestBody.messages.some((message) => message.role === 'tool') + + if (!hasToolResult) { + expect(requestBody.tools?.map((tool) => tool.function.name)).toEqual(['provided_tool']) + + return { + id: 'provider-tool-call', + object: 'chat.completion', + created: Math.floor(Date.now() / 1000), + model: 'mock', + choices: [ + { + index: 0, + message: { + role: 'assistant', + content: '', + tool_calls: [ + { + id: 'call-provider', + type: 'function', + function: { + name: 'provided_tool', + arguments: '{}', + }, + }, + ], + }, + finish_reason: 'tool_calls', + }, + ], + } as ChatCompletion + } + + return { + id: 'final-answer', + object: 'chat.completion', + created: Math.floor(Date.now() / 1000), + model: 'mock', + choices: [ + { + index: 0, + message: { + role: 'assistant', + content: 'done', + }, + finish_reason: 'stop', + }, + ], + } as ChatCompletion + }) + + const providerPlugin: MessageEnginePlugin & ToolProvider = { + name: 'external-tool-provider', + provideTools: async () => [ + { + type: 'function', + function: { + name: 'provided_tool', + description: 'Provided by another plugin', + }, + }, + ], + } + + const engine = createTestMessageEngine({ + plugins: [ + ...silentDefaultPlugins, + providerPlugin, + toolPlugin({ + getTools: async () => [], + callTool: fallbackCall, + }), + ], + responseProvider, + }) + + await engine.sendMessage('call provided tool') + + expect(fallbackCall).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'call-provider', + }), + expect.objectContaining({ + toolSource: { + type: 'toolProvider', + pluginName: 'external-tool-provider', + }, + }), + ) + }) +}) diff --git a/packages/kit/src/message/types.ts b/packages/kit/src/message/types.ts index 6ef9a9baa..a8addaadd 100644 --- a/packages/kit/src/message/types.ts +++ b/packages/kit/src/message/types.ts @@ -2,10 +2,11 @@ import { ChatCompletion, ChatCompletionChunk, + ChatCompletionFunctionTool, ChatCompletionMessageParam, ChatCompletionMessageToolCall, -} from 'openai/resources/index' -import { MaybePromise } from '../types' +} from 'openai/resources' +import type { AsyncStreamableResult, MaybePromise } from '../types' export type DeepReadonly = T extends (...args: any[]) => any ? T @@ -32,13 +33,14 @@ export type ChatMessage< export interface MessageRequestBody { messages: Array + tools?: Array [key: string]: any } export type ResponseProvider = ( requestBody: MessageRequestBody, abortSignal: AbortSignal, -) => Promise | AsyncGenerator | Promise> +) => AsyncStreamableResult export interface PublicMessageState { requestState: RequestState @@ -128,6 +130,12 @@ export interface BasePluginContext { mutate: MutateMessageStateFn abortSignal: AbortSignal currentTurn: ChatMessage[] + /** + * 当前 engine 中已注册的插件列表。 + * + * 插件可基于该列表发现其他插件暴露的轻量协议,例如 toolPlugin 收集 provideTools。 + */ + plugins: readonly MessageEnginePlugin[] customContext: Record setRequestState: (state: RequestState, processingState?: RequestProcessingState) => void setCustomContext: (data: Record) => void diff --git a/packages/kit/src/message/utils.ts b/packages/kit/src/message/utils.ts index 5ab1937c2..758c599a1 100644 --- a/packages/kit/src/message/utils.ts +++ b/packages/kit/src/message/utils.ts @@ -1,4 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import type { MaybeStreamableResult } from '../types' + export class AbortError extends Error { constructor(message: string) { super(message) @@ -84,7 +86,7 @@ export function omitFields, K extends keyof T> } export async function* normalizeToAsyncGenerator( - result: Promise | AsyncGenerator | Promise>, + result: MaybeStreamableResult, ): AsyncGenerator { // 情况 1:是 async generator 或 sync generator if (isAsyncGenerator(result)) { diff --git a/packages/kit/src/types.ts b/packages/kit/src/types.ts index 63fbdce43..9707cec0c 100644 --- a/packages/kit/src/types.ts +++ b/packages/kit/src/types.ts @@ -2,6 +2,8 @@ import { type BaseModelProvider } from './providers/base' export type MaybePromise = T | Promise +export type AsyncStreamableResult = Promise | AsyncGenerator | Promise> +export type MaybeStreamableResult = T | AsyncStreamableResult /** * 消息角色类型 diff --git a/packages/kit/src/vue/message/mockResponseProvider.ts b/packages/kit/src/vue/message/mockResponseProvider.ts index 6d2275032..03eb1c75d 100644 --- a/packages/kit/src/vue/message/mockResponseProvider.ts +++ b/packages/kit/src/vue/message/mockResponseProvider.ts @@ -1,4 +1,4 @@ -import type { ChatCompletionChunk } from 'openai/resources/index' +import type { ChatCompletionChunk } from 'openai/resources' import type { ToolCall } from '../../types' import type { MessageRequestBody, ResponseProvider } from './types' diff --git a/packages/kit/src/vue/message/plugins/toolPlugin.ts b/packages/kit/src/vue/message/plugins/toolPlugin.ts index 564bfb62e..2bd9dba40 100644 --- a/packages/kit/src/vue/message/plugins/toolPlugin.ts +++ b/packages/kit/src/vue/message/plugins/toolPlugin.ts @@ -1,12 +1,17 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { toolPlugin as createCoreToolPlugin } from '../../../message/plugins' +import type { ToolProviderItem, ToolSource } from '../../../message/plugins' import { normalizeToAsyncGenerator } from '../../../message/utils' -import { ChatMessage, ToolCall } from '../../../types' +import type { ChatMessage, MaybePromise, MaybeStreamableResult, ToolCall } from '../../../types' import type { VueMessagePluginRuntime } from '../types.internal' -import { BasePluginContext, Tool, UseMessagePlugin } from '../types' +import type { BasePluginContext, UseMessagePlugin } from '../types' export interface UseMessageToolActionContext extends BasePluginContext { assistantMessage: ChatMessage + /** + * 当前工具的来源。 + */ + toolSource?: ToolSource /** * @deprecated use `assistantMessage` instead */ @@ -19,6 +24,10 @@ export interface UseMessageCallToolContext extends UseMessageToolActionContext { export interface UseMessageToolCallContext extends BasePluginContext { assistantMessage: ChatMessage + /** + * 当前工具的来源。 + */ + toolSource: ToolSource /** * @deprecated use `assistantMessage` instead */ @@ -31,7 +40,7 @@ export const toolPlugin = ( /** * 获取工具列表的函数。 */ - getTools: () => Promise + getTools: (context: BasePluginContext) => MaybePromise /** * 在处理包含 tool_calls 的响应前调用。 */ @@ -42,7 +51,7 @@ export const toolPlugin = ( callTool: ( toolCall: ToolCall, context: UseMessageCallToolContext, - ) => Promise> | AsyncGenerator> + ) => MaybeStreamableResult> /** * 工具调用开始时的回调函数。 * 触发时机:工具消息已创建并追加后,调用 callTool 之前触发。 @@ -100,7 +109,7 @@ export const toolPlugin = ( return createCoreToolPlugin({ ...wrappedRestOptions, - getTools: async () => (await getTools()) as any, + getTools: async (context) => getTools(runtime.createVueBaseContext(context)), beforeCallTools: beforeCallTools ? async (toolCalls, context) => { const assistantMessage = runtime.resolveReactiveMessage(context.assistantMessage as ChatMessage) @@ -123,6 +132,7 @@ export const toolPlugin = ( assistantMessage, currentMessage: assistantMessage, toolMessage, + toolSource: context.toolSource, } as UseMessageCallToolContext, ) @@ -140,6 +150,7 @@ export const toolPlugin = ( assistantMessage, primaryMessage: assistantMessage, toolMessage, + toolSource: context.toolSource, }) } : undefined, @@ -153,6 +164,7 @@ export const toolPlugin = ( assistantMessage, primaryMessage: assistantMessage, toolMessage, + toolSource: context.toolSource, status: context.status, error: context.error, }) diff --git a/packages/kit/src/vue/message/types.ts b/packages/kit/src/vue/message/types.ts index 8f93ae222..38da050c1 100644 --- a/packages/kit/src/vue/message/types.ts +++ b/packages/kit/src/vue/message/types.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { ComputedRef, Ref } from 'vue' -import { ChatMessage, MaybePromise, ToolCall } from '../../types' +import type { AsyncStreamableResult, ChatMessage, MaybePromise, ToolCall } from '../../types' export interface Tool { type: 'function' @@ -80,7 +80,7 @@ export interface ChatCompletion { export type ResponseProvider = ( requestBody: MessageRequestBody, abortSignal: AbortSignal, -) => Promise | AsyncGenerator | Promise> +) => AsyncStreamableResult export interface UseMessageOptions { initialMessages?: ChatMessage[]