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 { Handle, NodeProps, Position } from '@xyflow/react';
import { get } from 'lodash';
import { memo } from 'react';
import { memo, useCallback } from 'react';
import { NodeHandleId } from '../../constant';
import { ToolCard } from '../../form/agent-form/agent-tools';
import useGraphStore from '../../store';
import { NodeWrapper } from './node-wrapper';
@ -16,6 +17,8 @@ function InnerToolNode({
const upstreamAgentNodeId = edges.find((x) => x.target === id)?.source;
const upstreamAgentNode = getNode(upstreamAgentNodeId);
const handleClick = useCallback(() => {}, []);
const tools: IAgentForm['tools'] = get(
upstreamAgentNode,
'data.form.tools',
@ -30,9 +33,16 @@ function InnerToolNode({
position={Position.Top}
isConnectable={isConnectable}
></Handle>
<ul className="space-y-1">
<ul className="space-y-2">
{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>
</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 { Output } from '../components/output';
import { PromptEditor } from '../components/prompt-editor';
import { ToolPopover } from './tool-popover';
import { useToolOptions, useValues } from './use-values';
import { AgentTools } from './agent-tools';
import { useValues } from './use-values';
import { useWatchFormChange } from './use-watch-change';
const FormSchema = z.object({
@ -67,8 +67,6 @@ const AgentForm = ({ node }: INextOperatorForm) => {
const { addCanvasNode } = useContext(AgentInstanceContext);
const toolOptions = useToolOptions();
return (
<Form {...form}>
<form
@ -113,17 +111,17 @@ const AgentForm = ({ node }: INextOperatorForm) => {
)}
/>
</FormContainer>
<ToolPopover>
<BlockButton>Add Tool</BlockButton>
</ToolPopover>
<BlockButton
onClick={addCanvasNode(Operator.Agent, {
nodeId: node?.id,
position: Position.Bottom,
})}
>
Add Agent
</BlockButton>
<FormContainer>
<AgentTools></AgentTools>
<BlockButton
onClick={addCanvasNode(Operator.Agent, {
nodeId: node?.id,
position: Position.Bottom,
})}
>
Add Agent
</BlockButton>
</FormContainer>
<Output list={outputList}></Output>
</form>
</Form>

View File

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

View File

@ -2,17 +2,26 @@ import { IAgentForm } from '@/interfaces/database/agent';
import { AgentFormContext } from '@/pages/agent/context';
import useGraphStore from '@/pages/agent/store';
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() {
const { updateNodeForm } = useGraphStore((state) => state);
const node = useContext(AgentFormContext);
const tools = useGetNodeTools();
const updateNodeTools = useCallback(
(value: string[]) => {
if (node?.id) {
const tools: IAgentForm['tools'] = get(node, 'data.form.tools');
const nextValue = value.reduce<IAgentForm['tools']>((pre, cur) => {
const tool = tools.find((x) => x.component_name === cur);
pre.push(tool ? tool : { component_name: cur, params: {} });
@ -22,8 +31,37 @@ export function useUpdateAgentNodeTools() {
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 { get, isEmpty } from 'lodash';
import { useMemo } from 'react';
import { Operator, initialAgentValues } from '../../constant';
import { initialAgentValues } from '../../constant';
export function useValues(node?: RAGFlowNodeType) {
const llmId = useFetchModelId();
@ -28,48 +28,3 @@ export function useValues(node?: RAGFlowNodeType) {
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 { useWatchFormChange } from './use-watch-change';
const MessageForm = ({ node }: INextOperatorForm) => {
const TavilyForm = ({ node }: INextOperatorForm) => {
const values = useValues(node);
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) => {
return <section>xxx</section>;
const EmptyContent = () => <div></div>;
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;

View File

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

View File

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

View File

@ -35,6 +35,7 @@ export type RFState = {
selectedNodeIds: string[];
selectedEdgeIds: string[];
clickedNodeId: string; // currently selected node
clickedToolId: string; // currently selected tool id
onNodesChange: OnNodesChange<RAGFlowNodeType>;
onEdgesChange: OnEdgesChange;
onConnect: OnConnect;
@ -73,6 +74,7 @@ export type RFState = {
updateNodeName: (id: string, name: string) => void;
generateNodeName: (name: string) => string;
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
@ -84,6 +86,7 @@ const useGraphStore = create<RFState>()(
selectedNodeIds: [] as string[],
selectedEdgeIds: [] as string[],
clickedNodeId: '',
clickedToolId: '',
onNodesChange: (changes) => {
set({
nodes: applyNodeChanges(changes, get().nodes),
@ -465,6 +468,9 @@ const useGraphStore = create<RFState>()(
return generateNodeNamesWithIncreasingIndex(name, nodes);
},
setClickedToolId: (id?: string) => {
set({ clickedToolId: id });
},
})),
{ name: 'graph', trace: true },
),