diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..a081ac138 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,39 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Next.js: debug server-side", + "type": "node-terminal", + "request": "launch", + "command": "pnpm run dev", + "cwd": "${workspaceFolder}/projects/app" + }, + { + "name": "Next.js: debug client-side", + "type": "chrome", + "request": "launch", + "url": "http://localhost:3000" + }, + { + "name": "Next.js: debug client-side (Edge)", + "type": "msedge", + "request": "launch", + "url": "http://localhost:3000" + }, + { + "name": "Next.js: debug full stack", + "type": "node-terminal", + "request": "launch", + "command": "pnpm run dev", + "cwd": "${workspaceFolder}/projects/app", + "skipFiles": ["/**"], + "serverReadyAction": { + "action": "debugWithEdge", + "killOnServerStop": true, + "pattern": "- Local:.+(https?://.+)", + "uriFormat": "%s", + "webRoot": "${workspaceFolder}/projects/app" + } + } + ] +} \ No newline at end of file diff --git a/packages/global/core/workflow/runtime/utils.ts b/packages/global/core/workflow/runtime/utils.ts index b40bbe684..643733a7f 100644 --- a/packages/global/core/workflow/runtime/utils.ts +++ b/packages/global/core/workflow/runtime/utils.ts @@ -10,7 +10,6 @@ import { FlowNodeOutputItemType, ReferenceValueType } from '../type/io'; import { ChatItemType, NodeOutputItemType } from '../../../core/chat/type'; import { ChatItemValueTypeEnum, ChatRoleEnum } from '../../../core/chat/constants'; import { replaceVariable, valToStr } from '../../../common/string/tools'; -import { ChatCompletionChunk } from 'openai/resources'; export const getMaxHistoryLimitFromNodes = (nodes: StoreNodeItemType[]): number => { let limit = 10; diff --git a/packages/global/core/workflow/template/system/interactive/type.d.ts b/packages/global/core/workflow/template/system/interactive/type.d.ts index cf7485f5d..9a28cc43f 100644 --- a/packages/global/core/workflow/template/system/interactive/type.d.ts +++ b/packages/global/core/workflow/template/system/interactive/type.d.ts @@ -5,10 +5,36 @@ import { FlowNodeInputTypeEnum } from 'core/workflow/node/constant'; import { WorkflowIOValueTypeEnum } from 'core/workflow/constants'; import type { ChatCompletionMessageParam } from '../../../../ai/type'; +type InteractiveBasicType = { + entryNodeIds: string[]; + memoryEdges: RuntimeEdgeItemType[]; + nodeOutputs: NodeOutputItemType[]; + + toolParams?: { + entryNodeIds: string[]; // 记录工具中,交互节点的 Id,而不是起始工作流的入口 + memoryMessages: ChatCompletionMessageParam[]; // 这轮工具中,产生的新的 messages + toolCallId: string; // 记录对应 tool 的id,用于后续交互节点可以替换掉 tool 的 response + }; +}; + +type InteractiveNodeType = { + entryNodeIds?: string[]; + memoryEdges?: RuntimeEdgeItemType[]; + nodeOutputs?: NodeOutputItemType[]; +}; + export type UserSelectOptionItemType = { key: string; value: string; }; +type UserSelectInteractive = InteractiveNodeType & { + type: 'userSelect'; + params: { + description: string; + userSelectOptions: UserSelectOptionItemType[]; + userSelectedVal?: string; + }; +}; export type UserInputFormItemType = { type: FlowNodeInputTypeEnum; @@ -28,29 +54,7 @@ export type UserInputFormItemType = { // select list?: { label: string; value: string }[]; }; - -type InteractiveBasicType = { - entryNodeIds: string[]; - memoryEdges: RuntimeEdgeItemType[]; - nodeOutputs: NodeOutputItemType[]; - - toolParams?: { - entryNodeIds: string[]; // 记录工具中,交互节点的 Id,而不是起始工作流的入口 - memoryMessages: ChatCompletionMessageParam[]; // 这轮工具中,产生的新的 messages - toolCallId: string; // 记录对应 tool 的id,用于后续交互节点可以替换掉 tool 的 response - }; -}; - -type UserSelectInteractive = { - type: 'userSelect'; - params: { - description: string; - userSelectOptions: UserSelectOptionItemType[]; - userSelectedVal?: string; - }; -}; - -type UserInputInteractive = { +type UserInputInteractive = InteractiveNodeType & { type: 'userInput'; params: { description: string; @@ -58,6 +62,5 @@ type UserInputInteractive = { submitted?: boolean; }; }; - export type InteractiveNodeResponseType = UserSelectInteractive | UserInputInteractive; export type WorkflowInteractiveResponseType = InteractiveBasicType & InteractiveNodeResponseType; diff --git a/packages/global/core/workflow/type/node.d.ts b/packages/global/core/workflow/type/node.d.ts index deb5b24f2..de7e88a1a 100644 --- a/packages/global/core/workflow/type/node.d.ts +++ b/packages/global/core/workflow/type/node.d.ts @@ -23,6 +23,7 @@ import { NextApiResponse } from 'next'; import { AppDetailType, AppSchema } from '../../app/type'; import { ParentIdType } from 'common/parentFolder/type'; import { AppTypeEnum } from 'core/app/constants'; +import { WorkflowInteractiveResponseType } from '../template/system/interactive/type'; export type FlowNodeCommonType = { parentNodeId?: string; @@ -120,6 +121,7 @@ export type FlowNodeItemType = FlowNodeTemplateType & { showResult?: boolean; // show and hide result modal response?: ChatHistoryItemResType; isExpired?: boolean; + workflowInteractiveResponse?: WorkflowInteractiveResponseType; }; isFolded?: boolean; }; diff --git a/packages/service/core/workflow/dispatch/index.ts b/packages/service/core/workflow/dispatch/index.ts index 24b279f5c..24aae6154 100644 --- a/packages/service/core/workflow/dispatch/index.ts +++ b/packages/service/core/workflow/dispatch/index.ts @@ -44,14 +44,14 @@ import { textAdaptGptResponse, replaceEditorVariable } from '@fastgpt/global/core/workflow/runtime/utils'; -import { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type'; +import type { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type'; import { dispatchRunTools } from './agent/runTool/index'; import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants'; -import { DispatchFlowResponse } from './type'; +import type { DispatchFlowResponse } from './type'; import { dispatchStopToolCall } from './agent/runTool/stopTool'; import { dispatchLafRequest } from './tools/runLaf'; import { dispatchIfElse } from './tools/runIfElse'; -import { RuntimeEdgeItemType } from '@fastgpt/global/core/workflow/type/edge'; +import type { RuntimeEdgeItemType } from '@fastgpt/global/core/workflow/type/edge'; import { getReferenceVariableValue } from '@fastgpt/global/core/workflow/runtime/utils'; import { dispatchSystemConfig } from './init/systemConfig'; import { dispatchUpdateVariable } from './tools/runUpdateVar'; @@ -62,7 +62,7 @@ import { dispatchTextEditor } from './tools/textEditor'; import { dispatchCustomFeedback } from './tools/customFeedback'; import { dispatchReadFiles } from './tools/readFiles'; import { dispatchUserSelect } from './interactive/userSelect'; -import { +import type { WorkflowInteractiveResponseType, InteractiveNodeResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type'; @@ -451,6 +451,11 @@ export async function dispatchWorkFlow(data: Props): Promise { if (value === undefined) return; - if (!type) return value; + if (!type || type === WorkflowIOValueTypeEnum.any) return value; if (type === 'string') { if (typeof value !== 'object') return String(value); @@ -118,7 +118,7 @@ export const valueTypeFormat = (value: any, type?: WorkflowIOValueTypeEnum) => { return Boolean(value); } try { - if (WorkflowIOValueTypeEnum.arrayString && typeof value === 'string') { + if (type === WorkflowIOValueTypeEnum.arrayString && typeof value === 'string') { return [value]; } if ( diff --git a/projects/app/src/components/core/chat/components/AIResponseBox.tsx b/projects/app/src/components/core/chat/components/AIResponseBox.tsx index 45759061f..cb3a26a8f 100644 --- a/projects/app/src/components/core/chat/components/AIResponseBox.tsx +++ b/projects/app/src/components/core/chat/components/AIResponseBox.tsx @@ -8,43 +8,80 @@ import { Box, Button, Flex, - HStack, - Textarea + HStack } from '@chakra-ui/react'; import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants'; -import { +import type { AIChatItemValueItemType, ToolModuleResponseItemType, UserChatItemValueItemType } from '@fastgpt/global/core/chat/type'; -import React, { useCallback, useEffect } from 'react'; +import React, { useCallback, useMemo } from 'react'; import MyIcon from '@fastgpt/web/components/common/Icon'; import Avatar from '@fastgpt/web/components/common/Avatar'; -import { +import type { InteractiveBasicType, UserInputInteractive, UserSelectInteractive } from '@fastgpt/global/core/workflow/template/system/interactive/type'; import { isEqual } from 'lodash'; -import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; -import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; -import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { useTranslation } from 'next-i18next'; -import { Controller, useForm } from 'react-hook-form'; -import MySelect from '@fastgpt/web/components/common/MySelect'; -import MyTextarea from '@/components/common/Textarea/MyTextarea'; -import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput'; -import { SendPromptFnType } from '../ChatContainer/ChatBox/type'; import { eventBus, EventNameEnum } from '@/web/common/utils/eventbus'; +import { SelectOptionsComponent, FormInputComponent } from './Interactive/InteractiveComponents'; -type props = { - value: UserChatItemValueItemType | AIChatItemValueItemType; - isLastResponseValue: boolean; - isChatting: boolean; +const accordionButtonStyle = { + w: 'auto', + bg: 'white', + borderRadius: 'md', + borderWidth: '1px', + borderColor: 'myGray.200', + boxShadow: '1', + pl: 3, + pr: 2.5, + _hover: { + bg: 'auto' + } }; -const onSendPrompt: SendPromptFnType = (e) => eventBus.emit(EventNameEnum.sendQuestion, e); +const RenderResoningContent = React.memo(function RenderResoningContent({ + content, + isChatting, + isLastResponseValue +}: { + content: string; + isChatting: boolean; + isLastResponseValue: boolean; +}) { + const { t } = useTranslation(); + const showAnimation = isChatting && isLastResponseValue; + return ( + + + + + + {t('chat:ai_reasoning')} + + + {showAnimation && } + + + + + + + + ); +}); const RenderText = React.memo(function RenderText({ showAnimation, text @@ -58,6 +95,7 @@ const RenderText = React.memo(function RenderText({ return ; }); + const RenderTool = React.memo( function RenderTool({ showAnimation, @@ -69,37 +107,20 @@ const RenderTool = React.memo( return ( {tools.map((tool) => { - const toolParams = (() => { + const formatJson = (string: string) => { try { - return JSON.stringify(JSON.parse(tool.params), null, 2); + return JSON.stringify(JSON.parse(string), null, 2); } catch (error) { - return tool.params; + return string; } - })(); - const toolResponse = (() => { - try { - return JSON.stringify(JSON.parse(tool.response), null, 2); - } catch (error) { - return tool.response; - } - })(); + }; + const toolParams = formatJson(tool.params); + const toolResponse = formatJson(tool.response); return ( - + {tool.toolName} @@ -140,99 +161,24 @@ ${toolResponse}`} }, (prevProps, nextProps) => isEqual(prevProps, nextProps) ); -const RenderResoningContent = React.memo(function RenderResoningContent({ - content, - isChatting, - isLastResponseValue -}: { - content: string; - isChatting: boolean; - isLastResponseValue: boolean; -}) { - const { t } = useTranslation(); - const showAnimation = isChatting && isLastResponseValue; - return ( - - - - - - {t('chat:ai_reasoning')} - - - {showAnimation && } - - - - - - - - ); -}); +const onSendPrompt = (e: { text: string; isInteractivePrompt: boolean }) => + eventBus.emit(EventNameEnum.sendQuestion, e); const RenderUserSelectInteractive = React.memo(function RenderInteractive({ interactive }: { interactive: InteractiveBasicType & UserSelectInteractive; }) { return ( - <> - {interactive?.params?.description && } - - {interactive.params.userSelectOptions?.map((option) => { - const selected = option.value === interactive?.params?.userSelectedVal; - - return ( - - ); - })} - - + { + onSendPrompt({ + text: value, + isInteractivePrompt: true + }); + }} + /> ); }); const RenderUserFormInteractive = React.memo(function RenderFormInput({ @@ -241,110 +187,52 @@ const RenderUserFormInteractive = React.memo(function RenderFormInput({ interactive: InteractiveBasicType & UserInputInteractive; }) { const { t } = useTranslation(); - const { register, setValue, handleSubmit: handleSubmitChat, control, reset } = useForm(); - const onSubmit = useCallback((data: any) => { + const defaultValues = useMemo(() => { + if (interactive.type === 'userInput') { + return interactive.params.inputForm?.reduce((acc: Record, item) => { + acc[item.label] = !!item.value ? item.value : item.defaultValue; + return acc; + }, {}); + } + return {}; + }, [interactive]); + + const handleFormSubmit = useCallback((data: Record) => { onSendPrompt({ text: JSON.stringify(data), isInteractivePrompt: true }); }, []); - useEffect(() => { - if (interactive.type === 'userInput') { - const defaultValues = interactive.params.inputForm?.reduce( - (acc: Record, item) => { - acc[item.label] = !!item.value ? item.value : item.defaultValue; - return acc; - }, - {} - ); - reset(defaultValues); - } - }, []); - return ( - {interactive.params.description && } - {interactive.params.inputForm?.map((input) => ( - - - {input.label} - {input.description && } - - {input.type === FlowNodeInputTypeEnum.input && ( - - )} - {input.type === FlowNodeInputTypeEnum.textarea && ( -