{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}
+
{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: '了解更多',
},
}