Feat: Add sql form #3221 (#8874)

### What problem does this PR solve?

Feat: Add sql form #3221

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-07-16 16:25:50 +08:00
committed by GitHub
parent 8b7dbb349e
commit d2df669135
13 changed files with 323 additions and 113 deletions

View File

@ -1,3 +1,4 @@
import message from '@/components/ui/message';
import { AgentGlobals } from '@/constants/agent'; import { AgentGlobals } from '@/constants/agent';
import { ITraceData } from '@/interfaces/database/agent'; import { ITraceData } from '@/interfaces/database/agent';
import { DSL, IFlow, IFlowTemplate } from '@/interfaces/database/flow'; import { DSL, IFlow, IFlowTemplate } from '@/interfaces/database/flow';
@ -8,7 +9,6 @@ import flowService from '@/services/flow-service';
import { buildMessageListWithUuid } from '@/utils/chat'; import { buildMessageListWithUuid } from '@/utils/chat';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useDebounce } from 'ahooks'; import { useDebounce } from 'ahooks';
import { message } from 'antd';
import { get, set } from 'lodash'; import { get, set } from 'lodash';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -29,6 +29,7 @@ export const enum AgentApiAction {
FetchAgentTemplates = 'fetchAgentTemplates', FetchAgentTemplates = 'fetchAgentTemplates',
UploadCanvasFile = 'uploadCanvasFile', UploadCanvasFile = 'uploadCanvasFile',
Trace = 'trace', Trace = 'trace',
TestDbConnect = 'testDbConnect',
} }
export const EmptyDsl = { export const EmptyDsl = {
@ -127,7 +128,7 @@ export const useFetchAgentListByPage = () => {
const onInputChange: React.ChangeEventHandler<HTMLInputElement> = useCallback( const onInputChange: React.ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => { (e) => {
// setPagination({ page: 1 }); // TODO: 这里导致重复请求 // setPagination({ page: 1 });
handleInputChange(e); handleInputChange(e);
}, },
[handleInputChange], [handleInputChange],
@ -331,3 +332,24 @@ export const useFetchMessageTrace = () => {
return { data, loading, refetch, setMessageId }; return { data, loading, refetch, setMessageId };
}; };
export const useTestDbConnect = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [AgentApiAction.TestDbConnect],
mutationFn: async (params: any) => {
const ret = await flowService.testDbConnect(params);
if (ret?.data?.code === 0) {
message.success(ret?.data?.data);
} else {
message.error(ret?.data?.data);
}
return ret;
},
});
return { data, loading, testDbConnect: mutateAsync };
};

View File

@ -98,7 +98,11 @@ function AccordionOperators() {
<AccordionTrigger className="text-xl">Tools</AccordionTrigger> <AccordionTrigger className="text-xl">Tools</AccordionTrigger>
<AccordionContent className="flex flex-col gap-4 text-balance"> <AccordionContent className="flex flex-col gap-4 text-balance">
<OperatorItemList <OperatorItemList
operators={[Operator.TavilySearch, Operator.Crawler]} operators={[
Operator.TavilySearch,
Operator.Crawler,
Operator.ExeSQL,
]}
></OperatorItemList> ></OperatorItemList>
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>

View File

@ -20,7 +20,6 @@ import {
import { ModelVariableType } from '@/constants/knowledge'; import { ModelVariableType } from '@/constants/knowledge';
import i18n from '@/locales/config'; import i18n from '@/locales/config';
import { setInitialChatVariableEnabledFieldValue } from '@/utils/chat'; import { setInitialChatVariableEnabledFieldValue } from '@/utils/chat';
import { omit } from 'lodash';
// DuckDuckGo's channel options // DuckDuckGo's channel options
export enum Channel { export enum Channel {
@ -562,7 +561,7 @@ export const initialExeSqlValues = {
password: '', password: '',
loop: 3, loop: 3,
top_n: 30, top_n: 30,
...initialQueryBaseValues, query: '',
}; };
export const initialSwitchValues = { export const initialSwitchValues = {
@ -960,16 +959,6 @@ export enum VariableType {
File = 'file', File = 'file',
} }
export const DefaultAgentToolValuesMap = {
[Operator.Retrieval]: {
...omit(initialRetrievalValues, 'query'),
description: '',
},
[Operator.TavilySearch]: {
api_key: '',
},
};
export enum AgentExceptionMethod { export enum AgentExceptionMethod {
Comment = 'comment', Comment = 'comment',
Goto = 'goto', Goto = 'goto',

View File

@ -1,6 +1,7 @@
import { IAgentForm } from '@/interfaces/database/agent'; import { IAgentForm } from '@/interfaces/database/agent';
import { DefaultAgentToolValuesMap } from '@/pages/agent/constant'; import { Operator } from '@/pages/agent/constant';
import { AgentFormContext } from '@/pages/agent/context'; import { AgentFormContext } from '@/pages/agent/context';
import { useAgentToolInitialValues } from '@/pages/agent/hooks/use-agent-tool-initial-values';
import useGraphStore from '@/pages/agent/store'; import useGraphStore from '@/pages/agent/store';
import { get } from 'lodash'; import { get } from 'lodash';
import { useCallback, useContext, useMemo } from 'react'; import { useCallback, useContext, useMemo } from 'react';
@ -18,6 +19,7 @@ export function useUpdateAgentNodeTools() {
const { updateNodeForm } = useGraphStore((state) => state); const { updateNodeForm } = useGraphStore((state) => state);
const node = useContext(AgentFormContext); const node = useContext(AgentFormContext);
const tools = useGetNodeTools(); const tools = useGetNodeTools();
const { initializeAgentToolValues } = useAgentToolInitialValues();
const updateNodeTools = useCallback( const updateNodeTools = useCallback(
(value: string[]) => { (value: string[]) => {
@ -30,10 +32,7 @@ export function useUpdateAgentNodeTools() {
: { : {
component_name: cur, component_name: cur,
name: cur, name: cur,
params: params: initializeAgentToolValues(cur as Operator),
DefaultAgentToolValuesMap[
cur as keyof typeof DefaultAgentToolValuesMap
] || {},
}, },
); );
return pre; return pre;
@ -42,7 +41,7 @@ export function useUpdateAgentNodeTools() {
updateNodeForm(node?.id, nextValue, ['tools']); updateNodeForm(node?.id, nextValue, ['tools']);
} }
}, },
[node?.id, tools, updateNodeForm], [initializeAgentToolValues, node?.id, tools, updateNodeForm],
); );
return { updateNodeTools }; return { updateNodeTools };

View File

@ -0,0 +1,16 @@
type FormProps = React.ComponentProps<'form'>;
export function FormWrapper({ children, ...props }: FormProps) {
return (
<form
className="space-y-6 p-4"
autoComplete="off"
onSubmit={(e) => {
e.preventDefault();
}}
{...props}
>
{children}
</form>
);
}

View File

@ -1,86 +1,158 @@
import LLMSelect from '@/components/llm-select'; import { LargeModelFormField } from '@/components/large-model-form-field';
import TopNItem from '@/components/top-n-item'; import { SelectWithSearch } from '@/components/originui/select-with-search';
import { TopNFormField } from '@/components/top-n-item';
import { ButtonLoading } from '@/components/ui/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input, NumberInput } from '@/components/ui/input';
import { useTranslate } from '@/hooks/common-hooks'; import { useTranslate } from '@/hooks/common-hooks';
import { useTestDbConnect } from '@/hooks/flow-hooks'; import { zodResolver } from '@hookform/resolvers/zod';
import { Button, Flex, Form, Input, InputNumber, Select } from 'antd'; import { useForm, useFormContext } from 'react-hook-form';
import { useCallback } from 'react'; import { z } from 'zod';
import { IOperatorForm } from '../../interface'; import { initialExeSqlValues } from '../../constant';
import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
import { ExeSQLOptions } from '../../options'; import { ExeSQLOptions } from '../../options';
import DynamicInputVariable from '../components/dynamic-input-variable'; import { FormWrapper } from '../components/form-wrapper';
import { QueryVariable } from '../components/query-variable';
import { FormSchema, useSubmitForm } from './use-submit-form';
const ExeSQLForm = ({ onValuesChange, form, node }: IOperatorForm) => { export function ExeSQLFormWidgets({ loading }: { loading: boolean }) {
const form = useFormContext();
const { t } = useTranslate('flow'); const { t } = useTranslate('flow');
const { testDbConnect, loading } = useTestDbConnect();
const handleTest = useCallback(async () => {
const ret = await form?.validateFields();
testDbConnect(ret);
}, [form, testDbConnect]);
return ( return (
<Form <>
name="basic" <LargeModelFormField></LargeModelFormField>
autoComplete="off" <FormField
form={form} control={form.control}
onValuesChange={onValuesChange} name="db_type"
layout={'vertical'} render={({ field }) => (
> <FormItem>
<DynamicInputVariable node={node}></DynamicInputVariable> <FormLabel>{t('dbType')}</FormLabel>
<Form.Item <FormControl>
name={'llm_id'} <SelectWithSearch
label={t('model', { keyPrefix: 'chat' })} {...field}
tooltip={t('modelTip', { keyPrefix: 'chat' })} options={ExeSQLOptions}
> ></SelectWithSearch>
<LLMSelect></LLMSelect> </FormControl>
</Form.Item> <FormMessage />
<Form.Item </FormItem>
label={t('dbType')} )}
name={'db_type'} />
rules={[{ required: true }]} <FormField
> control={form.control}
<Select options={ExeSQLOptions}></Select> name="database"
</Form.Item> render={({ field }) => (
<Form.Item <FormItem>
label={t('database')} <FormLabel>{t('database')}</FormLabel>
name={'database'} <FormControl>
rules={[{ required: true }]} <Input {...field}></Input>
> </FormControl>
<Input></Input> <FormMessage />
</Form.Item> </FormItem>
<Form.Item )}
label={t('username')} />
name={'username'} <FormField
rules={[{ required: true }]} control={form.control}
> name="username"
<Input></Input> render={({ field }) => (
</Form.Item> <FormItem>
<Form.Item label={t('host')} name={'host'} rules={[{ required: true }]}> <FormLabel>{t('username')}</FormLabel>
<Input></Input> <FormControl>
</Form.Item> <Input {...field}></Input>
<Form.Item label={t('port')} name={'port'} rules={[{ required: true }]}> </FormControl>
<InputNumber></InputNumber> <FormMessage />
</Form.Item> </FormItem>
<Form.Item )}
label={t('password')} />
name={'password'} <FormField
rules={[{ required: true }]} control={form.control}
> name="host"
<Input.Password></Input.Password> render={({ field }) => (
</Form.Item> <FormItem>
<Form.Item <FormLabel>{t('host')}</FormLabel>
label={t('loop')} <FormControl>
name={'loop'} <Input {...field}></Input>
tooltip={t('loopTip')} </FormControl>
rules={[{ required: true }]} <FormMessage />
> </FormItem>
<InputNumber></InputNumber> )}
</Form.Item> />
<TopNItem initialValue={30} max={1000}></TopNItem> <FormField
<Flex justify={'end'}> control={form.control}
<Button type={'primary'} loading={loading} onClick={handleTest}> name="port"
render={({ field }) => (
<FormItem>
<FormLabel>{t('port')}</FormLabel>
<FormControl>
<NumberInput {...field}></NumberInput>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>{t('password')}</FormLabel>
<FormControl>
<Input {...field} type="password"></Input>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="loop"
render={({ field }) => (
<FormItem>
<FormLabel tooltip={t('loopTip')}>{t('loop')}</FormLabel>
<FormControl>
<NumberInput {...field}></NumberInput>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<TopNFormField max={1000}></TopNFormField>
<div className="flex justify-end">
<ButtonLoading loading={loading} type="submit">
Test Test
</Button> </ButtonLoading>
</Flex> </div>
</>
);
}
const ExeSQLForm = ({ node }: INextOperatorForm) => {
const defaultValues = useFormValues(initialExeSqlValues, node);
const { onSubmit, loading } = useSubmitForm();
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues,
});
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper onSubmit={form.handleSubmit(onSubmit)}>
<QueryVariable></QueryVariable>
<ExeSQLFormWidgets loading={loading}></ExeSQLFormWidgets>
</FormWrapper>
</Form> </Form>
); );
}; };

View File

@ -0,0 +1,33 @@
import { useTestDbConnect } from '@/hooks/use-agent-request';
import { useCallback } from 'react';
import { z } from 'zod';
export const ExeSQLFormSchema = {
llm_id: z.string().min(1),
db_type: z.string().min(1),
database: z.string().min(1),
username: z.string().min(1),
host: z.string().min(1),
port: z.number(),
password: z.string().min(1),
loop: z.number(),
top_n: z.number(),
};
export const FormSchema = z.object({
query: z.string().optional(),
...ExeSQLFormSchema,
});
export function useSubmitForm() {
const { testDbConnect, loading } = useTestDbConnect();
const onSubmit = useCallback(
async (data: z.infer<typeof FormSchema>) => {
testDbConnect(data);
},
[testDbConnect],
);
return { loading, onSubmit };
}

View File

@ -5,7 +5,6 @@ import BingForm from '../bing-form';
import DeepLForm from '../deepl-form'; import DeepLForm from '../deepl-form';
import DuckDuckGoForm from '../duckduckgo-form'; import DuckDuckGoForm from '../duckduckgo-form';
import EmailForm from '../email-form'; import EmailForm from '../email-form';
import ExeSQLForm from '../exesql-form';
import GithubForm from '../github-form'; import GithubForm from '../github-form';
import GoogleForm from '../google-form'; import GoogleForm from '../google-form';
import GoogleScholarForm from '../google-scholar-form'; import GoogleScholarForm from '../google-scholar-form';
@ -13,6 +12,7 @@ import PubMedForm from '../pubmed-form';
import WikipediaForm from '../wikipedia-form'; import WikipediaForm from '../wikipedia-form';
import YahooFinanceForm from '../yahoo-finance-form'; import YahooFinanceForm from '../yahoo-finance-form';
import CrawlerForm from './crawler-form'; import CrawlerForm from './crawler-form';
import ExeSQLForm from './exesql-form';
import RetrievalForm from './retrieval-form'; import RetrievalForm from './retrieval-form';
import TavilyForm from './tavily-form'; import TavilyForm from './tavily-form';

View File

@ -2,6 +2,7 @@ import { Form } from '@/components/ui/form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { FormWrapper } from '../../components/form-wrapper';
import { import {
CrawlerExtractTypeFormField, CrawlerExtractTypeFormField,
CrawlerFormSchema, CrawlerFormSchema,
@ -27,15 +28,10 @@ const CrawlerForm = () => {
return ( return (
<Form {...form}> <Form {...form}>
<form <FormWrapper>
className="space-y-6 p-4"
onSubmit={(e) => {
e.preventDefault();
}}
>
<CrawlerProxyFormField></CrawlerProxyFormField> <CrawlerProxyFormField></CrawlerProxyFormField>
<CrawlerExtractTypeFormField></CrawlerExtractTypeFormField> <CrawlerExtractTypeFormField></CrawlerExtractTypeFormField>
</form> </FormWrapper>
</Form> </Form>
); );
}; };

View File

@ -0,0 +1,39 @@
import { Form } from '@/components/ui/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { FormWrapper } from '../../components/form-wrapper';
import { ExeSQLFormWidgets } from '../../exesql-form';
import {
ExeSQLFormSchema,
useSubmitForm,
} from '../../exesql-form/use-submit-form';
import { useValues } from '../use-values';
import { useWatchFormChange } from '../use-watch-change';
const FormSchema = z.object(ExeSQLFormSchema);
type FormType = z.infer<typeof FormSchema>;
const ExeSQLForm = () => {
const { onSubmit, loading } = useSubmitForm();
const defaultValues = useValues();
const form = useForm<FormType>({
resolver: zodResolver(FormSchema),
defaultValues: defaultValues as FormType,
});
useWatchFormChange(form);
return (
<Form {...form}>
<FormWrapper onSubmit={form.handleSubmit(onSubmit)}>
<ExeSQLFormWidgets loading={loading}></ExeSQLFormWidgets>
</FormWrapper>
</Form>
);
};
export default ExeSQLForm;

View File

@ -1,6 +1,7 @@
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { DefaultAgentToolValuesMap } from '../../constant'; import { Operator } from '../../constant';
import { useAgentToolInitialValues } from '../../hooks/use-agent-tool-initial-values';
import useGraphStore from '../../store'; import useGraphStore from '../../store';
import { getAgentNodeTools } from '../../utils'; import { getAgentNodeTools } from '../../utils';
@ -18,6 +19,7 @@ export function useValues() {
const { clickedToolId, clickedNodeId, findUpstreamNodeById } = useGraphStore( const { clickedToolId, clickedNodeId, findUpstreamNodeById } = useGraphStore(
(state) => state, (state) => state,
); );
const { initializeAgentToolValues } = useAgentToolInitialValues();
const values = useMemo(() => { const values = useMemo(() => {
const agentNode = findUpstreamNodeById(clickedNodeId); const agentNode = findUpstreamNodeById(clickedNodeId);
@ -28,10 +30,9 @@ export function useValues() {
)?.params; )?.params;
if (isEmpty(formData)) { if (isEmpty(formData)) {
const defaultValues = const defaultValues = initializeAgentToolValues(
DefaultAgentToolValuesMap[ clickedNodeId as Operator,
clickedToolId as keyof typeof DefaultAgentToolValuesMap );
];
return defaultValues; return defaultValues;
} }
@ -39,7 +40,12 @@ export function useValues() {
return { return {
...formData, ...formData,
}; };
}, [clickedNodeId, clickedToolId, findUpstreamNodeById]); }, [
clickedNodeId,
clickedToolId,
findUpstreamNodeById,
initializeAgentToolValues,
]);
return values; return values;
} }

View File

@ -135,7 +135,7 @@ export const useInitializeOperatorParams = () => {
[initialFormValuesMap], [initialFormValuesMap],
); );
return initializeOperatorParams; return { initializeOperatorParams, initialFormValuesMap };
}; };
export const useGetNodeName = () => { export const useGetNodeName = () => {
@ -287,7 +287,7 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) {
(state) => state, (state) => state,
); );
const getNodeName = useGetNodeName(); const getNodeName = useGetNodeName();
const initializeOperatorParams = useInitializeOperatorParams(); const { initializeOperatorParams } = useInitializeOperatorParams();
const { calculateNewlyBackChildPosition } = useCalculateNewlyChildPosition(); const { calculateNewlyBackChildPosition } = useCalculateNewlyChildPosition();
const { addChildEdge } = useAddChildEdge(); const { addChildEdge } = useAddChildEdge();
const { addToolNode } = useAddToolNode(); const { addToolNode } = useAddToolNode();

View File

@ -0,0 +1,34 @@
import { omit } from 'lodash';
import { useCallback } from 'react';
import { Operator } from '../constant';
import { useInitializeOperatorParams } from './use-add-node';
export function useAgentToolInitialValues() {
const { initialFormValuesMap } = useInitializeOperatorParams();
const initializeAgentToolValues = useCallback(
(operatorName: Operator) => {
const initialValues = initialFormValuesMap[operatorName];
switch (operatorName) {
case Operator.Retrieval:
return {
...omit(initialValues, 'query'),
description: '',
};
case Operator.TavilySearch:
return {
api_key: '',
};
case Operator.ExeSQL:
return omit(initialValues, 'query');
default:
return initialValues;
}
},
[initialFormValuesMap],
);
return { initializeAgentToolValues };
}