Browse Source

Feat: Render the mcp list on the agent page #3221 (#8829)

### What problem does this PR solve?
Feat: Render the mcp list on the agent page #3221

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
tags/v0.20.0
balibabu 3 months ago
parent
commit
ab4ad0f373
No account linked to committer's email address

+ 4
- 0
web/src/interfaces/database/agent.ts View File

@@ -159,6 +159,10 @@ export interface IAgentForm {
component_name: string;
params: Record<string, any>;
}>;
mcp: Array<{
mcp_id: string;
tools: Record<string, Record<string, any>>;
}>;
outputs: {
structured_output: Record<string, Record<string, any>>;
content: Record<string, any>;

+ 19
- 6
web/src/pages/agent/canvas/node/tool-node.tsx View File

@@ -4,18 +4,15 @@ import { get } from 'lodash';
import { memo, useCallback } from 'react';
import { NodeHandleId } from '../../constant';
import { ToolCard } from '../../form/agent-form/agent-tools';
import { useFindMcpById } from '../../hooks/use-find-mcp-by-id';
import useGraphStore from '../../store';
import { NodeWrapper } from './node-wrapper';

function InnerToolNode({
id,
data,
isConnectable = true,
selected,
}: NodeProps<IToolNode>) {
function InnerToolNode({ id, isConnectable = true }: NodeProps<IToolNode>) {
const { edges, getNode } = useGraphStore((state) => state);
const upstreamAgentNodeId = edges.find((x) => x.target === id)?.source;
const upstreamAgentNode = getNode(upstreamAgentNodeId);
const { findMcpById } = useFindMcpById();

const handleClick = useCallback(() => {}, []);

@@ -25,6 +22,12 @@ function InnerToolNode({
[],
);

const mcpList: IAgentForm['mcp'] = get(
upstreamAgentNode,
'data.form.mcp',
[],
);

return (
<NodeWrapper>
<Handle
@@ -44,6 +47,16 @@ function InnerToolNode({
{x.component_name}
</ToolCard>
))}
{mcpList.map((x) => (
<ToolCard
key={x.mcp_id}
onClick={handleClick}
className="cursor-pointer"
data-tool={x.mcp_id}
>
{findMcpById(x.mcp_id)?.name}
</ToolCard>
))}
</ul>
</NodeWrapper>
);

+ 1
- 0
web/src/pages/agent/constant.tsx View File

@@ -697,6 +697,7 @@ export const initialAgentValues = {
exception_comment: '',
exception_goto: '',
tools: [],
mcp: [],
outputs: {
structured_output: {
// topic: {

+ 16
- 1
web/src/pages/agent/form/agent-form/agent-tools.tsx View File

@@ -5,12 +5,14 @@ import { PencilLine, X } from 'lucide-react';
import { PropsWithChildren, useCallback, useContext, useMemo } from 'react';
import { Operator } from '../../constant';
import { AgentInstanceContext } from '../../context';
import { useFindMcpById } from '../../hooks/use-find-mcp-by-id';
import { INextOperatorForm } from '../../interface';
import useGraphStore from '../../store';
import { filterDownstreamAgentNodeIds } from '../../utils/filter-downstream-nodes';
import { ToolPopover } from './tool-popover';
import { useDeleteAgentNodeMCP } from './tool-popover/use-update-mcp';
import { useDeleteAgentNodeTools } from './tool-popover/use-update-tools';
import { useGetAgentToolNames } from './use-get-tools';
import { useGetAgentMCPIds, useGetAgentToolNames } from './use-get-tools';

export function ToolCard({
children,
@@ -59,6 +61,9 @@ function ActionButton<T>({ edit, deleteRecord, record }: ActionButtonProps<T>) {
export function AgentTools() {
const { toolNames } = useGetAgentToolNames();
const { deleteNodeTool } = useDeleteAgentNodeTools();
const { mcpIds } = useGetAgentMCPIds();
const { findMcpById } = useFindMcpById();
const { deleteNodeMCP } = useDeleteAgentNodeMCP();

return (
<section className="space-y-2.5">
@@ -74,6 +79,16 @@ export function AgentTools() {
></ActionButton>
</ToolCard>
))}
{mcpIds.map((id) => (
<ToolCard key={id}>
{findMcpById(id)?.name}
<ActionButton
record={id}
edit={() => {}}
deleteRecord={deleteNodeMCP(id)}
></ActionButton>
</ToolCard>
))}
</ul>
<ToolPopover>
<BlockButton>Add Tool</BlockButton>

+ 49
- 14
web/src/pages/agent/form/agent-form/tool-popover/index.tsx View File

@@ -3,15 +3,22 @@ import {
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Operator } from '@/pages/agent/constant';
import { AgentFormContext, AgentInstanceContext } from '@/pages/agent/context';
import useGraphStore from '@/pages/agent/store';
import { Position } from '@xyflow/react';
import { PropsWithChildren, useCallback, useContext } from 'react';
import { useGetAgentToolNames } from '../use-get-tools';
import { ToolCommand } from './tool-command';
import { PropsWithChildren, useCallback, useContext, useEffect } from 'react';
import { useGetAgentMCPIds, useGetAgentToolNames } from '../use-get-tools';
import { MCPCommand, ToolCommand } from './tool-command';
import { useUpdateAgentNodeMCP } from './use-update-mcp';
import { useUpdateAgentNodeTools } from './use-update-tools';

enum ToolType {
Common = 'common',
MCP = 'mcp',
}

export function ToolPopover({ children }: PropsWithChildren) {
const { addCanvasNode } = useContext(AgentInstanceContext);
const node = useContext(AgentFormContext);
@@ -20,29 +27,57 @@ export function ToolPopover({ children }: PropsWithChildren) {
const deleteAgentToolNodeById = useGraphStore(
(state) => state.deleteAgentToolNodeById,
);
const { mcpIds } = useGetAgentMCPIds();
const { updateNodeMCP } = useUpdateAgentNodeMCP();

const handleChange = useCallback(
(value: string[]) => {
if (Array.isArray(value) && node?.id) {
updateNodeTools(value);
if (value.length > 0) {
addCanvasNode(Operator.Tool, {
position: Position.Bottom,
nodeId: node?.id,
})();
} else {
deleteAgentToolNodeById(node.id); // TODO: The tool node should be derived from the agent tools data
}
}
},
[addCanvasNode, deleteAgentToolNodeById, node?.id, updateNodeTools],
[node?.id, updateNodeTools],
);

useEffect(() => {
const total = toolNames.length + mcpIds.length;
if (node?.id) {
if (total > 0) {
addCanvasNode(Operator.Tool, {
position: Position.Bottom,
nodeId: node?.id,
})();
} else {
deleteAgentToolNodeById(node.id);
}
}
}, [
addCanvasNode,
deleteAgentToolNodeById,
mcpIds.length,
node?.id,
toolNames.length,
]);

return (
<Popover>
<PopoverTrigger asChild>{children}</PopoverTrigger>
<PopoverContent className="w-80 p-0">
<ToolCommand onChange={handleChange} value={toolNames}></ToolCommand>
<PopoverContent className="w-80 p-4">
<Tabs defaultValue={ToolType.Common}>
<TabsList>
<TabsTrigger value={ToolType.Common}>Built-in</TabsTrigger>
<TabsTrigger value={ToolType.MCP}>MCP</TabsTrigger>
</TabsList>
<TabsContent value={ToolType.Common}>
<ToolCommand
onChange={handleChange}
value={toolNames}
></ToolCommand>
</TabsContent>
<TabsContent value={ToolType.MCP}>
<MCPCommand value={mcpIds} onChange={updateNodeMCP}></MCPCommand>
</TabsContent>
</Tabs>
</PopoverContent>
</Popover>
);

+ 85
- 23
web/src/pages/agent/form/agent-form/tool-popover/tool-command.tsx View File

@@ -8,9 +8,10 @@ import {
CommandItem,
CommandList,
} from '@/components/ui/command';
import { useListMcpServer } from '@/hooks/use-mcp-request';
import { cn } from '@/lib/utils';
import { Operator } from '@/pages/agent/constant';
import { useCallback, useEffect, useState } from 'react';
import { PropsWithChildren, useCallback, useEffect, useState } from 'react';

const Menus = [
{
@@ -52,7 +53,36 @@ type ToolCommandProps = {
onChange?(values: string[]): void;
};

export function ToolCommand({ value, onChange }: ToolCommandProps) {
type ToolCommandItemProps = {
toggleOption(id: string): void;
id: string;
isSelected: boolean;
} & ToolCommandProps;

function ToolCommandItem({
toggleOption,
id,
isSelected,
children,
}: ToolCommandItemProps & PropsWithChildren) {
return (
<CommandItem className="cursor-pointer" onSelect={() => toggleOption(id)}>
<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>
{children}
</CommandItem>
);
}

function useHandleSelectChange({ onChange, value }: ToolCommandProps) {
const [currentValue, setCurrentValue] = useState<string[]>([]);

const toggleOption = useCallback(
@@ -72,8 +102,20 @@ export function ToolCommand({ value, onChange }: ToolCommandProps) {
}
}, [value]);

return {
toggleOption,
currentValue,
};
}

export function ToolCommand({ value, onChange }: ToolCommandProps) {
const { toggleOption, currentValue } = useHandleSelectChange({
onChange,
value,
});

return (
<Command className="rounded-lg border shadow-md md:min-w-[450px]">
<Command>
<CommandInput placeholder="Type a command or search..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
@@ -82,28 +124,17 @@ export function ToolCommand({ value, onChange }: ToolCommandProps) {
{x.list.map((y) => {
const isSelected = currentValue.includes(y);
return (
<CommandItem
<ToolCommandItem
key={y}
className="cursor-pointer"
onSelect={() => toggleOption(y)}
id={y}
toggleOption={toggleOption}
isSelected={isSelected}
>
<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>
<>
<Calendar />
<span>{y}</span>
</>
</ToolCommandItem>
);
})}
</CommandGroup>
@@ -112,3 +143,34 @@ export function ToolCommand({ value, onChange }: ToolCommandProps) {
</Command>
);
}

export function MCPCommand({ onChange, value }: ToolCommandProps) {
const { data } = useListMcpServer();
const { toggleOption, currentValue } = useHandleSelectChange({
onChange,
value,
});

return (
<Command>
<CommandInput placeholder="Type a command or search..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
{data.mcp_servers.map((item) => {
const isSelected = currentValue.includes(item.id);

return (
<ToolCommandItem
key={item.id}
id={item.id}
isSelected={isSelected}
toggleOption={toggleOption}
>
{item.name}
</ToolCommandItem>
);
})}
</CommandList>
</Command>
);
}

+ 74
- 0
web/src/pages/agent/form/agent-form/tool-popover/use-update-mcp.ts View File

@@ -0,0 +1,74 @@
import { useListMcpServer } from '@/hooks/use-mcp-request';
import { IAgentForm } from '@/interfaces/database/agent';
import { AgentFormContext } from '@/pages/agent/context';
import useGraphStore from '@/pages/agent/store';
import { get } from 'lodash';
import { useCallback, useContext, useMemo } from 'react';

export function useGetNodeMCP() {
const node = useContext(AgentFormContext);

return useMemo(() => {
const mcp: IAgentForm['mcp'] = get(node, 'data.form.mcp');
return mcp;
}, [node]);
}

export function useUpdateAgentNodeMCP() {
const { updateNodeForm } = useGraphStore((state) => state);
const node = useContext(AgentFormContext);
const mcpList = useGetNodeMCP();
const { data } = useListMcpServer();
const mcpServers = data.mcp_servers;

const findMcpTools = useCallback(
(mcpId: string) => {
const mcp = mcpServers.find((x) => x.id === mcpId);
return mcp?.variables.tools;
},
[mcpServers],
);

const updateNodeMCP = useCallback(
(value: string[]) => {
if (node?.id) {
const nextValue = value.reduce<IAgentForm['mcp']>((pre, cur) => {
const mcp = mcpList.find((x) => x.mcp_id === cur);
const tools = findMcpTools(cur);
if (mcp) {
pre.push(mcp);
} else if (tools) {
pre.push({
mcp_id: cur,
tools,
});
}
return pre;
}, []);

updateNodeForm(node?.id, nextValue, ['mcp']);
}
},
[node?.id, updateNodeForm, mcpList, findMcpTools],
);

return { updateNodeMCP };
}

export function useDeleteAgentNodeMCP() {
const { updateNodeForm } = useGraphStore((state) => state);
const mcpList = useGetNodeMCP();
const node = useContext(AgentFormContext);

const deleteNodeMCP = useCallback(
(value: string) => () => {
const nextMCP = mcpList.filter((x) => x.mcp_id !== value);
if (node?.id) {
updateNodeForm(node?.id, nextMCP, ['mcp']);
}
},
[node?.id, mcpList, updateNodeForm],
);

return { deleteNodeMCP };
}

+ 2
- 15
web/src/pages/agent/form/agent-form/tool-popover/use-update-tools.ts View File

@@ -45,35 +45,22 @@ export function useUpdateAgentNodeTools() {
[node?.id, tools, updateNodeForm],
);

const deleteNodeTool = useCallback(
(value: string) => {
updateNodeTools([value]);
},
[updateNodeTools],
);

return { updateNodeTools, deleteNodeTool };
return { updateNodeTools };
}

export function useDeleteAgentNodeTools() {
const { updateNodeForm } = useGraphStore((state) => state);
const tools = useGetNodeTools();
const node = useContext(AgentFormContext);
const deleteAgentToolNodeById = useGraphStore(
(state) => state.deleteAgentToolNodeById,
);

const deleteNodeTool = useCallback(
(value: string) => () => {
const nextTools = tools.filter((x) => x.component_name !== value);
if (node?.id) {
updateNodeForm(node?.id, nextTools, ['tools']);
if (nextTools.length === 0) {
deleteAgentToolNodeById(node?.id);
}
}
},
[deleteAgentToolNodeById, node?.id, tools, updateNodeForm],
[node?.id, tools, updateNodeForm],
);

return { deleteNodeTool };

+ 11
- 0
web/src/pages/agent/form/agent-form/use-get-tools.ts View File

@@ -13,3 +13,14 @@ export function useGetAgentToolNames() {

return { toolNames };
}

export function useGetAgentMCPIds() {
const node = useContext(AgentFormContext);

const mcpIds = useMemo(() => {
const ids: IAgentForm['mcp'] = get(node, 'data.form.mcp', []);
return ids.map((x) => x.mcp_id);
}, [node]);

return { mcpIds };
}

+ 0
- 0
web/src/pages/agent/form/tool-form/mcp-form/index.tsx View File


+ 12
- 0
web/src/pages/agent/hooks/use-find-mcp-by-id.ts View File

@@ -0,0 +1,12 @@
import { useListMcpServer } from '@/hooks/use-mcp-request';

export function useFindMcpById() {
const { data } = useListMcpServer();

const findMcpById = (id: string) =>
data.mcp_servers.find((item) => item.id === id);

return {
findMcpById,
};
}

+ 1
- 1
web/src/pages/dataset/dataset/index.tsx View File

@@ -90,7 +90,7 @@ export default function Dataset() {
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={showCreateModal}>
{t('fileManager.newFolder')}
{t('knowledgeDetails.emptyFiles')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

+ 26
- 1
web/src/pages/profile-setting/mcp/edit-mcp-form.tsx View File

@@ -15,9 +15,12 @@ import { Input } from '@/components/ui/input';
import { RAGFlowSelect } from '@/components/ui/select';
import { IModalProps } from '@/interfaces/common';
import { buildOptions } from '@/utils/form';
import { Editor, loader } from '@monaco-editor/react';
import { Dispatch, SetStateAction } from 'react';
import { useTranslation } from 'react-i18next';

loader.config({ paths: { vs: '/vs' } });

export const FormId = 'EditMcpForm';

export enum ServerType {
@@ -50,7 +53,7 @@ export function useBuildFormSchema() {
message: t('common.namePlaceholder'),
})
.trim(),
// variables: z.object({}).optional(),
headers: z.record(z.string(), z.any()).optional(),
});

return FormSchema;
@@ -137,6 +140,28 @@ export function EditMcpForm({
</FormItem>
)}
/>
<FormField
control={form.control}
name="headers"
render={({ field }) => (
<FormItem>
<FormLabel>Headers</FormLabel>
<FormControl>
<Editor
height={200}
defaultLanguage="json"
theme="vs-dark"
{...field}
onChange={(value) => {
field.onChange(value);
setFieldChanged(true);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
);

Loading…
Cancel
Save