### 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)tags/v0.19.1
| end_cpn_ids: string[]; | end_cpn_ids: string[]; | ||||
| no: string; | no: string; | ||||
| } | } | ||||
| import { Edge, Node } from '@xyflow/react'; | |||||
| import { IReference, Message } from './chat'; | |||||
| export type DSLComponents = Record<string, IOperator>; | |||||
| export interface DSL { | |||||
| components: DSLComponents; | |||||
| history: any[]; | |||||
| path?: string[][]; | |||||
| answer?: any[]; | |||||
| graph?: IGraph; | |||||
| messages: Message[]; | |||||
| reference: IReference[]; | |||||
| globals: Record<string, any>; | |||||
| retrieval: IReference[]; | |||||
| } | |||||
| export interface IOperator { | |||||
| obj: IOperatorNode; | |||||
| downstream: string[]; | |||||
| upstream: string[]; | |||||
| parent_id?: string; | |||||
| } | |||||
| export interface IOperatorNode { | |||||
| component_name: string; | |||||
| params: Record<string, unknown>; | |||||
| } | |||||
| 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<TForm extends any> = { | |||||
| label: string; // operator type | |||||
| name: string; // operator name | |||||
| color?: string; | |||||
| form?: TForm; | |||||
| }; | |||||
| export type BaseNode<T = any> = Node<BaseNodeData<T>>; | |||||
| export type IBeginNode = BaseNode<IBeginForm>; | |||||
| export type IRetrievalNode = BaseNode<IRetrievalForm>; | |||||
| export type IGenerateNode = BaseNode<IGenerateForm>; | |||||
| export type ICategorizeNode = BaseNode<ICategorizeForm>; | |||||
| export type ISwitchNode = BaseNode<ISwitchForm>; | |||||
| 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<ICodeForm>; | |||||
| 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[]; | |||||
| } |
| import { RewriteNode } from './node/rewrite-node'; | import { RewriteNode } from './node/rewrite-node'; | ||||
| import { SwitchNode } from './node/switch-node'; | import { SwitchNode } from './node/switch-node'; | ||||
| import { TemplateNode } from './node/template-node'; | import { TemplateNode } from './node/template-node'; | ||||
| import { ToolNode } from './node/tool-node'; | |||||
| const nodeTypes: NodeTypes = { | const nodeTypes: NodeTypes = { | ||||
| ragNode: RagNode, | ragNode: RagNode, | ||||
| group: IterationNode, | group: IterationNode, | ||||
| iterationStartNode: IterationStartNode, | iterationStartNode: IterationStartNode, | ||||
| agentNode: AgentNode, | agentNode: AgentNode, | ||||
| toolNode: ToolNode, | |||||
| }; | }; | ||||
| const edgeTypes = { | const edgeTypes = { |
| 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<IToolNode>) { | |||||
| return ( | |||||
| <ToolBar selected={selected} id={id} label={data.label}> | |||||
| <NodeWrapper> | |||||
| <CommonHandle | |||||
| id={NodeHandleId.End} | |||||
| type="target" | |||||
| position={Position.Top} | |||||
| isConnectable={isConnectable} | |||||
| style={LeftHandleStyle} | |||||
| nodeId={id} | |||||
| ></CommonHandle> | |||||
| <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> | |||||
| </NodeWrapper> | |||||
| </ToolBar> | |||||
| ); | |||||
| } | |||||
| export const ToolNode = memo(InnerToolNode); |
| 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 ( | |||||
| <FormItem> | |||||
| <div className="space-y-4"> | |||||
| {fields.map((field, index) => ( | |||||
| <div key={field.id} className="flex"> | |||||
| <div className="space-y-2 flex-1"> | |||||
| <FormField | |||||
| control={form.control} | |||||
| name={`${name}.${index}.component_name`} | |||||
| render={({ field }) => ( | |||||
| <FormItem className="flex-1"> | |||||
| <FormControl> | |||||
| <section> | |||||
| <PromptEditor | |||||
| {...field} | |||||
| showToolbar={false} | |||||
| ></PromptEditor> | |||||
| </section> | |||||
| </FormControl> | |||||
| </FormItem> | |||||
| )} | |||||
| /> | |||||
| </div> | |||||
| <Button | |||||
| type="button" | |||||
| variant={'ghost'} | |||||
| onClick={() => remove(index)} | |||||
| > | |||||
| <X /> | |||||
| </Button> | |||||
| </div> | |||||
| ))} | |||||
| </div> | |||||
| <FormMessage /> | |||||
| <BlockButton onClick={() => append({ component_name: '' })}> | |||||
| Add | |||||
| </BlockButton> | |||||
| </FormItem> | |||||
| ); | |||||
| }; | |||||
| export default memo(DynamicTool); |
| import { INextOperatorForm } from '../../interface'; | import { INextOperatorForm } from '../../interface'; | ||||
| import { Output } from '../components/output'; | import { Output } from '../components/output'; | ||||
| import { PromptEditor } from '../components/prompt-editor'; | 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'; | import { useWatchFormChange } from './use-watch-change'; | ||||
| const FormSchema = z.object({ | const FormSchema = z.object({ | ||||
| const { addCanvasNode } = useContext(AgentInstanceContext); | const { addCanvasNode } = useContext(AgentInstanceContext); | ||||
| const toolOptions = useToolOptions(); | |||||
| return ( | return ( | ||||
| <Form {...form}> | <Form {...form}> | ||||
| <form | <form | ||||
| )} | )} | ||||
| /> | /> | ||||
| </FormContainer> | </FormContainer> | ||||
| <ToolPopover> | |||||
| <BlockButton>Add Tool</BlockButton> | |||||
| </ToolPopover> | |||||
| <BlockButton | <BlockButton | ||||
| onClick={addCanvasNode(Operator.Agent, { | onClick={addCanvasNode(Operator.Agent, { | ||||
| nodeId: node?.id, | nodeId: node?.id, |
| import { | |||||
| Popover, | |||||
| PopoverContent, | |||||
| PopoverTrigger, | |||||
| } from '@/components/ui/popover'; | |||||
| import { PropsWithChildren } from 'react'; | |||||
| import { ToolCommand } from './tool-command'; | |||||
| export function ToolPopover({ children }: PropsWithChildren) { | |||||
| return ( | |||||
| <Popover> | |||||
| <PopoverTrigger asChild>{children}</PopoverTrigger> | |||||
| <PopoverContent className="w-80 p-0"> | |||||
| <ToolCommand></ToolCommand> | |||||
| </PopoverContent> | |||||
| </Popover> | |||||
| ); | |||||
| } |
| 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<string[]>((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<string[]>([]); | |||||
| 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 ( | |||||
| <Command className="rounded-lg border shadow-md md:min-w-[450px]"> | |||||
| <CommandInput placeholder="Type a command or search..." /> | |||||
| <CommandList> | |||||
| <CommandEmpty>No results found.</CommandEmpty> | |||||
| {Menus.map((x) => ( | |||||
| <CommandGroup heading={x.label} key={x.label}> | |||||
| {x.list.map((y) => { | |||||
| const isSelected = currentValue.includes(y); | |||||
| return ( | |||||
| <CommandItem | |||||
| key={y} | |||||
| className="cursor-pointer" | |||||
| onSelect={() => toggleOption(y)} | |||||
| > | |||||
| <div | |||||
| className={cn( | |||||
| 'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary', | |||||
| isSelected | |||||
| ? 'bg-primary text-primary-foreground' | |||||
| : 'opacity-50 [&_svg]:invisible', | |||||
| )} | |||||
| > | |||||
| <CheckIcon className="h-4 w-4" /> | |||||
| </div> | |||||
| {/* {option.icon && ( | |||||
| <option.icon className="mr-2 h-4 w-4 text-muted-foreground" /> | |||||
| )} */} | |||||
| {/* <span>{option.label}</span> */} | |||||
| <Calendar /> | |||||
| <span>{y}</span> | |||||
| </CommandItem> | |||||
| ); | |||||
| })} | |||||
| </CommandGroup> | |||||
| ))} | |||||
| </CommandList> | |||||
| </Command> | |||||
| ); | |||||
| } |
| import { RAGFlowNodeType } from '@/interfaces/database/flow'; | import { RAGFlowNodeType } from '@/interfaces/database/flow'; | ||||
| import { get, isEmpty } from 'lodash'; | import { get, isEmpty } from 'lodash'; | ||||
| import { useMemo } from 'react'; | import { useMemo } from 'react'; | ||||
| import { initialAgentValues } from '../../constant'; | |||||
| import { Operator, initialAgentValues } from '../../constant'; | |||||
| export function useValues(node?: RAGFlowNodeType) { | export function useValues(node?: RAGFlowNodeType) { | ||||
| const llmId = useFetchModelId(); | const llmId = useFetchModelId(); | ||||
| return values; | 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; | |||||
| } |