UNPKG

@lobehub/chat

Version:

Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.

553 lines (448 loc) 17.8 kB
/* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */ import { PluginErrorType } from '@lobehub/chat-plugin-sdk'; import isEqual from 'fast-deep-equal'; import { t } from 'i18next'; import { StateCreator } from 'zustand/vanilla'; import { LOADING_FLAT } from '@/const/message'; import { PLUGIN_SCHEMA_API_MD5_PREFIX, PLUGIN_SCHEMA_SEPARATOR } from '@/const/plugin'; import { chatService } from '@/services/chat'; import { mcpService } from '@/services/mcp'; import { messageService } from '@/services/message'; import { ChatStore } from '@/store/chat/store'; import { useToolStore } from '@/store/tool'; import { pluginSelectors } from '@/store/tool/selectors'; import { builtinTools } from '@/tools'; import { ChatErrorType } from '@/types/fetch'; import { ChatMessage, ChatMessageError, ChatToolPayload, CreateMessageParams, MessageToolCall, ToolsCallingContext, } from '@/types/message'; import { merge } from '@/utils/merge'; import { safeParseJSON } from '@/utils/safeParseJSON'; import { setNamespace } from '@/utils/storeDebug'; import { genToolCallShortMD5Hash } from '@/utils/toolCall'; import { chatSelectors } from '../message/selectors'; import { threadSelectors } from '../thread/selectors'; const n = setNamespace('plugin'); export interface ChatPluginAction { createAssistantMessageByPlugin: (content: string, parentId: string) => Promise<void>; fillPluginMessageContent: ( id: string, content: string, triggerAiMessage?: boolean, ) => Promise<void>; invokeBuiltinTool: (id: string, payload: ChatToolPayload) => Promise<void>; invokeDefaultTypePlugin: (id: string, payload: any) => Promise<string | undefined>; invokeMarkdownTypePlugin: (id: string, payload: ChatToolPayload) => Promise<void>; invokeMCPTypePlugin: (id: string, payload: ChatToolPayload) => Promise<string | undefined>; invokeStandaloneTypePlugin: (id: string, payload: ChatToolPayload) => Promise<void>; reInvokeToolMessage: (id: string) => Promise<void>; triggerAIMessage: (params: { parentId?: string; traceId?: string; threadId?: string; inPortalThread?: boolean; inSearchWorkflow?: boolean; }) => Promise<void>; summaryPluginContent: (id: string) => Promise<void>; triggerToolCalls: ( id: string, params?: { threadId?: string; inPortalThread?: boolean; inSearchWorkflow?: boolean }, ) => Promise<void>; updatePluginState: (id: string, value: any) => Promise<void>; updatePluginArguments: <T = any>(id: string, value: T, replace?: boolean) => Promise<void>; internal_addToolToAssistantMessage: (id: string, tool: ChatToolPayload) => Promise<void>; internal_removeToolToAssistantMessage: (id: string, tool_call_id?: string) => Promise<void>; /** * use the optimistic update value to update the message tools to database */ internal_refreshToUpdateMessageTools: (id: string) => Promise<void>; internal_callPluginApi: (id: string, payload: ChatToolPayload) => Promise<string | undefined>; internal_invokeDifferentTypePlugin: (id: string, payload: ChatToolPayload) => Promise<any>; internal_togglePluginApiCalling: ( loading: boolean, id?: string, action?: string, ) => AbortController | undefined; internal_transformToolCalls: (toolCalls: MessageToolCall[]) => ChatToolPayload[]; internal_updatePluginError: (id: string, error: ChatMessageError) => Promise<void>; internal_constructToolsCallingContext: (id: string) => ToolsCallingContext | undefined; } export const chatPlugin: StateCreator< ChatStore, [['zustand/devtools', never]], [], ChatPluginAction > = (set, get) => ({ createAssistantMessageByPlugin: async (content, parentId) => { const newMessage: CreateMessageParams = { content, parentId, role: 'assistant', sessionId: get().activeId, topicId: get().activeTopicId, // if there is activeTopicId,then add it to topicId }; await messageService.createMessage(newMessage); await get().refreshMessages(); }, fillPluginMessageContent: async (id, content, triggerAiMessage) => { const { triggerAIMessage, internal_updateMessageContent } = get(); await internal_updateMessageContent(id, content); if (triggerAiMessage) await triggerAIMessage({ parentId: id }); }, invokeBuiltinTool: async (id, payload) => { const { internal_togglePluginApiCalling, internal_updateMessageContent, internal_updatePluginError, } = get(); const params = JSON.parse(payload.arguments); internal_togglePluginApiCalling(true, id, n('invokeBuiltinTool/start') as string); let data; try { data = await useToolStore.getState().transformApiArgumentsToAiState(payload.apiName, params); } catch (error) { const err = error as Error; console.error(err); const tool = builtinTools.find((tool) => tool.identifier === payload.identifier); const schema = tool?.manifest?.api.find((api) => api.name === payload.apiName)?.parameters; await internal_updatePluginError(id, { type: ChatErrorType.PluginFailToTransformArguments, body: { message: "[plugin] fail to transform plugin arguments to ai state, it may due to model's limited tools calling capacity. You can refer to https://lobehub.com/docs/usage/tools-calling for more detail.", stack: err.stack, arguments: params, schema, }, message: '', }); } internal_togglePluginApiCalling(false, id, n('invokeBuiltinTool/end') as string); if (!data) return; await internal_updateMessageContent(id, data); // run tool api call // postToolCalling // @ts-ignore const { [payload.apiName]: action } = get(); if (!action) return; let content; try { content = JSON.parse(data); } catch { /* empty block */ } if (!content) return; return await action(id, content); }, invokeDefaultTypePlugin: async (id, payload) => { const { internal_callPluginApi } = get(); const data = await internal_callPluginApi(id, payload); if (!data) return; return data; }, invokeMarkdownTypePlugin: async (id, payload) => { const { internal_callPluginApi } = get(); await internal_callPluginApi(id, payload); }, invokeStandaloneTypePlugin: async (id, payload) => { const result = await useToolStore.getState().validatePluginSettings(payload.identifier); if (!result) return; // if the plugin settings is not valid, then set the message with error type if (!result.valid) { await messageService.updateMessageError(id, { body: { error: result.errors, message: '[plugin] your settings is invalid with plugin manifest setting schema', }, message: t('response.PluginSettingsInvalid', { ns: 'error' }), type: PluginErrorType.PluginSettingsInvalid as any, }); await get().refreshMessages(); return; } }, reInvokeToolMessage: async (id) => { const message = chatSelectors.getMessageById(id)(get()); if (!message || message.role !== 'tool' || !message.plugin) return; // if there is error content, then clear the error if (!!message.pluginError) { get().internal_updateMessagePluginError(id, null); } const payload: ChatToolPayload = { ...message.plugin, id: message.tool_call_id! }; await get().internal_invokeDifferentTypePlugin(id, payload); }, triggerAIMessage: async ({ parentId, traceId, threadId, inPortalThread, inSearchWorkflow }) => { const { internal_coreProcessMessage } = get(); const chats = inPortalThread ? threadSelectors.portalAIChatsWithHistoryConfig(get()) : chatSelectors.mainAIChatsWithHistoryConfig(get()); await internal_coreProcessMessage(chats, parentId ?? chats.at(-1)!.id, { traceId, threadId, inPortalThread, inSearchWorkflow, }); }, summaryPluginContent: async (id) => { const message = chatSelectors.getMessageById(id)(get()); if (!message || message.role !== 'tool') return; await get().internal_coreProcessMessage( [ { role: 'assistant', content: '作为一名总结专家,请结合以上系统提示词,将以下内容进行总结:', }, { ...message, content: message.content, role: 'assistant', name: undefined, tool_call_id: undefined, }, ] as ChatMessage[], message.id, ); }, triggerToolCalls: async (assistantId, { threadId, inPortalThread, inSearchWorkflow } = {}) => { const message = chatSelectors.getMessageById(assistantId)(get()); if (!message || !message.tools) return; let shouldCreateMessage = false; let latestToolId = ''; const messagePools = message.tools.map(async (payload) => { const toolMessage: CreateMessageParams = { content: LOADING_FLAT, parentId: assistantId, plugin: payload, role: 'tool', sessionId: get().activeId, tool_call_id: payload.id, threadId, topicId: get().activeTopicId, // if there is activeTopicId,then add it to topicId }; const id = await get().internal_createMessage(toolMessage); if (!id) return; // trigger the plugin call const data = await get().internal_invokeDifferentTypePlugin(id, payload); if (data && !['markdown', 'standalone'].includes(payload.type)) { shouldCreateMessage = true; latestToolId = id; } }); await Promise.all(messagePools); // only default type tool calls should trigger AI message if (!shouldCreateMessage) return; const traceId = chatSelectors.getTraceIdByMessageId(latestToolId)(get()); await get().triggerAIMessage({ traceId, threadId, inPortalThread, inSearchWorkflow }); }, updatePluginState: async (id, value) => { const { refreshMessages } = get(); // optimistic update get().internal_dispatchMessage({ id, type: 'updateMessage', value: { pluginState: value } }); await messageService.updateMessagePluginState(id, value); await refreshMessages(); }, updatePluginArguments: async (id, value, replace = false) => { const { refreshMessages } = get(); const toolMessage = chatSelectors.getMessageById(id)(get()); if (!toolMessage || !toolMessage?.tool_call_id) return; let assistantMessage = chatSelectors.getMessageById(toolMessage?.parentId || '')(get()); const prevArguments = toolMessage?.plugin?.arguments; const prevJson = safeParseJSON(prevArguments || ''); const nextValue = replace ? (value as any) : merge(prevJson || {}, value); if (isEqual(prevJson, nextValue)) return; // optimistic update get().internal_dispatchMessage({ id, type: 'updateMessagePlugin', value: { arguments: JSON.stringify(nextValue) }, }); // 同样需要更新 assistantMessage 的 pluginArguments if (assistantMessage) { get().internal_dispatchMessage({ id: assistantMessage.id, type: 'updateMessageTools', tool_call_id: toolMessage?.tool_call_id, value: { arguments: JSON.stringify(nextValue) }, }); assistantMessage = chatSelectors.getMessageById(assistantMessage?.id)(get()); } const updateAssistantMessage = async () => { if (!assistantMessage) return; await messageService.updateMessage(assistantMessage!.id, { tools: assistantMessage?.tools, }); }; await Promise.all([ messageService.updateMessagePluginArguments(id, nextValue), updateAssistantMessage(), ]); await refreshMessages(); }, internal_addToolToAssistantMessage: async (id, tool) => { const assistantMessage = chatSelectors.getMessageById(id)(get()); if (!assistantMessage) return; const { internal_dispatchMessage, internal_refreshToUpdateMessageTools } = get(); internal_dispatchMessage({ type: 'addMessageTool', value: tool, id: assistantMessage.id, }); await internal_refreshToUpdateMessageTools(id); }, internal_removeToolToAssistantMessage: async (id, tool_call_id) => { const message = chatSelectors.getMessageById(id)(get()); if (!message || !tool_call_id) return; const { internal_dispatchMessage, internal_refreshToUpdateMessageTools } = get(); // optimistic update internal_dispatchMessage({ type: 'deleteMessageTool', tool_call_id, id: message.id }); // update the message tools await internal_refreshToUpdateMessageTools(id); }, internal_refreshToUpdateMessageTools: async (id) => { const message = chatSelectors.getMessageById(id)(get()); if (!message || !message.tools) return; const { internal_toggleMessageLoading, refreshMessages } = get(); internal_toggleMessageLoading(true, id); await messageService.updateMessage(id, { tools: message.tools }); internal_toggleMessageLoading(false, id); await refreshMessages(); }, internal_callPluginApi: async (id, payload) => { const { internal_updateMessageContent, refreshMessages, internal_togglePluginApiCalling } = get(); let data: string; try { const abortController = internal_togglePluginApiCalling( true, id, n('fetchPlugin/start') as string, ); const message = chatSelectors.getMessageById(id)(get()); const res = await chatService.runPluginApi(payload, { signal: abortController?.signal, trace: { observationId: message?.observationId, traceId: message?.traceId }, }); data = res.text; // save traceId if (res.traceId) { await messageService.updateMessage(id, { traceId: res.traceId }); } } catch (error) { console.log(error); const err = error as Error; // ignore the aborted request error if (!err.message.includes('The user aborted a request.')) { await messageService.updateMessageError(id, error as any); await refreshMessages(); } data = ''; } internal_togglePluginApiCalling(false, id, n('fetchPlugin/end') as string); // 如果报错则结束了 if (!data) return; await internal_updateMessageContent(id, data); return data; }, internal_invokeDifferentTypePlugin: async (id, payload) => { switch (payload.type) { case 'standalone': { return await get().invokeStandaloneTypePlugin(id, payload); } case 'markdown': { return await get().invokeMarkdownTypePlugin(id, payload); } case 'builtin': { return await get().invokeBuiltinTool(id, payload); } // @ts-ignore case 'mcp': { return await get().invokeMCPTypePlugin(id, payload); } default: { return await get().invokeDefaultTypePlugin(id, payload); } } }, invokeMCPTypePlugin: async (id, payload) => { const { internal_updateMessageContent, refreshMessages, internal_togglePluginApiCalling, internal_constructToolsCallingContext, } = get(); let data: string = ''; try { const abortController = internal_togglePluginApiCalling( true, id, n('fetchPlugin/start') as string, ); const context = internal_constructToolsCallingContext(id); const result = await mcpService.invokeMcpToolCall(payload, { signal: abortController?.signal, topicId: context?.topicId, }); if (!!result) data = result; } catch (error) { console.log(error); const err = error as Error; // ignore the aborted request error if (!err.message.includes('The user aborted a request.')) { await messageService.updateMessageError(id, error as any); await refreshMessages(); } } internal_togglePluginApiCalling(false, id, n('fetchPlugin/end') as string); // 如果报错则结束了 if (!data) return; await internal_updateMessageContent(id, data); return data; }, internal_togglePluginApiCalling: (loading, id, action) => { return get().internal_toggleLoadingArrays('pluginApiLoadingIds', loading, id, action); }, internal_transformToolCalls: (toolCalls) => { return toolCalls .map((toolCall): ChatToolPayload | null => { let payload: ChatToolPayload; const [identifier, apiName, type] = toolCall.function.name.split(PLUGIN_SCHEMA_SEPARATOR); if (!apiName) return null; payload = { apiName, arguments: toolCall.function.arguments, id: toolCall.id, identifier, type: (type ?? 'default') as any, }; // if the apiName is md5, try to find the correct apiName in the plugins if (apiName.startsWith(PLUGIN_SCHEMA_API_MD5_PREFIX)) { const md5 = apiName.replace(PLUGIN_SCHEMA_API_MD5_PREFIX, ''); const manifest = pluginSelectors.getToolManifestById(identifier)(useToolStore.getState()); const api = manifest?.api.find((api) => genToolCallShortMD5Hash(api.name) === md5); if (api) { payload.apiName = api.name; } } return payload; }) .filter(Boolean) as ChatToolPayload[]; }, internal_updatePluginError: async (id, error) => { const { refreshMessages } = get(); get().internal_dispatchMessage({ id, type: 'updateMessage', value: { error } }); await messageService.updateMessage(id, { error }); await refreshMessages(); }, internal_constructToolsCallingContext: (id: string) => { const message = chatSelectors.getMessageById(id)(get()); if (!message) return; return { topicId: message.topicId, }; }, });