Feat: Add variable aggregator node #10427 (#11070)

### What problem does this PR solve?

Feat: Add variable aggregator node #10427

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-11-06 16:18:00 +08:00
committed by GitHub
parent 23b81eae77
commit e18c408759
12 changed files with 137 additions and 12 deletions

View File

@ -110,6 +110,7 @@ export enum Operator {
Placeholder = 'Placeholder',
DataOperations = 'DataOperations',
VariableAssigner = 'VariableAssigner',
VariableAggregator = 'VariableAggregator',
File = 'File', // pipeline
Parser = 'Parser',
Tokenizer = 'Tokenizer',

View File

@ -1550,6 +1550,10 @@ This delimiter is used to split the input text into several text pieces echo of
variableAssigner: 'Variable assigner',
variableAssignerDescription:
'This component performs operations on Data objects, including extracting, filtering, and editing keys and values in the Data.',
variableAggregator: 'Variable aggregator',
variableAggregatorDescription: `This process aggregates variables from multiple branches into a single variable to achieve unified configuration for downstream nodes.
The variable aggregation node (originally the variable assignment node) is a crucial node in the workflow. It is responsible for integrating the output results of different branches, ensuring that regardless of which branch is executed, its result can be referenced and accessed through a unified variable. This is extremely useful in multi-branch scenarios, as it maps variables with the same function across different branches to a single output variable, avoiding redundant definitions in downstream nodes.`,
inputVariables: 'Input variables',
runningHintText: 'is running...🕞',
openingSwitch: 'Opening switch',

View File

@ -1471,6 +1471,9 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
variableAssigner: '变量赋值器',
variableAssignerDescription:
'此组件对数据对象执行操作,包括提取、筛选和编辑数据中的键和值。',
variableAggregator: '变量聚合',
variableAggregatorDescription: `将多路分支的变量聚合为一个变量,以实现下游节点统一配置。
变量聚合节点(原变量赋值节点)是工作流程中的一个关键节点,它负责整合不同分支的输出结果,确保无论哪个分支被执行,其结果都能通过一个统一的变量来引用和访问。这在多分支的情况下非常有用,可将不同分支下相同作用的变量映射为一个输出变量,避免下游节点重复定义。`,
inputVariables: '输入变量',
addVariable: '新增变量',
runningHintText: '正在运行中...🕞',

View File

@ -72,6 +72,7 @@ import { SwitchNode } from './node/switch-node';
import { TemplateNode } from './node/template-node';
import TokenizerNode from './node/tokenizer-node';
import { ToolNode } from './node/tool-node';
import { VariableAggregatorNode } from './node/variable-aggregator-node';
import { VariableAssignerNode } from './node/variable-assigner-node';
export const nodeTypes: NodeTypes = {
@ -100,6 +101,7 @@ export const nodeTypes: NodeTypes = {
contextNode: ExtractorNode,
dataOperationsNode: DataOperationsNode,
variableAssignerNode: VariableAssignerNode,
variableAggregatorNode: VariableAggregatorNode,
};
const edgeTypes = {

View File

@ -80,6 +80,7 @@ export function AccordionOperators({
Operator.StringTransform,
Operator.DataOperations,
Operator.VariableAssigner,
Operator.VariableAggregator,
]}
isCustomDropdown={isCustomDropdown}
mousePosition={mousePosition}

View File

@ -0,0 +1,11 @@
import { IRagNode } from '@/interfaces/database/agent';
import { NodeProps } from '@xyflow/react';
import { RagNode } from '.';
export function VariableAggregatorNode({ ...props }: NodeProps<IRagNode>) {
return (
<RagNode {...props}>
<section>VariableAggregatorNode</section>
</RagNode>
);
}

View File

@ -22,7 +22,6 @@ export enum AgentDialogueMode {
}
import { ModelVariableType } from '@/constants/knowledge';
import i18n from '@/locales/config';
import { t } from 'i18next';
// DuckDuckGo's channel options
@ -109,14 +108,6 @@ export const initialBeginValues = {
prologue: `Hi! I'm your assistant. What can I do for you?`,
};
export const initialGenerateValues = {
...initialLlmBaseValues,
prompt: i18n.t('flow.promptText'),
cite: true,
message_history_window_size: 12,
parameters: [],
};
export const initialRewriteQuestionValues = {
...initialLlmBaseValues,
language: '',
@ -607,6 +598,8 @@ export const initialDataOperationsValues = {
export const initialVariableAssignerValues = {};
export const initialVariableAggregatorValues = {};
export const CategorizeAnchorPointPositions = [
{ top: 1, right: 34 },
{ top: 8, right: 18 },
@ -737,6 +730,7 @@ export const NodeMap = {
[Operator.Extractor]: 'contextNode',
[Operator.DataOperations]: 'dataOperationsNode',
[Operator.VariableAssigner]: 'variableAssignerNode',
[Operator.VariableAggregator]: 'variableAggregatorNode',
};
export enum BeginQueryType {

View File

@ -38,6 +38,7 @@ import TokenizerForm from '../form/tokenizer-form';
import ToolForm from '../form/tool-form';
import TuShareForm from '../form/tushare-form';
import UserFillUpForm from '../form/user-fill-up-form';
import VariableAssignerForm from '../form/variable-assigner-form';
import WenCaiForm from '../form/wencai-form';
import WikipediaForm from '../form/wikipedia-form';
import YahooFinanceForm from '../form/yahoo-finance-form';
@ -182,4 +183,7 @@ export const FormConfigMap = {
[Operator.DataOperations]: {
component: DataOperationsForm,
},
[Operator.VariableAssigner]: {
component: VariableAssignerForm,
},
};

View File

@ -0,0 +1,100 @@
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Form } from '@/components/ui/form';
import { Separator } from '@/components/ui/separator';
import { buildOptions } from '@/utils/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { memo } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import {
JsonSchemaDataType,
Operations,
initialDataOperationsValues,
} from '../../constant';
import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
import { buildOutputList } from '../../utils/build-output-list';
import { FormWrapper } from '../components/form-wrapper';
import { Output, OutputSchema } from '../components/output';
import { QueryVariableList } from '../components/query-variable-list';
export const RetrievalPartialSchema = {
query: z.array(z.object({ input: z.string().optional() })),
operations: z.string(),
select_keys: z.array(z.object({ name: z.string().optional() })).optional(),
remove_keys: z.array(z.object({ name: z.string().optional() })).optional(),
updates: z
.array(
z.object({ key: z.string().optional(), value: z.string().optional() }),
)
.optional(),
rename_keys: z
.array(
z.object({
old_key: z.string().optional(),
new_key: z.string().optional(),
}),
)
.optional(),
filter_values: z
.array(
z.object({
key: z.string().optional(),
value: z.string().optional(),
operator: z.string().optional(),
}),
)
.optional(),
...OutputSchema,
};
export const FormSchema = z.object(RetrievalPartialSchema);
export type DataOperationsFormSchemaType = z.infer<typeof FormSchema>;
const outputList = buildOutputList(initialDataOperationsValues.outputs);
function VariableAssignerForm({ node }: INextOperatorForm) {
const { t } = useTranslation();
const defaultValues = useFormValues(initialDataOperationsValues, node);
const form = useForm<DataOperationsFormSchemaType>({
defaultValues: defaultValues,
mode: 'onChange',
resolver: zodResolver(FormSchema),
shouldUnregister: true,
});
const OperationsOptions = buildOptions(
Operations,
t,
`flow.operationsOptions`,
true,
);
useWatchFormChange(node?.id, form, true);
return (
<Form {...form}>
<FormWrapper>
<QueryVariableList
tooltip={t('flow.queryTip')}
label={t('flow.query')}
types={[JsonSchemaDataType.Array, JsonSchemaDataType.Object]}
></QueryVariableList>
<Separator />
<RAGFlowFormItem name="operations" label={t('flow.operations')}>
<SelectWithSearch options={OperationsOptions} allowClear />
</RAGFlowFormItem>
<Output list={outputList} isFormRequired></Output>
</FormWrapper>
</Form>
);
}
export default memo(VariableAssignerForm);

View File

@ -48,6 +48,7 @@ import {
initialTokenizerValues,
initialTuShareValues,
initialUserFillUpValues,
initialVariableAggregatorValues,
initialVariableAssignerValues,
initialWaitingDialogueValues,
initialWenCaiValues,
@ -129,6 +130,7 @@ export const useInitializeOperatorParams = () => {
},
[Operator.DataOperations]: initialDataOperationsValues,
[Operator.VariableAssigner]: initialVariableAssignerValues,
[Operator.VariableAggregator]: initialVariableAggregatorValues,
};
}, [llmId]);

View File

@ -25,7 +25,10 @@ export function LogSheet({
}: LogSheetProps) {
return (
<Sheet open onOpenChange={hideModal} modal={false}>
<SheetContent className={cn('top-20 right-[620px]')}>
<SheetContent
className={cn('top-20 right-[620px]')}
onInteractOutside={(e) => e.preventDefault()}
>
<SheetHeader>
<SheetTitle className="flex items-center gap-1">
<NotebookText className="size-4" />

View File

@ -14,7 +14,7 @@ import { ReactComponent as YahooFinanceIcon } from '@/assets/svg/yahoo-finance.s
import { IconFont } from '@/components/icon-font';
import { cn } from '@/lib/utils';
import { Equal, FileCode, HousePlus } from 'lucide-react';
import { Equal, FileCode, HousePlus, Variable } from 'lucide-react';
import { Operator } from './constant';
interface IProps {
@ -55,10 +55,10 @@ export const SVGIconMap = {
[Operator.WenCai]: WenCaiIcon,
[Operator.Crawler]: CrawlerIcon,
};
export const LucideIconMap = {
[Operator.DataOperations]: FileCode,
[Operator.VariableAssigner]: Equal,
[Operator.VariableAggregator]: Variable,
};
const Empty = () => {