mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-09-26 08:34:02 +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[];
|
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[];
|
||||||
|
}
|
||||||
|
@ -43,6 +43,7 @@ import { RetrievalNode } from './node/retrieval-node';
|
|||||||
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,
|
||||||
@ -63,6 +64,7 @@ const nodeTypes: NodeTypes = {
|
|||||||
group: IterationNode,
|
group: IterationNode,
|
||||||
iterationStartNode: IterationStartNode,
|
iterationStartNode: IterationStartNode,
|
||||||
agentNode: AgentNode,
|
agentNode: AgentNode,
|
||||||
|
toolNode: ToolNode,
|
||||||
};
|
};
|
||||||
|
|
||||||
const edgeTypes = {
|
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 { 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({
|
||||||
@ -66,6 +67,8 @@ const AgentForm = ({ node }: INextOperatorForm) => {
|
|||||||
|
|
||||||
const { addCanvasNode } = useContext(AgentInstanceContext);
|
const { addCanvasNode } = useContext(AgentInstanceContext);
|
||||||
|
|
||||||
|
const toolOptions = useToolOptions();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
@ -110,6 +113,9 @@ const AgentForm = ({ node }: INextOperatorForm) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</FormContainer>
|
</FormContainer>
|
||||||
|
<ToolPopover>
|
||||||
|
<BlockButton>Add Tool</BlockButton>
|
||||||
|
</ToolPopover>
|
||||||
<BlockButton
|
<BlockButton
|
||||||
onClick={addCanvasNode(Operator.Agent, {
|
onClick={addCanvasNode(Operator.Agent, {
|
||||||
nodeId: node?.id,
|
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 { 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();
|
||||||
@ -28,3 +28,48 @@ export function useValues(node?: RAGFlowNodeType) {
|
|||||||
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user