Skip to content

Commit 47005eb

Browse files
feat: supports multiple retrieval tool under an agent (#12046)
### What problem does this PR solve? Add support for multiple Retrieval tools under an agent ### Type of change - [x] New Feature (non-breaking change which adds functionality)
1 parent 3ee47e4 commit 47005eb

File tree

20 files changed

+442
-226
lines changed

20 files changed

+442
-226
lines changed

web/src/components/large-model-form-field.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export function LargeModelFormField({
6868
<FormItem>
6969
<FormControl>
7070
<DropdownMenu>
71-
<DropdownMenuTrigger>
71+
<DropdownMenuTrigger asChild>
7272
<Button variant={'ghost'}>
7373
<Funnel className="text-text-disabled" />
7474
</Button>

web/src/interfaces/database/agent.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ export interface IAgentForm {
170170
tools: Array<{
171171
name: string;
172172
component_name: string;
173+
id: string;
173174
params: Record<string, any>;
174175
}>;
175176
mcp: Array<{

web/src/pages/agent/canvas/node/tool-node.tsx

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { NodeCollapsible } from '@/components/collapse';
22
import { IAgentForm, IToolNode } from '@/interfaces/database/agent';
33
import { Handle, NodeProps, Position } from '@xyflow/react';
44
import { get } from 'lodash';
5-
import { MouseEventHandler, memo, useCallback } from 'react';
5+
import { memo } from 'react';
66
import { NodeHandleId, Operator } from '../../constant';
77
import { ToolCard } from '../../form/agent-form/agent-tools';
88
import { useFindMcpById } from '../../hooks/use-find-mcp-by-id';
@@ -15,22 +15,11 @@ function InnerToolNode({
1515
isConnectable = true,
1616
selected,
1717
}: NodeProps<IToolNode>) {
18-
const { edges, getNode } = useGraphStore((state) => state);
18+
const { edges, getNode, setClickedToolId } = useGraphStore();
1919
const upstreamAgentNodeId = edges.find((x) => x.target === id)?.source;
2020
const upstreamAgentNode = getNode(upstreamAgentNodeId);
2121
const { findMcpById } = useFindMcpById();
2222

23-
const handleClick = useCallback(
24-
(operator: string): MouseEventHandler<HTMLLIElement> =>
25-
(e) => {
26-
if (operator === Operator.Code) {
27-
e.preventDefault();
28-
e.stopPropagation();
29-
}
30-
},
31-
[],
32-
);
33-
3423
const tools: IAgentForm['tools'] = get(
3524
upstreamAgentNode,
3625
'data.form.tools',
@@ -51,36 +40,53 @@ function InnerToolNode({
5140
position={Position.Top}
5241
isConnectable={isConnectable}
5342
className="!bg-accent-primary !size-2"
54-
></Handle>
43+
/>
44+
5545
<NodeCollapsible items={[tools, mcpList]}>
5646
{(x) => {
57-
if ('mcp_id' in x) {
47+
if (Reflect.has(x, 'mcp_id')) {
5848
const mcp = x as unknown as IAgentForm['mcp'][number];
49+
5950
return (
6051
<ToolCard
6152
key={mcp.mcp_id}
62-
onClick={handleClick(mcp.mcp_id)}
53+
onClick={(e) => {
54+
if (mcp.mcp_id === Operator.Code) {
55+
e.preventDefault();
56+
e.stopPropagation();
57+
}
58+
}}
6359
className="cursor-pointer"
64-
data-tool={x.mcp_id}
60+
data-tool={mcp.mcp_id}
6561
>
6662
{findMcpById(mcp.mcp_id)?.name}
6763
</ToolCard>
6864
);
6965
}
7066

7167
const tool = x as unknown as IAgentForm['tools'][number];
68+
7269
return (
7370
<ToolCard
74-
key={tool.component_name}
75-
onClick={handleClick(tool.component_name)}
71+
key={tool.id}
72+
onClick={(e) => {
73+
if (tool.component_name === Operator.Code) {
74+
e.preventDefault();
75+
e.stopPropagation();
76+
}
77+
78+
setClickedToolId(tool.id || tool.component_name);
79+
}}
7680
className="cursor-pointer"
7781
data-tool={tool.component_name}
82+
data-tool-id={tool.id}
7883
>
7984
<div className="flex gap-1 items-center pointer-events-none">
80-
<OperatorIcon
81-
name={tool.component_name as Operator}
82-
></OperatorIcon>
83-
{tool.component_name}
85+
<OperatorIcon name={tool.component_name as Operator} />
86+
87+
{tool.component_name === Operator.Retrieval
88+
? tool.name
89+
: tool.component_name}
8490
</div>
8591
</ToolCard>
8692
);

web/src/pages/agent/canvas/node/toolbar.tsx

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Button, ButtonProps } from '@/components/ui/button';
12
import {
23
TooltipContent,
34
TooltipNode,
@@ -6,31 +7,24 @@ import {
67
import { cn } from '@/lib/utils';
78
import { Position } from '@xyflow/react';
89
import { Copy, Play, Trash2 } from 'lucide-react';
9-
import {
10-
HTMLAttributes,
11-
MouseEventHandler,
12-
PropsWithChildren,
13-
useCallback,
14-
} from 'react';
10+
import { MouseEventHandler, PropsWithChildren, useCallback } from 'react';
1511
import { Operator } from '../../constant';
1612
import { useDuplicateNode } from '../../hooks';
1713
import useGraphStore from '../../store';
1814

19-
function IconWrapper({
20-
children,
21-
className,
22-
...props
23-
}: HTMLAttributes<HTMLDivElement>) {
15+
function IconWrapper({ children, className, ...props }: ButtonProps) {
2416
return (
25-
<div
17+
<Button
18+
variant="secondary"
19+
size="icon"
2620
className={cn(
27-
'p-1.5 bg-bg-component border border-border-button rounded-sm cursor-pointer hover:text-text-primary',
21+
'size-7 p-0 bg-bg-component text-current hover:text-text-primary focus-visible:text-text-primary',
2822
className,
2923
)}
3024
{...props}
3125
>
3226
{children}
33-
</div>
27+
</Button>
3428
);
3529
}
3630

@@ -55,7 +49,7 @@ export function ToolBar({
5549
(store) => store.deleteIterationNodeById,
5650
);
5751

58-
const deleteNode: MouseEventHandler<HTMLDivElement> = useCallback(
52+
const deleteNode: MouseEventHandler<HTMLButtonElement> = useCallback(
5953
(e) => {
6054
e.stopPropagation();
6155
if ([Operator.Iteration, Operator.Loop].includes(label as Operator)) {
@@ -69,7 +63,7 @@ export function ToolBar({
6963

7064
const duplicateNode = useDuplicateNode();
7165

72-
const handleDuplicate: MouseEventHandler<HTMLDivElement> = useCallback(
66+
const handleDuplicate: MouseEventHandler<HTMLButtonElement> = useCallback(
7367
(e) => {
7468
e.stopPropagation();
7569
duplicateNode(id, label);
@@ -82,7 +76,7 @@ export function ToolBar({
8276
<TooltipTrigger className="h-full">{children}</TooltipTrigger>
8377

8478
<TooltipContent position={Position.Top}>
85-
<section className="flex gap-2 items-center text-text-secondary">
79+
<section className="flex gap-2 items-center text-text-secondary pb-2">
8680
{showRun && (
8781
<IconWrapper>
8882
<Play className="size-3.5" data-play />
@@ -94,8 +88,8 @@ export function ToolBar({
9488
</IconWrapper>
9589
)}
9690
<IconWrapper
97-
onClick={deleteNode}
9891
className="hover:text-state-error hover:border-state-error"
92+
onClick={deleteNode}
9993
>
10094
<Trash2 className="size-3.5" />
10195
</IconWrapper>

web/src/pages/agent/constant/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,7 @@ export const NoDebugOperatorsList = [
778778
Operator.Splitter,
779779
Operator.HierarchicalMerger,
780780
Operator.Extractor,
781+
Operator.Tool,
781782
];
782783

783784
export const NoCopyOperatorsList = [

web/src/pages/agent/form-sheet/next.tsx

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Button } from '@/components/ui/button';
12
import {
23
Sheet,
34
SheetContent,
@@ -41,49 +42,69 @@ const FormSheet = ({
4142
showSingleDebugDrawer,
4243
}: IModalProps<any> & IProps) => {
4344
const operatorName: Operator = node?.data.label as Operator;
44-
const clickedToolId = useGraphStore((state) => state.clickedToolId);
45+
const { clickedToolId, getAgentToolById } = useGraphStore();
4546

4647
const currentFormMap = FormConfigMap[operatorName];
47-
4848
const OperatorForm = currentFormMap?.component ?? EmptyContent;
49-
5049
const isMcp = useIsMcp(operatorName);
51-
5250
const { t } = useTranslate('flow');
51+
const { component_name: toolComponentName } = (getAgentToolById(
52+
clickedToolId,
53+
) ?? {}) as {
54+
component_name: Operator;
55+
name: string;
56+
id: string;
57+
};
5358

5459
return (
5560
<Sheet open={visible} modal={false}>
5661
<SheetContent
57-
className={cn('top-20 p-0 flex flex-col pb-20', {
62+
className={cn('top-20 p-0 flex flex-col pb-20 gap-0', {
5863
'right-[clamp(0px,34%,620px)]': chatVisible,
5964
})}
6065
closeIcon={false}
6166
>
6267
<SheetHeader>
6368
<SheetTitle className="hidden"></SheetTitle>
64-
<section className="flex-col border-b py-2 px-5">
69+
<section className="flex-col border-b pt-2 pb-4 px-5">
6570
<div className="flex items-center gap-2 pb-3">
66-
<OperatorIcon name={operatorName}></OperatorIcon>
71+
<OperatorIcon
72+
name={toolComponentName || operatorName}
73+
></OperatorIcon>
6774
<TitleInput node={node}></TitleInput>
6875
{needsSingleStepDebugging(operatorName) && (
6976
<RunTooltip>
70-
<CirclePlay
71-
className="size-3.5 cursor-pointer"
77+
<Button
78+
variant="ghost"
79+
size="icon"
80+
className="size-6 !p-0 bg-transparent"
7281
onClick={showSingleDebugDrawer}
73-
/>
82+
>
83+
<CirclePlay className="size-3.5 cursor-pointer" />
84+
</Button>
7485
</RunTooltip>
7586
)}
76-
<X onClick={hideModal} className="size-3.5 cursor-pointer" />
87+
88+
<Button
89+
variant="ghost"
90+
size="icon"
91+
className="size-6 !p-0 bg-transparent"
92+
onClick={hideModal}
93+
>
94+
<X className="size-3.5 cursor-pointer" />
95+
</Button>
7796
</div>
78-
{isMcp || (
79-
<span className="text-text-secondary">
97+
98+
{!isMcp && (
99+
<p className="text-text-secondary">
80100
{t(
81-
`${lowerFirst(operatorName === Operator.Tool ? clickedToolId : operatorName)}Description`,
101+
`${lowerFirst(operatorName === Operator.Tool ? toolComponentName : operatorName)}Description`,
82102
)}
83-
</span>
103+
</p>
84104
)}
85105
</section>
86106
</SheetHeader>
107+
87108
<section className="pt-4 overflow-auto flex-1">
88109
{visible && (
89110
<AgentFormContext.Provider value={node}>

web/src/pages/agent/form-sheet/title-input.tsx

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { Button } from '@/components/ui/button';
12
import { Input } from '@/components/ui/input';
23
import { RAGFlowNodeType } from '@/interfaces/database/agent';
34
import { PenLine } from 'lucide-react';
4-
import { useCallback, useState } from 'react';
5+
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
56
import { useTranslation } from 'react-i18next';
67
import { BeginId, Operator } from '../constant';
78
import { useHandleNodeNameChange } from '../hooks/use-change-node-name';
@@ -13,47 +14,75 @@ type TitleInputProps = {
1314

1415
export function TitleInput({ node }: TitleInputProps) {
1516
const { t } = useTranslation();
17+
const inputRef = useRef<HTMLInputElement>(null);
1618
const { name, handleNameBlur, handleNameChange } = useHandleNodeNameChange({
1719
id: node?.id,
1820
data: node?.data,
1921
});
2022

2123
const operatorName: Operator = node?.data.label as Operator;
22-
2324
const isMcp = useIsMcp(operatorName);
24-
2525
const [isEditingMode, setIsEditingMode] = useState(false);
2626

2727
const switchIsEditingMode = useCallback(() => {
2828
setIsEditingMode((prev) => !prev);
2929
}, []);
3030

31-
const handleBlur = useCallback(() => {
32-
handleNameBlur();
33-
setIsEditingMode(false);
34-
}, [handleNameBlur]);
31+
const handleBlur = useCallback(
32+
(e: React.FocusEvent<HTMLInputElement>) => {
33+
if (handleNameBlur()) {
34+
setIsEditingMode(false);
35+
} else {
36+
// Re-focus the input if name doesn't change successfully
37+
e.target.focus();
38+
e.target.select();
39+
}
40+
},
41+
[handleNameBlur],
42+
);
43+
44+
useLayoutEffect(() => {
45+
if (isEditingMode && inputRef.current) {
46+
inputRef.current.focus();
47+
inputRef.current.select();
48+
}
49+
}, [isEditingMode]);
3550

3651
if (isMcp) {
3752
return <div className="flex-1 text-base">MCP Config</div>;
3853
}
3954

4055
return (
41-
<div className="flex items-center gap-1 flex-1">
56+
// Give a fixed height to prevent layout shift when switching between edit and view modes
57+
<div className="flex items-center gap-1 flex-1 h-8 mr-2">
4258
{node?.id === BeginId ? (
43-
<span>{t(BeginId)}</span>
59+
// Begin node is not editable
60+
<span>{t(`flow.${BeginId}`)}</span>
4461
) : isEditingMode ? (
4562
<Input
63+
ref={inputRef}
4664
value={name}
4765
onBlur={handleBlur}
66+
onKeyDown={(e) => {
67+
// Support committing the value changes by pressing Enter
68+
if (e.key === 'Enter') {
69+
handleBlur(e as unknown as React.FocusEvent<HTMLInputElement>);
70+
}
71+
}}
4872
onChange={handleNameChange}
49-
></Input>
73+
/>
5074
) : (
5175
<div className="flex items-center gap-2.5 text-base">
5276
{name}
53-
<PenLine
77+
78+
<Button
79+
variant="transparent"
80+
size="icon"
81+
className="size-6 !p-0 border-0 bg-transparent"
5482
onClick={switchIsEditingMode}
55-
className="size-3.5 text-text-secondary cursor-pointer"
56-
/>
83+
>
84+
<PenLine className="size-3.5 text-text-secondary cursor-pointer" />
85+
</Button>
5786
</div>
5887
)}
5988
</div>

0 commit comments

Comments
 (0)