### 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
| @@ -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<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[]; | |||
| } | |||
| @@ -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 = { | |||
| @@ -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<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); | |||
| @@ -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 ( | |||
| <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); | |||
| @@ -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 ( | |||
| <Form {...form}> | |||
| <form | |||
| @@ -110,6 +113,9 @@ const AgentForm = ({ node }: INextOperatorForm) => { | |||
| )} | |||
| /> | |||
| </FormContainer> | |||
| <ToolPopover> | |||
| <BlockButton>Add Tool</BlockButton> | |||
| </ToolPopover> | |||
| <BlockButton | |||
| onClick={addCanvasNode(Operator.Agent, { | |||
| nodeId: node?.id, | |||
| @@ -0,0 +1,18 @@ | |||
| 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> | |||
| ); | |||
| } | |||
| @@ -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<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> | |||
| ); | |||
| } | |||
| @@ -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; | |||
| } | |||