Feature: Added global variable functionality #10703 (#11117)

### What problem does this PR solve?

Feature: Added global variable functionality

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
chanx
2025-11-10 10:16:12 +08:00
committed by GitHub
parent b6cd282ccd
commit 7423a5806e
10 changed files with 402 additions and 4 deletions

View File

@ -70,6 +70,10 @@ interface DynamicFormProps<T extends FieldValues> {
className?: string; className?: string;
children?: React.ReactNode; children?: React.ReactNode;
defaultValues?: DefaultValues<T>; defaultValues?: DefaultValues<T>;
onFieldUpdate?: (
fieldName: string,
updatedField: Partial<FormFieldConfig>,
) => void;
} }
// Form ref interface // Form ref interface
@ -77,6 +81,8 @@ export interface DynamicFormRef {
submit: () => void; submit: () => void;
getValues: () => any; getValues: () => any;
reset: (values?: any) => void; reset: (values?: any) => void;
watch: (field: string, callback: (value: any) => void) => () => void;
updateFieldType: (fieldName: string, newType: FormFieldType) => void;
} }
// Generate Zod validation schema based on field configurations // Generate Zod validation schema based on field configurations
@ -277,6 +283,7 @@ const DynamicForm = {
className = '', className = '',
children, children,
defaultValues: formDefaultValues = {} as DefaultValues<T>, defaultValues: formDefaultValues = {} as DefaultValues<T>,
onFieldUpdate,
}: DynamicFormProps<T>, }: DynamicFormProps<T>,
ref: React.Ref<any>, ref: React.Ref<any>,
) => { ) => {
@ -311,6 +318,29 @@ const DynamicForm = {
setError: form.setError, setError: form.setError,
clearErrors: form.clearErrors, clearErrors: form.clearErrors,
trigger: form.trigger, trigger: form.trigger,
watch: (field: string, callback: (value: any) => void) => {
const { unsubscribe } = form.watch((values: any) => {
if (values && values[field] !== undefined) {
callback(values[field]);
}
});
return unsubscribe;
},
onFieldUpdate: (
fieldName: string,
updatedField: Partial<FormFieldConfig>,
) => {
setTimeout(() => {
if (onFieldUpdate) {
onFieldUpdate(fieldName, updatedField);
} else {
console.warn(
'onFieldUpdate prop is not provided. Cannot update field type.',
);
}
}, 0);
},
})); }));
useEffect(() => { useEffect(() => {

View File

@ -45,6 +45,7 @@ export interface DSL {
messages?: Message[]; messages?: Message[];
reference?: IReference[]; reference?: IReference[];
globals: Record<string, any>; globals: Record<string, any>;
variables: Record<string, GobalVariableType>;
retrieval: IReference[]; retrieval: IReference[];
} }
@ -283,3 +284,10 @@ export interface IPipeLineListRequest {
desc?: boolean; desc?: boolean;
canvas_category?: AgentCategory; canvas_category?: AgentCategory;
} }
export interface GobalVariableType {
name: string;
value: any;
description: string;
type: string;
}

View File

@ -981,6 +981,11 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
pleaseUploadAtLeastOneFile: 'Please upload at least one file', pleaseUploadAtLeastOneFile: 'Please upload at least one file',
}, },
flow: { flow: {
variableNameMessage:
'Variable name can only contain letters and underscores',
variableDescription: 'Variable Description',
defaultValue: 'Default Value',
gobalVariable: 'Global Variable',
recommended: 'Recommended', recommended: 'Recommended',
customerSupport: 'Customer Support', customerSupport: 'Customer Support',
marketing: 'Marketing', marketing: 'Marketing',

View File

@ -930,6 +930,10 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
pleaseUploadAtLeastOneFile: '请上传至少一个文件', pleaseUploadAtLeastOneFile: '请上传至少一个文件',
}, },
flow: { flow: {
variableNameMessage: '名称只能包含字母和下划线',
variableDescription: '变量的描述',
defaultValue: '默认值',
gobalVariable: '全局变量',
recommended: '推荐', recommended: '推荐',
customerSupport: '客户支持', customerSupport: '客户支持',
marketing: '营销', marketing: '营销',

View File

@ -0,0 +1,73 @@
import { FormFieldConfig, FormFieldType } from '@/components/dynamic-form';
import { buildSelectOptions } from '@/utils/component-util';
import { t } from 'i18next';
// const TypesWithoutArray = Object.values(JsonSchemaDataType).filter(
// (item) => item !== JsonSchemaDataType.Array,
// );
// const TypesWithArray = [
// ...TypesWithoutArray,
// ...TypesWithoutArray.map((item) => `array<${item}>`),
// ];
export enum TypesWithArray {
String = 'string',
Number = 'number',
Boolean = 'boolean',
// Object = 'object',
// ArrayString = 'array<string>',
// ArrayNumber = 'array<number>',
// ArrayBoolean = 'array<boolean>',
// ArrayObject = 'array<object>',
}
export const GobalFormFields = [
{
label: t('flow.name'),
name: 'name',
placeholder: t('common.namePlaceholder'),
required: true,
validation: {
pattern: /^[a-zA-Z_]+$/,
message: t('flow.variableNameMessage'),
},
type: FormFieldType.Text,
},
{
label: t('flow.type'),
name: 'type',
placeholder: '',
required: true,
type: FormFieldType.Select,
options: buildSelectOptions(Object.values(TypesWithArray)),
},
{
label: t('flow.defaultValue'),
name: 'value',
placeholder: '',
type: FormFieldType.Textarea,
},
{
label: t('flow.description'),
name: 'description',
placeholder: t('flow.variableDescription'),
type: 'textarea',
},
] as FormFieldConfig[];
export const GobalVariableFormDefaultValues = {
name: '',
type: TypesWithArray.String,
value: '',
description: '',
};
export const TypeMaps = {
[TypesWithArray.String]: FormFieldType.Textarea,
[TypesWithArray.Number]: FormFieldType.Number,
[TypesWithArray.Boolean]: FormFieldType.Checkbox,
// [TypesWithArray.Object]: FormFieldType.Textarea,
// [TypesWithArray.ArrayString]: FormFieldType.Textarea,
// [TypesWithArray.ArrayNumber]: FormFieldType.Textarea,
// [TypesWithArray.ArrayBoolean]: FormFieldType.Textarea,
// [TypesWithArray.ArrayObject]: FormFieldType.Textarea,
};

View File

@ -0,0 +1,221 @@
import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
import {
DynamicForm,
DynamicFormRef,
FormFieldConfig,
} from '@/components/dynamic-form';
import { Button } from '@/components/ui/button';
import { Modal } from '@/components/ui/modal/modal';
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet';
import { useSetModalState } from '@/hooks/common-hooks';
import { useFetchAgent } from '@/hooks/use-agent-request';
import { GobalVariableType } from '@/interfaces/database/agent';
import { cn } from '@/lib/utils';
import { t } from 'i18next';
import { Trash2 } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { FieldValues } from 'react-hook-form';
import { useSaveGraph } from '../hooks/use-save-graph';
import {
GobalFormFields,
GobalVariableFormDefaultValues,
TypeMaps,
TypesWithArray,
} from './contant';
export type IGobalParamModalProps = {
data: any;
hideModal: (open: boolean) => void;
};
export const GobalParamSheet = (props: IGobalParamModalProps) => {
const { hideModal } = props;
const { data, refetch } = useFetchAgent();
const [fields, setFields] = useState<FormFieldConfig[]>(GobalFormFields);
const { visible, showModal, hideModal: hideAddModal } = useSetModalState();
const [defaultValues, setDefaultValues] = useState<FieldValues>(
GobalVariableFormDefaultValues,
);
const formRef = useRef<DynamicFormRef>(null);
const handleFieldUpdate = (
fieldName: string,
updatedField: Partial<FormFieldConfig>,
) => {
setFields((prevFields) =>
prevFields.map((field) =>
field.name === fieldName ? { ...field, ...updatedField } : field,
),
);
};
useEffect(() => {
const typefileld = fields.find((item) => item.name === 'type');
if (typefileld) {
typefileld.onChange = (value) => {
// setWatchType(value);
handleFieldUpdate('value', {
type: TypeMaps[value as keyof typeof TypeMaps],
});
const values = formRef.current?.getValues();
setTimeout(() => {
switch (value) {
case TypesWithArray.Boolean:
setDefaultValues({ ...values, value: false });
break;
case TypesWithArray.Number:
setDefaultValues({ ...values, value: 0 });
break;
default:
setDefaultValues({ ...values, value: '' });
}
}, 0);
};
}
}, [fields]);
const { saveGraph, loading } = useSaveGraph();
const handleSubmit = (value: FieldValues) => {
const param = {
...(data.dsl?.variables || {}),
[value.name]: value,
} as Record<string, GobalVariableType>;
saveGraph(undefined, {
gobalVariables: param,
});
if (!loading) {
setTimeout(() => {
refetch();
}, 500);
}
hideAddModal();
};
const handleDeleteGobalVariable = (key: string) => {
const param = {
...(data.dsl?.variables || {}),
} as Record<string, GobalVariableType>;
delete param[key];
saveGraph(undefined, {
gobalVariables: param,
});
refetch();
};
const handleEditGobalVariable = (item: FieldValues) => {
setDefaultValues(item);
showModal();
};
return (
<>
<Sheet open onOpenChange={hideModal} modal={false}>
<SheetContent
className={cn('top-20 h-auto flex flex-col p-0 gap-0')}
onInteractOutside={(e) => e.preventDefault()}
>
<SheetHeader className="p-5">
<SheetTitle className="flex items-center gap-2.5">
{t('flow.gobalVariable')}
</SheetTitle>
</SheetHeader>
<div className="px-5 pb-5">
<Button
variant={'secondary'}
onClick={() => {
setFields(GobalFormFields);
setDefaultValues(GobalVariableFormDefaultValues);
showModal();
}}
>
{t('flow.add')}
</Button>
</div>
<div className="flex flex-col gap-2 px-5 ">
{data?.dsl?.variables &&
Object.keys(data.dsl.variables).map((key) => {
const item = data.dsl.variables[key];
return (
<div
key={key}
className="flex items-center gap-3 min-h-14 justify-between px-5 py-3 border border-border-default rounded-lg hover:bg-bg-card group"
onClick={() => {
handleEditGobalVariable(item);
}}
>
<div className="flex flex-col">
<div className="flex items-center gap-2">
<span className=" font-medium">{item.name}</span>
<span className="text-sm font-medium text-text-secondary">
{item.type}
</span>
</div>
<div>
<span className="text-text-primary">{item.value}</span>
</div>
</div>
<div>
<ConfirmDeleteDialog
onOk={() => handleDeleteGobalVariable(key)}
>
<Button
variant={'secondary'}
className="bg-transparent hidden text-text-secondary border-none group-hover:bg-bg-card group-hover:text-text-primary group-hover:border group-hover:block"
onClick={(e) => {
e.stopPropagation();
}}
>
<Trash2 className="w-4 h-4" />
</Button>
</ConfirmDeleteDialog>
</div>
</div>
);
})}
</div>
</SheetContent>
<Modal
title={t('flow.add') + t('flow.gobalVariable')}
open={visible}
onCancel={hideAddModal}
showfooter={false}
>
<DynamicForm.Root
ref={formRef}
fields={fields}
onSubmit={(data) => {
console.log(data);
}}
defaultValues={defaultValues}
onFieldUpdate={handleFieldUpdate}
>
<div className="flex items-center justify-end w-full gap-2">
<DynamicForm.CancelButton
handleCancel={() => {
hideAddModal?.();
}}
/>
<DynamicForm.SavingButton
submitLoading={loading || false}
buttonText={t('common.ok')}
submitFunc={(values: FieldValues) => {
handleSubmit(values);
// console.log(values);
// console.log(nodes, edges);
// handleOk(values);
}}
/>
</div>
</DynamicForm.Root>
</Modal>
</Sheet>
</>
);
};

View File

@ -1,16 +1,20 @@
import { useFetchAgent } from '@/hooks/use-agent-request'; import { useFetchAgent } from '@/hooks/use-agent-request';
import { GobalVariableType } from '@/interfaces/database/agent';
import { RAGFlowNodeType } from '@/interfaces/database/flow'; import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { Operator } from '../constant'; import { Operator } from '../constant';
import useGraphStore from '../store'; import useGraphStore from '../store';
import { buildDslComponentsByGraph } from '../utils'; import { buildDslComponentsByGraph, buildDslGobalVariables } from '../utils';
export const useBuildDslData = () => { export const useBuildDslData = () => {
const { data } = useFetchAgent(); const { data } = useFetchAgent();
const { nodes, edges } = useGraphStore((state) => state); const { nodes, edges } = useGraphStore((state) => state);
const buildDslData = useCallback( const buildDslData = useCallback(
(currentNodes?: RAGFlowNodeType[]) => { (
currentNodes?: RAGFlowNodeType[],
otherParam?: { gobalVariables: Record<string, GobalVariableType> },
) => {
const nodesToProcess = currentNodes ?? nodes; const nodesToProcess = currentNodes ?? nodes;
// Filter out placeholder nodes and related edges // Filter out placeholder nodes and related edges
@ -37,8 +41,13 @@ export const useBuildDslData = () => {
data.dsl.components, data.dsl.components,
); );
const gobalVariables = buildDslGobalVariables(
data.dsl,
otherParam?.gobalVariables,
);
return { return {
...data.dsl, ...data.dsl,
...gobalVariables,
graph: { nodes: filteredNodes, edges: filteredEdges }, graph: { nodes: filteredNodes, edges: filteredEdges },
components: dslComponents, components: dslComponents,
}; };

View File

@ -3,6 +3,7 @@ import {
useResetAgent, useResetAgent,
useSetAgent, useSetAgent,
} from '@/hooks/use-agent-request'; } from '@/hooks/use-agent-request';
import { GobalVariableType } from '@/interfaces/database/agent';
import { RAGFlowNodeType } from '@/interfaces/database/flow'; import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { formatDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
import { useDebounceEffect } from 'ahooks'; import { useDebounceEffect } from 'ahooks';
@ -18,11 +19,14 @@ export const useSaveGraph = (showMessage: boolean = true) => {
const { buildDslData } = useBuildDslData(); const { buildDslData } = useBuildDslData();
const saveGraph = useCallback( const saveGraph = useCallback(
async (currentNodes?: RAGFlowNodeType[]) => { async (
currentNodes?: RAGFlowNodeType[],
otherParam?: { gobalVariables: Record<string, GobalVariableType> },
) => {
return setAgent({ return setAgent({
id, id,
title: data.title, title: data.title,
dsl: buildDslData(currentNodes), dsl: buildDslData(currentNodes, otherParam),
}); });
}, },
[setAgent, data, id, buildDslData], [setAgent, data, id, buildDslData],

View File

@ -38,6 +38,7 @@ import { useParams } from 'umi';
import AgentCanvas from './canvas'; import AgentCanvas from './canvas';
import { DropdownProvider } from './canvas/context'; import { DropdownProvider } from './canvas/context';
import { Operator } from './constant'; import { Operator } from './constant';
import { GobalParamSheet } from './gobal-variable-sheet';
import { useCancelCurrentDataflow } from './hooks/use-cancel-dataflow'; import { useCancelCurrentDataflow } from './hooks/use-cancel-dataflow';
import { useHandleExportJsonFile } from './hooks/use-export-json'; import { useHandleExportJsonFile } from './hooks/use-export-json';
import { useFetchDataOnMount } from './hooks/use-fetch-data'; import { useFetchDataOnMount } from './hooks/use-fetch-data';
@ -123,6 +124,12 @@ export default function Agent() {
hideModal: hidePipelineLogSheet, hideModal: hidePipelineLogSheet,
} = useSetModalState(); } = useSetModalState();
const {
visible: gobalParamSheetVisible,
showModal: showGobalParamSheet,
hideModal: hideGobalParamSheet,
} = useSetModalState();
const { const {
isParsing, isParsing,
logs, logs,
@ -206,6 +213,13 @@ export default function Agent() {
> >
<LaptopMinimalCheck /> {t('flow.save')} <LaptopMinimalCheck /> {t('flow.save')}
</ButtonLoading> </ButtonLoading>
<ButtonLoading
variant={'secondary'}
onClick={() => showGobalParamSheet()}
loading={loading}
>
{t('flow.gobalVariable')}
</ButtonLoading>
<Button variant={'secondary'} onClick={handleButtonRunClick}> <Button variant={'secondary'} onClick={handleButtonRunClick}>
<CirclePlay /> <CirclePlay />
{t('flow.run')} {t('flow.run')}
@ -299,6 +313,12 @@ export default function Agent() {
loading={pipelineRunning} loading={pipelineRunning}
></PipelineRunSheet> ></PipelineRunSheet>
)} )}
{gobalParamSheetVisible && (
<GobalParamSheet
data={{}}
hideModal={hideGobalParamSheet}
></GobalParamSheet>
)}
</section> </section>
); );
} }

View File

@ -1,4 +1,6 @@
import { import {
DSL,
GobalVariableType,
IAgentForm, IAgentForm,
ICategorizeForm, ICategorizeForm,
ICategorizeItem, ICategorizeItem,
@ -346,6 +348,28 @@ export const buildDslComponentsByGraph = (
return components; return components;
}; };
export const buildDslGobalVariables = (
dsl: DSL,
gobalVariables?: Record<string, GobalVariableType>,
) => {
if (!gobalVariables) {
return { globals: dsl.globals, variables: dsl.variables || {} };
}
let gobalVariablesTemp = {};
Object.keys(gobalVariables).forEach((key) => {
gobalVariablesTemp = {
['env.' + key]: gobalVariables[key].value,
};
});
const gobalVariablesResult = {
...dsl.globals,
...gobalVariablesTemp,
};
return { globals: gobalVariablesResult, variables: gobalVariables };
};
export const receiveMessageError = (res: any) => export const receiveMessageError = (res: any) =>
res && (res?.response.status !== 200 || res?.data?.code !== 0); res && (res?.response.status !== 200 || res?.data?.code !== 0);