Feat: Deleting the last tool of the agent will delete the tool node #3221 (#8376)

### What problem does this PR solve?

Feat: Deleting the last tool of the agent will delete the tool node
#3221

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu 2025-06-19 19:23:16 +08:00 committed by GitHub
parent fa3e90c72e
commit 972fd919b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 245 additions and 94 deletions

View File

@ -1,8 +1,9 @@
import { IAgentForm, IToolNode } from '@/interfaces/database/agent'; import { IAgentForm, IToolNode } from '@/interfaces/database/agent';
import { Handle, NodeProps, Position } from '@xyflow/react'; import { Handle, NodeProps, Position } from '@xyflow/react';
import { get } from 'lodash'; import { get } from 'lodash';
import { memo } from 'react'; import { memo, useCallback } from 'react';
import { NodeHandleId } from '../../constant'; import { NodeHandleId } from '../../constant';
import { ToolCard } from '../../form/agent-form/agent-tools';
import useGraphStore from '../../store'; import useGraphStore from '../../store';
import { NodeWrapper } from './node-wrapper'; import { NodeWrapper } from './node-wrapper';
@ -16,6 +17,8 @@ function InnerToolNode({
const upstreamAgentNodeId = edges.find((x) => x.target === id)?.source; const upstreamAgentNodeId = edges.find((x) => x.target === id)?.source;
const upstreamAgentNode = getNode(upstreamAgentNodeId); const upstreamAgentNode = getNode(upstreamAgentNodeId);
const handleClick = useCallback(() => {}, []);
const tools: IAgentForm['tools'] = get( const tools: IAgentForm['tools'] = get(
upstreamAgentNode, upstreamAgentNode,
'data.form.tools', 'data.form.tools',
@ -30,9 +33,16 @@ function InnerToolNode({
position={Position.Top} position={Position.Top}
isConnectable={isConnectable} isConnectable={isConnectable}
></Handle> ></Handle>
<ul className="space-y-1"> <ul className="space-y-2">
{tools.map((x) => ( {tools.map((x) => (
<li key={x.component_name}>{x.component_name}</li> <ToolCard
key={x.component_name}
onClick={handleClick}
className="cursor-pointer"
data-tool={x.component_name}
>
{x.component_name}
</ToolCard>
))} ))}
</ul> </ul>
</NodeWrapper> </NodeWrapper>

View File

@ -0,0 +1,53 @@
import { BlockButton } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { PencilLine, X } from 'lucide-react';
import { PropsWithChildren } from 'react';
import { ToolPopover } from './tool-popover';
import { useDeleteAgentNodeTools } from './tool-popover/use-update-tools';
import { useGetAgentToolNames } from './use-get-tools';
export function ToolCard({
children,
className,
...props
}: PropsWithChildren & React.HTMLAttributes<HTMLLIElement>) {
return (
<li
{...props}
className={cn(
'flex bg-background-card p-1 rounded-sm justify-between',
className,
)}
>
{children}
</li>
);
}
export function AgentTools() {
const { toolNames } = useGetAgentToolNames();
const { deleteNodeTool } = useDeleteAgentNodeTools();
return (
<section className="space-y-2.5">
<span className="text-text-sub-title">Tools</span>
<ul className="space-y-2">
{toolNames.map((x) => (
<ToolCard key={x}>
{x}
<div className="flex items-center gap-2 text-text-sub-title">
<PencilLine className="size-4 cursor-pointer" />
<X
className="size-4 cursor-pointer"
onClick={deleteNodeTool(x)}
/>
</div>
</ToolCard>
))}
</ul>
<ToolPopover>
<BlockButton>Add Tool</BlockButton>
</ToolPopover>
</section>
);
}

View File

@ -21,8 +21,8 @@ import { AgentInstanceContext } from '../../context';
import { INextOperatorForm } from '../../interface'; import { INextOperatorForm } from '../../interface';
import { Output } from '../components/output'; import { Output } from '../components/output';
import { PromptEditor } from '../components/prompt-editor'; import { PromptEditor } from '../components/prompt-editor';
import { ToolPopover } from './tool-popover'; import { AgentTools } from './agent-tools';
import { useToolOptions, useValues } from './use-values'; import { useValues } from './use-values';
import { useWatchFormChange } from './use-watch-change'; import { useWatchFormChange } from './use-watch-change';
const FormSchema = z.object({ const FormSchema = z.object({
@ -67,8 +67,6 @@ const AgentForm = ({ node }: INextOperatorForm) => {
const { addCanvasNode } = useContext(AgentInstanceContext); const { addCanvasNode } = useContext(AgentInstanceContext);
const toolOptions = useToolOptions();
return ( return (
<Form {...form}> <Form {...form}>
<form <form
@ -113,17 +111,17 @@ const AgentForm = ({ node }: INextOperatorForm) => {
)} )}
/> />
</FormContainer> </FormContainer>
<ToolPopover> <FormContainer>
<BlockButton>Add Tool</BlockButton> <AgentTools></AgentTools>
</ToolPopover> <BlockButton
<BlockButton onClick={addCanvasNode(Operator.Agent, {
onClick={addCanvasNode(Operator.Agent, { nodeId: node?.id,
nodeId: node?.id, position: Position.Bottom,
position: Position.Bottom, })}
})} >
> Add Agent
Add Agent </BlockButton>
</BlockButton> </FormContainer>
<Output list={outputList}></Output> <Output list={outputList}></Output>
</form> </form>
</Form> </Form>

View File

@ -3,12 +3,12 @@ import {
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from '@/components/ui/popover'; } from '@/components/ui/popover';
import { IAgentForm } from '@/interfaces/database/agent';
import { Operator } from '@/pages/agent/constant'; import { Operator } from '@/pages/agent/constant';
import { AgentFormContext, AgentInstanceContext } from '@/pages/agent/context'; import { AgentFormContext, AgentInstanceContext } from '@/pages/agent/context';
import { Position } from '@xyflow/react'; import { Position } from '@xyflow/react';
import { get } from 'lodash'; import { PropsWithChildren, useCallback, useContext } from 'react';
import { PropsWithChildren, useCallback, useContext, useMemo } from 'react'; import { useDeleteToolNode } from '../use-delete-tool-node';
import { useGetAgentToolNames } from '../use-get-tools';
import { ToolCommand } from './tool-command'; import { ToolCommand } from './tool-command';
import { useUpdateAgentNodeTools } from './use-update-tools'; import { useUpdateAgentNodeTools } from './use-update-tools';
@ -16,23 +16,24 @@ export function ToolPopover({ children }: PropsWithChildren) {
const { addCanvasNode } = useContext(AgentInstanceContext); const { addCanvasNode } = useContext(AgentInstanceContext);
const node = useContext(AgentFormContext); const node = useContext(AgentFormContext);
const { updateNodeTools } = useUpdateAgentNodeTools(); const { updateNodeTools } = useUpdateAgentNodeTools();
const { toolNames } = useGetAgentToolNames();
const toolNames = useMemo(() => { const { deleteToolNode } = useDeleteToolNode();
const tools: IAgentForm['tools'] = get(node, 'data.form.tools', []);
return tools.map((x) => x.component_name);
}, [node]);
const handleChange = useCallback( const handleChange = useCallback(
(value: string[]) => { (value: string[]) => {
if (Array.isArray(value) && value.length > 0 && node?.id) { if (Array.isArray(value) && node?.id) {
updateNodeTools(value); updateNodeTools(value);
addCanvasNode(Operator.Tool, { if (value.length > 0) {
position: Position.Bottom, addCanvasNode(Operator.Tool, {
nodeId: node?.id, position: Position.Bottom,
})(); nodeId: node?.id,
})();
} else {
deleteToolNode(node.id); // TODO: The tool node should be derived from the agent tools data
}
} }
}, },
[addCanvasNode, node?.id, updateNodeTools], [addCanvasNode, deleteToolNode, node?.id, updateNodeTools],
); );
return ( return (

View File

@ -2,17 +2,26 @@ import { IAgentForm } from '@/interfaces/database/agent';
import { AgentFormContext } from '@/pages/agent/context'; import { AgentFormContext } from '@/pages/agent/context';
import useGraphStore from '@/pages/agent/store'; import useGraphStore from '@/pages/agent/store';
import { get } from 'lodash'; import { get } from 'lodash';
import { useCallback, useContext } from 'react'; import { useCallback, useContext, useMemo } from 'react';
import { useDeleteToolNode } from '../use-delete-tool-node';
export function useGetNodeTools() {
const node = useContext(AgentFormContext);
return useMemo(() => {
const tools: IAgentForm['tools'] = get(node, 'data.form.tools');
return tools;
}, [node]);
}
export function useUpdateAgentNodeTools() { export function useUpdateAgentNodeTools() {
const { updateNodeForm } = useGraphStore((state) => state); const { updateNodeForm } = useGraphStore((state) => state);
const node = useContext(AgentFormContext); const node = useContext(AgentFormContext);
const tools = useGetNodeTools();
const updateNodeTools = useCallback( const updateNodeTools = useCallback(
(value: string[]) => { (value: string[]) => {
if (node?.id) { if (node?.id) {
const tools: IAgentForm['tools'] = get(node, 'data.form.tools');
const nextValue = value.reduce<IAgentForm['tools']>((pre, cur) => { const nextValue = value.reduce<IAgentForm['tools']>((pre, cur) => {
const tool = tools.find((x) => x.component_name === cur); const tool = tools.find((x) => x.component_name === cur);
pre.push(tool ? tool : { component_name: cur, params: {} }); pre.push(tool ? tool : { component_name: cur, params: {} });
@ -22,8 +31,37 @@ export function useUpdateAgentNodeTools() {
updateNodeForm(node?.id, nextValue, ['tools']); updateNodeForm(node?.id, nextValue, ['tools']);
} }
}, },
[node, updateNodeForm], [node?.id, tools, updateNodeForm],
); );
return { updateNodeTools }; const deleteNodeTool = useCallback(
(value: string) => {
updateNodeTools([value]);
},
[updateNodeTools],
);
return { updateNodeTools, deleteNodeTool };
}
export function useDeleteAgentNodeTools() {
const { updateNodeForm } = useGraphStore((state) => state);
const tools = useGetNodeTools();
const node = useContext(AgentFormContext);
const { deleteToolNode } = useDeleteToolNode();
const deleteNodeTool = useCallback(
(value: string) => () => {
const nextTools = tools.filter((x) => x.component_name !== value);
if (node?.id) {
updateNodeForm(node?.id, nextTools, ['tools']);
if (nextTools.length === 0) {
deleteToolNode(node?.id);
}
}
},
[deleteToolNode, node?.id, tools, updateNodeForm],
);
return { deleteNodeTool };
} }

View File

@ -0,0 +1,24 @@
import { useCallback } from 'react';
import { NodeHandleId } from '../../constant';
import useGraphStore from '../../store';
export function useDeleteToolNode() {
const { edges, deleteEdgeById, deleteNodeById } = useGraphStore(
(state) => state,
);
const deleteToolNode = useCallback(
(agentNodeId: string) => {
const edge = edges.find(
(x) => x.source === agentNodeId && x.sourceHandle === NodeHandleId.Tool,
);
if (edge) {
deleteEdgeById(edge.id);
deleteNodeById(edge.target);
}
},
[deleteEdgeById, deleteNodeById, edges],
);
return { deleteToolNode };
}

View File

@ -0,0 +1,15 @@
import { IAgentForm } from '@/interfaces/database/agent';
import { get } from 'lodash';
import { useContext, useMemo } from 'react';
import { AgentFormContext } from '../../context';
export function useGetAgentToolNames() {
const node = useContext(AgentFormContext);
const toolNames = useMemo(() => {
const tools: IAgentForm['tools'] = get(node, 'data.form.tools', []);
return tools.map((x) => x.component_name);
}, [node]);
return { toolNames };
}

View File

@ -2,7 +2,7 @@ import { useFetchModelId } from '@/hooks/logic-hooks';
import { RAGFlowNodeType } from '@/interfaces/database/flow'; import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { get, isEmpty } from 'lodash'; import { get, isEmpty } from 'lodash';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { Operator, initialAgentValues } from '../../constant'; import { initialAgentValues } from '../../constant';
export function useValues(node?: RAGFlowNodeType) { export function useValues(node?: RAGFlowNodeType) {
const llmId = useFetchModelId(); const llmId = useFetchModelId();
@ -28,48 +28,3 @@ export function useValues(node?: RAGFlowNodeType) {
return values; return values;
} }
function buildOptions(list: string[]) {
return list.map((x) => ({ label: x, value: x }));
}
export function useToolOptions() {
const options = useMemo(() => {
const options = [
{
label: 'Search',
options: buildOptions([
Operator.Google,
Operator.Bing,
Operator.DuckDuckGo,
Operator.Wikipedia,
Operator.YahooFinance,
Operator.PubMed,
Operator.GoogleScholar,
]),
},
{
label: 'Communication',
options: buildOptions([Operator.Email]),
},
{
label: 'Productivity',
options: [],
},
{
label: 'Developer',
options: buildOptions([
Operator.GitHub,
Operator.ExeSQL,
Operator.Invoke,
Operator.Crawler,
Operator.Code,
]),
},
];
return options;
}, []);
return options;
}

View File

@ -15,7 +15,7 @@ import { INextOperatorForm } from '../../interface';
import { useValues } from './use-values'; import { useValues } from './use-values';
import { useWatchFormChange } from './use-watch-change'; import { useWatchFormChange } from './use-watch-change';
const MessageForm = ({ node }: INextOperatorForm) => { const TavilyForm = ({ node }: INextOperatorForm) => {
const values = useValues(node); const values = useValues(node);
const FormSchema = z.object({ const FormSchema = z.object({
@ -58,4 +58,4 @@ const MessageForm = ({ node }: INextOperatorForm) => {
); );
}; };
export default MessageForm; export default TavilyForm;

View File

@ -0,0 +1,36 @@
import { Operator } from '../../constant';
import AkShareForm from '../akshare-form';
import ArXivForm from '../arxiv-form';
import BingForm from '../bing-form';
import CodeForm from '../code-form';
import CrawlerForm from '../crawler-form';
import DeepLForm from '../deepl-form';
import DuckDuckGoForm from '../duckduckgo-form';
import EmailForm from '../email-form';
import ExeSQLForm from '../exesql-form';
import GithubForm from '../github-form';
import GoogleForm from '../google-form';
import GoogleScholarForm from '../google-scholar-form';
import PubMedForm from '../pubmed-form';
import RetrievalForm from '../retrieval-form/next';
import WikipediaForm from '../wikipedia-form';
import YahooFinanceForm from '../yahoo-finance-form';
export const ToolFormConfigMap = {
[Operator.Retrieval]: RetrievalForm,
[Operator.Code]: CodeForm,
[Operator.DuckDuckGo]: DuckDuckGoForm,
[Operator.Wikipedia]: WikipediaForm,
[Operator.PubMed]: PubMedForm,
[Operator.ArXiv]: ArXivForm,
[Operator.Google]: GoogleForm,
[Operator.Bing]: BingForm,
[Operator.GoogleScholar]: GoogleScholarForm,
[Operator.DeepL]: DeepLForm,
[Operator.GitHub]: GithubForm,
[Operator.ExeSQL]: ExeSQLForm,
[Operator.AkShare]: AkShareForm,
[Operator.YahooFinance]: YahooFinanceForm,
[Operator.Crawler]: CrawlerForm,
[Operator.Email]: EmailForm,
};

View File

@ -1,7 +1,20 @@
import { INextOperatorForm } from '../../interface'; import useGraphStore from '../../store';
import { ToolFormConfigMap } from './constant';
const ToolForm = ({ node }: INextOperatorForm) => { const EmptyContent = () => <div></div>;
return <section>xxx</section>;
const ToolForm = () => {
const clickedToolId = useGraphStore((state) => state.clickedToolId);
const ToolForm =
ToolFormConfigMap[clickedToolId as keyof typeof ToolFormConfigMap] ??
EmptyContent;
return (
<section>
<ToolForm key={clickedToolId}></ToolForm>
</section>
);
}; };
export default ToolForm; export default ToolForm;

View File

@ -184,7 +184,7 @@ function useAddChildEdge() {
return { addChildEdge }; return { addChildEdge };
} }
function useAddTooNode() { function useAddToolNode() {
const addNode = useGraphStore((state) => state.addNode); const addNode = useGraphStore((state) => state.addNode);
const getNode = useGraphStore((state) => state.getNode); const getNode = useGraphStore((state) => state.getNode);
const addEdge = useGraphStore((state) => state.addEdge); const addEdge = useGraphStore((state) => state.addEdge);
@ -241,7 +241,7 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) {
const initializeOperatorParams = useInitializeOperatorParams(); const initializeOperatorParams = useInitializeOperatorParams();
const { calculateNewlyBackChildPosition } = useCalculateNewlyChildPosition(); const { calculateNewlyBackChildPosition } = useCalculateNewlyChildPosition();
const { addChildEdge } = useAddChildEdge(); const { addChildEdge } = useAddChildEdge();
const { addToolNode } = useAddTooNode(); const { addToolNode } = useAddToolNode();
// const [reactFlowInstance, setReactFlowInstance] = // const [reactFlowInstance, setReactFlowInstance] =
// useState<ReactFlowInstance<any, any>>(); // useState<ReactFlowInstance<any, any>>();

View File

@ -14,6 +14,7 @@ export const useShowFormDrawer = () => {
clickedNodeId: clickNodeId, clickedNodeId: clickNodeId,
setClickedNodeId, setClickedNodeId,
getNode, getNode,
setClickedToolId,
} = useGraphStore((state) => state); } = useGraphStore((state) => state);
const { const {
visible: formDrawerVisible, visible: formDrawerVisible,
@ -21,12 +22,13 @@ export const useShowFormDrawer = () => {
showModal: showFormDrawer, showModal: showFormDrawer,
} = useSetModalState(); } = useSetModalState();
const handleShow = useCallback( const handleShow: NodeMouseHandler = useCallback(
(node: Node) => { (e, node: Node) => {
setClickedNodeId(node.id); setClickedNodeId(node.id);
setClickedToolId(get(e.target, 'dataset.tool'));
showFormDrawer(); showFormDrawer();
}, },
[showFormDrawer, setClickedNodeId], [setClickedNodeId, setClickedToolId, showFormDrawer],
); );
return { return {
@ -118,7 +120,7 @@ export function useShowDrawer({
if (!ExcludedNodes.some((x) => x === node.data.label)) { if (!ExcludedNodes.some((x) => x === node.data.label)) {
hideSingleDebugDrawer(); hideSingleDebugDrawer();
hideRunOrChatDrawer(); hideRunOrChatDrawer();
showFormDrawer(node); showFormDrawer(e, node);
} }
// handle single debug icon click // handle single debug icon click
if ( if (

View File

@ -35,6 +35,7 @@ export type RFState = {
selectedNodeIds: string[]; selectedNodeIds: string[];
selectedEdgeIds: string[]; selectedEdgeIds: string[];
clickedNodeId: string; // currently selected node clickedNodeId: string; // currently selected node
clickedToolId: string; // currently selected tool id
onNodesChange: OnNodesChange<RAGFlowNodeType>; onNodesChange: OnNodesChange<RAGFlowNodeType>;
onEdgesChange: OnEdgesChange; onEdgesChange: OnEdgesChange;
onConnect: OnConnect; onConnect: OnConnect;
@ -73,6 +74,7 @@ export type RFState = {
updateNodeName: (id: string, name: string) => void; updateNodeName: (id: string, name: string) => void;
generateNodeName: (name: string) => string; generateNodeName: (name: string) => string;
setClickedNodeId: (id?: string) => void; setClickedNodeId: (id?: string) => void;
setClickedToolId: (id?: string) => void;
}; };
// this is our useStore hook that we can use in our components to get parts of the store and call actions // this is our useStore hook that we can use in our components to get parts of the store and call actions
@ -84,6 +86,7 @@ const useGraphStore = create<RFState>()(
selectedNodeIds: [] as string[], selectedNodeIds: [] as string[],
selectedEdgeIds: [] as string[], selectedEdgeIds: [] as string[],
clickedNodeId: '', clickedNodeId: '',
clickedToolId: '',
onNodesChange: (changes) => { onNodesChange: (changes) => {
set({ set({
nodes: applyNodeChanges(changes, get().nodes), nodes: applyNodeChanges(changes, get().nodes),
@ -465,6 +468,9 @@ const useGraphStore = create<RFState>()(
return generateNodeNamesWithIncreasingIndex(name, nodes); return generateNodeNamesWithIncreasingIndex(name, nodes);
}, },
setClickedToolId: (id?: string) => {
set({ clickedToolId: id });
},
})), })),
{ name: 'graph', trace: true }, { name: 'graph', trace: true },
), ),