mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 12:32:30 +08:00
### 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)
This commit is contained in:
@ -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>;
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -697,6 +697,7 @@ export const initialAgentValues = {
|
||||
exception_comment: '',
|
||||
exception_goto: '',
|
||||
tools: [],
|
||||
mcp: [],
|
||||
outputs: {
|
||||
structured_output: {
|
||||
// topic: {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
@ -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 };
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
|
||||
12
web/src/pages/agent/hooks/use-find-mcp-by-id.ts
Normal file
12
web/src/pages/agent/hooks/use-find-mcp-by-id.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@ -90,7 +90,7 @@ export default function Dataset() {
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={showCreateModal}>
|
||||
{t('fileManager.newFolder')}
|
||||
{t('knowledgeDetails.emptyFiles')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user