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