mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-09-25 08:04:59 +00:00
### 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:
parent
6ce282d462
commit
371f61972d
@ -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 = {
|
||||
|
34
web/src/pages/agent/canvas/node/tool-node.tsx
Normal file
34
web/src/pages/agent/canvas/node/tool-node.tsx
Normal 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);
|
63
web/src/pages/agent/form/agent-form/dynamic-tool.tsx
Normal file
63
web/src/pages/agent/form/agent-form/dynamic-tool.tsx
Normal 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);
|
@ -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,
|
||||
|
18
web/src/pages/agent/form/agent-form/tool-popover/index.tsx
Normal file
18
web/src/pages/agent/form/agent-form/tool-popover/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user