Переглянути джерело

Feat: Synchronize MCP data to agent #3221 (#8832)

### What problem does this PR solve?

Feat: Synchronize MCP data to agent #3221
### Type of change


- [x] New Feature (non-breaking change which adds functionality)
tags/v0.20.0
balibabu 3 місяці тому
джерело
коміт
f683580310
Аккаунт користувача з таким Email не знайдено

+ 9
- 5
web/src/hooks/use-mcp-request.ts Переглянути файл

@@ -5,6 +5,7 @@ import {
IMcpServer,
IMcpServerListResponse,
IMCPTool,
IMCPToolRecord,
} from '@/interfaces/database/mcp';
import {
IImportMcpServersRequestBody,
@@ -16,6 +17,7 @@ import mcpServerService, {
} from '@/services/mcp-server-service';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useDebounce } from 'ahooks';
import { useState } from 'react';
import {
useGetPaginationWithRouter,
useHandleSearchChange,
@@ -201,17 +203,19 @@ export const useExportMcpServer = () => {
};

export const useListMcpServerTools = () => {
const { data, isFetching: loading } = useQuery({
const [ids, setIds] = useState<string[]>([]);
const { data, isFetching: loading } = useQuery<IMCPToolRecord>({
queryKey: [McpApiAction.ListMcpServerTools],
initialData: [],
initialData: {} as IMCPToolRecord,
gcTime: 0,
enabled: ids.length > 0,
queryFn: async () => {
const { data } = await mcpServerService.listTools();
return data?.data ?? [];
const { data } = await mcpServerService.listTools({ mcp_ids: ids });
return data?.data ?? {};
},
});

return { data, loading };
return { data, loading, setIds };
};

export const useTestMcpServer = () => {

+ 2
- 0
web/src/interfaces/database/mcp.ts Переглянути файл

@@ -11,6 +11,8 @@ export interface IMcpServer {

export type IMCPToolObject = Record<string, Omit<IMCPTool, 'name'>>;

export type IMCPToolRecord = Record<string, IMCPTool>;

export interface IMcpServerListResponse {
mcp_servers: IMcpServer[];
total: number;

+ 27
- 12
web/src/pages/agent/form-sheet/next.tsx Переглянути файл

@@ -11,11 +11,13 @@ import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { cn } from '@/lib/utils';
import { lowerFirst } from 'lodash';
import { Play, X } from 'lucide-react';
import { useMemo } from 'react';
import { BeginId, Operator } from '../constant';
import { AgentFormContext } from '../context';
import { RunTooltip } from '../flow-tooltip';
import { useHandleNodeNameChange } from '../hooks/use-change-node-name';
import OperatorIcon from '../operator-icon';
import useGraphStore from '../store';
import { needsSingleStepDebugging } from '../utils';
import SingleDebugDrawer from './single-debug-drawer';
import { useFormConfigMap } from './use-form-config-map';
@@ -40,6 +42,7 @@ const FormSheet = ({
showSingleDebugDrawer,
}: IModalProps<any> & IProps) => {
const operatorName: Operator = node?.data.label as Operator;
const clickedToolId = useGraphStore((state) => state.clickedToolId);

const FormConfigMap = useFormConfigMap();

@@ -52,6 +55,13 @@ const FormSheet = ({
data: node?.data,
});

const isMcp = useMemo(() => {
return (
operatorName === Operator.Tool &&
Object.values(Operator).every((x) => x !== clickedToolId)
);
}, [clickedToolId, operatorName]);

const { t } = useTranslate('flow');

return (
@@ -67,18 +77,23 @@ const FormSheet = ({
<section className="flex-col border-b py-2 px-5">
<div className="flex items-center gap-2 pb-3">
<OperatorIcon name={operatorName}></OperatorIcon>
<div className="flex items-center gap-1 flex-1">
<label htmlFor="">{t('title')}</label>
{node?.id === BeginId ? (
<span>{t(BeginId)}</span>
) : (
<Input
value={name}
onBlur={handleNameBlur}
onChange={handleNameChange}
></Input>
)}
</div>

{isMcp ? (
<div className="flex-1">MCP Config</div>
) : (
<div className="flex items-center gap-1 flex-1">
<label htmlFor="">{t('title')}</label>
{node?.id === BeginId ? (
<span>{t(BeginId)}</span>
) : (
<Input
value={name}
onBlur={handleNameBlur}
onChange={handleNameChange}
></Input>
)}
</div>
)}

{needsSingleStepDebugging(operatorName) && (
<RunTooltip>

+ 2
- 0
web/src/pages/agent/form/tool-form/index.tsx Переглянути файл

@@ -1,5 +1,6 @@
import useGraphStore from '../../store';
import { ToolFormConfigMap } from './constant';
import MCPForm from './mcp-form';

const EmptyContent = () => <div></div>;

@@ -8,6 +9,7 @@ const ToolForm = () => {

const ToolForm =
ToolFormConfigMap[clickedToolId as keyof typeof ToolFormConfigMap] ??
MCPForm ??
EmptyContent;

return (

+ 103
- 0
web/src/pages/agent/form/tool-form/mcp-form/index.tsx Переглянути файл

@@ -0,0 +1,103 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Checkbox } from '@/components/ui/checkbox';
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from '@/components/ui/form';
import { useGetMcpServer } from '@/hooks/use-mcp-request';
import useGraphStore from '@/pages/agent/store';
import { zodResolver } from '@hookform/resolvers/zod';
import { memo } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { MCPCard } from './mcp-card';
import { useValues } from './use-values';
import { useWatchFormChange } from './use-watch-change';

const FormSchema = z.object({
items: z.array(z.string()),
});

function MCPForm() {
const clickedToolId = useGraphStore((state) => state.clickedToolId);
const values = useValues();
const form = useForm({
defaultValues: values,
resolver: zodResolver(FormSchema),
});
const { data } = useGetMcpServer(clickedToolId);

useWatchFormChange(form);

return (
<Form {...form}>
<form
className="space-y-6 p-4"
onSubmit={(e) => {
e.preventDefault();
}}
>
<Card className="bg-background-highlight p-5">
<CardHeader className="p-0 pb-3">
<CardTitle>{data.name}</CardTitle>
</CardHeader>
<CardContent className="p-0">
<span className="pr-2"> URL:</span>
<a href={data.url} className="text-background-checked">
{data.url}
</a>
</CardContent>
</Card>
<FormField
control={form.control}
name="items"
render={() => (
<FormItem className="space-y-2">
{Object.entries(data.variables?.tools || {}).map(
([name, mcp]) => (
<FormField
key={name}
control={form.control}
name="items"
render={({ field }) => {
return (
<FormItem
key={name}
className="flex flex-row items-center gap-2"
>
<FormControl>
<MCPCard key={name} data={{ ...mcp, name }}>
<Checkbox
className="translate-y-1"
checked={field.value?.includes(name)}
onCheckedChange={(checked) => {
return checked
? field.onChange([...field.value, name])
: field.onChange(
field.value?.filter(
(value) => value !== name,
),
);
}}
/>
</MCPCard>
</FormControl>
</FormItem>
);
}}
/>
),
)}
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
);
}

export default memo(MCPForm);

+ 20
- 0
web/src/pages/agent/form/tool-form/mcp-form/mcp-card.tsx Переглянути файл

@@ -0,0 +1,20 @@
import { Card, CardContent, CardTitle } from '@/components/ui/card';
import { IMCPTool } from '@/interfaces/database/mcp';
import { PropsWithChildren } from 'react';

export function MCPCard({
data,
children,
}: { data: IMCPTool } & PropsWithChildren) {
return (
<Card className="p-3">
<CardContent className="p-0 flex gap-3">
{children}
<section>
<CardTitle className="pb-3">{data.name}</CardTitle>
<p>{data.description}</p>
</section>
</CardContent>
</Card>
);
}

+ 26
- 0
web/src/pages/agent/form/tool-form/mcp-form/use-values.ts Переглянути файл

@@ -0,0 +1,26 @@
import useGraphStore from '@/pages/agent/store';
import { getAgentNodeMCP } from '@/pages/agent/utils';
import { isEmpty } from 'lodash';
import { useMemo } from 'react';

export function useValues() {
const { clickedToolId, clickedNodeId, findUpstreamNodeById } = useGraphStore(
(state) => state,
);

const values = useMemo(() => {
const agentNode = findUpstreamNodeById(clickedNodeId);
const mcpList = getAgentNodeMCP(agentNode);

const formData =
mcpList.find((x) => x.mcp_id === clickedToolId)?.tools || {};

if (isEmpty(formData)) {
return { items: [] };
}

return { items: Object.keys(formData) };
}, [clickedNodeId, clickedToolId, findUpstreamNodeById]);

return values;
}

+ 47
- 0
web/src/pages/agent/form/tool-form/mcp-form/use-watch-change.ts Переглянути файл

@@ -0,0 +1,47 @@
import { useGetMcpServer } from '@/hooks/use-mcp-request';
import useGraphStore from '@/pages/agent/store';
import { getAgentNodeMCP } from '@/pages/agent/utils';
import { pick } from 'lodash';
import { useEffect, useMemo } from 'react';
import { UseFormReturn, useWatch } from 'react-hook-form';

export function useWatchFormChange(form?: UseFormReturn<any>) {
let values = useWatch({ control: form?.control });
const { clickedToolId, clickedNodeId, findUpstreamNodeById, updateNodeForm } =
useGraphStore((state) => state);
const { data } = useGetMcpServer(clickedToolId);

const nextMCPTools = useMemo(() => {
const mcpTools = data.variables?.tools || [];
values = form?.getValues();

return pick(mcpTools, values.items);
}, [values, data?.variables]);

useEffect(() => {
const agentNode = findUpstreamNodeById(clickedNodeId);
// Manually triggered form updates are synchronized to the canvas
if (agentNode) {
const agentNodeId = agentNode?.id;
const mcpList = getAgentNodeMCP(agentNode);

const nextMCP = mcpList.map((x) => {
if (x.mcp_id === clickedToolId) {
return {
...x,
tools: nextMCPTools,
};
}
return x;
});

updateNodeForm(agentNodeId, nextMCP, ['mcp']);
}
}, [
clickedNodeId,
clickedToolId,
findUpstreamNodeById,
nextMCPTools,
updateNodeForm,
]);
}

+ 5
- 0
web/src/pages/agent/utils.ts Переглянути файл

@@ -569,6 +569,11 @@ export function getAgentNodeTools(agentNode?: RAGFlowNodeType) {
return tools;
}

export function getAgentNodeMCP(agentNode?: RAGFlowNodeType) {
const tools: IAgentForm['mcp'] = get(agentNode, 'data.form.mcp', []);
return tools;
}

export function mapEdgeMouseEvent(
edges: Edge[],
edgeId: string,

+ 1
- 1
web/src/services/mcp-server-service.ts Переглянути файл

@@ -48,7 +48,7 @@ const methods = {
},
listTools: {
url: listMcpServerTools,
method: 'get',
method: 'post',
},
testTool: {
url: testMcpServerTool,

Завантаження…
Відмінити
Зберегти