### What problem does this PR solve? Feat: Add sql form #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.20.0
| @@ -1,3 +1,4 @@ | |||
| import message from '@/components/ui/message'; | |||
| import { AgentGlobals } from '@/constants/agent'; | |||
| import { ITraceData } from '@/interfaces/database/agent'; | |||
| import { DSL, IFlow, IFlowTemplate } from '@/interfaces/database/flow'; | |||
| @@ -8,7 +9,6 @@ import flowService from '@/services/flow-service'; | |||
| import { buildMessageListWithUuid } from '@/utils/chat'; | |||
| import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; | |||
| import { useDebounce } from 'ahooks'; | |||
| import { message } from 'antd'; | |||
| import { get, set } from 'lodash'; | |||
| import { useCallback, useState } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| @@ -29,6 +29,7 @@ export const enum AgentApiAction { | |||
| FetchAgentTemplates = 'fetchAgentTemplates', | |||
| UploadCanvasFile = 'uploadCanvasFile', | |||
| Trace = 'trace', | |||
| TestDbConnect = 'testDbConnect', | |||
| } | |||
| export const EmptyDsl = { | |||
| @@ -127,7 +128,7 @@ export const useFetchAgentListByPage = () => { | |||
| const onInputChange: React.ChangeEventHandler<HTMLInputElement> = useCallback( | |||
| (e) => { | |||
| // setPagination({ page: 1 }); // TODO: 这里导致重复请求 | |||
| // setPagination({ page: 1 }); | |||
| handleInputChange(e); | |||
| }, | |||
| [handleInputChange], | |||
| @@ -331,3 +332,24 @@ export const useFetchMessageTrace = () => { | |||
| 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 }; | |||
| }; | |||
| @@ -98,7 +98,11 @@ function AccordionOperators() { | |||
| <AccordionTrigger className="text-xl">Tools</AccordionTrigger> | |||
| <AccordionContent className="flex flex-col gap-4 text-balance"> | |||
| <OperatorItemList | |||
| operators={[Operator.TavilySearch, Operator.Crawler]} | |||
| operators={[ | |||
| Operator.TavilySearch, | |||
| Operator.Crawler, | |||
| Operator.ExeSQL, | |||
| ]} | |||
| ></OperatorItemList> | |||
| </AccordionContent> | |||
| </AccordionItem> | |||
| @@ -20,7 +20,6 @@ import { | |||
| import { ModelVariableType } from '@/constants/knowledge'; | |||
| import i18n from '@/locales/config'; | |||
| import { setInitialChatVariableEnabledFieldValue } from '@/utils/chat'; | |||
| import { omit } from 'lodash'; | |||
| // DuckDuckGo's channel options | |||
| export enum Channel { | |||
| @@ -562,7 +561,7 @@ export const initialExeSqlValues = { | |||
| password: '', | |||
| loop: 3, | |||
| top_n: 30, | |||
| ...initialQueryBaseValues, | |||
| query: '', | |||
| }; | |||
| export const initialSwitchValues = { | |||
| @@ -960,16 +959,6 @@ export enum VariableType { | |||
| File = 'file', | |||
| } | |||
| export const DefaultAgentToolValuesMap = { | |||
| [Operator.Retrieval]: { | |||
| ...omit(initialRetrievalValues, 'query'), | |||
| description: '', | |||
| }, | |||
| [Operator.TavilySearch]: { | |||
| api_key: '', | |||
| }, | |||
| }; | |||
| export enum AgentExceptionMethod { | |||
| Comment = 'comment', | |||
| Goto = 'goto', | |||
| @@ -1,6 +1,7 @@ | |||
| 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 { useAgentToolInitialValues } from '@/pages/agent/hooks/use-agent-tool-initial-values'; | |||
| import useGraphStore from '@/pages/agent/store'; | |||
| import { get } from 'lodash'; | |||
| import { useCallback, useContext, useMemo } from 'react'; | |||
| @@ -18,6 +19,7 @@ export function useUpdateAgentNodeTools() { | |||
| const { updateNodeForm } = useGraphStore((state) => state); | |||
| const node = useContext(AgentFormContext); | |||
| const tools = useGetNodeTools(); | |||
| const { initializeAgentToolValues } = useAgentToolInitialValues(); | |||
| const updateNodeTools = useCallback( | |||
| (value: string[]) => { | |||
| @@ -30,10 +32,7 @@ export function useUpdateAgentNodeTools() { | |||
| : { | |||
| component_name: cur, | |||
| name: cur, | |||
| params: | |||
| DefaultAgentToolValuesMap[ | |||
| cur as keyof typeof DefaultAgentToolValuesMap | |||
| ] || {}, | |||
| params: initializeAgentToolValues(cur as Operator), | |||
| }, | |||
| ); | |||
| return pre; | |||
| @@ -42,7 +41,7 @@ export function useUpdateAgentNodeTools() { | |||
| updateNodeForm(node?.id, nextValue, ['tools']); | |||
| } | |||
| }, | |||
| [node?.id, tools, updateNodeForm], | |||
| [initializeAgentToolValues, node?.id, tools, updateNodeForm], | |||
| ); | |||
| return { updateNodeTools }; | |||
| @@ -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> | |||
| ); | |||
| } | |||
| @@ -1,86 +1,158 @@ | |||
| import LLMSelect from '@/components/llm-select'; | |||
| import TopNItem from '@/components/top-n-item'; | |||
| import { LargeModelFormField } from '@/components/large-model-form-field'; | |||
| 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 { useTestDbConnect } from '@/hooks/flow-hooks'; | |||
| import { Button, Flex, Form, Input, InputNumber, Select } from 'antd'; | |||
| import { useCallback } from 'react'; | |||
| import { IOperatorForm } from '../../interface'; | |||
| import { zodResolver } from '@hookform/resolvers/zod'; | |||
| import { useForm, useFormContext } from 'react-hook-form'; | |||
| import { z } from 'zod'; | |||
| 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 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 { testDbConnect, loading } = useTestDbConnect(); | |||
| const handleTest = useCallback(async () => { | |||
| const ret = await form?.validateFields(); | |||
| testDbConnect(ret); | |||
| }, [form, testDbConnect]); | |||
| return ( | |||
| <Form | |||
| name="basic" | |||
| autoComplete="off" | |||
| form={form} | |||
| onValuesChange={onValuesChange} | |||
| layout={'vertical'} | |||
| > | |||
| <DynamicInputVariable node={node}></DynamicInputVariable> | |||
| <Form.Item | |||
| name={'llm_id'} | |||
| label={t('model', { keyPrefix: 'chat' })} | |||
| tooltip={t('modelTip', { keyPrefix: 'chat' })} | |||
| > | |||
| <LLMSelect></LLMSelect> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label={t('dbType')} | |||
| name={'db_type'} | |||
| rules={[{ required: true }]} | |||
| > | |||
| <Select options={ExeSQLOptions}></Select> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label={t('database')} | |||
| name={'database'} | |||
| rules={[{ required: true }]} | |||
| > | |||
| <Input></Input> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label={t('username')} | |||
| name={'username'} | |||
| rules={[{ required: true }]} | |||
| > | |||
| <Input></Input> | |||
| </Form.Item> | |||
| <Form.Item label={t('host')} name={'host'} rules={[{ required: true }]}> | |||
| <Input></Input> | |||
| </Form.Item> | |||
| <Form.Item label={t('port')} name={'port'} rules={[{ required: true }]}> | |||
| <InputNumber></InputNumber> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label={t('password')} | |||
| name={'password'} | |||
| rules={[{ required: true }]} | |||
| > | |||
| <Input.Password></Input.Password> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label={t('loop')} | |||
| name={'loop'} | |||
| tooltip={t('loopTip')} | |||
| rules={[{ required: true }]} | |||
| > | |||
| <InputNumber></InputNumber> | |||
| </Form.Item> | |||
| <TopNItem initialValue={30} max={1000}></TopNItem> | |||
| <Flex justify={'end'}> | |||
| <Button type={'primary'} loading={loading} onClick={handleTest}> | |||
| <> | |||
| <LargeModelFormField></LargeModelFormField> | |||
| <FormField | |||
| control={form.control} | |||
| name="db_type" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>{t('dbType')}</FormLabel> | |||
| <FormControl> | |||
| <SelectWithSearch | |||
| {...field} | |||
| options={ExeSQLOptions} | |||
| ></SelectWithSearch> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <FormField | |||
| control={form.control} | |||
| name="database" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>{t('database')}</FormLabel> | |||
| <FormControl> | |||
| <Input {...field}></Input> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <FormField | |||
| control={form.control} | |||
| name="username" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>{t('username')}</FormLabel> | |||
| <FormControl> | |||
| <Input {...field}></Input> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <FormField | |||
| control={form.control} | |||
| name="host" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>{t('host')}</FormLabel> | |||
| <FormControl> | |||
| <Input {...field}></Input> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <FormField | |||
| control={form.control} | |||
| 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 | |||
| </Button> | |||
| </Flex> | |||
| </ButtonLoading> | |||
| </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> | |||
| ); | |||
| }; | |||
| @@ -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 }; | |||
| } | |||
| @@ -5,7 +5,6 @@ import BingForm from '../bing-form'; | |||
| import DeepLForm from '../deepl-form'; | |||
| import DuckDuckGoForm from '../duckduckgo-form'; | |||
| import EmailForm from '../email-form'; | |||
| import ExeSQLForm from '../exesql-form'; | |||
| import GithubForm from '../github-form'; | |||
| import GoogleForm from '../google-form'; | |||
| import GoogleScholarForm from '../google-scholar-form'; | |||
| @@ -13,6 +12,7 @@ import PubMedForm from '../pubmed-form'; | |||
| import WikipediaForm from '../wikipedia-form'; | |||
| import YahooFinanceForm from '../yahoo-finance-form'; | |||
| import CrawlerForm from './crawler-form'; | |||
| import ExeSQLForm from './exesql-form'; | |||
| import RetrievalForm from './retrieval-form'; | |||
| import TavilyForm from './tavily-form'; | |||
| @@ -2,6 +2,7 @@ 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 { | |||
| CrawlerExtractTypeFormField, | |||
| CrawlerFormSchema, | |||
| @@ -27,15 +28,10 @@ const CrawlerForm = () => { | |||
| return ( | |||
| <Form {...form}> | |||
| <form | |||
| className="space-y-6 p-4" | |||
| onSubmit={(e) => { | |||
| e.preventDefault(); | |||
| }} | |||
| > | |||
| <FormWrapper> | |||
| <CrawlerProxyFormField></CrawlerProxyFormField> | |||
| <CrawlerExtractTypeFormField></CrawlerExtractTypeFormField> | |||
| </form> | |||
| </FormWrapper> | |||
| </Form> | |||
| ); | |||
| }; | |||
| @@ -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; | |||
| @@ -1,6 +1,7 @@ | |||
| import { isEmpty } from 'lodash'; | |||
| 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 { getAgentNodeTools } from '../../utils'; | |||
| @@ -18,6 +19,7 @@ export function useValues() { | |||
| const { clickedToolId, clickedNodeId, findUpstreamNodeById } = useGraphStore( | |||
| (state) => state, | |||
| ); | |||
| const { initializeAgentToolValues } = useAgentToolInitialValues(); | |||
| const values = useMemo(() => { | |||
| const agentNode = findUpstreamNodeById(clickedNodeId); | |||
| @@ -28,10 +30,9 @@ export function useValues() { | |||
| )?.params; | |||
| if (isEmpty(formData)) { | |||
| const defaultValues = | |||
| DefaultAgentToolValuesMap[ | |||
| clickedToolId as keyof typeof DefaultAgentToolValuesMap | |||
| ]; | |||
| const defaultValues = initializeAgentToolValues( | |||
| clickedNodeId as Operator, | |||
| ); | |||
| return defaultValues; | |||
| } | |||
| @@ -39,7 +40,12 @@ export function useValues() { | |||
| return { | |||
| ...formData, | |||
| }; | |||
| }, [clickedNodeId, clickedToolId, findUpstreamNodeById]); | |||
| }, [ | |||
| clickedNodeId, | |||
| clickedToolId, | |||
| findUpstreamNodeById, | |||
| initializeAgentToolValues, | |||
| ]); | |||
| return values; | |||
| } | |||
| @@ -135,7 +135,7 @@ export const useInitializeOperatorParams = () => { | |||
| [initialFormValuesMap], | |||
| ); | |||
| return initializeOperatorParams; | |||
| return { initializeOperatorParams, initialFormValuesMap }; | |||
| }; | |||
| export const useGetNodeName = () => { | |||
| @@ -287,7 +287,7 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) { | |||
| (state) => state, | |||
| ); | |||
| const getNodeName = useGetNodeName(); | |||
| const initializeOperatorParams = useInitializeOperatorParams(); | |||
| const { initializeOperatorParams } = useInitializeOperatorParams(); | |||
| const { calculateNewlyBackChildPosition } = useCalculateNewlyChildPosition(); | |||
| const { addChildEdge } = useAddChildEdge(); | |||
| const { addToolNode } = useAddToolNode(); | |||
| @@ -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 }; | |||
| } | |||