From 569f94280f710c3684dc21ef879424c697d9c472 Mon Sep 17 00:00:00 2001 From: "lengjs@wsidomidata.com" Date: Tue, 10 Jun 2025 09:03:24 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90amc=E3=80=91=20=E5=90=88=E5=B9=B6?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workflow/block-selector/constants.tsx | 5 + web/app/components/workflow/constants.ts | 13 +- .../nodes/_base/components/variable/utils.ts | 15 ++ .../components/workflow/nodes/constants.ts | 4 + .../metadata/condition-list/utils.ts | 4 + .../workflow/nodes/vanna/default.ts | 45 +++++ .../components/workflow/nodes/vanna/node.tsx | 35 ++++ .../components/workflow/nodes/vanna/panel.tsx | 155 +++++++++++++++++ .../components/workflow/nodes/vanna/types.ts | 20 +++ .../workflow/nodes/vanna/use-config.ts | 162 ++++++++++++++++++ .../components/workflow/nodes/vanna/utils.ts | 5 + web/app/components/workflow/types.ts | 1 + web/app/components/workflow/utils/workflow.ts | 1 + web/i18n/zh-Hans/workflow.ts | 5 +- 14 files changed, 467 insertions(+), 3 deletions(-) create mode 100644 web/app/components/workflow/nodes/vanna/default.ts create mode 100644 web/app/components/workflow/nodes/vanna/node.tsx create mode 100644 web/app/components/workflow/nodes/vanna/panel.tsx create mode 100644 web/app/components/workflow/nodes/vanna/types.ts create mode 100644 web/app/components/workflow/nodes/vanna/use-config.ts create mode 100644 web/app/components/workflow/nodes/vanna/utils.ts diff --git a/web/app/components/workflow/block-selector/constants.tsx b/web/app/components/workflow/block-selector/constants.tsx index 680cbf45b9..edbc21eb18 100644 --- a/web/app/components/workflow/block-selector/constants.tsx +++ b/web/app/components/workflow/block-selector/constants.tsx @@ -100,6 +100,11 @@ export const BLOCKS: Block[] = [ type: BlockEnum.Agent, title: 'Agent', }, + { + classification: BlockClassificationEnum.Default, + type: BlockEnum.Vanna, + title: 'Vanna', + }, ] export const BLOCK_CLASSIFICATIONS: string[] = [ diff --git a/web/app/components/workflow/constants.ts b/web/app/components/workflow/constants.ts index cdfd963cfa..0dd0c2114c 100644 --- a/web/app/components/workflow/constants.ts +++ b/web/app/components/workflow/constants.ts @@ -22,6 +22,7 @@ import IterationStartDefault from './nodes/iteration-start/default' import AgentDefault from './nodes/agent/default' import LoopStartDefault from './nodes/loop-start/default' import LoopEndDefault from './nodes/loop-end/default' +import VannaDefault from './nodes/vanna/default' type NodesExtraData = { author: string @@ -240,6 +241,16 @@ export const NODES_EXTRA_DATA: Record = { getAvailableNextNodes: ListFilterDefault.getAvailableNextNodes, checkValid: AgentDefault.checkValid, }, + + [BlockEnum.Vanna]: { + author: '维斯德', + about: '', + availablePrevNodes: [], + availableNextNodes: [], + getAvailablePrevNodes: VannaDefault.getAvailablePrevNodes, + getAvailableNextNodes: VannaDefault.getAvailableNextNodes, + checkValid: VannaDefault.checkValid, + }, } export const NODES_INITIAL_DATA = { @@ -471,7 +482,7 @@ export const SUPPORT_OUTPUT_VARS_NODE = [ BlockEnum.HttpRequest, BlockEnum.Tool, BlockEnum.VariableAssigner, BlockEnum.VariableAggregator, BlockEnum.QuestionClassifier, BlockEnum.ParameterExtractor, BlockEnum.Iteration, BlockEnum.Loop, BlockEnum.DocExtractor, BlockEnum.ListFilter, - BlockEnum.Agent, + BlockEnum.Agent,BlockEnum.Vanna ] export const LLM_OUTPUT_STRUCT: Var[] = [ diff --git a/web/app/components/workflow/nodes/_base/components/variable/utils.ts b/web/app/components/workflow/nodes/_base/components/variable/utils.ts index 428c204dd3..6d69589646 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/utils.ts +++ b/web/app/components/workflow/nodes/_base/components/variable/utils.ts @@ -477,6 +477,21 @@ const formatItem = ( }) as Var[] break } + + case BlockEnum.Vanna: { + res.vars = [ + { + variable: 'output', + type: VarType.string, + }, + { + variable: 'sql', + type: VarType.string, + }, + ] + break + } + } const { error_strategy } = data diff --git a/web/app/components/workflow/nodes/constants.ts b/web/app/components/workflow/nodes/constants.ts index 0cd6922233..2d39632a64 100644 --- a/web/app/components/workflow/nodes/constants.ts +++ b/web/app/components/workflow/nodes/constants.ts @@ -38,6 +38,8 @@ import ListFilterNode from './list-operator/node' import ListFilterPanel from './list-operator/panel' import AgentNode from './agent/node' import AgentPanel from './agent/panel' +import VannaNode from './vanna/node' +import VannaPanel from './vanna/panel' import { TransferMethod } from '@/types/app' export const NodeComponentMap: Record> = { @@ -61,6 +63,7 @@ export const NodeComponentMap: Record> = { [BlockEnum.DocExtractor]: DocExtractorNode, [BlockEnum.ListFilter]: ListFilterNode, [BlockEnum.Agent]: AgentNode, + [BlockEnum.Vanna]: VannaNode, } export const PanelComponentMap: Record> = { @@ -84,6 +87,7 @@ export const PanelComponentMap: Record> = { [BlockEnum.DocExtractor]: DocExtractorPanel, [BlockEnum.ListFilter]: ListFilterPanel, [BlockEnum.Agent]: AgentPanel, + [BlockEnum.Vanna]: VannaPanel, } export const CUSTOM_NODE_TYPE = 'custom' diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/utils.ts b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/utils.ts index 6397023991..1171d8d40d 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/utils.ts +++ b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/utils.ts @@ -32,6 +32,8 @@ export const getOperators = (type?: MetadataFilteringVariableType) => { ComparisonOperator.endWith, ComparisonOperator.empty, ComparisonOperator.notEmpty, + ComparisonOperator.in, + ComparisonOperator.notIn, ] case MetadataFilteringVariableType.number: return [ @@ -43,6 +45,8 @@ export const getOperators = (type?: MetadataFilteringVariableType) => { ComparisonOperator.lessThanOrEqual, ComparisonOperator.empty, ComparisonOperator.notEmpty, + ComparisonOperator.in, + ComparisonOperator.notIn, ] default: return [ diff --git a/web/app/components/workflow/nodes/vanna/default.ts b/web/app/components/workflow/nodes/vanna/default.ts new file mode 100644 index 0000000000..39b061f756 --- /dev/null +++ b/web/app/components/workflow/nodes/vanna/default.ts @@ -0,0 +1,45 @@ +import type {NodeDefault} from '../../types' +import {BlockEnum} from '../../types' +import type {VannaNodeType} from './types' +import {ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS} from '@/app/components/workflow/blocks' +import {ReasoningModeType} from "@/app/components/workflow/nodes/parameter-extractor/types"; + +const nodeDefault: NodeDefault = { + defaultValue: { + query: [], + model: { + provider: '', + name: '', + mode: 'chat', + completion_params: { + temperature: 0.7, + }, + }, + reasoning_mode: ReasoningModeType.prompt, + vision: { + enabled: false, + }, + }, + getAvailablePrevNodes(isChatMode: boolean) { + return isChatMode + ? ALL_CHAT_AVAILABLE_BLOCKS + : ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End) + }, + getAvailableNextNodes(isChatMode: boolean) { + return isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS + }, + checkValid(payload: VannaNodeType) { + let isValid = true + let errorMessages = '' + if (!payload.query || payload.query.length === 0){ + errorMessages = '输入变量不能为空' + } + + return { + isValid, + errorMessage: errorMessages, + } + }, +} + +export default nodeDefault diff --git a/web/app/components/workflow/nodes/vanna/node.tsx b/web/app/components/workflow/nodes/vanna/node.tsx new file mode 100644 index 0000000000..86eb1e20c1 --- /dev/null +++ b/web/app/components/workflow/nodes/vanna/node.tsx @@ -0,0 +1,35 @@ +import type { FC } from 'react' +import React from 'react' +import type { VannaNodeType } from './types' +import type { NodeProps } from '@/app/components/workflow/types' + +import ModelSelector from "@/app/components/header/account-setting/model-provider-page/model-selector"; +import { + useTextGenerationCurrentProviderAndModelAndModelList +} from "@/app/components/header/account-setting/model-provider-page/hooks"; + +const Node: FC> = ({ + data, +}) => { + + const { provider, name: modelId } = data.model || {} + const { + textGenerationModelList, + } = useTextGenerationCurrentProviderAndModelAndModelList() + const hasSetModel = provider && modelId + + return ( +
+ {hasSetModel && ( + + )} +
+ ) +} + +export default React.memo(Node) diff --git a/web/app/components/workflow/nodes/vanna/panel.tsx b/web/app/components/workflow/nodes/vanna/panel.tsx new file mode 100644 index 0000000000..75def5ab05 --- /dev/null +++ b/web/app/components/workflow/nodes/vanna/panel.tsx @@ -0,0 +1,155 @@ +import React, {FC} from 'react' +import {useTranslation} from 'react-i18next' +import useConfig from './use-config' +import type {VannaNodeType} from './types' +import Field from '@/app/components/workflow/nodes/_base/components/field' +import {InputVarType, NodePanelProps} from '@/app/components/workflow/types' +import ModelParameterModal from "@/app/components/header/account-setting/model-provider-page/model-parameter-modal"; +import VarReferencePicker from "@/app/components/workflow/nodes/_base/components/variable/var-reference-picker"; +import BeforeRunForm from "@/app/components/workflow/nodes/_base/components/before-run-form"; +import ResultPanel from "@/app/components/workflow/run/result-panel"; +import type {Props as FormProps} from "@/app/components/workflow/nodes/_base/components/before-run-form/form"; +import {findVariableWhenOnLLMVision} from "@/app/components/workflow/nodes/utils"; +import Split from "@/app/components/workflow/nodes/_base/components/split"; +import OutputVars, {VarItem} from "@/app/components/workflow/nodes/_base/components/output-vars"; + +const i18nPrefix = 'workflow.nodes.parameterExtractor' + +const Panel: FC> = ( + { + id, + data, + }) => { + const {t} = useTranslation() + + const { + readOnly, + inputs, + handleCompletionParamsChange, + handleInputVarChange, + handleModelChanged, + isShowSingleRun, + hideSingleRun, + handleRun, + handleStop, + runResult, + runningStatus, + filterVar, + varInputs, + inputVarValues, + setInputVarValues, + isVisionModel, + availableVisionVars, + visionFiles, + setVisionFiles, + } = useConfig(id, data) + + const model = inputs.model + + const singleRunForms = (() => { + const forms: FormProps[] = [] + + forms.push( + { + label: t('workflow.nodes.llm.singleRun.variable')!, + inputs: [{ + label: t(`${i18nPrefix}.inputVar`)!, + variable: 'query', + type: InputVarType.paragraph, + required: true, + }, ...varInputs], + values: inputVarValues, + onChange: setInputVarValues, + }, + ) + + if (isVisionModel && data.vision?.enabled && data.vision?.configs?.variable_selector) { + const currentVariable = findVariableWhenOnLLMVision(data.vision.configs.variable_selector, availableVisionVars) + + forms.push( + { + label: t('workflow.nodes.llm.vision')!, + inputs: [{ + label: currentVariable?.variable as any, + variable: '#files#', + type: currentVariable?.formType as any, + required: false, + }], + values: {'#files#': visionFiles}, + onChange: keyValue => setVisionFiles((keyValue as any)['#files#']), + }, + ) + } + + return forms + })() + + + return ( +
+
+ + + + + + + + +
+ +
+ + + + +
+ + {isShowSingleRun && ( + } + /> + )} +
+ ) +} + +export default React.memo(Panel) diff --git a/web/app/components/workflow/nodes/vanna/types.ts b/web/app/components/workflow/nodes/vanna/types.ts new file mode 100644 index 0000000000..660249ef9d --- /dev/null +++ b/web/app/components/workflow/nodes/vanna/types.ts @@ -0,0 +1,20 @@ +import type { + CommonNodeType, + Memory, + ModelConfig, ValueSelector, + VisionSetting +} from '@/app/components/workflow/types' +import {Param, ReasoningModeType} from "@/app/components/workflow/nodes/parameter-extractor/types"; + +export type VannaNodeType = CommonNodeType & { + model: ModelConfig + query: ValueSelector + instruction: string + reasoning_mode: ReasoningModeType + parameters: Param[] + memory?: Memory + vision: { + enabled: boolean + configs?: VisionSetting + } +} diff --git a/web/app/components/workflow/nodes/vanna/use-config.ts b/web/app/components/workflow/nodes/vanna/use-config.ts new file mode 100644 index 0000000000..680d27da32 --- /dev/null +++ b/web/app/components/workflow/nodes/vanna/use-config.ts @@ -0,0 +1,162 @@ +import type {VannaNodeType} from './types' +import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' +import { + useNodesReadOnly, +} from '@/app/components/workflow/hooks' +import {useCallback, useRef, useState} from "react"; +import produce from "immer"; +import {ValueSelector, Var, VarType} from "@/app/components/workflow/types"; +import useOneStepRun from "@/app/components/workflow/nodes/_base/hooks/use-one-step-run"; +import useConfigVision from "@/app/components/workflow/hooks/use-config-vision"; +import useAvailableVarList from "@/app/components/workflow/nodes/_base/hooks/use-available-var-list"; + +const useConfig = (id: string, payload: VannaNodeType) => { + const {nodesReadOnly: readOnly} = useNodesReadOnly() + const {inputs, setInputs} = useNodeCrud(id, payload) + const inputRef = useRef(inputs) + const [modelChanged, setModelChanged] = useState(false) + + // model + const model = inputs.model || { + provider: '', + name: '', + mode: 'chat', + completion_params: { + temperature: 0.7, + }, + } + + const modelMode = inputs.model?.mode + + // single run + const { + isShowSingleRun, + hideSingleRun, + getInputVars, + runningStatus, + handleRun, + handleStop, + runInputData, + runInputDataRef, + setRunInputData, + runResult, + } = useOneStepRun({ + id, + data: inputs, + defaultRunInputData: { + 'query': '', + '#files#': [], + }, + }) + + const { + isVisionModel, + handleVisionResolutionEnabledChange, + handleVisionResolutionChange, + handleModelChanged: handleVisionConfigAfterModelChanged, + } = useConfigVision(model, { + payload: inputs.vision, + onChange: (newPayload) => { + const newInputs = produce(inputs, (draft) => { + draft.vision = newPayload + }) + setInputs(newInputs) + }, + }) + + + const filterVisionInputVar = useCallback((varPayload: Var) => { + return [VarType.file, VarType.arrayFile].includes(varPayload.type) + }, []) + + + const { + availableVars: availableVisionVars, + } = useAvailableVarList(id, { + onlyLeafNodeVar: false, + filterVar: filterVisionInputVar, + }) + + + const handleModelChanged = useCallback((model: { provider: string; modelId: string; mode?: string }) => { + const newInputs = produce(inputRef.current, (draft) => { + draft.model.provider = model.provider + draft.model.name = model.modelId + draft.model.mode = model.mode! + }) + setInputs(newInputs) + setModelChanged(true) + }, [setInputs]) + + + const handleCompletionParamsChange = useCallback((newParams: Record) => { + debugger + const newInputs = produce(inputs, (draft) => { + draft.model.completion_params = newParams + }) + setInputs(newInputs) + }, [inputs, setInputs]) + + const handleInputVarChange = useCallback((newInputVar: ValueSelector | string) => { + const newInputs = produce(inputs, (draft) => { + draft.query = newInputVar as ValueSelector || [] + }) + setInputs(newInputs) + }, [inputs, setInputs]) + + const filterVar = useCallback((varPayload: Var) => { + return [VarType.string].includes(varPayload.type) + }, []) + + const varInputs = getInputVars([inputs.instruction]) + const inputVarValues = (() => { + const vars: Record = {} + Object.keys(runInputData) + .forEach((key) => { + vars[key] = runInputData[key] + }) + return vars + })() + + + const setInputVarValues = useCallback((newPayload: Record) => { + setRunInputData(newPayload) + }, [setRunInputData]) + + const visionFiles = runInputData['#files#'] + const setVisionFiles = useCallback((newFiles: any[]) => { + setRunInputData({ + ...runInputDataRef.current, + '#files#': newFiles, + }) + }, [runInputDataRef, setRunInputData]) + + + return { + readOnly, + inputs, + filterVar, + isShowSingleRun, + hideSingleRun, + getInputVars, + runningStatus, + handleRun, + handleStop, + runInputData, + runInputDataRef, + setRunInputData, + isVisionModel, + runResult, + varInputs, + inputVarValues, + setInputVarValues, + handleModelChanged, + handleInputVarChange, + handleCompletionParamsChange, + availableVisionVars, + visionFiles, + setVisionFiles + } +} + +export default useConfig diff --git a/web/app/components/workflow/nodes/vanna/utils.ts b/web/app/components/workflow/nodes/vanna/utils.ts new file mode 100644 index 0000000000..5a39d427ca --- /dev/null +++ b/web/app/components/workflow/nodes/vanna/utils.ts @@ -0,0 +1,5 @@ +import type {VannaNodeType} from './types' + +export const checkNodeValid = (payload: VannaNodeType) => { + return true +} diff --git a/web/app/components/workflow/types.ts b/web/app/components/workflow/types.ts index 5218c23084..77eeb6f48f 100644 --- a/web/app/components/workflow/types.ts +++ b/web/app/components/workflow/types.ts @@ -41,6 +41,7 @@ export enum BlockEnum { Loop = 'loop', LoopStart = 'loop-start', LoopEnd = 'loop-end', + Vanna = 'vanna', } export enum ControlMode { diff --git a/web/app/components/workflow/utils/workflow.ts b/web/app/components/workflow/utils/workflow.ts index 88c31f09b5..e8837a151c 100644 --- a/web/app/components/workflow/utils/workflow.ts +++ b/web/app/components/workflow/utils/workflow.ts @@ -32,6 +32,7 @@ export const canRunBySingle = (nodeType: BlockEnum) => { || nodeType === BlockEnum.Agent || nodeType === BlockEnum.DocExtractor || nodeType === BlockEnum.Loop + || nodeType === BlockEnum.Vanna } type ConnectedSourceOrTargetNodesChange = { diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index 79b9e674fd..b5601d6ac4 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -242,6 +242,7 @@ const translation = { 'code': '代码执行', 'template-transform': '模板转换', 'http-request': 'HTTP 请求', + 'vanna': 'Vanna', 'variable-assigner': '变量赋值器', 'variable-aggregator': '变量聚合器', 'assigner': '变量赋值', @@ -576,8 +577,8 @@ const translation = { 'not empty': '不为空', 'null': '空', 'not null': '不为空', - 'in': '是', - 'not in': '不是', + 'in': '在', + 'not in': '不在', 'all of': '全部是', 'exists': '存在', 'not exists': '不存在',