Feat: Add AgentNode component #3221 (#8019)

### What problem does this PR solve?

Feat: Add AgentNode component #3221

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu 2025-06-03 17:42:30 +08:00 committed by GitHub
parent b6f1cd7809
commit e47186cc42
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 187 additions and 15 deletions

View File

@ -152,6 +152,7 @@ export type IIterationNode = BaseNode;
export type IIterationStartNode = BaseNode; export type IIterationStartNode = BaseNode;
export type IKeywordNode = BaseNode; export type IKeywordNode = BaseNode;
export type ICodeNode = BaseNode<ICodeForm>; export type ICodeNode = BaseNode<ICodeForm>;
export type IAgentNode = BaseNode;
export type RAGFlowNodeType = export type RAGFlowNodeType =
| IBeginNode | IBeginNode

View File

@ -19,6 +19,7 @@ import { useShowDrawer } from '../hooks/use-show-drawer';
import { ButtonEdge } from './edge'; import { ButtonEdge } from './edge';
import styles from './index.less'; import styles from './index.less';
import { RagNode } from './node'; import { RagNode } from './node';
import { AgentNode } from './node/agent-node';
import { BeginNode } from './node/begin-node'; import { BeginNode } from './node/begin-node';
import { CategorizeNode } from './node/categorize-node'; import { CategorizeNode } from './node/categorize-node';
import { EmailNode } from './node/email-node'; import { EmailNode } from './node/email-node';
@ -53,6 +54,7 @@ const nodeTypes: NodeTypes = {
emailNode: EmailNode, emailNode: EmailNode,
group: IterationNode, group: IterationNode,
iterationStartNode: IterationStartNode, iterationStartNode: IterationStartNode,
agentNode: AgentNode,
}; };
const edgeTypes = { const edgeTypes = {

View File

@ -0,0 +1,48 @@
import { useTheme } from '@/components/theme-provider';
import { IAgentNode } from '@/interfaces/database/flow';
import { Handle, NodeProps, Position } from '@xyflow/react';
import classNames from 'classnames';
import { memo } from 'react';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import styles from './index.less';
import NodeHeader from './node-header';
function InnerAgentNode({
id,
data,
isConnectable = true,
selected,
}: NodeProps<IAgentNode>) {
const { theme } = useTheme();
return (
<section
className={classNames(
styles.ragNode,
theme === 'dark' ? styles.dark : '',
{
[styles.selectedNode]: selected,
},
)}
>
<Handle
id="c"
type="source"
position={Position.Left}
isConnectable={isConnectable}
className={styles.handle}
style={LeftHandleStyle}
></Handle>
<Handle
type="source"
position={Position.Right}
isConnectable={isConnectable}
className={styles.handle}
id="b"
style={RightHandleStyle}
></Handle>
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
</section>
);
}
export const AgentNode = memo(InnerAgentNode);

View File

@ -59,6 +59,7 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import upperFirst from 'lodash/upperFirst'; import upperFirst from 'lodash/upperFirst';
import { import {
Box,
CirclePower, CirclePower,
CloudUpload, CloudUpload,
CodeXml, CodeXml,
@ -112,6 +113,7 @@ export enum Operator {
IterationStart = 'IterationItem', IterationStart = 'IterationItem',
Code = 'Code', Code = 'Code',
WaitingDialogue = 'WaitingDialogue', WaitingDialogue = 'WaitingDialogue',
Agent = 'Agent',
} }
export const CommonOperatorList = Object.values(Operator).filter( export const CommonOperatorList = Object.values(Operator).filter(
@ -132,6 +134,7 @@ export const AgentOperatorList = [
Operator.Iteration, Operator.Iteration,
Operator.WaitingDialogue, Operator.WaitingDialogue,
Operator.Note, Operator.Note,
Operator.Agent,
]; ];
export const operatorIconMap = { export const operatorIconMap = {
@ -173,6 +176,7 @@ export const operatorIconMap = {
[Operator.IterationStart]: CirclePower, [Operator.IterationStart]: CirclePower,
[Operator.Code]: CodeXml, [Operator.Code]: CodeXml,
[Operator.WaitingDialogue]: MessageSquareMore, [Operator.WaitingDialogue]: MessageSquareMore,
[Operator.Agent]: Box,
}; };
export const operatorMap: Record< export const operatorMap: Record<
@ -313,6 +317,7 @@ export const operatorMap: Record<
[Operator.IterationStart]: { backgroundColor: '#e6f7ff' }, [Operator.IterationStart]: { backgroundColor: '#e6f7ff' },
[Operator.Code]: { backgroundColor: '#4c5458' }, [Operator.Code]: { backgroundColor: '#4c5458' },
[Operator.WaitingDialogue]: { backgroundColor: '#a5d65c' }, [Operator.WaitingDialogue]: { backgroundColor: '#a5d65c' },
[Operator.Agent]: { backgroundColor: '#a5d65c' },
}; };
export const componentMenuList = [ export const componentMenuList = [
@ -356,6 +361,9 @@ export const componentMenuList = [
{ {
name: Operator.WaitingDialogue, name: Operator.WaitingDialogue,
}, },
{
name: Operator.Agent,
},
{ {
name: Operator.Note, name: Operator.Note,
}, },
@ -682,6 +690,14 @@ export const initialCodeValues = {
export const initialWaitingDialogueValues = {}; export const initialWaitingDialogueValues = {};
export const initialAgentValues = {
...initialLlmBaseValues,
sys_prompt: ``,
prompts: [],
message_history_window_size: 12,
tools: [],
};
export const CategorizeAnchorPointPositions = [ export const CategorizeAnchorPointPositions = [
{ top: 1, right: 34 }, { top: 1, right: 34 },
{ top: 8, right: 18 }, { top: 8, right: 18 },
@ -806,6 +822,7 @@ export const NodeMap = {
[Operator.IterationStart]: 'iterationStartNode', [Operator.IterationStart]: 'iterationStartNode',
[Operator.Code]: 'ragNode', [Operator.Code]: 'ragNode',
[Operator.WaitingDialogue]: 'ragNode', [Operator.WaitingDialogue]: 'ragNode',
[Operator.Agent]: 'agentNode',
}; };
export const LanguageOptions = [ export const LanguageOptions = [

View File

@ -3,6 +3,7 @@ import { CodeTemplateStrMap, ProgrammingLanguage } from '@/constants/agent';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { z } from 'zod'; import { z } from 'zod';
import { Operator } from '../constant'; import { Operator } from '../constant';
import AgentForm from '../form/agent-form';
import AkShareForm from '../form/akshare-form'; import AkShareForm from '../form/akshare-form';
import AnswerForm from '../form/answer-form'; import AnswerForm from '../form/answer-form';
import ArXivForm from '../form/arxiv-form'; import ArXivForm from '../form/arxiv-form';
@ -192,6 +193,11 @@ export function useFormConfigMap() {
), ),
}), }),
}, },
[Operator.Agent]: {
component: AgentForm,
defaultValues: {},
schema: z.object({}),
},
[Operator.Baidu]: { [Operator.Baidu]: {
component: BaiduForm, component: BaiduForm,
defaultValues: { top_n: 10 }, defaultValues: { top_n: 10 },

View File

@ -0,0 +1,74 @@
import { FormContainer } from '@/components/form-container';
import { LargeModelFormField } from '@/components/large-model-form-field';
import { LlmSettingSchema } from '@/components/llm-setting-items/next';
import { PromptEditor } from '@/components/prompt-editor';
import { Form, FormControl, FormField, FormItem } from '@/components/ui/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { initialAgentValues } from '../../constant';
import { useFormValues } from '../../hooks/use-form-values';
import { INextOperatorForm } from '../../interface';
const FormSchema = z.object({
sys_prompt: z.string(),
prompts: z
.array(
z.object({
role: z.string(),
content: z.string(),
}),
)
.optional(),
message_history_window_size: z.coerce.number(),
...LlmSettingSchema,
tools: z
.array(
z.object({
component_name: z.string(),
}),
)
.optional(),
});
const AgentForm = ({ node }: INextOperatorForm) => {
const { t } = useTranslation();
const defaultValues = useFormValues(initialAgentValues, node);
const form = useForm({
defaultValues: defaultValues,
resolver: zodResolver(FormSchema),
});
return (
<Form {...form}>
<form
className="space-y-6 p-4"
onSubmit={(e) => {
e.preventDefault();
}}
>
<FormContainer>
<LargeModelFormField></LargeModelFormField>
<FormField
control={form.control}
name={`sys_prompt`}
render={({ field }) => (
<FormItem className="flex-1">
<FormControl>
<PromptEditor
{...field}
placeholder={t('flow.messagePlaceholder')}
></PromptEditor>
</FormControl>
</FormItem>
)}
/>
</FormContainer>
</form>
</Form>
);
};
export default AgentForm;

View File

@ -1,6 +1,5 @@
import { FormContainer } from '@/components/form-container'; import { FormContainer } from '@/components/form-container';
import { KnowledgeBaseFormField } from '@/components/knowledge-base-item'; import { KnowledgeBaseFormField } from '@/components/knowledge-base-item';
import { LargeModelFormField } from '@/components/large-model-form-field';
import { RerankFormFields } from '@/components/rerank'; import { RerankFormFields } from '@/components/rerank';
import { import {
initialKeywordsSimilarityWeightValue, initialKeywordsSimilarityWeightValue,
@ -64,7 +63,7 @@ const RetrievalForm = ({ node }: INextOperatorForm) => {
> >
<FormContainer> <FormContainer>
<QueryVariable></QueryVariable> <QueryVariable></QueryVariable>
<LargeModelFormField></LargeModelFormField> <KnowledgeBaseFormField></KnowledgeBaseFormField>
</FormContainer> </FormContainer>
<FormContainer> <FormContainer>
<SimilaritySliderFormField <SimilaritySliderFormField
@ -73,7 +72,7 @@ const RetrievalForm = ({ node }: INextOperatorForm) => {
></SimilaritySliderFormField> ></SimilaritySliderFormField>
<TopNFormField></TopNFormField> <TopNFormField></TopNFormField>
<RerankFormFields></RerankFormFields> <RerankFormFields></RerankFormFields>
<KnowledgeBaseFormField></KnowledgeBaseFormField>
<FormField <FormField
control={form.control} control={form.control}
name="empty_response" name="empty_response"

View File

@ -33,6 +33,7 @@ import {
Operator, Operator,
RestrictedUpstreamMap, RestrictedUpstreamMap,
SwitchElseTo, SwitchElseTo,
initialAgentValues,
initialAkShareValues, initialAkShareValues,
initialArXivValues, initialArXivValues,
initialBaiduFanyiValues, initialBaiduFanyiValues,
@ -65,6 +66,7 @@ import {
initialSwitchValues, initialSwitchValues,
initialTemplateValues, initialTemplateValues,
initialTuShareValues, initialTuShareValues,
initialWaitingDialogueValues,
initialWenCaiValues, initialWenCaiValues,
initialWikipediaValues, initialWikipediaValues,
initialYahooFinanceValues, initialYahooFinanceValues,
@ -143,6 +145,8 @@ export const useInitializeOperatorParams = () => {
[Operator.Iteration]: initialIterationValues, [Operator.Iteration]: initialIterationValues,
[Operator.IterationStart]: initialIterationValues, [Operator.IterationStart]: initialIterationValues,
[Operator.Code]: initialCodeValues, [Operator.Code]: initialCodeValues,
[Operator.WaitingDialogue]: initialWaitingDialogueValues,
[Operator.Agent]: initialAgentValues,
}; };
}, [llmId]); }, [llmId]);

View File

@ -0,0 +1,20 @@
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { isEmpty } from 'lodash';
import { useMemo } from 'react';
export function useFormValues(
defaultValues: Record<string, any>,
node?: RAGFlowNodeType,
) {
const values = useMemo(() => {
const formData = node?.data?.form;
if (isEmpty(formData)) {
return defaultValues;
}
return formData;
}, [defaultValues, node?.data?.form]);
return values;
}

View File

@ -1,5 +1,5 @@
import { PageHeader } from '@/components/page-header'; import { PageHeader } from '@/components/page-header';
import { Button } from '@/components/ui/button'; import { Button, ButtonLoading } from '@/components/ui/button';
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@ -19,6 +19,7 @@ import FlowCanvas from './canvas';
import { useHandleExportOrImportJsonFile } from './hooks/use-export-json'; import { useHandleExportOrImportJsonFile } from './hooks/use-export-json';
import { useFetchDataOnMount } from './hooks/use-fetch-data'; import { useFetchDataOnMount } from './hooks/use-fetch-data';
import { useOpenDocument } from './hooks/use-open-document'; import { useOpenDocument } from './hooks/use-open-document';
import { useSaveGraph } from './hooks/use-save-graph';
import { UploadAgentDialog } from './upload-agent-dialog'; import { UploadAgentDialog } from './upload-agent-dialog';
function AgentDropdownMenuItem({ function AgentDropdownMenuItem({
@ -48,6 +49,7 @@ export default function Agent() {
onFileUploadOk, onFileUploadOk,
hideFileUploadModal, hideFileUploadModal,
} = useHandleExportOrImportJsonFile(); } = useHandleExportOrImportJsonFile();
const { saveGraph, loading } = useSaveGraph();
const { flowDetail } = useFetchDataOnMount(); const { flowDetail } = useFetchDataOnMount();
@ -55,6 +57,16 @@ export default function Agent() {
<section> <section>
<PageHeader back={navigateToAgentList} title={flowDetail.title}> <PageHeader back={navigateToAgentList} title={flowDetail.title}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ButtonLoading
variant={'outline'}
onClick={() => saveGraph()}
loading={loading}
>
Save
</ButtonLoading>
<Button variant={'outline'}>Run app</Button>
<Button variant={'outline'}>Publish</Button>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant={'icon'} size={'icon'}> <Button variant={'icon'} size={'icon'}>
@ -83,17 +95,6 @@ export default function Agent() {
</AgentDropdownMenuItem> </AgentDropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<Button variant={'outline'} size={'sm'}>
Save
</Button>
<Button variant={'outline'} size={'sm'}>
Run app
</Button>
<Button variant={'tertiary'} size={'sm'}>
Publish
</Button>
</div> </div>
</PageHeader> </PageHeader>
<ReactFlowProvider> <ReactFlowProvider>