From f55a0dd269b0b4d8b6766d0c08aa3cddadba371a Mon Sep 17 00:00:00 2001 From: Joel Date: Tue, 11 Mar 2025 18:06:50 +0800 Subject: [PATCH 1/2] feat: structral output varlist --- .../object-child-tree-panel/picker/index.tsx | 20 +++- .../nodes/_base/components/variable/utils.ts | 110 ++++++++++++++---- .../variable/var-reference-vars.tsx | 12 +- 3 files changed, 114 insertions(+), 28 deletions(-) diff --git a/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/index.tsx b/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/index.tsx index fbcd7c379e..7fa27fd585 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/index.tsx @@ -1,9 +1,10 @@ 'use client' import type { FC } from 'react' -import React from 'react' +import React, { useRef } from 'react' import type { Field as FieldType, StructuredOutput } from '../../../../../llm/types' import Field from './field' import cn from '@/utils/classnames' +import { useHover } from 'ahooks' type Props = { className?: string @@ -11,6 +12,7 @@ type Props = { payload: StructuredOutput readonly?: boolean onSelect?: (field: FieldType) => void + onHovering?: (value: boolean) => void } export const PickerPanelMain: FC = ({ @@ -18,11 +20,25 @@ export const PickerPanelMain: FC = ({ root, payload, readonly, + onHovering, }) => { + const ref = useRef(null) + useHover(ref, { + onChange: (hovering) => { + if (hovering) { + onHovering?.(true) + } + else { + setTimeout(() => { + onHovering?.(false) + }, 100) + } + }, + }) const schema = payload.schema const fieldNames = Object.keys(schema.properties) return ( -
+
{/* Root info */}
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 f7365c7b41..15d0a16d2b 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/utils.ts +++ b/web/app/components/workflow/nodes/_base/components/variable/utils.ts @@ -3,7 +3,7 @@ import { isArray, uniq } from 'lodash-es' import type { CodeNodeType } from '../../../code/types' import type { EndNodeType } from '../../../end/types' import type { AnswerNodeType } from '../../../answer/types' -import type { LLMNodeType, StructuredOutput } from '../../../llm/types' +import { type LLMNodeType, type StructuredOutput, Type } from '../../../llm/types' import type { KnowledgeRetrievalNodeType } from '../../../knowledge-retrieval/types' import type { IfElseNodeType } from '../../../if-else/types' import type { TemplateTransformNodeType } from '../../../template-transform/types' @@ -21,6 +21,7 @@ import type { StartNodeType } from '@/app/components/workflow/nodes/start/types' import type { ConversationVariable, EnvironmentVariable, Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types' import type { VariableAssignerNodeType } from '@/app/components/workflow/nodes/variable-assigner/types' import mockStructData from '@/app/components/workflow/nodes/llm/mock-struct-data' +import type { Field as StructField } from '@/app/components/workflow/nodes/llm/types' import { HTTP_REQUEST_OUTPUT_STRUCT, @@ -56,23 +57,72 @@ const inputVarTypeToVarType = (type: InputVarType): VarType => { } as any)[type] || VarType.string } +const structTypeToVarType = (type: Type): VarType => { + return ({ + [Type.string]: VarType.string, + [Type.number]: VarType.number, + [Type.boolean]: VarType.boolean, + [Type.object]: VarType.object, + [Type.array]: VarType.array, + } as any)[type] || VarType.string +} + +const findExceptVarInStructuredProperties = (properties: Record, filterVar: (payload: Var, selector: ValueSelector) => boolean): Record => { + const res = produce(properties, (draft) => { + Object.keys(properties).forEach((key) => { + const item = properties[key] + const isObj = item.type === Type.object + if (!isObj && !filterVar({ + variable: key, + type: structTypeToVarType(item.type), + }, [key])) { + delete properties[key] + return + } + if (item.type === Type.object && item.properties) + item.properties = findExceptVarInStructuredProperties(item.properties, filterVar) + }) + return draft + }) + return res +} + +const findExceptVarInStructuredOutput = (structuredOutput: StructuredOutput, filterVar: (payload: Var, selector: ValueSelector) => boolean): StructuredOutput => { + const res = produce(structuredOutput, (draft) => { + const properties = draft.schema.properties + Object.keys(properties).forEach((key) => { + const item = properties[key] + const isObj = item.type === Type.object + if (!isObj && !filterVar({ + variable: key, + type: structTypeToVarType(item.type), + }, [key])) { + delete properties[key] + return + } + if (item.type === Type.object && item.properties) + item.properties = findExceptVarInStructuredProperties(item.properties, filterVar) + }) + return draft + }) + return res +} + const findExceptVarInObject = (obj: any, filterVar: (payload: Var, selector: ValueSelector) => boolean, value_selector: ValueSelector, isFile?: boolean): Var => { const { children } = obj + const isStructuredOutput = !!(children as StructuredOutput)?.schema?.properties + const res: Var = { variable: obj.variable, type: isFile ? VarType.file : VarType.object, - children: children.length > 0 ? children.filter((item: Var) => { + children: isStructuredOutput ? findExceptVarInStructuredOutput(children, filterVar) : children.filter((item: Var) => { const { children } = item - const isStructuredOutput = !!(children as StructuredOutput)?.schema - if (!isStructuredOutput) { - const currSelector = [...value_selector, item.variable] - if (!children) - return filterVar(item, currSelector) - const obj = findExceptVarInObject(item, filterVar, currSelector, false) // File doesn't contains file children - return obj.children && (obj.children as Var[])?.length > 0 - } - return true // TODO: handle structured output - }) : (children || []), + const currSelector = [...value_selector, item.variable] + if (!children) + return filterVar(item, currSelector) + const obj = findExceptVarInObject(item, filterVar, currSelector, false) // File doesn't contains file children + return obj.children && (obj.children as Var[])?.length > 0 + }), } return res } @@ -440,8 +490,8 @@ const formatItem = ( return findExceptVarInObject(isFile ? { ...v, children } : v, filterVar, selector, isFile) }) - if (res.nodeId === 'llm') - console.log(res) + // if (res.nodeId === 'llm') + // console.log(res) return res } @@ -1103,18 +1153,26 @@ export const updateNodeVars = (oldNode: Node, oldVarSelector: ValueSelector, new }) return newNode } + const varToValueSelectorList = (v: Var, parentValueSelector: ValueSelector, res: ValueSelector[]) => { if (!v.variable) return res.push([...parentValueSelector, v.variable]) - if (v.children) { - if ((v.children as Var[])?.length > 0) { - (v.children as Var[]).forEach((child) => { - varToValueSelectorList(child, [...parentValueSelector, v.variable], res) - }) - } - // TODO: handle structured output + const isStructuredOutput = !!(v.children as StructuredOutput)?.schema?.properties + + if ((v.children as Var[])?.length > 0) { + (v.children as Var[]).forEach((child) => { + varToValueSelectorList(child, [...parentValueSelector, v.variable], res) + }) + } + if (isStructuredOutput) { + Object.keys((v.children as StructuredOutput)?.schema?.properties || {}).forEach((key) => { + varToValueSelectorList({ + variable: key, + type: structTypeToVarType((v.children as StructuredOutput)?.schema?.properties[key].type), + }, [...parentValueSelector, v.variable], res) + }) } } @@ -1149,7 +1207,15 @@ export const getNodeOutputVars = (node: Node, isChatMode: boolean): ValueSelecto } case BlockEnum.LLM: { - varsToValueSelectorList(LLM_OUTPUT_STRUCT, [id], res) + varsToValueSelectorList([ + ...LLM_OUTPUT_STRUCT, + { + variable: 'structured_output', + type: VarType.object, + children: mockStructData, + }, + ], [id], res) + console.log(res) break } diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx index 762709e7a4..4d98d242be 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx @@ -67,7 +67,7 @@ const Item: FC = ({ setIsItemHovering(true) } else { - if (isObj) { + if (isObj || isStructureOutput) { setTimeout(() => { setIsItemHovering(false) }, 100) @@ -107,8 +107,8 @@ const Item: FC = ({
= ({ zIndex: 100, }}> {isStructureOutput && ( - + )} {(isObj && !isFile) && ( // eslint-disable-next-line ts/no-use-before-define From a1684791fc97af896c8d9dc0276ad6fbf7d870e4 Mon Sep 17 00:00:00 2001 From: Joel Date: Tue, 11 Mar 2025 18:32:43 +0800 Subject: [PATCH 2/2] feat: support choose var --- .../object-child-tree-panel/picker/field.tsx | 12 +++++++++++- .../object-child-tree-panel/picker/index.tsx | 10 +++++++--- .../_base/components/variable/var-reference-vars.tsx | 5 ++++- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/field.tsx b/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/field.tsx index 2a940f75e3..7c8ad80f88 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/field.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/field.tsx @@ -8,22 +8,27 @@ import cn from '@/utils/classnames' import TreeIndentLine from '../tree-indent-line' import { RiMoreFill } from '@remixicon/react' import Tooltip from '@/app/components/base/tooltip' +import type { ValueSelector } from '@/app/components/workflow/types' import { useTranslation } from 'react-i18next' const MAX_DEPTH = 10 type Props = { + valueSelector: ValueSelector name: string, payload: FieldType, depth?: number readonly?: boolean + onSelect?: (valueSelector: ValueSelector) => void } const Field: FC = ({ + valueSelector, name, payload, depth = 1, readonly, + onSelect, }) => { const { t } = useTranslation() const isLastFieldHighlight = readonly @@ -34,7 +39,10 @@ const Field: FC = ({ return (
-
+
!readonly && onSelect?.([...valueSelector, name])} + >
{depth === MAX_DEPTH + 1 ? ( @@ -57,6 +65,8 @@ const Field: FC = ({ payload={payload.properties?.[name] as FieldType} depth={depth + 1} readonly={readonly} + valueSelector={[...valueSelector, name]} + onSelect={onSelect} /> ))}
diff --git a/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/index.tsx b/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/index.tsx index 7fa27fd585..305d29aeea 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/index.tsx @@ -1,17 +1,18 @@ 'use client' import type { FC } from 'react' import React, { useRef } from 'react' -import type { Field as FieldType, StructuredOutput } from '../../../../../llm/types' +import type { StructuredOutput } from '../../../../../llm/types' import Field from './field' import cn from '@/utils/classnames' import { useHover } from 'ahooks' +import type { ValueSelector } from '@/app/components/workflow/types' type Props = { className?: string - root: { nodeName?: string, attrName: string } + root: { nodeId?: string, nodeName?: string, attrName: string } payload: StructuredOutput readonly?: boolean - onSelect?: (field: FieldType) => void + onSelect?: (valueSelector: ValueSelector) => void onHovering?: (value: boolean) => void } @@ -21,6 +22,7 @@ export const PickerPanelMain: FC = ({ payload, readonly, onHovering, + onSelect, }) => { const ref = useRef(null) useHover(ref, { @@ -59,6 +61,8 @@ export const PickerPanelMain: FC = ({ name={name} payload={schema.properties[name]} readonly={readonly} + valueSelector={[root.nodeId!, root.attrName]} + onSelect={onSelect} /> ))}
diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx index 4d98d242be..64047dda84 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx @@ -138,9 +138,12 @@ const Item: FC = ({ }}> {isStructureOutput && ( { + onChange(valueSelector, itemData) + }} /> )} {(isObj && !isFile) && (