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 IKeywordNode = BaseNode;
export type ICodeNode = BaseNode<ICodeForm>;
export type IAgentNode = BaseNode;
export type RAGFlowNodeType =
| IBeginNode

View File

@ -19,6 +19,7 @@ import { useShowDrawer } from '../hooks/use-show-drawer';
import { ButtonEdge } from './edge';
import styles from './index.less';
import { RagNode } from './node';
import { AgentNode } from './node/agent-node';
import { BeginNode } from './node/begin-node';
import { CategorizeNode } from './node/categorize-node';
import { EmailNode } from './node/email-node';
@ -53,6 +54,7 @@ const nodeTypes: NodeTypes = {
emailNode: EmailNode,
group: IterationNode,
iterationStartNode: IterationStartNode,
agentNode: AgentNode,
};
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';
import upperFirst from 'lodash/upperFirst';
import {
Box,
CirclePower,
CloudUpload,
CodeXml,
@ -112,6 +113,7 @@ export enum Operator {
IterationStart = 'IterationItem',
Code = 'Code',
WaitingDialogue = 'WaitingDialogue',
Agent = 'Agent',
}
export const CommonOperatorList = Object.values(Operator).filter(
@ -132,6 +134,7 @@ export const AgentOperatorList = [
Operator.Iteration,
Operator.WaitingDialogue,
Operator.Note,
Operator.Agent,
];
export const operatorIconMap = {
@ -173,6 +176,7 @@ export const operatorIconMap = {
[Operator.IterationStart]: CirclePower,
[Operator.Code]: CodeXml,
[Operator.WaitingDialogue]: MessageSquareMore,
[Operator.Agent]: Box,
};
export const operatorMap: Record<
@ -313,6 +317,7 @@ export const operatorMap: Record<
[Operator.IterationStart]: { backgroundColor: '#e6f7ff' },
[Operator.Code]: { backgroundColor: '#4c5458' },
[Operator.WaitingDialogue]: { backgroundColor: '#a5d65c' },
[Operator.Agent]: { backgroundColor: '#a5d65c' },
};
export const componentMenuList = [
@ -356,6 +361,9 @@ export const componentMenuList = [
{
name: Operator.WaitingDialogue,
},
{
name: Operator.Agent,
},
{
name: Operator.Note,
},
@ -682,6 +690,14 @@ export const initialCodeValues = {
export const initialWaitingDialogueValues = {};
export const initialAgentValues = {
...initialLlmBaseValues,
sys_prompt: ``,
prompts: [],
message_history_window_size: 12,
tools: [],
};
export const CategorizeAnchorPointPositions = [
{ top: 1, right: 34 },
{ top: 8, right: 18 },
@ -806,6 +822,7 @@ export const NodeMap = {
[Operator.IterationStart]: 'iterationStartNode',
[Operator.Code]: 'ragNode',
[Operator.WaitingDialogue]: 'ragNode',
[Operator.Agent]: 'agentNode',
};
export const LanguageOptions = [

View File

@ -3,6 +3,7 @@ import { CodeTemplateStrMap, ProgrammingLanguage } from '@/constants/agent';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { Operator } from '../constant';
import AgentForm from '../form/agent-form';
import AkShareForm from '../form/akshare-form';
import AnswerForm from '../form/answer-form';
import ArXivForm from '../form/arxiv-form';
@ -192,6 +193,11 @@ export function useFormConfigMap() {
),
}),
},
[Operator.Agent]: {
component: AgentForm,
defaultValues: {},
schema: z.object({}),
},
[Operator.Baidu]: {
component: BaiduForm,
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 { KnowledgeBaseFormField } from '@/components/knowledge-base-item';
import { LargeModelFormField } from '@/components/large-model-form-field';
import { RerankFormFields } from '@/components/rerank';
import {
initialKeywordsSimilarityWeightValue,
@ -64,7 +63,7 @@ const RetrievalForm = ({ node }: INextOperatorForm) => {
>
<FormContainer>
<QueryVariable></QueryVariable>
<LargeModelFormField></LargeModelFormField>
<KnowledgeBaseFormField></KnowledgeBaseFormField>
</FormContainer>
<FormContainer>
<SimilaritySliderFormField
@ -73,7 +72,7 @@ const RetrievalForm = ({ node }: INextOperatorForm) => {
></SimilaritySliderFormField>
<TopNFormField></TopNFormField>
<RerankFormFields></RerankFormFields>
<KnowledgeBaseFormField></KnowledgeBaseFormField>
<FormField
control={form.control}
name="empty_response"

View File

@ -33,6 +33,7 @@ import {
Operator,
RestrictedUpstreamMap,
SwitchElseTo,
initialAgentValues,
initialAkShareValues,
initialArXivValues,
initialBaiduFanyiValues,
@ -65,6 +66,7 @@ import {
initialSwitchValues,
initialTemplateValues,
initialTuShareValues,
initialWaitingDialogueValues,
initialWenCaiValues,
initialWikipediaValues,
initialYahooFinanceValues,
@ -143,6 +145,8 @@ export const useInitializeOperatorParams = () => {
[Operator.Iteration]: initialIterationValues,
[Operator.IterationStart]: initialIterationValues,
[Operator.Code]: initialCodeValues,
[Operator.WaitingDialogue]: initialWaitingDialogueValues,
[Operator.Agent]: initialAgentValues,
};
}, [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 { Button } from '@/components/ui/button';
import { Button, ButtonLoading } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
@ -19,6 +19,7 @@ import FlowCanvas from './canvas';
import { useHandleExportOrImportJsonFile } from './hooks/use-export-json';
import { useFetchDataOnMount } from './hooks/use-fetch-data';
import { useOpenDocument } from './hooks/use-open-document';
import { useSaveGraph } from './hooks/use-save-graph';
import { UploadAgentDialog } from './upload-agent-dialog';
function AgentDropdownMenuItem({
@ -48,6 +49,7 @@ export default function Agent() {
onFileUploadOk,
hideFileUploadModal,
} = useHandleExportOrImportJsonFile();
const { saveGraph, loading } = useSaveGraph();
const { flowDetail } = useFetchDataOnMount();
@ -55,6 +57,16 @@ export default function Agent() {
<section>
<PageHeader back={navigateToAgentList} title={flowDetail.title}>
<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>
<DropdownMenuTrigger asChild>
<Button variant={'icon'} size={'icon'}>
@ -83,17 +95,6 @@ export default function Agent() {
</AgentDropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button variant={'outline'} size={'sm'}>
Save
</Button>
<Button variant={'outline'} size={'sm'}>
Run app
</Button>
<Button variant={'tertiary'} size={'sm'}>
Publish
</Button>
</div>
</PageHeader>
<ReactFlowProvider>