浏览代码

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)
tags/v0.19.1
balibabu 4 个月前
父节点
当前提交
972fd919b4
没有帐户链接到提交者的电子邮件

+ 13
- 3
web/src/pages/agent/canvas/node/tool-node.tsx 查看文件

@@ -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>

+ 53
- 0
web/src/pages/agent/form/agent-form/agent-tools.tsx 查看文件

@@ -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>
);
}

+ 13
- 15
web/src/pages/agent/form/agent-form/index.tsx 查看文件

@@ -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>

+ 15
- 14
web/src/pages/agent/form/agent-form/tool-popover/index.tsx 查看文件

@@ -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 (

+ 43
- 5
web/src/pages/agent/form/agent-form/tool-popover/use-update-tools.ts 查看文件

@@ -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],
);

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 { updateNodeTools };
return { deleteNodeTool };
}

+ 24
- 0
web/src/pages/agent/form/agent-form/use-delete-tool-node.ts 查看文件

@@ -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 };
}

+ 15
- 0
web/src/pages/agent/form/agent-form/use-get-tools.ts 查看文件

@@ -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 };
}

+ 1
- 46
web/src/pages/agent/form/agent-form/use-values.ts 查看文件

@@ -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;
}

+ 2
- 2
web/src/pages/agent/form/tavily-form/index.tsx 查看文件

@@ -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;

+ 36
- 0
web/src/pages/agent/form/tool-form/constant.ts 查看文件

@@ -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,
};

+ 16
- 3
web/src/pages/agent/form/tool-form/index.tsx 查看文件

@@ -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;

+ 2
- 2
web/src/pages/agent/hooks/use-add-node.ts 查看文件

@@ -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>>();


+ 6
- 4
web/src/pages/agent/hooks/use-show-drawer.tsx 查看文件

@@ -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 (

+ 6
- 0
web/src/pages/agent/store.ts 查看文件

@@ -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 },
),

正在加载...
取消
保存