mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-06-26 22:19:57 +00:00
### 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:
parent
fa3e90c72e
commit
972fd919b4
@ -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
web/src/pages/agent/form/agent-form/agent-tools.tsx
Normal file
53
web/src/pages/agent/form/agent-form/agent-tools.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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],
|
||||
);
|
||||
|
||||
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 };
|
||||
}
|
||||
|
24
web/src/pages/agent/form/agent-form/use-delete-tool-node.ts
Normal file
24
web/src/pages/agent/form/agent-form/use-delete-tool-node.ts
Normal 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 };
|
||||
}
|
15
web/src/pages/agent/form/agent-form/use-get-tools.ts
Normal file
15
web/src/pages/agent/form/agent-form/use-get-tools.ts
Normal 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 };
|
||||
}
|
@ -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;
|
||||
|
36
web/src/pages/agent/form/tool-form/constant.ts
Normal file
36
web/src/pages/agent/form/tool-form/constant.ts
Normal 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,
|
||||
};
|
@ -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 },
|
||||
),
|
||||
|
Loading…
x
Reference in New Issue
Block a user