From 0ed99d8c9ae51d597df33f5a3fef1c6b337813dd Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Fri, 28 Mar 2025 17:09:08 +0800 Subject: [PATCH] Check debug (#4384) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat : Added support for interactive nodes in the debugging interface (#4339) * feat: add VSCode launch configuration and enhance debug API handler * feat: refactor debug API handler to streamline workflow processing and enhance interactive chat features * feat: enhance debug API handler with structured input forms and improved query handling * feat: enhance debug API handler to support optional query and histories parameters * feat: simplify query and histories initialization in debug API handler * feat: add realmode parameter to workflow dispatch and update interactive handling * feat: add optional query parameter to PostWorkflowDebugProps and remove realmode from ModuleDispatchProps * feat: add history parameter to PostWorkflowDebugProps and update related components * feat: remove realmode * feat: simplify handler parameter destructuring in debug.ts * feat: remove unused interactive prop from WholeResponseContent component * feat: refactor onNextNodeDebug to use parameter object for better readability * feat: Merge selections and next actions to remove unused state management * feat: 添加 NodeDebugResponse 组件以增强调试功能 * feat: Simplify the import statements in InteractiveComponents.tsx * feat: Update the handler function to use default parameters to simplify the code * feat: Add optional workflowInteractiveResponse field to PostWorkflowDebugResponse type * feat: Add the workflowInteractiveResponse field in the debugging handler to enhance response capabilities * feat: Added workflowInteractiveResponse field in FlowNodeItemType to enhance responsiveness * feat: Refactor NodeDebugResponse to utilize workflowInteractiveResponse for improved interactivity * feat: Extend UserSelectInteractive and UserInputInteractive types to inherit from InteractiveBasicType * feat: Refactor NodeDebugResponse to streamline interactive handling and improve code clarity * feat: 重构交互式调试逻辑,创建共用 Hook 以简化用户选择和输入处理 * fix: type error * feat: 重构 AIResponseBox 组件,简化用户交互逻辑并引入共用表单组件 * feat: 清理 AIResponseBox 和表单组件代码,移除冗余注释和未使用的导入 * fix: type error * feat: 重构 AIResponseBox 组件,简化类型定义并优化代码结构 * refactor: 将 FormItem 接口更改为类型定义,优化代码结构 * refactor: 将 NodeDebugResponseProps 接口更改为类型定义,优化代码结构 * refactor: 移除不必要的入口节点检查,简化调试处理逻辑 * feat: 移动调试交互组件位置 * refactor: 将 InteractiveBasicType 中的属性设为可选,简化数据结构 * refactor: 优化类型定义 * refactor: 移除未使用的 ChatItemType 和 UserChatItemValueItemType 导入 * refactor: 将接口定义更改为类型别名,简化代码结构 * refactor: 更新类型定义,使用类型别名简化代码结构 * refactor: 使用类型导入简化代码结构,重构 AIResponseBox 组件 * refactor: 提取描述框和表单项标签组件,简化代码结构 * refactor: 移除多余的空行 * refactor: 移除多余的空行和注释 * refactor: 移除多余的空行,简化 AIResponseBox 组件代码 * refactor: 重构组件,移动 FormComponents 到 InteractiveComponents,简化代码结构 * refactor: 移除多余的空行,简化 NodeDebugResponse 组件代码 * refactor: 更新导入语句,使用 type 关键字优化类型导入 * refactor: 在 tsconfig.json 中启用 verbatimModuleSyntax 选项 * Revert "refactor: 在 tsconfig.json 中启用 verbatimModuleSyntax 选项" This reverts commit 2b335a993887aa539badc028b672d387efff4cef. * revert: rendertool * refactor: Remove unused imports and functions to simplify code * perf: debug interactive --------- Co-authored-by: Theresa <63280168+sd0ric4@users.noreply.github.com> --- .vscode/launch.json | 39 ++ .../global/core/workflow/runtime/utils.ts | 1 - .../template/system/interactive/type.d.ts | 51 +-- packages/global/core/workflow/type/node.d.ts | 2 + .../service/core/workflow/dispatch/index.ts | 13 +- .../dispatch/interactive/formInput.ts | 5 +- .../dispatch/interactive/userSelect.ts | 3 +- .../service/core/workflow/dispatch/utils.ts | 4 +- .../core/chat/components/AIResponseBox.tsx | 340 ++++++------------ .../Interactive/InteractiveComponents.tsx | 206 +++++++++++ .../app/src/global/core/workflow/api.d.ts | 4 + .../Flow/nodes/render/NodeCard.tsx | 173 +-------- .../render/RenderDebug/NodeDebugResponse.tsx | 269 ++++++++++++++ .../WorkflowComponents/context/index.tsx | 104 ++++-- .../app/src/pages/api/core/chat/chatTest.ts | 8 +- .../app/src/pages/api/core/workflow/debug.ts | 73 ++-- 16 files changed, 792 insertions(+), 503 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 projects/app/src/components/core/chat/components/Interactive/InteractiveComponents.tsx create mode 100644 projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderDebug/NodeDebugResponse.tsx 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 && ( -