mirror of
https://github.com/langgenius/dify.git
synced 2024-11-16 11:42:29 +08:00
The list action node adds methods to extract specific list objects (#10421)
Co-authored-by: luowei <glpat-EjySCyNjWiLqAED-YmwM> Co-authored-by: crazywoola <427733928@qq.com> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
This commit is contained in:
parent
55edd5047e
commit
fbee41f8c7
|
@ -49,8 +49,14 @@ class Limit(BaseModel):
|
||||||
size: int = -1
|
size: int = -1
|
||||||
|
|
||||||
|
|
||||||
|
class ExtractConfig(BaseModel):
|
||||||
|
enabled: bool = False
|
||||||
|
serial: str = "1"
|
||||||
|
|
||||||
|
|
||||||
class ListOperatorNodeData(BaseNodeData):
|
class ListOperatorNodeData(BaseNodeData):
|
||||||
variable: Sequence[str] = Field(default_factory=list)
|
variable: Sequence[str] = Field(default_factory=list)
|
||||||
filter_by: FilterBy
|
filter_by: FilterBy
|
||||||
order_by: OrderBy
|
order_by: OrderBy
|
||||||
limit: Limit
|
limit: Limit
|
||||||
|
extract_by: ExtractConfig
|
||||||
|
|
|
@ -58,6 +58,10 @@ class ListOperatorNode(BaseNode[ListOperatorNodeData]):
|
||||||
if self.node_data.filter_by.enabled:
|
if self.node_data.filter_by.enabled:
|
||||||
variable = self._apply_filter(variable)
|
variable = self._apply_filter(variable)
|
||||||
|
|
||||||
|
# Extract
|
||||||
|
if self.node_data.extract_by.enabled:
|
||||||
|
variable = self._extract_slice(variable)
|
||||||
|
|
||||||
# Order
|
# Order
|
||||||
if self.node_data.order_by.enabled:
|
if self.node_data.order_by.enabled:
|
||||||
variable = self._apply_order(variable)
|
variable = self._apply_order(variable)
|
||||||
|
@ -140,6 +144,16 @@ class ListOperatorNode(BaseNode[ListOperatorNodeData]):
|
||||||
result = variable.value[: self.node_data.limit.size]
|
result = variable.value[: self.node_data.limit.size]
|
||||||
return variable.model_copy(update={"value": result})
|
return variable.model_copy(update={"value": result})
|
||||||
|
|
||||||
|
def _extract_slice(
|
||||||
|
self, variable: Union[ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment]
|
||||||
|
) -> Union[ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment]:
|
||||||
|
value = int(self.graph_runtime_state.variable_pool.convert_template(self.node_data.extract_by.serial).text) - 1
|
||||||
|
if len(variable.value) > int(value):
|
||||||
|
result = variable.value[value]
|
||||||
|
else:
|
||||||
|
result = ""
|
||||||
|
return variable.model_copy(update={"value": [result]})
|
||||||
|
|
||||||
|
|
||||||
def _get_file_extract_number_func(*, key: str) -> Callable[[File], int]:
|
def _get_file_extract_number_func(*, key: str) -> Callable[[File], int]:
|
||||||
match key:
|
match key:
|
||||||
|
|
|
@ -4,7 +4,14 @@ import pytest
|
||||||
|
|
||||||
from core.file import File, FileTransferMethod, FileType
|
from core.file import File, FileTransferMethod, FileType
|
||||||
from core.variables import ArrayFileSegment
|
from core.variables import ArrayFileSegment
|
||||||
from core.workflow.nodes.list_operator.entities import FilterBy, FilterCondition, Limit, ListOperatorNodeData, OrderBy
|
from core.workflow.nodes.list_operator.entities import (
|
||||||
|
ExtractConfig,
|
||||||
|
FilterBy,
|
||||||
|
FilterCondition,
|
||||||
|
Limit,
|
||||||
|
ListOperatorNodeData,
|
||||||
|
OrderBy,
|
||||||
|
)
|
||||||
from core.workflow.nodes.list_operator.exc import InvalidKeyError
|
from core.workflow.nodes.list_operator.exc import InvalidKeyError
|
||||||
from core.workflow.nodes.list_operator.node import ListOperatorNode, _get_file_extract_string_func
|
from core.workflow.nodes.list_operator.node import ListOperatorNode, _get_file_extract_string_func
|
||||||
from models.workflow import WorkflowNodeExecutionStatus
|
from models.workflow import WorkflowNodeExecutionStatus
|
||||||
|
@ -22,6 +29,7 @@ def list_operator_node():
|
||||||
),
|
),
|
||||||
"order_by": OrderBy(enabled=False, value="asc"),
|
"order_by": OrderBy(enabled=False, value="asc"),
|
||||||
"limit": Limit(enabled=False, size=0),
|
"limit": Limit(enabled=False, size=0),
|
||||||
|
"extract_by": ExtractConfig(enabled=False, serial="1"),
|
||||||
"title": "Test Title",
|
"title": "Test Title",
|
||||||
}
|
}
|
||||||
node_data = ListOperatorNodeData(**config)
|
node_data = ListOperatorNodeData(**config)
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { VarType } from '../../../types'
|
||||||
|
import type { Var } from '../../../types'
|
||||||
|
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
nodeId: string
|
||||||
|
readOnly: boolean
|
||||||
|
value: string
|
||||||
|
onChange: (value: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExtractInput: FC<Props> = ({
|
||||||
|
nodeId,
|
||||||
|
readOnly,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const [isFocus, setIsFocus] = useState(false)
|
||||||
|
const { availableVars, availableNodesWithParent } = useAvailableVarList(nodeId, {
|
||||||
|
onlyLeafNodeVar: false,
|
||||||
|
filterVar: (varPayload: Var) => {
|
||||||
|
return [VarType.number].includes(varPayload.type)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex items-start space-x-1'>
|
||||||
|
<Input
|
||||||
|
instanceId='http-extract-number'
|
||||||
|
className={cn(isFocus ? 'shadow-xs bg-gray-50 border-gray-300' : 'bg-gray-100 border-gray-100', 'w-0 grow rounded-lg px-3 py-[6px] border')}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
readOnly={readOnly}
|
||||||
|
nodesOutputVars={availableVars}
|
||||||
|
availableNodes={availableNodesWithParent}
|
||||||
|
onFocusChange={setIsFocus}
|
||||||
|
placeholder={!readOnly ? t('workflow.nodes.http.extractListPlaceholder')! : ''}
|
||||||
|
placeholderClassName='!leading-[21px]'
|
||||||
|
/>
|
||||||
|
</div >
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default React.memo(ExtractInput)
|
|
@ -12,6 +12,10 @@ const nodeDefault: NodeDefault<ListFilterNodeType> = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
conditions: [],
|
conditions: [],
|
||||||
},
|
},
|
||||||
|
extract_by: {
|
||||||
|
enabled: false,
|
||||||
|
serial: '1',
|
||||||
|
},
|
||||||
order_by: {
|
order_by: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
key: '',
|
key: '',
|
||||||
|
|
|
@ -13,6 +13,7 @@ import FilterCondition from './components/filter-condition'
|
||||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||||
import { type NodePanelProps } from '@/app/components/workflow/types'
|
import { type NodePanelProps } from '@/app/components/workflow/types'
|
||||||
import Switch from '@/app/components/base/switch'
|
import Switch from '@/app/components/base/switch'
|
||||||
|
import ExtractInput from '@/app/components/workflow/nodes/list-operator/components/extract-input'
|
||||||
|
|
||||||
const i18nPrefix = 'workflow.nodes.listFilter'
|
const i18nPrefix = 'workflow.nodes.listFilter'
|
||||||
|
|
||||||
|
@ -32,6 +33,8 @@ const Panel: FC<NodePanelProps<ListFilterNodeType>> = ({
|
||||||
filterVar,
|
filterVar,
|
||||||
handleFilterEnabledChange,
|
handleFilterEnabledChange,
|
||||||
handleFilterChange,
|
handleFilterChange,
|
||||||
|
handleExtractsEnabledChange,
|
||||||
|
handleExtractsChange,
|
||||||
handleLimitChange,
|
handleLimitChange,
|
||||||
handleOrderByEnabledChange,
|
handleOrderByEnabledChange,
|
||||||
handleOrderByKeyChange,
|
handleOrderByKeyChange,
|
||||||
|
@ -79,6 +82,41 @@ const Panel: FC<NodePanelProps<ListFilterNodeType>> = ({
|
||||||
: null}
|
: null}
|
||||||
</Field>
|
</Field>
|
||||||
<Split />
|
<Split />
|
||||||
|
<Field
|
||||||
|
title={t(`${i18nPrefix}.extractsCondition`)}
|
||||||
|
operations={
|
||||||
|
<Switch
|
||||||
|
defaultValue={inputs.extract_by?.enabled}
|
||||||
|
onChange={handleExtractsEnabledChange}
|
||||||
|
size='md'
|
||||||
|
disabled={readOnly}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{inputs.extract_by?.enabled
|
||||||
|
? (
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
{hasSubVariable && (
|
||||||
|
<div className='grow mr-2'>
|
||||||
|
<ExtractInput
|
||||||
|
value={inputs.extract_by.serial as string}
|
||||||
|
onChange={handleExtractsChange}
|
||||||
|
readOnly={readOnly}
|
||||||
|
nodeId={id}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
</Field>
|
||||||
|
<Split />
|
||||||
|
<LimitConfig
|
||||||
|
config={inputs.limit}
|
||||||
|
onChange={handleLimitChange}
|
||||||
|
readonly={readOnly}
|
||||||
|
/>
|
||||||
|
<Split />
|
||||||
<Field
|
<Field
|
||||||
title={t(`${i18nPrefix}.orderBy`)}
|
title={t(`${i18nPrefix}.orderBy`)}
|
||||||
operations={
|
operations={
|
||||||
|
@ -118,13 +156,7 @@ const Panel: FC<NodePanelProps<ListFilterNodeType>> = ({
|
||||||
: null}
|
: null}
|
||||||
</Field>
|
</Field>
|
||||||
<Split />
|
<Split />
|
||||||
<LimitConfig
|
|
||||||
config={inputs.limit}
|
|
||||||
onChange={handleLimitChange}
|
|
||||||
readonly={readOnly}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<Split />
|
|
||||||
<div className='px-4 pt-4 pb-2'>
|
<div className='px-4 pt-4 pb-2'>
|
||||||
<OutputVars>
|
<OutputVars>
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -25,6 +25,10 @@ export type ListFilterNodeType = CommonNodeType & {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
conditions: Condition[]
|
conditions: Condition[]
|
||||||
}
|
}
|
||||||
|
extract_by: {
|
||||||
|
enabled: boolean
|
||||||
|
serial?: string
|
||||||
|
}
|
||||||
order_by: {
|
order_by: {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
key: ValueSelector | string
|
key: ValueSelector | string
|
||||||
|
|
|
@ -119,6 +119,22 @@ const useConfig = (id: string, payload: ListFilterNodeType) => {
|
||||||
setInputs(newInputs)
|
setInputs(newInputs)
|
||||||
}, [inputs, setInputs])
|
}, [inputs, setInputs])
|
||||||
|
|
||||||
|
const handleExtractsEnabledChange = useCallback((enabled: boolean) => {
|
||||||
|
const newInputs = produce(inputs, (draft) => {
|
||||||
|
draft.extract_by.enabled = enabled
|
||||||
|
if (enabled)
|
||||||
|
draft.extract_by.serial = '1'
|
||||||
|
})
|
||||||
|
setInputs(newInputs)
|
||||||
|
}, [inputs, setInputs])
|
||||||
|
|
||||||
|
const handleExtractsChange = useCallback((value: string) => {
|
||||||
|
const newInputs = produce(inputs, (draft) => {
|
||||||
|
draft.extract_by.serial = value
|
||||||
|
})
|
||||||
|
setInputs(newInputs)
|
||||||
|
}, [inputs, setInputs])
|
||||||
|
|
||||||
const handleOrderByEnabledChange = useCallback((enabled: boolean) => {
|
const handleOrderByEnabledChange = useCallback((enabled: boolean) => {
|
||||||
const newInputs = produce(inputs, (draft) => {
|
const newInputs = produce(inputs, (draft) => {
|
||||||
draft.order_by.enabled = enabled
|
draft.order_by.enabled = enabled
|
||||||
|
@ -162,6 +178,8 @@ const useConfig = (id: string, payload: ListFilterNodeType) => {
|
||||||
handleOrderByEnabledChange,
|
handleOrderByEnabledChange,
|
||||||
handleOrderByKeyChange,
|
handleOrderByKeyChange,
|
||||||
handleOrderByTypeChange,
|
handleOrderByTypeChange,
|
||||||
|
handleExtractsEnabledChange,
|
||||||
|
handleExtractsChange,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -369,6 +369,7 @@ const translation = {
|
||||||
inputVars: 'Input Variables',
|
inputVars: 'Input Variables',
|
||||||
api: 'API',
|
api: 'API',
|
||||||
apiPlaceholder: 'Enter URL, type ‘/’ insert variable',
|
apiPlaceholder: 'Enter URL, type ‘/’ insert variable',
|
||||||
|
extractListPlaceholder: 'Enter list item index, type ‘/’ insert variable',
|
||||||
notStartWithHttp: 'API should start with http:// or https://',
|
notStartWithHttp: 'API should start with http:// or https://',
|
||||||
key: 'Key',
|
key: 'Key',
|
||||||
type: 'Type',
|
type: 'Type',
|
||||||
|
@ -605,6 +606,7 @@ const translation = {
|
||||||
inputVar: 'Input Variable',
|
inputVar: 'Input Variable',
|
||||||
filterCondition: 'Filter Condition',
|
filterCondition: 'Filter Condition',
|
||||||
filterConditionKey: 'Filter Condition Key',
|
filterConditionKey: 'Filter Condition Key',
|
||||||
|
extractsCondition: 'Extract the N item',
|
||||||
filterConditionComparisonOperator: 'Filter Condition Comparison Operator',
|
filterConditionComparisonOperator: 'Filter Condition Comparison Operator',
|
||||||
filterConditionComparisonValue: 'Filter Condition value',
|
filterConditionComparisonValue: 'Filter Condition value',
|
||||||
selectVariableKeyPlaceholder: 'Select sub variable key',
|
selectVariableKeyPlaceholder: 'Select sub variable key',
|
||||||
|
|
|
@ -369,6 +369,7 @@ const translation = {
|
||||||
inputVars: '输入变量',
|
inputVars: '输入变量',
|
||||||
api: 'API',
|
api: 'API',
|
||||||
apiPlaceholder: '输入 URL,输入变量时请键入‘/’',
|
apiPlaceholder: '输入 URL,输入变量时请键入‘/’',
|
||||||
|
extractListPlaceholder: '输入提取列表编号,输入变量时请键入‘/’',
|
||||||
notStartWithHttp: 'API 应该以 http:// 或 https:// 开头',
|
notStartWithHttp: 'API 应该以 http:// 或 https:// 开头',
|
||||||
key: '键',
|
key: '键',
|
||||||
type: '类型',
|
type: '类型',
|
||||||
|
@ -608,6 +609,7 @@ const translation = {
|
||||||
filterConditionComparisonOperator: '过滤条件比较操作符',
|
filterConditionComparisonOperator: '过滤条件比较操作符',
|
||||||
filterConditionComparisonValue: '过滤条件比较值',
|
filterConditionComparisonValue: '过滤条件比较值',
|
||||||
selectVariableKeyPlaceholder: '选择子变量的 Key',
|
selectVariableKeyPlaceholder: '选择子变量的 Key',
|
||||||
|
extractsCondition: '取第 N 项',
|
||||||
limit: '取前 N 项',
|
limit: '取前 N 项',
|
||||||
orderBy: '排序',
|
orderBy: '排序',
|
||||||
asc: '升序',
|
asc: '升序',
|
||||||
|
|
Loading…
Reference in New Issue
Block a user