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)
This commit is contained in:
balibabu 2025-06-18 12:36:44 +08:00 committed by GitHub
parent 6ce282d462
commit 371f61972d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 453 additions and 2 deletions

View File

@ -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[];
}

View File

@ -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 = {

View File

@ -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);

View File

@ -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);

View File

@ -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,

View File

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

View File

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

View File

@ -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;
}