mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-18 23:55:52 +08:00
Merge branch 'feat/llm-struct-output' of https://github.com/langgenius/dify into feat/llm-struct-output
This commit is contained in:
commit
b305c17bc3
@ -29,18 +29,24 @@ import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow
|
|||||||
import Tooltip from '@/app/components/base/tooltip'
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
import { isExceptionVariable } from '@/app/components/workflow/utils'
|
import { isExceptionVariable } from '@/app/components/workflow/utils'
|
||||||
import VarFullPathPanel from '@/app/components/workflow/nodes/_base/components/variable/var-full-path-panel'
|
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 = {
|
type WorkflowVariableBlockComponentProps = {
|
||||||
nodeKey: string
|
nodeKey: string
|
||||||
variables: string[]
|
variables: string[]
|
||||||
workflowNodesMap: WorkflowNodesMap
|
workflowNodesMap: WorkflowNodesMap
|
||||||
|
getVarType: (payload: {
|
||||||
|
nodeId: string,
|
||||||
|
valueSelector: ValueSelector,
|
||||||
|
}) => Type
|
||||||
}
|
}
|
||||||
|
|
||||||
const WorkflowVariableBlockComponent = ({
|
const WorkflowVariableBlockComponent = ({
|
||||||
nodeKey,
|
nodeKey,
|
||||||
variables,
|
variables,
|
||||||
workflowNodesMap = {},
|
workflowNodesMap = {},
|
||||||
|
getVarType,
|
||||||
}: WorkflowVariableBlockComponentProps) => {
|
}: WorkflowVariableBlockComponentProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [editor] = useLexicalComposerContext()
|
const [editor] = useLexicalComposerContext()
|
||||||
@ -144,7 +150,10 @@ const WorkflowVariableBlockComponent = ({
|
|||||||
<VarFullPathPanel
|
<VarFullPathPanel
|
||||||
nodeName={node.title}
|
nodeName={node.title}
|
||||||
path={variables.slice(1)}
|
path={variables.slice(1)}
|
||||||
varType={Type.string}
|
varType={getVarType({
|
||||||
|
nodeId: variables[0],
|
||||||
|
valueSelector: variables,
|
||||||
|
})}
|
||||||
nodeType={node?.type}
|
nodeType={node?.type}
|
||||||
/>}
|
/>}
|
||||||
disabled={!isShowAPart}
|
disabled={!isShowAPart}
|
||||||
|
@ -25,11 +25,13 @@ export type WorkflowVariableBlockProps = {
|
|||||||
getWorkflowNode: (nodeId: string) => Node
|
getWorkflowNode: (nodeId: string) => Node
|
||||||
onInsert?: () => void
|
onInsert?: () => void
|
||||||
onDelete?: () => void
|
onDelete?: () => void
|
||||||
|
getVarType: any
|
||||||
}
|
}
|
||||||
const WorkflowVariableBlock = memo(({
|
const WorkflowVariableBlock = memo(({
|
||||||
workflowNodesMap,
|
workflowNodesMap,
|
||||||
onInsert,
|
onInsert,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
getVarType,
|
||||||
}: WorkflowVariableBlockType) => {
|
}: WorkflowVariableBlockType) => {
|
||||||
const [editor] = useLexicalComposerContext()
|
const [editor] = useLexicalComposerContext()
|
||||||
|
|
||||||
@ -48,7 +50,7 @@ const WorkflowVariableBlock = memo(({
|
|||||||
INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND,
|
INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND,
|
||||||
(variables: string[]) => {
|
(variables: string[]) => {
|
||||||
editor.dispatchCommand(CLEAR_HIDE_MENU_TIMEOUT, undefined)
|
editor.dispatchCommand(CLEAR_HIDE_MENU_TIMEOUT, undefined)
|
||||||
const workflowVariableBlockNode = $createWorkflowVariableBlockNode(variables, workflowNodesMap)
|
const workflowVariableBlockNode = $createWorkflowVariableBlockNode(variables, workflowNodesMap, getVarType)
|
||||||
|
|
||||||
$insertNodes([workflowVariableBlockNode])
|
$insertNodes([workflowVariableBlockNode])
|
||||||
if (onInsert)
|
if (onInsert)
|
||||||
@ -69,7 +71,7 @@ const WorkflowVariableBlock = memo(({
|
|||||||
COMMAND_PRIORITY_EDITOR,
|
COMMAND_PRIORITY_EDITOR,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}, [editor, onInsert, onDelete, workflowNodesMap])
|
}, [editor, onInsert, onDelete, workflowNodesMap, getVarType])
|
||||||
|
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
@ -7,29 +7,32 @@ export type WorkflowNodesMap = WorkflowVariableBlockType['workflowNodesMap']
|
|||||||
export type SerializedNode = SerializedLexicalNode & {
|
export type SerializedNode = SerializedLexicalNode & {
|
||||||
variables: string[]
|
variables: string[]
|
||||||
workflowNodesMap: WorkflowNodesMap
|
workflowNodesMap: WorkflowNodesMap
|
||||||
|
getVarType: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WorkflowVariableBlockNode extends DecoratorNode<JSX.Element> {
|
export class WorkflowVariableBlockNode extends DecoratorNode<JSX.Element> {
|
||||||
__variables: string[]
|
__variables: string[]
|
||||||
__workflowNodesMap: WorkflowNodesMap
|
__workflowNodesMap: WorkflowNodesMap
|
||||||
|
__getVarType: any
|
||||||
|
|
||||||
static getType(): string {
|
static getType(): string {
|
||||||
return 'workflow-variable-block'
|
return 'workflow-variable-block'
|
||||||
}
|
}
|
||||||
|
|
||||||
static clone(node: WorkflowVariableBlockNode): WorkflowVariableBlockNode {
|
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 {
|
isInline(): boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(variables: string[], workflowNodesMap: WorkflowNodesMap, key?: NodeKey) {
|
constructor(variables: string[], workflowNodesMap: WorkflowNodesMap, getVarType: any, key?: NodeKey) {
|
||||||
super(key)
|
super(key)
|
||||||
|
|
||||||
this.__variables = variables
|
this.__variables = variables
|
||||||
this.__workflowNodesMap = workflowNodesMap
|
this.__workflowNodesMap = workflowNodesMap
|
||||||
|
this.__getVarType = getVarType
|
||||||
}
|
}
|
||||||
|
|
||||||
createDOM(): HTMLElement {
|
createDOM(): HTMLElement {
|
||||||
@ -48,12 +51,13 @@ export class WorkflowVariableBlockNode extends DecoratorNode<JSX.Element> {
|
|||||||
nodeKey={this.getKey()}
|
nodeKey={this.getKey()}
|
||||||
variables={this.__variables}
|
variables={this.__variables}
|
||||||
workflowNodesMap={this.__workflowNodesMap}
|
workflowNodesMap={this.__workflowNodesMap}
|
||||||
|
getVarType={this.__getVarType!}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
static importJSON(serializedNode: SerializedNode): WorkflowVariableBlockNode {
|
static importJSON(serializedNode: SerializedNode): WorkflowVariableBlockNode {
|
||||||
const node = $createWorkflowVariableBlockNode(serializedNode.variables, serializedNode.workflowNodesMap)
|
const node = $createWorkflowVariableBlockNode(serializedNode.variables, serializedNode.workflowNodesMap, serializedNode.getVarType)
|
||||||
|
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
@ -77,12 +81,17 @@ export class WorkflowVariableBlockNode extends DecoratorNode<JSX.Element> {
|
|||||||
return self.__workflowNodesMap
|
return self.__workflowNodesMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getVarType(): any {
|
||||||
|
const self = this.getLatest()
|
||||||
|
return self.__getVarType
|
||||||
|
}
|
||||||
|
|
||||||
getTextContent(): string {
|
getTextContent(): string {
|
||||||
return `{{#${this.getVariables().join('.')}#}}`
|
return `{{#${this.getVariables().join('.')}#}}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export function $createWorkflowVariableBlockNode(variables: string[], workflowNodesMap: WorkflowNodesMap): WorkflowVariableBlockNode {
|
export function $createWorkflowVariableBlockNode(variables: string[], workflowNodesMap: WorkflowNodesMap, getVarType: any): WorkflowVariableBlockNode {
|
||||||
return new WorkflowVariableBlockNode(variables, workflowNodesMap)
|
return new WorkflowVariableBlockNode(variables, workflowNodesMap, getVarType)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $isWorkflowVariableBlockNode(
|
export function $isWorkflowVariableBlockNode(
|
||||||
|
@ -16,6 +16,7 @@ import { VAR_REGEX as REGEX, resetReg } from '@/config'
|
|||||||
|
|
||||||
const WorkflowVariableBlockReplacementBlock = ({
|
const WorkflowVariableBlockReplacementBlock = ({
|
||||||
workflowNodesMap,
|
workflowNodesMap,
|
||||||
|
getVarType,
|
||||||
onInsert,
|
onInsert,
|
||||||
}: WorkflowVariableBlockType) => {
|
}: WorkflowVariableBlockType) => {
|
||||||
const [editor] = useLexicalComposerContext()
|
const [editor] = useLexicalComposerContext()
|
||||||
@ -30,8 +31,8 @@ const WorkflowVariableBlockReplacementBlock = ({
|
|||||||
onInsert()
|
onInsert()
|
||||||
|
|
||||||
const nodePathString = textNode.getTextContent().slice(3, -3)
|
const nodePathString = textNode.getTextContent().slice(3, -3)
|
||||||
return $applyNodeReplacement($createWorkflowVariableBlockNode(nodePathString.split('.'), workflowNodesMap))
|
return $applyNodeReplacement($createWorkflowVariableBlockNode(nodePathString.split('.'), workflowNodesMap, getVarType))
|
||||||
}, [onInsert, workflowNodesMap])
|
}, [onInsert, workflowNodesMap, getVarType])
|
||||||
|
|
||||||
const getMatch = useCallback((text: string) => {
|
const getMatch = useCallback((text: string) => {
|
||||||
const matchArr = REGEX.exec(text)
|
const matchArr = REGEX.exec(text)
|
||||||
|
@ -3,6 +3,7 @@ import type { RoleName } from './plugins/history-block'
|
|||||||
import type {
|
import type {
|
||||||
Node,
|
Node,
|
||||||
NodeOutPutVar,
|
NodeOutPutVar,
|
||||||
|
ValueSelector,
|
||||||
} from '@/app/components/workflow/types'
|
} from '@/app/components/workflow/types'
|
||||||
|
|
||||||
export type Option = {
|
export type Option = {
|
||||||
@ -60,6 +61,10 @@ export type WorkflowVariableBlockType = {
|
|||||||
workflowNodesMap?: Record<string, Pick<Node['data'], 'title' | 'type'>>
|
workflowNodesMap?: Record<string, Pick<Node['data'], 'title' | 'type'>>
|
||||||
onInsert?: () => void
|
onInsert?: () => void
|
||||||
onDelete?: () => void
|
onDelete?: () => void
|
||||||
|
getVarType?: (payload: {
|
||||||
|
nodeId: string,
|
||||||
|
valueSelector: ValueSelector,
|
||||||
|
}) => string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MenuTextMatch = {
|
export type MenuTextMatch = {
|
||||||
|
@ -10,6 +10,8 @@ import Slider from '@/app/components/base/slider'
|
|||||||
import Radio from '@/app/components/base/radio'
|
import Radio from '@/app/components/base/radio'
|
||||||
import { SimpleSelect } from '@/app/components/base/select'
|
import { SimpleSelect } from '@/app/components/base/select'
|
||||||
import TagInput from '@/app/components/base/tag-input'
|
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
|
export type ParameterValue = number | string | string[] | boolean | undefined
|
||||||
|
|
||||||
@ -27,6 +29,7 @@ const ParameterItem: FC<ParameterItemProps> = ({
|
|||||||
onSwitch,
|
onSwitch,
|
||||||
isInWorkflow,
|
isInWorkflow,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const [localValue, setLocalValue] = useState(value)
|
const [localValue, setLocalValue] = useState(value)
|
||||||
const numberInputRef = useRef<HTMLInputElement>(null)
|
const numberInputRef = useRef<HTMLInputElement>(null)
|
||||||
@ -278,6 +281,19 @@ const ParameterItem: FC<ParameterItemProps> = ({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
{/* TODO: wait api return and product design */}
|
||||||
|
{parameterRule.name === 'json_schema' && (
|
||||||
|
<Tooltip
|
||||||
|
popupContent={(
|
||||||
|
<div className='w-[232px]'>
|
||||||
|
<div className='mb-1 body-xs-regular text-text-secondary'>{t('app.structOutput.legacyTip')}</div>
|
||||||
|
<a className='' target='_blank' href='https://todo'>{t('app.structOutput.learnMore')}</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Badge uppercase className='text-text-accent-secondary'>{t('app.structOutput.legacy')}</Badge>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
parameterRule.type === 'tag' && (
|
parameterRule.type === 'tag' && (
|
||||||
|
@ -8,6 +8,8 @@ import type {
|
|||||||
ValueSelector,
|
ValueSelector,
|
||||||
Var,
|
Var,
|
||||||
} from '@/app/components/workflow/types'
|
} from '@/app/components/workflow/types'
|
||||||
|
import { useIsChatMode, useWorkflow } from './use-workflow'
|
||||||
|
import { useStoreApi } from 'reactflow'
|
||||||
|
|
||||||
export const useWorkflowVariables = () => {
|
export const useWorkflowVariables = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -72,3 +74,40 @@ export const useWorkflowVariables = () => {
|
|||||||
getCurrentVariableType,
|
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
|
||||||
|
}
|
||||||
|
@ -4,10 +4,12 @@ import Collapse from '.'
|
|||||||
type FieldCollapseProps = {
|
type FieldCollapseProps = {
|
||||||
title: string
|
title: string
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
|
operations?: ReactNode
|
||||||
}
|
}
|
||||||
const FieldCollapse = ({
|
const FieldCollapse = ({
|
||||||
title,
|
title,
|
||||||
children,
|
children,
|
||||||
|
operations,
|
||||||
}: FieldCollapseProps) => {
|
}: FieldCollapseProps) => {
|
||||||
return (
|
return (
|
||||||
<div className='py-4'>
|
<div className='py-4'>
|
||||||
@ -15,6 +17,7 @@ const FieldCollapse = ({
|
|||||||
trigger={
|
trigger={
|
||||||
<div className='flex items-center h-6 system-sm-semibold-uppercase text-text-secondary cursor-pointer'>{title}</div>
|
<div className='flex items-center h-6 system-sm-semibold-uppercase text-text-secondary cursor-pointer'>{title}</div>
|
||||||
}
|
}
|
||||||
|
operations={operations}
|
||||||
>
|
>
|
||||||
<div className='px-4'>
|
<div className='px-4'>
|
||||||
{children}
|
{children}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import type { ReactNode } from 'react'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { RiArrowDropRightLine } from '@remixicon/react'
|
import { RiArrowDropRightLine } from '@remixicon/react'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
@ -10,6 +11,8 @@ type CollapseProps = {
|
|||||||
children: JSX.Element
|
children: JSX.Element
|
||||||
collapsed?: boolean
|
collapsed?: boolean
|
||||||
onCollapse?: (collapsed: boolean) => void
|
onCollapse?: (collapsed: boolean) => void
|
||||||
|
operations?: ReactNode
|
||||||
|
|
||||||
}
|
}
|
||||||
const Collapse = ({
|
const Collapse = ({
|
||||||
disabled,
|
disabled,
|
||||||
@ -17,34 +20,38 @@ const Collapse = ({
|
|||||||
children,
|
children,
|
||||||
collapsed,
|
collapsed,
|
||||||
onCollapse,
|
onCollapse,
|
||||||
|
operations,
|
||||||
}: CollapseProps) => {
|
}: CollapseProps) => {
|
||||||
const [collapsedLocal, setCollapsedLocal] = useState(true)
|
const [collapsedLocal, setCollapsedLocal] = useState(true)
|
||||||
const collapsedMerged = collapsed !== undefined ? collapsed : collapsedLocal
|
const collapsedMerged = collapsed !== undefined ? collapsed : collapsedLocal
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div className='flex justify-between items-center'>
|
||||||
className='flex items-center'
|
<div
|
||||||
onClick={() => {
|
className='flex items-center'
|
||||||
if (!disabled) {
|
onClick={() => {
|
||||||
setCollapsedLocal(!collapsedMerged)
|
if (!disabled) {
|
||||||
onCollapse?.(!collapsedMerged)
|
setCollapsedLocal(!collapsedMerged)
|
||||||
}
|
onCollapse?.(!collapsedMerged)
|
||||||
}}
|
}
|
||||||
>
|
}}
|
||||||
<div className='shrink-0 w-4 h-4'>
|
>
|
||||||
{
|
<div className='shrink-0 w-4 h-4'>
|
||||||
!disabled && (
|
{
|
||||||
<RiArrowDropRightLine
|
!disabled && (
|
||||||
className={cn(
|
<RiArrowDropRightLine
|
||||||
'w-4 h-4 text-text-tertiary',
|
className={cn(
|
||||||
!collapsedMerged && 'transform rotate-90',
|
'w-4 h-4 text-text-tertiary',
|
||||||
)}
|
!collapsedMerged && 'transform rotate-90',
|
||||||
/>
|
)}
|
||||||
)
|
/>
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{trigger}
|
||||||
</div>
|
</div>
|
||||||
{trigger}
|
{operations}
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
!collapsedMerged && children
|
!collapsedMerged && children
|
||||||
|
@ -8,15 +8,20 @@ type Props = {
|
|||||||
className?: string
|
className?: string
|
||||||
title?: string
|
title?: string
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
|
operations?: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
const OutputVars: FC<Props> = ({
|
const OutputVars: FC<Props> = ({
|
||||||
title,
|
title,
|
||||||
children,
|
children,
|
||||||
|
operations,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
return (
|
return (
|
||||||
<FieldCollapse title={title || t('workflow.nodes.common.outputVars')}>
|
<FieldCollapse
|
||||||
|
title={title || t('workflow.nodes.common.outputVars')}
|
||||||
|
operations={operations}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</FieldCollapse>
|
</FieldCollapse>
|
||||||
)
|
)
|
||||||
@ -40,9 +45,11 @@ export const VarItem: FC<VarItemProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className='py-1'>
|
<div className='py-1'>
|
||||||
<div className='flex leading-[18px] items-center'>
|
<div className='flex justify-between'>
|
||||||
<div className='code-sm-semibold text-text-secondary'>{name}</div>
|
<div className='flex leading-[18px] items-center'>
|
||||||
<div className='ml-2 system-xs-regular text-text-tertiary'>{type}</div>
|
<div className='code-sm-semibold text-text-secondary'>{name}</div>
|
||||||
|
<div className='ml-2 system-xs-regular text-text-tertiary'>{type}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-0.5 system-xs-regular text-text-tertiary'>
|
<div className='mt-0.5 system-xs-regular text-text-tertiary'>
|
||||||
{description}
|
{description}
|
||||||
|
@ -36,6 +36,7 @@ import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/
|
|||||||
import Switch from '@/app/components/base/switch'
|
import Switch from '@/app/components/base/switch'
|
||||||
import { Jinja } from '@/app/components/base/icons/src/vender/workflow'
|
import { Jinja } from '@/app/components/base/icons/src/vender/workflow'
|
||||||
import { useStore } from '@/app/components/workflow/store'
|
import { useStore } from '@/app/components/workflow/store'
|
||||||
|
import { useWorkflowVariableType } from '@/app/components/workflow/hooks'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
className?: string
|
className?: string
|
||||||
@ -143,6 +144,8 @@ const Editor: FC<Props> = ({
|
|||||||
eventEmitter?.emit({ type: PROMPT_EDITOR_INSERT_QUICKLY, instanceId } as any)
|
eventEmitter?.emit({ type: PROMPT_EDITOR_INSERT_QUICKLY, instanceId } as any)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getVarType = useWorkflowVariableType()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrap className={cn(className, wrapClassName)} style={wrapStyle} isInNode isExpand={isExpand}>
|
<Wrap className={cn(className, wrapClassName)} style={wrapStyle} isInNode isExpand={isExpand}>
|
||||||
<div ref={ref} className={cn(isFocus ? (gradientBorder && s.gradientBorder) : 'bg-gray-100', isExpand && 'h-full', '!rounded-[9px] p-0.5', containerClassName)}>
|
<div ref={ref} className={cn(isFocus ? (gradientBorder && s.gradientBorder) : 'bg-gray-100', isExpand && 'h-full', '!rounded-[9px] p-0.5', containerClassName)}>
|
||||||
@ -249,6 +252,7 @@ const Editor: FC<Props> = ({
|
|||||||
workflowVariableBlock={{
|
workflowVariableBlock={{
|
||||||
show: true,
|
show: true,
|
||||||
variables: nodesOutputVars || [],
|
variables: nodesOutputVars || [],
|
||||||
|
getVarType,
|
||||||
workflowNodesMap: availableNodes.reduce((acc, node) => {
|
workflowNodesMap: availableNodes.reduce((acc, node) => {
|
||||||
acc[node.id] = {
|
acc[node.id] = {
|
||||||
title: node.data.title,
|
title: node.data.title,
|
||||||
|
@ -665,7 +665,7 @@ export const getVarType = ({
|
|||||||
if (!targetVar)
|
if (!targetVar)
|
||||||
return VarType.string
|
return VarType.string
|
||||||
|
|
||||||
const isStructuredOutputVar = !!targetVar.children.schema?.properties
|
const isStructuredOutputVar = !!targetVar.children?.schema?.properties
|
||||||
if (isStructuredOutputVar) {
|
if (isStructuredOutputVar) {
|
||||||
let currProperties = targetVar.children.schema;
|
let currProperties = targetVar.children.schema;
|
||||||
(valueSelector as ValueSelector).slice(2).forEach((key, i) => {
|
(valueSelector as ValueSelector).slice(2).forEach((key, i) => {
|
||||||
|
@ -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<Props> = ({
|
||||||
|
className,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [showConfig, {
|
||||||
|
setTrue: showConfigModal,
|
||||||
|
setFalse: hideConfigModal,
|
||||||
|
}] = useBoolean(false)
|
||||||
|
|
||||||
|
const handleChange = useCallback((value: SchemaRoot) => {
|
||||||
|
onChange({
|
||||||
|
schema: value,
|
||||||
|
})
|
||||||
|
}, [onChange])
|
||||||
|
return (
|
||||||
|
<div className={cn(className)}>
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<div className='flex leading-[18px] items-center'>
|
||||||
|
<div className='code-sm-semibold text-text-secondary'>structured_output</div>
|
||||||
|
<div className='ml-2 system-xs-regular text-text-tertiary'>object</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
variant='secondary'
|
||||||
|
className='flex'
|
||||||
|
onClick={showConfigModal}
|
||||||
|
>
|
||||||
|
<RiEditLine className='size-3.5 mr-1' />
|
||||||
|
<div className='system-xs-medium text-components-button-secondary-text'>{t('app.structOutput.configure')}</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{value?.schema ? (
|
||||||
|
<ShowPanel
|
||||||
|
payload={value}
|
||||||
|
/>) : (
|
||||||
|
<div className='mt-1.5 flex items-center h-10 justify-center rounded-[10px] bg-background-section system-xs-regular text-text-tertiary'>{t('app.structOutput.notConfiguredTip')}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showConfig && (
|
||||||
|
<JsonSchemaConfigModal
|
||||||
|
isShow
|
||||||
|
defaultSchema={(value?.schema || {}) as any} // wait for types change
|
||||||
|
onSave={handleChange as any} // wait for types change
|
||||||
|
onClose={hideConfigModal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default React.memo(StructureOutput)
|
@ -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 ResultPanel from '@/app/components/workflow/run/result-panel'
|
||||||
import Tooltip from '@/app/components/base/tooltip'
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor'
|
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'
|
const i18nPrefix = 'workflow.nodes.llm'
|
||||||
|
|
||||||
@ -64,6 +67,9 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
|
|||||||
contexts,
|
contexts,
|
||||||
setContexts,
|
setContexts,
|
||||||
runningStatus,
|
runningStatus,
|
||||||
|
isModelSupportStructuredOutput,
|
||||||
|
handleStructureOutputEnableChange,
|
||||||
|
handleStructureOutputChange,
|
||||||
handleRun,
|
handleRun,
|
||||||
handleStop,
|
handleStop,
|
||||||
varInputs,
|
varInputs,
|
||||||
@ -272,13 +278,55 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Split />
|
<Split />
|
||||||
<OutputVars>
|
<OutputVars
|
||||||
|
operations={
|
||||||
|
<div className='mr-4 flex items-center'>
|
||||||
|
{!isModelSupportStructuredOutput && (
|
||||||
|
<Tooltip noDecoration popupContent={
|
||||||
|
<div className='w-[232px] px-4 py-3.5 rounded-xl bg-components-tooltip-bg border-[0.5px] border-components-panel-border shadow-lg backdrop-blur-[5px]'>
|
||||||
|
<div className='title-xs-semi-bold text-text-primary'>{t('app.structOutput.modelNotSupported')}</div>
|
||||||
|
<div className='mt-1 body-xs-regular text-text-secondary'>{t('app.structOutput.modelNotSupportedTip')}</div>
|
||||||
|
</div>
|
||||||
|
}>
|
||||||
|
<div>
|
||||||
|
<RiAlertFill className='mr-1 size-4 text-text-warning-secondary' />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<div className='mr-0.5 system-xs-medium-uppercase text-text-tertiary'>{t('app.structOutput.structured')}</div>
|
||||||
|
<Tooltip popupContent={
|
||||||
|
<div className='max-w-[150px]'>{t('app.structOutput.structuredTip')}</div>
|
||||||
|
}>
|
||||||
|
<div>
|
||||||
|
<RiQuestionLine className='size-3.5 text-text-quaternary' />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
<Switch
|
||||||
|
className='ml-2'
|
||||||
|
defaultValue={!!inputs.structured_output_enabled}
|
||||||
|
onChange={handleStructureOutputEnableChange}
|
||||||
|
size='md'
|
||||||
|
disabled={readOnly}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
<>
|
<>
|
||||||
<VarItem
|
<VarItem
|
||||||
name='text'
|
name='text'
|
||||||
type='string'
|
type='string'
|
||||||
description={t(`${i18nPrefix}.outputVars.output`)}
|
description={t(`${i18nPrefix}.outputVars.output`)}
|
||||||
/>
|
/>
|
||||||
|
{inputs.structured_output_enabled && (
|
||||||
|
<>
|
||||||
|
<Split className='mt-3' />
|
||||||
|
<StructureOutput
|
||||||
|
className='mt-4'
|
||||||
|
value={inputs.structured_output}
|
||||||
|
onChange={handleStructureOutputChange}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
</OutputVars>
|
</OutputVars>
|
||||||
{isShowSingleRun && (
|
{isShowSingleRun && (
|
||||||
|
@ -15,6 +15,8 @@ export type LLMNodeType = CommonNodeType & {
|
|||||||
enabled: boolean
|
enabled: boolean
|
||||||
configs?: VisionSetting
|
configs?: VisionSetting
|
||||||
}
|
}
|
||||||
|
structured_output_enabled?: boolean
|
||||||
|
structured_output?: StructuredOutput
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Type {
|
export enum Type {
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
} from '../../hooks'
|
} from '../../hooks'
|
||||||
import useAvailableVarList from '../_base/hooks/use-available-var-list'
|
import useAvailableVarList from '../_base/hooks/use-available-var-list'
|
||||||
import useConfigVision from '../../hooks/use-config-vision'
|
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 { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||||
import {
|
import {
|
||||||
ModelTypeEnum,
|
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 useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run'
|
||||||
import { RETRIEVAL_OUTPUT_STRUCT } from '@/app/components/workflow/constants'
|
import { RETRIEVAL_OUTPUT_STRUCT } from '@/app/components/workflow/constants'
|
||||||
import { checkHasContextBlock, checkHasHistoryBlock, checkHasQueryBlock } from '@/app/components/base/prompt-editor/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 useConfig = (id: string, payload: LLMNodeType) => {
|
||||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||||
@ -277,6 +279,25 @@ const useConfig = (id: string, payload: LLMNodeType) => {
|
|||||||
setInputs(newInputs)
|
setInputs(newInputs)
|
||||||
}, [inputs, setInputs])
|
}, [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) => {
|
const filterInputVar = useCallback((varPayload: Var) => {
|
||||||
return [VarType.number, VarType.string, VarType.secret, VarType.arrayString, VarType.arrayNumber, VarType.file, VarType.arrayFile].includes(varPayload.type)
|
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,
|
setContexts,
|
||||||
varInputs,
|
varInputs,
|
||||||
runningStatus,
|
runningStatus,
|
||||||
|
isModelSupportStructuredOutput,
|
||||||
|
handleStructureOutputChange,
|
||||||
|
handleStructureOutputEnableChange,
|
||||||
handleRun,
|
handleRun,
|
||||||
handleStop,
|
handleStop,
|
||||||
runResult,
|
runResult,
|
||||||
|
@ -184,6 +184,15 @@ const translation = {
|
|||||||
moreFillTip: 'Showing max 10 levels of nesting',
|
moreFillTip: 'Showing max 10 levels of nesting',
|
||||||
required: 'Required',
|
required: 'Required',
|
||||||
LLMResponse: 'LLM Response',
|
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',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,6 +185,15 @@ const translation = {
|
|||||||
moreFillTip: '最多显示 10 级嵌套',
|
moreFillTip: '最多显示 10 级嵌套',
|
||||||
required: '必填',
|
required: '必填',
|
||||||
LLMResponse: 'LLM 的响应',
|
LLMResponse: 'LLM 的响应',
|
||||||
|
configure: '配置',
|
||||||
|
notConfiguredTip: '结构化输出尚未配置',
|
||||||
|
structured: '结构化',
|
||||||
|
structuredTip: '结构化输出是一项功能,可确保模型始终生成符合您提供的 JSON 模式的响应',
|
||||||
|
modelNotSupported: '模型不支持',
|
||||||
|
modelNotSupportedTip: '当前模型不支持此功能,将自动降级为提示注入。',
|
||||||
|
legacy: '遗留',
|
||||||
|
legacyTip: '此功能将在未来版本中删除',
|
||||||
|
learnMore: '了解更多',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user