From 371f61972dd25b95c36c580aed076c6dc06a2e9c Mon Sep 17 00:00:00 2001 From: balibabu Date: Wed, 18 Jun 2025 12:36:44 +0800 Subject: [PATCH] Feat: Add tool nodes and tool drop-down menu #3221 (#8335) ### What problem does this PR solve? Feat: Add tool nodes and tool drop-down menu #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- web/src/interfaces/database/agent.ts | 165 ++++++++++++++++++ web/src/pages/agent/canvas/index.tsx | 2 + web/src/pages/agent/canvas/node/tool-node.tsx | 34 ++++ .../agent/form/agent-form/dynamic-tool.tsx | 63 +++++++ web/src/pages/agent/form/agent-form/index.tsx | 8 +- .../form/agent-form/tool-popover/index.tsx | 18 ++ .../agent-form/tool-popover/tool-command.tsx | 118 +++++++++++++ .../pages/agent/form/agent-form/use-values.ts | 47 ++++- 8 files changed, 453 insertions(+), 2 deletions(-) create mode 100644 web/src/pages/agent/canvas/node/tool-node.tsx create mode 100644 web/src/pages/agent/form/agent-form/dynamic-tool.tsx create mode 100644 web/src/pages/agent/form/agent-form/tool-popover/index.tsx create mode 100644 web/src/pages/agent/form/agent-form/tool-popover/tool-command.tsx diff --git a/web/src/interfaces/database/agent.ts b/web/src/interfaces/database/agent.ts index 2b6f23926..74d254d15 100644 --- a/web/src/interfaces/database/agent.ts +++ b/web/src/interfaces/database/agent.ts @@ -27,3 +27,168 @@ export interface ISwitchForm { end_cpn_ids: string[]; no: string; } + +import { Edge, Node } from '@xyflow/react'; +import { IReference, Message } from './chat'; + +export type DSLComponents = Record; + +export interface DSL { + components: DSLComponents; + history: any[]; + path?: string[][]; + answer?: any[]; + graph?: IGraph; + messages: Message[]; + reference: IReference[]; + globals: Record; + retrieval: IReference[]; +} + +export interface IOperator { + obj: IOperatorNode; + downstream: string[]; + upstream: string[]; + parent_id?: string; +} + +export interface IOperatorNode { + component_name: string; + params: Record; +} + +export declare interface IFlow { + avatar?: string; + canvas_type: null; + create_date: string; + create_time: number; + description: null; + dsl: DSL; + id: string; + title: string; + update_date: string; + update_time: number; + user_id: string; + permission: string; + nickname: string; +} + +export interface IFlowTemplate { + avatar: string; + canvas_type: string; + create_date: string; + create_time: number; + description: string; + dsl: DSL; + id: string; + title: string; + update_date: string; + update_time: number; +} + +export interface IGenerateForm { + max_tokens?: number; + temperature?: number; + top_p?: number; + presence_penalty?: number; + frequency_penalty?: number; + cite?: boolean; + prompt: number; + llm_id: string; + parameters: { key: string; component_id: string }; +} + +export interface ICategorizeForm extends IGenerateForm { + category_description: ICategorizeItemResult; +} + +export interface IRelevantForm extends IGenerateForm { + yes: string; + no: string; +} + +export interface ISwitchItem { + cpn_id: string; + operator: string; + value: string; +} + +export interface ISwitchForm { + conditions: ISwitchCondition[]; + end_cpn_id: string; + no: string; +} + +export interface IBeginForm { + prologue?: string; +} + +export interface IRetrievalForm { + similarity_threshold?: number; + keywords_similarity_weight?: number; + top_n?: number; + top_k?: number; + rerank_id?: string; + empty_response?: string; + kb_ids: string[]; +} + +export interface ICodeForm { + inputs?: Array<{ name?: string; component_id?: string }>; + lang: string; + script?: string; +} + +export type BaseNodeData = { + label: string; // operator type + name: string; // operator name + color?: string; + form?: TForm; +}; + +export type BaseNode = Node>; + +export type IBeginNode = BaseNode; +export type IRetrievalNode = BaseNode; +export type IGenerateNode = BaseNode; +export type ICategorizeNode = BaseNode; +export type ISwitchNode = BaseNode; +export type IRagNode = BaseNode; +export type IRelevantNode = BaseNode; +export type ILogicNode = BaseNode; +export type INoteNode = BaseNode; +export type IMessageNode = BaseNode; +export type IRewriteNode = BaseNode; +export type IInvokeNode = BaseNode; +export type ITemplateNode = BaseNode; +export type IEmailNode = BaseNode; +export type IIterationNode = BaseNode; +export type IIterationStartNode = BaseNode; +export type IKeywordNode = BaseNode; +export type ICodeNode = BaseNode; +export type IAgentNode = BaseNode; +export type IToolNode = BaseNode; + +export type RAGFlowNodeType = + | IBeginNode + | IRetrievalNode + | IGenerateNode + | ICategorizeNode + | ISwitchNode + | IRagNode + | IRelevantNode + | ILogicNode + | INoteNode + | IMessageNode + | IRewriteNode + | IInvokeNode + | ITemplateNode + | IEmailNode + | IIterationNode + | IIterationStartNode + | IKeywordNode; + +export interface IGraph { + nodes: RAGFlowNodeType[]; + edges: Edge[]; +} diff --git a/web/src/pages/agent/canvas/index.tsx b/web/src/pages/agent/canvas/index.tsx index 7833b8763..adae43a75 100644 --- a/web/src/pages/agent/canvas/index.tsx +++ b/web/src/pages/agent/canvas/index.tsx @@ -43,6 +43,7 @@ import { RetrievalNode } from './node/retrieval-node'; import { RewriteNode } from './node/rewrite-node'; import { SwitchNode } from './node/switch-node'; import { TemplateNode } from './node/template-node'; +import { ToolNode } from './node/tool-node'; const nodeTypes: NodeTypes = { ragNode: RagNode, @@ -63,6 +64,7 @@ const nodeTypes: NodeTypes = { group: IterationNode, iterationStartNode: IterationStartNode, agentNode: AgentNode, + toolNode: ToolNode, }; const edgeTypes = { diff --git a/web/src/pages/agent/canvas/node/tool-node.tsx b/web/src/pages/agent/canvas/node/tool-node.tsx new file mode 100644 index 000000000..fc963efdf --- /dev/null +++ b/web/src/pages/agent/canvas/node/tool-node.tsx @@ -0,0 +1,34 @@ +import { IToolNode } from '@/interfaces/database/agent'; +import { NodeProps, Position } from '@xyflow/react'; +import { memo } from 'react'; +import { NodeHandleId } from '../../constant'; +import { CommonHandle } from './handle'; +import { LeftHandleStyle } from './handle-icon'; +import NodeHeader from './node-header'; +import { NodeWrapper } from './node-wrapper'; +import { ToolBar } from './toolbar'; + +function InnerToolNode({ + id, + data, + isConnectable = true, + selected, +}: NodeProps) { + return ( + + + + + + + ); +} + +export const ToolNode = memo(InnerToolNode); diff --git a/web/src/pages/agent/form/agent-form/dynamic-tool.tsx b/web/src/pages/agent/form/agent-form/dynamic-tool.tsx new file mode 100644 index 000000000..afda465b6 --- /dev/null +++ b/web/src/pages/agent/form/agent-form/dynamic-tool.tsx @@ -0,0 +1,63 @@ +import { BlockButton, Button } from '@/components/ui/button'; +import { + FormControl, + FormField, + FormItem, + FormMessage, +} from '@/components/ui/form'; +import { X } from 'lucide-react'; +import { memo } from 'react'; +import { useFieldArray, useFormContext } from 'react-hook-form'; +import { PromptEditor } from '../components/prompt-editor'; + +const DynamicTool = () => { + const form = useFormContext(); + const name = 'tools'; + + const { fields, append, remove } = useFieldArray({ + name: name, + control: form.control, + }); + + return ( + +
+ {fields.map((field, index) => ( +
+
+ ( + + +
+ +
+
+
+ )} + /> +
+ +
+ ))} +
+ + append({ component_name: '' })}> + Add + +
+ ); +}; + +export default memo(DynamicTool); diff --git a/web/src/pages/agent/form/agent-form/index.tsx b/web/src/pages/agent/form/agent-form/index.tsx index d7998c618..99d18425c 100644 --- a/web/src/pages/agent/form/agent-form/index.tsx +++ b/web/src/pages/agent/form/agent-form/index.tsx @@ -21,7 +21,8 @@ import { AgentInstanceContext } from '../../context'; import { INextOperatorForm } from '../../interface'; import { Output } from '../components/output'; import { PromptEditor } from '../components/prompt-editor'; -import { useValues } from './use-values'; +import { ToolPopover } from './tool-popover'; +import { useToolOptions, useValues } from './use-values'; import { useWatchFormChange } from './use-watch-change'; const FormSchema = z.object({ @@ -66,6 +67,8 @@ const AgentForm = ({ node }: INextOperatorForm) => { const { addCanvasNode } = useContext(AgentInstanceContext); + const toolOptions = useToolOptions(); + return (
{ )} /> + + Add Tool + + {children} + + + + + ); +} diff --git a/web/src/pages/agent/form/agent-form/tool-popover/tool-command.tsx b/web/src/pages/agent/form/agent-form/tool-popover/tool-command.tsx new file mode 100644 index 000000000..6ee3c5f12 --- /dev/null +++ b/web/src/pages/agent/form/agent-form/tool-popover/tool-command.tsx @@ -0,0 +1,118 @@ +import { Calendar, CheckIcon } from 'lucide-react'; + +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@/components/ui/command'; +import { cn } from '@/lib/utils'; +import { Operator } from '@/pages/flow/constant'; +import { useCallback, useEffect, useState } from 'react'; + +const Menus = [ + { + label: 'Search', + list: [ + Operator.Google, + Operator.Bing, + Operator.DuckDuckGo, + Operator.Wikipedia, + Operator.YahooFinance, + Operator.PubMed, + Operator.GoogleScholar, + ], + }, + { + label: 'Communication', + list: [Operator.Email], + }, + { + label: 'Productivity', + list: [], + }, + { + label: 'Developer', + list: [ + Operator.GitHub, + Operator.ExeSQL, + Operator.Invoke, + Operator.Crawler, + Operator.Code, + ], + }, +]; + +const Options = Menus.reduce((pre, cur) => { + pre.push(...cur.list); + return pre; +}, []); + +type ToolCommandProps = { + value?: string[]; + onChange?(values: string[]): void; +}; + +export function ToolCommand({ value, onChange }: ToolCommandProps) { + const [currentValue, setCurrentValue] = useState([]); + console.log('🚀 ~ ToolCommand ~ currentValue:', currentValue); + + const toggleOption = useCallback( + (option: string) => { + const newSelectedValues = currentValue.includes(option) + ? currentValue.filter((value) => value !== option) + : [...currentValue, option]; + setCurrentValue(newSelectedValues); + onChange?.(newSelectedValues); + }, + [currentValue, onChange], + ); + + useEffect(() => { + if (Array.isArray(value)) { + setCurrentValue(value); + } + }, [value]); + + return ( + + + + No results found. + {Menus.map((x) => ( + + {x.list.map((y) => { + const isSelected = currentValue.includes(y); + return ( + toggleOption(y)} + > +
+ +
+ {/* {option.icon && ( + + )} */} + {/* {option.label} */} + + {y} +
+ ); + })} +
+ ))} +
+
+ ); +} diff --git a/web/src/pages/agent/form/agent-form/use-values.ts b/web/src/pages/agent/form/agent-form/use-values.ts index 3e6b057a6..5a854bcd0 100644 --- a/web/src/pages/agent/form/agent-form/use-values.ts +++ b/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 { initialAgentValues } from '../../constant'; +import { Operator, initialAgentValues } from '../../constant'; export function useValues(node?: RAGFlowNodeType) { const llmId = useFetchModelId(); @@ -28,3 +28,48 @@ 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; +}