diff --git a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx index 47bb297789..daf8d0ec17 100644 --- a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx +++ b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx @@ -29,18 +29,24 @@ import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow import Tooltip from '@/app/components/base/tooltip' import { isExceptionVariable } from '@/app/components/workflow/utils' import VarFullPathPanel from '@/app/components/workflow/nodes/_base/components/variable/var-full-path-panel' -import { Type } from '@/app/components/workflow/nodes/llm/types' +import type { Type } from '@/app/components/workflow/nodes/llm/types' +import type { ValueSelector } from '@/app/components/workflow/types' type WorkflowVariableBlockComponentProps = { nodeKey: string variables: string[] workflowNodesMap: WorkflowNodesMap + getVarType: (payload: { + nodeId: string, + valueSelector: ValueSelector, + }) => Type } const WorkflowVariableBlockComponent = ({ nodeKey, variables, workflowNodesMap = {}, + getVarType, }: WorkflowVariableBlockComponentProps) => { const { t } = useTranslation() const [editor] = useLexicalComposerContext() @@ -144,7 +150,10 @@ const WorkflowVariableBlockComponent = ({ } disabled={!isShowAPart} diff --git a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/index.tsx b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/index.tsx index 05d4505e20..424cdcbdc9 100644 --- a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/index.tsx +++ b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/index.tsx @@ -25,11 +25,13 @@ export type WorkflowVariableBlockProps = { getWorkflowNode: (nodeId: string) => Node onInsert?: () => void onDelete?: () => void + getVarType: any } const WorkflowVariableBlock = memo(({ workflowNodesMap, onInsert, onDelete, + getVarType, }: WorkflowVariableBlockType) => { const [editor] = useLexicalComposerContext() @@ -48,7 +50,7 @@ const WorkflowVariableBlock = memo(({ INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, (variables: string[]) => { editor.dispatchCommand(CLEAR_HIDE_MENU_TIMEOUT, undefined) - const workflowVariableBlockNode = $createWorkflowVariableBlockNode(variables, workflowNodesMap) + const workflowVariableBlockNode = $createWorkflowVariableBlockNode(variables, workflowNodesMap, getVarType) $insertNodes([workflowVariableBlockNode]) if (onInsert) @@ -69,7 +71,7 @@ const WorkflowVariableBlock = memo(({ COMMAND_PRIORITY_EDITOR, ), ) - }, [editor, onInsert, onDelete, workflowNodesMap]) + }, [editor, onInsert, onDelete, workflowNodesMap, getVarType]) return null }) diff --git a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/node.tsx b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/node.tsx index 9fa518218f..f302fd2ebe 100644 --- a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/node.tsx +++ b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/node.tsx @@ -7,29 +7,32 @@ export type WorkflowNodesMap = WorkflowVariableBlockType['workflowNodesMap'] export type SerializedNode = SerializedLexicalNode & { variables: string[] workflowNodesMap: WorkflowNodesMap + getVarType: any } export class WorkflowVariableBlockNode extends DecoratorNode { __variables: string[] __workflowNodesMap: WorkflowNodesMap + __getVarType: any static getType(): string { return 'workflow-variable-block' } static clone(node: WorkflowVariableBlockNode): WorkflowVariableBlockNode { - return new WorkflowVariableBlockNode(node.__variables, node.__workflowNodesMap, node.__key) + return new WorkflowVariableBlockNode(node.__variables, node.__workflowNodesMap, node.__getVarType, node.__key) } isInline(): boolean { return true } - constructor(variables: string[], workflowNodesMap: WorkflowNodesMap, key?: NodeKey) { + constructor(variables: string[], workflowNodesMap: WorkflowNodesMap, getVarType: any, key?: NodeKey) { super(key) this.__variables = variables this.__workflowNodesMap = workflowNodesMap + this.__getVarType = getVarType } createDOM(): HTMLElement { @@ -48,12 +51,13 @@ export class WorkflowVariableBlockNode extends DecoratorNode { nodeKey={this.getKey()} variables={this.__variables} workflowNodesMap={this.__workflowNodesMap} + getVarType={this.__getVarType!} /> ) } static importJSON(serializedNode: SerializedNode): WorkflowVariableBlockNode { - const node = $createWorkflowVariableBlockNode(serializedNode.variables, serializedNode.workflowNodesMap) + const node = $createWorkflowVariableBlockNode(serializedNode.variables, serializedNode.workflowNodesMap, serializedNode.getVarType) return node } @@ -77,12 +81,17 @@ export class WorkflowVariableBlockNode extends DecoratorNode { return self.__workflowNodesMap } + getVarType(): any { + const self = this.getLatest() + return self.__getVarType + } + getTextContent(): string { return `{{#${this.getVariables().join('.')}#}}` } } -export function $createWorkflowVariableBlockNode(variables: string[], workflowNodesMap: WorkflowNodesMap): WorkflowVariableBlockNode { - return new WorkflowVariableBlockNode(variables, workflowNodesMap) +export function $createWorkflowVariableBlockNode(variables: string[], workflowNodesMap: WorkflowNodesMap, getVarType: any): WorkflowVariableBlockNode { + return new WorkflowVariableBlockNode(variables, workflowNodesMap, getVarType) } export function $isWorkflowVariableBlockNode( diff --git a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/workflow-variable-block-replacement-block.tsx b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/workflow-variable-block-replacement-block.tsx index 0a80ecc220..3aaa8726d5 100644 --- a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/workflow-variable-block-replacement-block.tsx +++ b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/workflow-variable-block-replacement-block.tsx @@ -16,6 +16,7 @@ import { VAR_REGEX as REGEX, resetReg } from '@/config' const WorkflowVariableBlockReplacementBlock = ({ workflowNodesMap, + getVarType, onInsert, }: WorkflowVariableBlockType) => { const [editor] = useLexicalComposerContext() @@ -30,8 +31,8 @@ const WorkflowVariableBlockReplacementBlock = ({ onInsert() const nodePathString = textNode.getTextContent().slice(3, -3) - return $applyNodeReplacement($createWorkflowVariableBlockNode(nodePathString.split('.'), workflowNodesMap)) - }, [onInsert, workflowNodesMap]) + return $applyNodeReplacement($createWorkflowVariableBlockNode(nodePathString.split('.'), workflowNodesMap, getVarType)) + }, [onInsert, workflowNodesMap, getVarType]) const getMatch = useCallback((text: string) => { const matchArr = REGEX.exec(text) diff --git a/web/app/components/base/prompt-editor/types.ts b/web/app/components/base/prompt-editor/types.ts index 6d0f307c17..cc398fdb2f 100644 --- a/web/app/components/base/prompt-editor/types.ts +++ b/web/app/components/base/prompt-editor/types.ts @@ -3,6 +3,7 @@ import type { RoleName } from './plugins/history-block' import type { Node, NodeOutPutVar, + ValueSelector, } from '@/app/components/workflow/types' export type Option = { @@ -60,6 +61,10 @@ export type WorkflowVariableBlockType = { workflowNodesMap?: Record> onInsert?: () => void onDelete?: () => void + getVarType?: (payload: { + nodeId: string, + valueSelector: ValueSelector, + }) => string } export type MenuTextMatch = { diff --git a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/parameter-item.tsx b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/parameter-item.tsx index f43598fc38..48cf05c6de 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/parameter-item.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/parameter-item.tsx @@ -10,6 +10,8 @@ import Slider from '@/app/components/base/slider' import Radio from '@/app/components/base/radio' import { SimpleSelect } from '@/app/components/base/select' import TagInput from '@/app/components/base/tag-input' +import Badge from '@/app/components/base/badge' +import { useTranslation } from 'react-i18next' export type ParameterValue = number | string | string[] | boolean | undefined @@ -27,6 +29,7 @@ const ParameterItem: FC = ({ onSwitch, isInWorkflow, }) => { + const { t } = useTranslation() const language = useLanguage() const [localValue, setLocalValue] = useState(value) const numberInputRef = useRef(null) @@ -278,6 +281,19 @@ const ParameterItem: FC = ({ /> ) } + {/* TODO: wait api return and product design */} + {parameterRule.name === 'json_schema' && ( + +
{t('app.structOutput.legacyTip')}
+ {t('app.structOutput.learnMore')} + + )} + > + {t('app.structOutput.legacy')} +
+ )} { parameterRule.type === 'tag' && ( diff --git a/web/app/components/workflow/hooks/use-workflow-variables.ts b/web/app/components/workflow/hooks/use-workflow-variables.ts index feadaf8659..fb6c466e03 100644 --- a/web/app/components/workflow/hooks/use-workflow-variables.ts +++ b/web/app/components/workflow/hooks/use-workflow-variables.ts @@ -8,6 +8,8 @@ import type { ValueSelector, Var, } from '@/app/components/workflow/types' +import { useIsChatMode, useWorkflow } from './use-workflow' +import { useStoreApi } from 'reactflow' export const useWorkflowVariables = () => { const { t } = useTranslation() @@ -72,3 +74,40 @@ export const useWorkflowVariables = () => { getCurrentVariableType, } } + +export const useWorkflowVariableType = () => { + const store = useStoreApi() + const { + getNodes, + } = store.getState() + const { getBeforeNodesInSameBranch } = useWorkflow() + const { getCurrentVariableType } = useWorkflowVariables() + + const isChatMode = useIsChatMode() + + const getVarType = ({ + nodeId, + valueSelector, + }: { + nodeId: string, + valueSelector: ValueSelector, + }) => { + // debugger + const node = getNodes().find(n => n.id === nodeId) + // console.log(nodeId, valueSelector) + const isInIteration = !!node?.data.isInIteration + const iterationNode = isInIteration ? getNodes().find(n => n.id === node.parentId) : null + const availableNodes = getBeforeNodesInSameBranch(nodeId) + + const type = getCurrentVariableType({ + parentNode: iterationNode, + valueSelector, + availableNodes, + isChatMode, + isConstant: false, + }) + return type + } + + return getVarType +} diff --git a/web/app/components/workflow/nodes/_base/components/collapse/field-collapse.tsx b/web/app/components/workflow/nodes/_base/components/collapse/field-collapse.tsx index 71ca0d28ea..cf1526d5c9 100644 --- a/web/app/components/workflow/nodes/_base/components/collapse/field-collapse.tsx +++ b/web/app/components/workflow/nodes/_base/components/collapse/field-collapse.tsx @@ -4,10 +4,12 @@ import Collapse from '.' type FieldCollapseProps = { title: string children: ReactNode + operations?: ReactNode } const FieldCollapse = ({ title, children, + operations, }: FieldCollapseProps) => { return (
@@ -15,6 +17,7 @@ const FieldCollapse = ({ trigger={
{title}
} + operations={operations} >
{children} diff --git a/web/app/components/workflow/nodes/_base/components/collapse/index.tsx b/web/app/components/workflow/nodes/_base/components/collapse/index.tsx index a798ff0a9e..943fd80024 100644 --- a/web/app/components/workflow/nodes/_base/components/collapse/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/collapse/index.tsx @@ -1,3 +1,4 @@ +import type { ReactNode } from 'react' import { useState } from 'react' import { RiArrowDropRightLine } from '@remixicon/react' import cn from '@/utils/classnames' @@ -10,6 +11,8 @@ type CollapseProps = { children: JSX.Element collapsed?: boolean onCollapse?: (collapsed: boolean) => void + operations?: ReactNode + } const Collapse = ({ disabled, @@ -17,34 +20,38 @@ const Collapse = ({ children, collapsed, onCollapse, + operations, }: CollapseProps) => { const [collapsedLocal, setCollapsedLocal] = useState(true) const collapsedMerged = collapsed !== undefined ? collapsed : collapsedLocal return ( <> -
{ - if (!disabled) { - setCollapsedLocal(!collapsedMerged) - onCollapse?.(!collapsedMerged) - } - }} - > -
- { - !disabled && ( - - ) - } +
+
{ + if (!disabled) { + setCollapsedLocal(!collapsedMerged) + onCollapse?.(!collapsedMerged) + } + }} + > +
+ { + !disabled && ( + + ) + } +
+ {trigger}
- {trigger} + {operations}
{ !collapsedMerged && children diff --git a/web/app/components/workflow/nodes/_base/components/output-vars.tsx b/web/app/components/workflow/nodes/_base/components/output-vars.tsx index 4a265a5a5b..953e9deb81 100644 --- a/web/app/components/workflow/nodes/_base/components/output-vars.tsx +++ b/web/app/components/workflow/nodes/_base/components/output-vars.tsx @@ -8,15 +8,20 @@ type Props = { className?: string title?: string children: ReactNode + operations?: ReactNode } const OutputVars: FC = ({ title, children, + operations, }) => { const { t } = useTranslation() return ( - + {children} ) @@ -40,9 +45,11 @@ export const VarItem: FC = ({ }) => { return (
-
-
{name}
-
{type}
+
+
+
{name}
+
{type}
+
{description} diff --git a/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx b/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx index 9b7c63d862..c9689def87 100644 --- a/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx +++ b/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx @@ -36,6 +36,7 @@ import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/ import Switch from '@/app/components/base/switch' import { Jinja } from '@/app/components/base/icons/src/vender/workflow' import { useStore } from '@/app/components/workflow/store' +import { useWorkflowVariableType } from '@/app/components/workflow/hooks' type Props = { className?: string @@ -143,6 +144,8 @@ const Editor: FC = ({ eventEmitter?.emit({ type: PROMPT_EDITOR_INSERT_QUICKLY, instanceId } as any) } + const getVarType = useWorkflowVariableType() + return (
@@ -249,6 +252,7 @@ const Editor: FC = ({ workflowVariableBlock={{ show: true, variables: nodesOutputVars || [], + getVarType, workflowNodesMap: availableNodes.reduce((acc, node) => { acc[node.id] = { title: node.data.title, 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 e407e7ea81..2c0959ec53 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/utils.ts +++ b/web/app/components/workflow/nodes/_base/components/variable/utils.ts @@ -665,7 +665,7 @@ export const getVarType = ({ if (!targetVar) return VarType.string - const isStructuredOutputVar = !!targetVar.children.schema?.properties + const isStructuredOutputVar = !!targetVar.children?.schema?.properties if (isStructuredOutputVar) { let currProperties = targetVar.children.schema; (valueSelector as ValueSelector).slice(2).forEach((key, i) => { diff --git a/web/app/components/workflow/nodes/llm/components/structure-output.tsx b/web/app/components/workflow/nodes/llm/components/structure-output.tsx new file mode 100644 index 0000000000..374262ccf8 --- /dev/null +++ b/web/app/components/workflow/nodes/llm/components/structure-output.tsx @@ -0,0 +1,70 @@ +'use client' +import Button from '@/app/components/base/button' +import { RiEditLine } from '@remixicon/react' +import type { FC } from 'react' +import React, { useCallback } from 'react' +import type { SchemaRoot, StructuredOutput } from '../types' +import ShowPanel from '@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show' +import { useBoolean } from 'ahooks' +import JsonSchemaConfigModal from './json-schema-config-modal' +import cn from '@/utils/classnames' +import { useTranslation } from 'react-i18next' + +type Props = { + className?: string + value?: StructuredOutput + onChange: (value: StructuredOutput) => void, +} + +const StructureOutput: FC = ({ + className, + value, + onChange, +}) => { + const { t } = useTranslation() + const [showConfig, { + setTrue: showConfigModal, + setFalse: hideConfigModal, + }] = useBoolean(false) + + const handleChange = useCallback((value: SchemaRoot) => { + onChange({ + schema: value, + }) + }, [onChange]) + return ( +
+
+
+
structured_output
+
object
+
+ +
+ {value?.schema ? ( + ) : ( +
{t('app.structOutput.notConfiguredTip')}
+ )} + + {showConfig && ( + + )} +
+ ) +} +export default React.memo(StructureOutput) diff --git a/web/app/components/workflow/nodes/llm/panel.tsx b/web/app/components/workflow/nodes/llm/panel.tsx index 6c3831f5bc..bf7fff222c 100644 --- a/web/app/components/workflow/nodes/llm/panel.tsx +++ b/web/app/components/workflow/nodes/llm/panel.tsx @@ -20,6 +20,9 @@ import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/c import ResultPanel from '@/app/components/workflow/run/result-panel' import Tooltip from '@/app/components/base/tooltip' import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor' +import StructureOutput from './components/structure-output' +import Switch from '@/app/components/base/switch' +import { RiAlertFill, RiQuestionLine } from '@remixicon/react' const i18nPrefix = 'workflow.nodes.llm' @@ -64,6 +67,9 @@ const Panel: FC> = ({ contexts, setContexts, runningStatus, + isModelSupportStructuredOutput, + handleStructureOutputEnableChange, + handleStructureOutputChange, handleRun, handleStop, varInputs, @@ -272,13 +278,55 @@ const Panel: FC> = ({ />
- + + {!isModelSupportStructuredOutput && ( + +
{t('app.structOutput.modelNotSupported')}
+
{t('app.structOutput.modelNotSupportedTip')}
+
+ }> +
+ +
+ + )} +
{t('app.structOutput.structured')}
+ {t('app.structOutput.structuredTip')}
+ }> +
+ +
+ + +
+ } + > <> + {inputs.structured_output_enabled && ( + <> + + + + )} {isShowSingleRun && ( diff --git a/web/app/components/workflow/nodes/llm/types.ts b/web/app/components/workflow/nodes/llm/types.ts index 2fcd110879..a4931f7017 100644 --- a/web/app/components/workflow/nodes/llm/types.ts +++ b/web/app/components/workflow/nodes/llm/types.ts @@ -15,6 +15,8 @@ export type LLMNodeType = CommonNodeType & { enabled: boolean configs?: VisionSetting } + structured_output_enabled?: boolean + structured_output?: StructuredOutput } export enum Type { diff --git a/web/app/components/workflow/nodes/llm/use-config.ts b/web/app/components/workflow/nodes/llm/use-config.ts index 6b2d27e70f..25d213e8f3 100644 --- a/web/app/components/workflow/nodes/llm/use-config.ts +++ b/web/app/components/workflow/nodes/llm/use-config.ts @@ -9,7 +9,7 @@ import { } from '../../hooks' import useAvailableVarList from '../_base/hooks/use-available-var-list' import useConfigVision from '../../hooks/use-config-vision' -import type { LLMNodeType } from './types' +import type { LLMNodeType, StructuredOutput } from './types' import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import { ModelTypeEnum, @@ -18,6 +18,8 @@ import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-cr import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run' import { RETRIEVAL_OUTPUT_STRUCT } from '@/app/components/workflow/constants' import { checkHasContextBlock, checkHasHistoryBlock, checkHasQueryBlock } from '@/app/components/base/prompt-editor/constants' +import useSWR from 'swr' +import { fetchModelParameterRules } from '@/service/common' const useConfig = (id: string, payload: LLMNodeType) => { const { nodesReadOnly: readOnly } = useNodesReadOnly() @@ -277,6 +279,25 @@ const useConfig = (id: string, payload: LLMNodeType) => { setInputs(newInputs) }, [inputs, setInputs]) + // structure output + // TODO: this method has problem, different model has different parameter rules that show support structured output + const { data: parameterRulesData } = useSWR((model?.provider && model?.name) ? `/workspaces/current/model-providers/${model.provider}/models/parameter-rules?model=${model.name}` : null, fetchModelParameterRules) + const isModelSupportStructuredOutput = parameterRulesData?.data?.some((rule: any) => rule.name === 'json_schema') + + const handleStructureOutputEnableChange = useCallback((enabled: boolean) => { + const newInputs = produce(inputs, (draft) => { + draft.structured_output_enabled = enabled + }) + setInputs(newInputs) + }, [inputs, setInputs]) + + const handleStructureOutputChange = useCallback((newOutput: StructuredOutput) => { + const newInputs = produce(inputs, (draft) => { + draft.structured_output = newOutput + }) + setInputs(newInputs) + }, [inputs, setInputs]) + const filterInputVar = useCallback((varPayload: Var) => { return [VarType.number, VarType.string, VarType.secret, VarType.arrayString, VarType.arrayNumber, VarType.file, VarType.arrayFile].includes(varPayload.type) }, []) @@ -408,6 +429,9 @@ const useConfig = (id: string, payload: LLMNodeType) => { setContexts, varInputs, runningStatus, + isModelSupportStructuredOutput, + handleStructureOutputChange, + handleStructureOutputEnableChange, handleRun, handleStop, runResult, diff --git a/web/i18n/en-US/app.ts b/web/i18n/en-US/app.ts index 089dd2888b..ae09a5e8c2 100644 --- a/web/i18n/en-US/app.ts +++ b/web/i18n/en-US/app.ts @@ -184,6 +184,15 @@ const translation = { moreFillTip: 'Showing max 10 levels of nesting', required: 'Required', LLMResponse: 'LLM Response', + configure: 'Configure', + notConfiguredTip: 'Structured output has not been configured yet', + structured: 'Structured', + structuredTip: 'Structured Outputs is a feature that ensures the model will always generate responses that adhere to your supplied JSON Schema', + modelNotSupported: 'Model not supported', + modelNotSupportedTip: 'The current model does not support this feature and is automatically downgraded to prompt injection.', + legacy: 'Legacy', + legacyTip: 'JSON Schema will be removed from model parameters, you can use the structured output functionality under nodes instead.', + learnMore: 'Learn more', }, } diff --git a/web/i18n/zh-Hans/app.ts b/web/i18n/zh-Hans/app.ts index 4b51f1a736..6f75663851 100644 --- a/web/i18n/zh-Hans/app.ts +++ b/web/i18n/zh-Hans/app.ts @@ -185,6 +185,15 @@ const translation = { moreFillTip: '最多显示 10 级嵌套', required: '必填', LLMResponse: 'LLM 的响应', + configure: '配置', + notConfiguredTip: '结构化输出尚未配置', + structured: '结构化', + structuredTip: '结构化输出是一项功能,可确保模型始终生成符合您提供的 JSON 模式的响应', + modelNotSupported: '模型不支持', + modelNotSupportedTip: '当前模型不支持此功能,将自动降级为提示注入。', + legacy: '遗留', + legacyTip: '此功能将在未来版本中删除', + learnMore: '了解更多', }, }