Feat: Add data operation node #10427 (#10985)

### What problem does this PR solve?

Feat: Add data operation node #10427

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-11-04 13:48:44 +08:00
committed by GitHub
parent 19f71a961a
commit 021b2ac51a
15 changed files with 109 additions and 68 deletions

View File

@ -29,7 +29,7 @@ export function HomeCard({
onClick?.();
}}
>
<CardContent className="p-4 flex gap-2 items-start group h-full w-full">
<CardContent className="p-4 flex gap-2 items-start group h-full w-full hover:shadow-md">
<div className="flex justify-between mb-4">
<RAGFlowAvatar
className="w-[32px] h-[32px]"

View File

@ -9,7 +9,7 @@ const Card = React.forwardRef<
<div
ref={ref}
className={cn(
'rounded-lg border-border-default border shadow-sm bg-bg-input hover:shadow-md',
'rounded-lg border-border-default border shadow-sm bg-bg-input',
className,
)}
{...props}

View File

@ -83,7 +83,6 @@ export enum Operator {
Google = 'Google',
Bing = 'Bing',
GoogleScholar = 'GoogleScholar',
DeepL = 'DeepL',
GitHub = 'GitHub',
BaiduFanyi = 'BaiduFanyi',
QWeather = 'QWeather',
@ -110,6 +109,7 @@ export enum Operator {
StringTransform = 'StringTransform',
SearXNG = 'SearXNG',
Placeholder = 'Placeholder',
DataOperations = 'DataOperations',
File = 'File', // pipeline
Parser = 'Parser',
Tokenizer = 'Tokenizer',

View File

@ -1528,6 +1528,8 @@ This delimiter is used to split the input text into several text pieces echo of
knowledgeBaseVars: 'Knowledge base variables',
code: 'Code',
codeDescription: 'It allows developers to write custom Python logic.',
dataOperations: 'Data operations',
dataOperationsDescription: 'Perform various operations on a Data object.',
inputVariables: 'Input variables',
runningHintText: 'is running...🕞',
openingSwitch: 'Opening switch',

View File

@ -1452,6 +1452,8 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
knowledgeBaseVars: '知识库变量',
code: '代码',
codeDescription: '它允许开发人员编写自定义 Python 逻辑。',
dataOperations: '数据操作',
dataOperationsDescription: '对数据对象执行各种操作。',
inputVariables: '输入变量',
addVariable: '新增变量',
runningHintText: '正在运行中...🕞',

View File

@ -53,6 +53,7 @@ import { RagNode } from './node';
import { AgentNode } from './node/agent-node';
import { BeginNode } from './node/begin-node';
import { CategorizeNode } from './node/categorize-node';
import { DataOperationsNode } from './node/data-operations-node';
import { NextStepDropdown } from './node/dropdown/next-step-dropdown';
import { ExtractorNode } from './node/extractor-node';
import { FileNode } from './node/file-node';
@ -96,6 +97,7 @@ export const nodeTypes: NodeTypes = {
tokenizerNode: TokenizerNode,
splitterNode: SplitterNode,
contextNode: ExtractorNode,
dataOperationsNode: DataOperationsNode,
};
const edgeTypes = {

View File

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

View File

@ -30,7 +30,7 @@ export function AccordionOperators({
return (
<Accordion
type="multiple"
className="px-2 text-text-title max-h-[45vh] overflow-auto scrollbar-none"
className="px-2 text-text-title max-h-[45vh] overflow-auto"
defaultValue={['item-1', 'item-2', 'item-3', 'item-4', 'item-5']}
>
<AccordionItem value="item-1">
@ -75,7 +75,11 @@ export function AccordionOperators({
</OperatorAccordionTrigger>
<AccordionContent className="flex flex-col gap-4 text-text-primary">
<OperatorItemList
operators={[Operator.Code, Operator.StringTransform]}
operators={[
Operator.Code,
Operator.StringTransform,
Operator.DataOperations,
]}
isCustomDropdown={isCustomDropdown}
mousePosition={mousePosition}
></OperatorItemList>

View File

@ -125,9 +125,6 @@ export const componentMenuList = [
{
name: Operator.GoogleScholar,
},
{
name: Operator.DeepL,
},
{
name: Operator.GitHub,
},
@ -388,11 +385,6 @@ export const initialGoogleScholarValues = {
},
};
export const initialDeepLValues = {
top_n: 5,
auth_key: 'relevance',
};
export const initialGithubValues = {
top_n: 5,
query: AgentGlobals.SysQuery,
@ -723,6 +715,10 @@ export const initialPlaceholderValues = {
// It's just a visual placeholder
};
export const initialDataOperationsValues = {
outputs: {},
};
export const CategorizeAnchorPointPositions = [
{ top: 1, right: 34 },
{ top: 8, right: 18 },
@ -771,7 +767,6 @@ export const RestrictedUpstreamMap = {
[Operator.Google]: [Operator.Begin, Operator.Retrieval],
[Operator.Bing]: [Operator.Begin, Operator.Retrieval],
[Operator.GoogleScholar]: [Operator.Begin, Operator.Retrieval],
[Operator.DeepL]: [Operator.Begin, Operator.Retrieval],
[Operator.GitHub]: [Operator.Begin, Operator.Retrieval],
[Operator.BaiduFanyi]: [Operator.Begin, Operator.Retrieval],
[Operator.QWeather]: [Operator.Begin, Operator.Retrieval],
@ -798,7 +793,8 @@ export const RestrictedUpstreamMap = {
[Operator.UserFillUp]: [Operator.Begin],
[Operator.Tool]: [Operator.Begin],
[Operator.Placeholder]: [Operator.Begin],
[Operator.Parser]: [Operator.Begin],
[Operator.DataOperations]: [Operator.Begin],
[Operator.Parser]: [Operator.Begin], // pipeline
[Operator.Splitter]: [Operator.Begin],
[Operator.HierarchicalMerger]: [Operator.Begin],
[Operator.Tokenizer]: [Operator.Begin],
@ -822,7 +818,6 @@ export const NodeMap = {
[Operator.Google]: 'ragNode',
[Operator.Bing]: 'ragNode',
[Operator.GoogleScholar]: 'ragNode',
[Operator.DeepL]: 'ragNode',
[Operator.GitHub]: 'ragNode',
[Operator.BaiduFanyi]: 'ragNode',
[Operator.QWeather]: 'ragNode',
@ -855,6 +850,7 @@ export const NodeMap = {
[Operator.Splitter]: 'splitterNode',
[Operator.HierarchicalMerger]: 'splitterNode',
[Operator.Extractor]: 'contextNode',
[Operator.DataOperations]: 'dataOperationsNode',
};
export enum BeginQueryType {

View File

@ -9,7 +9,7 @@ import BingForm from '../form/bing-form';
import CategorizeForm from '../form/categorize-form';
import CodeForm from '../form/code-form';
import CrawlerForm from '../form/crawler-form';
import DeepLForm from '../form/deepl-form';
import DataOperationsForm from '../form/data-operations-form';
import DuckDuckGoForm from '../form/duckduckgo-form';
import EmailForm from '../form/email-form';
import ExeSQLForm from '../form/exesql-form';
@ -99,9 +99,6 @@ export const FormConfigMap = {
[Operator.GoogleScholar]: {
component: GoogleScholarForm,
},
[Operator.DeepL]: {
component: DeepLForm,
},
[Operator.GitHub]: {
component: GithubForm,
},
@ -190,4 +187,7 @@ export const FormConfigMap = {
[Operator.Extractor]: {
component: ExtractorForm,
},
[Operator.DataOperations]: {
component: DataOperationsForm,
},
};

View File

@ -0,0 +1,47 @@
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Form } from '@/components/ui/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 { initialDataOperationsValues } from '../../constant';
import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
import { FormWrapper } from '../components/form-wrapper';
import { Output } from '../components/output';
export const RetrievalPartialSchema = {
select_operation: z.string(),
};
export const FormSchema = z.object(RetrievalPartialSchema);
function DataOperationsForm({ node }: INextOperatorForm) {
const { t } = useTranslation();
const defaultValues = useFormValues(initialDataOperationsValues, node);
const form = useForm({
defaultValues: defaultValues,
resolver: zodResolver(FormSchema),
});
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper>
<RAGFlowFormItem name="query" label={t('flow.query')}>
<SelectWithSearch options={[]} allowClear />
</RAGFlowFormItem>
<Output list={[]}></Output>
</FormWrapper>
</Form>
);
}
export default memo(DataOperationsForm);

View File

@ -1,36 +0,0 @@
import TopNItem from '@/components/top-n-item';
import { useTranslate } from '@/hooks/common-hooks';
import { Form, Select } from 'antd';
import { useBuildSortOptions } from '../../form-hooks';
import { IOperatorForm } from '../../interface';
import { DeepLSourceLangOptions, DeepLTargetLangOptions } from '../../options';
import DynamicInputVariable from '../components/dynamic-input-variable';
const DeepLForm = ({ onValuesChange, form, node }: IOperatorForm) => {
const { t } = useTranslate('flow');
const options = useBuildSortOptions();
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<TopNItem initialValue={5}></TopNItem>
<Form.Item label={t('authKey')} name={'auth_key'}>
<Select options={options}></Select>
</Form.Item>
<Form.Item label={t('sourceLang')} name={'source_lang'}>
<Select options={DeepLSourceLangOptions}></Select>
</Form.Item>
<Form.Item label={t('targetLang')} name={'target_lang'}>
<Select options={DeepLTargetLangOptions}></Select>
</Form.Item>
</Form>
);
};
export default DeepLForm;

View File

@ -1,6 +1,5 @@
import { Operator } from '../../constant';
import AkShareForm from '../akshare-form';
import DeepLForm from '../deepl-form';
import ArXivForm from './arxiv-form';
import BingForm from './bing-form';
import CrawlerForm from './crawler-form';
@ -28,7 +27,6 @@ export const ToolFormConfigMap = {
[Operator.Google]: GoogleForm,
[Operator.Bing]: BingForm,
[Operator.GoogleScholar]: GoogleScholarForm,
[Operator.DeepL]: DeepLForm,
[Operator.GitHub]: GithubForm,
[Operator.ExeSQL]: ExeSQLForm,
[Operator.AkShare]: AkShareForm,

View File

@ -19,7 +19,7 @@ import {
initialCategorizeValues,
initialCodeValues,
initialCrawlerValues,
initialDeepLValues,
initialDataOperationsValues,
initialDuckValues,
initialEmailValues,
initialExeSqlValues,
@ -93,7 +93,6 @@ export const useInitializeOperatorParams = () => {
[Operator.Google]: initialGoogleValues,
[Operator.Bing]: initialBingValues,
[Operator.GoogleScholar]: initialGoogleScholarValues,
[Operator.DeepL]: initialDeepLValues,
[Operator.SearXNG]: initialSearXNGValues,
[Operator.GitHub]: initialGithubValues,
[Operator.BaiduFanyi]: initialBaiduFanyiValues,
@ -131,6 +130,7 @@ export const useInitializeOperatorParams = () => {
sys_prompt: t('flow.prompts.system.summary'),
prompts: t('flow.prompts.user.summary'),
},
[Operator.DataOperations]: initialDataOperationsValues,
};
}, [llmId]);

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 { HousePlus } from 'lucide-react';
import { FileCode, HousePlus } from 'lucide-react';
import { Operator } from './constant';
interface IProps {
@ -56,13 +56,18 @@ export const SVGIconMap = {
[Operator.Crawler]: CrawlerIcon,
};
export const LucideIconMap = {
[Operator.DataOperations]: FileCode,
};
const Empty = () => {
return <div className="hidden"></div>;
};
const OperatorIcon = ({ name, className }: IProps) => {
const Icon = OperatorIconMap[name as keyof typeof OperatorIconMap] || Empty;
const SvgIcon = SVGIconMap[name as keyof typeof SVGIconMap] || Empty;
const Icon = OperatorIconMap[name as keyof typeof OperatorIconMap];
const SvgIcon = SVGIconMap[name as keyof typeof SVGIconMap];
const LucideIcon = LucideIconMap[name as keyof typeof LucideIconMap];
if (name === Operator.Begin) {
return (
@ -77,11 +82,21 @@ const OperatorIcon = ({ name, className }: IProps) => {
);
}
return typeof Icon === 'string' ? (
<IconFont name={Icon} className={cn('size-5 ', className)}></IconFont>
) : (
<SvgIcon className={cn('size-5 fill-current', className)}></SvgIcon>
);
if (Icon) {
return (
<IconFont name={Icon} className={cn('size-5 ', className)}></IconFont>
);
}
if (LucideIcon) {
return <LucideIcon className={cn('size-5', className)} />;
}
if (SvgIcon) {
return <SvgIcon className={cn('size-5 fill-current', className)}></SvgIcon>;
}
return <Empty></Empty>;
};
export default OperatorIcon;