### What problem does this PR solve? Feat: Create empty agent #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.19.1
| @@ -1,9 +1,17 @@ | |||
| import { IFlow } from '@/interfaces/database/flow'; | |||
| import { DSL, IFlow, IFlowTemplate } from '@/interfaces/database/flow'; | |||
| import i18n from '@/locales/config'; | |||
| import { BeginId } from '@/pages/agent/constant'; | |||
| import { useGetSharedChatSearchParams } from '@/pages/chat/shared-hooks'; | |||
| 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 } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { useParams } from 'umi'; | |||
| import { v4 as uuid } from 'uuid'; | |||
| import { | |||
| useGetPaginationWithRouter, | |||
| useHandleSearchChange, | |||
| @@ -13,8 +21,77 @@ export const enum AgentApiAction { | |||
| FetchAgentList = 'fetchAgentList', | |||
| UpdateAgentSetting = 'updateAgentSetting', | |||
| DeleteAgent = 'deleteAgent', | |||
| FetchAgentDetail = 'fetchAgentDetail', | |||
| ResetAgent = 'resetAgent', | |||
| SetAgent = 'setAgent', | |||
| FetchAgentTemplates = 'fetchAgentTemplates', | |||
| } | |||
| export const EmptyDsl = { | |||
| graph: { | |||
| nodes: [ | |||
| { | |||
| id: BeginId, | |||
| type: 'beginNode', | |||
| position: { | |||
| x: 50, | |||
| y: 200, | |||
| }, | |||
| data: { | |||
| label: 'Begin', | |||
| name: 'begin', | |||
| }, | |||
| sourcePosition: 'left', | |||
| targetPosition: 'right', | |||
| }, | |||
| ], | |||
| edges: [], | |||
| }, | |||
| components: { | |||
| begin: { | |||
| obj: { | |||
| component_name: 'Begin', | |||
| params: {}, | |||
| }, | |||
| downstream: ['Answer:China'], // other edge target is downstream, edge source is current node id | |||
| upstream: [], // edge source is upstream, edge target is current node id | |||
| }, | |||
| }, | |||
| retrieval: [], // reference | |||
| history: [], | |||
| path: [], | |||
| globals: { | |||
| 'sys.query': '', | |||
| 'sys.user_id': '', | |||
| 'sys.conversation_turns': 0, | |||
| 'sys.files': [], | |||
| }, | |||
| }; | |||
| export const useFetchAgentTemplates = () => { | |||
| const { t } = useTranslation(); | |||
| const { data } = useQuery<IFlowTemplate[]>({ | |||
| queryKey: [AgentApiAction.FetchAgentTemplates], | |||
| initialData: [], | |||
| queryFn: async () => { | |||
| const { data } = await flowService.listTemplates(); | |||
| if (Array.isArray(data?.data)) { | |||
| data.data.unshift({ | |||
| id: uuid(), | |||
| title: t('flow.blank'), | |||
| description: t('flow.createFromNothing'), | |||
| dsl: EmptyDsl, | |||
| }); | |||
| } | |||
| return data.data; | |||
| }, | |||
| }); | |||
| return data; | |||
| }; | |||
| export const useFetchAgentListByPage = () => { | |||
| const { searchString, handleInputChange } = useHandleSearchChange(); | |||
| const { pagination, setPagination } = useGetPaginationWithRouter(); | |||
| @@ -109,3 +186,84 @@ export const useDeleteAgent = () => { | |||
| return { data, loading, deleteAgent: mutateAsync }; | |||
| }; | |||
| export const useFetchAgent = (): { | |||
| data: IFlow; | |||
| loading: boolean; | |||
| refetch: () => void; | |||
| } => { | |||
| const { id } = useParams(); | |||
| const { sharedId } = useGetSharedChatSearchParams(); | |||
| const { | |||
| data, | |||
| isFetching: loading, | |||
| refetch, | |||
| } = useQuery({ | |||
| queryKey: [AgentApiAction.FetchAgentDetail], | |||
| initialData: {} as IFlow, | |||
| refetchOnReconnect: false, | |||
| refetchOnMount: false, | |||
| refetchOnWindowFocus: false, | |||
| gcTime: 0, | |||
| queryFn: async () => { | |||
| const { data } = await flowService.getCanvas({}, sharedId || id); | |||
| const messageList = buildMessageListWithUuid( | |||
| get(data, 'data.dsl.messages', []), | |||
| ); | |||
| set(data, 'data.dsl.messages', messageList); | |||
| return data?.data ?? {}; | |||
| }, | |||
| }); | |||
| return { data, loading, refetch }; | |||
| }; | |||
| export const useResetAgent = () => { | |||
| const { id } = useParams(); | |||
| const { | |||
| data, | |||
| isPending: loading, | |||
| mutateAsync, | |||
| } = useMutation({ | |||
| mutationKey: [AgentApiAction.ResetAgent], | |||
| mutationFn: async () => { | |||
| const { data } = await flowService.resetCanvas({ id }); | |||
| return data; | |||
| }, | |||
| }); | |||
| return { data, loading, resetAgent: mutateAsync }; | |||
| }; | |||
| export const useSetAgent = () => { | |||
| const queryClient = useQueryClient(); | |||
| const { | |||
| data, | |||
| isPending: loading, | |||
| mutateAsync, | |||
| } = useMutation({ | |||
| mutationKey: [AgentApiAction.SetAgent], | |||
| mutationFn: async (params: { | |||
| id?: string; | |||
| title?: string; | |||
| dsl?: DSL; | |||
| avatar?: string; | |||
| }) => { | |||
| const { data = {} } = await flowService.setCanvas(params); | |||
| if (data.code === 0) { | |||
| message.success( | |||
| i18n.t(`message.${params?.id ? 'modified' : 'created'}`), | |||
| ); | |||
| queryClient.invalidateQueries({ | |||
| queryKey: [AgentApiAction.FetchAgentList], | |||
| }); | |||
| } | |||
| return data; | |||
| }, | |||
| }); | |||
| return { data, loading, setAgent: mutateAsync }; | |||
| }; | |||
| @@ -1,5 +0,0 @@ | |||
| .formWrapper { | |||
| :global(.ant-form-item-label) { | |||
| font-weight: 600 !important; | |||
| } | |||
| } | |||
| @@ -23,6 +23,21 @@ import { z } from 'zod'; | |||
| import { BeginQueryType } from '../constant'; | |||
| import { BeginQuery } from '../interface'; | |||
| export const BeginQueryComponentMap = { | |||
| [BeginQueryType.Line]: 'string', | |||
| [BeginQueryType.Paragraph]: 'string', | |||
| [BeginQueryType.Options]: 'string', | |||
| [BeginQueryType.File]: 'file', | |||
| [BeginQueryType.Integer]: 'number', | |||
| [BeginQueryType.Boolean]: 'boolean', | |||
| }; | |||
| const StringFields = [ | |||
| BeginQueryType.Line, | |||
| BeginQueryType.Paragraph, | |||
| BeginQueryType.Options, | |||
| ]; | |||
| interface IProps { | |||
| parameters: BeginQuery[]; | |||
| ok(parameters: any[]): void; | |||
| @@ -44,9 +59,27 @@ const DebugContent = ({ | |||
| const FormSchema = useMemo(() => { | |||
| const obj = parameters.reduce((pre, cur, idx) => { | |||
| pre[idx] = z.string().optional(); | |||
| const type = cur.type; | |||
| let fieldSchema; | |||
| if (StringFields.some((x) => x === type)) { | |||
| fieldSchema = z.string(); | |||
| } else if (type === BeginQueryType.Boolean) { | |||
| fieldSchema = z.boolean(); | |||
| } else if (type === BeginQueryType.Integer) { | |||
| fieldSchema = z.coerce.number(); | |||
| } else { | |||
| fieldSchema = z.instanceof(File); | |||
| } | |||
| if (cur.optional) { | |||
| fieldSchema.optional(); | |||
| } | |||
| pre[idx.toString()] = fieldSchema; | |||
| return pre; | |||
| }, {}); | |||
| return z.object(obj); | |||
| }, [parameters]); | |||
| @@ -61,6 +94,7 @@ const DebugContent = ({ | |||
| switchVisible, | |||
| showModal: showPopover, | |||
| } = useSetModalState(); | |||
| const { setRecord, currentRecord } = useSetSelectedRecord<number>(); | |||
| // const { submittable } = useHandleSubmittable(form); | |||
| const submittable = true; | |||
| @@ -226,29 +260,10 @@ const DebugContent = ({ | |||
| [form, t], | |||
| ); | |||
| const onOk = useCallback(async () => { | |||
| // const values = await form.validateFields(); | |||
| const nextValues = Object.entries(values).map(([key, value]) => { | |||
| const item = parameters[Number(key)]; | |||
| let nextValue = value; | |||
| if (Array.isArray(value)) { | |||
| nextValue = ``; | |||
| value.forEach((x) => { | |||
| nextValue += | |||
| x?.originFileObj instanceof File | |||
| ? `${x.name}\n${x.response?.data}\n----\n` | |||
| : `${x.url}\n${x.result}\n----\n`; | |||
| }); | |||
| } | |||
| return { ...item, value: nextValue }; | |||
| }); | |||
| ok(nextValues); | |||
| }, [ok, parameters]); | |||
| const onSubmit = useCallback( | |||
| (values: z.infer<typeof FormSchema>) => { | |||
| console.log('🚀 ~ values:', values); | |||
| return values; | |||
| const nextValues = Object.entries(values).map(([key, value]) => { | |||
| const item = parameters[Number(key)]; | |||
| let nextValue = value; | |||
| @@ -274,20 +289,21 @@ const DebugContent = ({ | |||
| <> | |||
| <section> | |||
| <Form {...form}> | |||
| <form onSubmit={form.handleSubmit(onSubmit)}> | |||
| <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> | |||
| {parameters.map((x, idx) => { | |||
| return <div key={idx}>{renderWidget(x, idx.toString())}</div>; | |||
| })} | |||
| <ButtonLoading | |||
| type="submit" | |||
| loading={loading} | |||
| disabled={!submittable || isUploading || submitButtonDisabled} | |||
| className="w-full" | |||
| > | |||
| {t(isNext ? 'common.next' : 'flow.run')} | |||
| </ButtonLoading> | |||
| </form> | |||
| </Form> | |||
| </section> | |||
| <ButtonLoading | |||
| onClick={onOk} | |||
| loading={loading} | |||
| disabled={!submittable || isUploading || submitButtonDisabled} | |||
| > | |||
| {t(isNext ? 'common.next' : 'flow.run')} | |||
| </ButtonLoading> | |||
| </> | |||
| ); | |||
| }; | |||
| @@ -191,7 +191,6 @@ const BeginForm = ({ node }: INextOperatorForm) => { | |||
| {visible && ( | |||
| <ParameterDialog | |||
| visible={visible} | |||
| hideModal={hideModal} | |||
| initialValue={currentRecord} | |||
| onOk={ok} | |||
| @@ -19,6 +19,7 @@ import { RAGFlowSelect, RAGFlowSelectOptionType } from '@/components/ui/select'; | |||
| import { Switch } from '@/components/ui/switch'; | |||
| import { IModalProps } from '@/interfaces/common'; | |||
| import { zodResolver } from '@hookform/resolvers/zod'; | |||
| import { isEmpty } from 'lodash'; | |||
| import { useEffect, useMemo } from 'react'; | |||
| import { useForm, useWatch } from 'react-hook-form'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| @@ -60,11 +61,13 @@ function ParameterForm({ | |||
| const form = useForm<z.infer<typeof FormSchema>>({ | |||
| resolver: zodResolver(FormSchema), | |||
| mode: 'onChange', | |||
| defaultValues: { | |||
| type: BeginQueryType.Line, | |||
| optional: false, | |||
| key: '', | |||
| name: '', | |||
| options: [], | |||
| }, | |||
| }); | |||
| @@ -98,10 +101,12 @@ function ParameterForm({ | |||
| }); | |||
| useEffect(() => { | |||
| form.reset({ | |||
| ...initialValue, | |||
| options: initialValue.options?.map((x) => ({ value: x })), | |||
| }); | |||
| if (!isEmpty(initialValue)) { | |||
| form.reset({ | |||
| ...initialValue, | |||
| options: initialValue.options?.map((x) => ({ value: x })), | |||
| }); | |||
| } | |||
| }, [form, initialValue]); | |||
| function onSubmit(data: z.infer<typeof FormSchema>) { | |||
| @@ -1,4 +1,8 @@ | |||
| import { useFetchFlow, useResetFlow, useSetFlow } from '@/hooks/flow-hooks'; | |||
| import { | |||
| useFetchAgent, | |||
| useResetAgent, | |||
| useSetAgent, | |||
| } from '@/hooks/use-agent-request'; | |||
| import { RAGFlowNodeType } from '@/interfaces/database/flow'; | |||
| import { useDebounceEffect } from 'ahooks'; | |||
| import dayjs from 'dayjs'; | |||
| @@ -8,20 +12,20 @@ import useGraphStore from '../store'; | |||
| import { useBuildDslData } from './use-build-dsl'; | |||
| export const useSaveGraph = () => { | |||
| const { data } = useFetchFlow(); | |||
| const { setFlow, loading } = useSetFlow(); | |||
| const { data } = useFetchAgent(); | |||
| const { setAgent, loading } = useSetAgent(); | |||
| const { id } = useParams(); | |||
| const { buildDslData } = useBuildDslData(); | |||
| const saveGraph = useCallback( | |||
| async (currentNodes?: RAGFlowNodeType[]) => { | |||
| return setFlow({ | |||
| return setAgent({ | |||
| id, | |||
| title: data.title, | |||
| dsl: buildDslData(currentNodes), | |||
| }); | |||
| }, | |||
| [setFlow, id, data.title, buildDslData], | |||
| [setAgent, id, data.title, buildDslData], | |||
| ); | |||
| return { saveGraph, loading }; | |||
| @@ -29,21 +33,21 @@ export const useSaveGraph = () => { | |||
| export const useSaveGraphBeforeOpeningDebugDrawer = (show: () => void) => { | |||
| const { saveGraph, loading } = useSaveGraph(); | |||
| const { resetFlow } = useResetFlow(); | |||
| const { resetAgent } = useResetAgent(); | |||
| const handleRun = useCallback( | |||
| async (nextNodes?: RAGFlowNodeType[]) => { | |||
| const saveRet = await saveGraph(nextNodes); | |||
| if (saveRet?.code === 0) { | |||
| // Call the reset api before opening the run drawer each time | |||
| const resetRet = await resetFlow(); | |||
| const resetRet = await resetAgent(); | |||
| // After resetting, all previous messages will be cleared. | |||
| if (resetRet?.code === 0) { | |||
| show(); | |||
| } | |||
| } | |||
| }, | |||
| [saveGraph, resetFlow, show], | |||
| [saveGraph, resetAgent, show], | |||
| ); | |||
| return { handleRun, loading }; | |||
| @@ -54,7 +58,7 @@ export const useWatchAgentChange = (chatDrawerVisible: boolean) => { | |||
| const nodes = useGraphStore((state) => state.nodes); | |||
| const edges = useGraphStore((state) => state.edges); | |||
| const { saveGraph } = useSaveGraph(); | |||
| const { data: flowDetail } = useFetchFlow(); | |||
| const { data: flowDetail } = useFetchAgent(); | |||
| const setSaveTime = useCallback((updateTime: number) => { | |||
| setTime(dayjs(updateTime).format('YYYY-MM-DD HH:mm:ss')); | |||
| @@ -1,8 +1,9 @@ | |||
| import { PageHeader } from '@/components/page-header'; | |||
| import { useSetModalState } from '@/hooks/common-hooks'; | |||
| import { useFetchFlowTemplates } from '@/hooks/flow-hooks'; | |||
| import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; | |||
| import { useCallback } from 'react'; | |||
| import { useFetchAgentTemplates, useSetAgent } from '@/hooks/use-agent-request'; | |||
| import { IFlowTemplate } from '@/interfaces/database/flow'; | |||
| import { useCallback, useState } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { CreateAgentDialog } from './create-agent-dialog'; | |||
| import { TemplateCard } from './template-card'; | |||
| @@ -10,16 +11,49 @@ import { TemplateCard } from './template-card'; | |||
| export default function AgentTemplates() { | |||
| const { navigateToAgentList } = useNavigatePage(); | |||
| const { t } = useTranslation(); | |||
| const { data: list } = useFetchFlowTemplates(); | |||
| const list = useFetchAgentTemplates(); | |||
| const { loading, setAgent } = useSetAgent(); | |||
| const { | |||
| visible: creatingVisible, | |||
| hideModal: hideCreatingModal, | |||
| showModal: showCreatingModal, | |||
| } = useSetModalState(); | |||
| const handleOk = useCallback(async () => { | |||
| // return onOk(name, checkedId); | |||
| }, []); | |||
| const [template, setTemplate] = useState<IFlowTemplate>(); | |||
| const showModal = useCallback( | |||
| (record: IFlowTemplate) => { | |||
| setTemplate(record); | |||
| showCreatingModal(); | |||
| }, | |||
| [showCreatingModal], | |||
| ); | |||
| const { navigateToAgent } = useNavigatePage(); | |||
| const handleOk = useCallback( | |||
| async (payload: any) => { | |||
| let dsl = template?.dsl; | |||
| const ret = await setAgent({ | |||
| title: payload.name, | |||
| dsl, | |||
| avatar: template?.avatar, | |||
| }); | |||
| if (ret?.code === 0) { | |||
| hideCreatingModal(); | |||
| navigateToAgent(ret.data.id)(); | |||
| } | |||
| }, | |||
| [ | |||
| hideCreatingModal, | |||
| navigateToAgent, | |||
| setAgent, | |||
| template?.avatar, | |||
| template?.dsl, | |||
| ], | |||
| ); | |||
| return ( | |||
| <section> | |||
| @@ -33,14 +67,14 @@ export default function AgentTemplates() { | |||
| <TemplateCard | |||
| key={x.id} | |||
| data={x} | |||
| showModal={showCreatingModal} | |||
| showModal={showModal} | |||
| ></TemplateCard> | |||
| ); | |||
| })} | |||
| </div> | |||
| {creatingVisible && ( | |||
| <CreateAgentDialog | |||
| loading={false} | |||
| loading={loading} | |||
| visible={creatingVisible} | |||
| hideModal={hideCreatingModal} | |||
| onOk={handleOk} | |||
| @@ -1,3 +1,4 @@ | |||
| import { ButtonLoading } from '@/components/ui/button'; | |||
| import { | |||
| Dialog, | |||
| DialogContent, | |||
| @@ -5,7 +6,6 @@ import { | |||
| DialogHeader, | |||
| DialogTitle, | |||
| } from '@/components/ui/dialog'; | |||
| import { LoadingButton } from '@/components/ui/loading-button'; | |||
| import { IModalProps } from '@/interfaces/common'; | |||
| import { TagRenameId } from '@/pages/add-knowledge/constant'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| @@ -26,9 +26,9 @@ export function CreateAgentDialog({ | |||
| </DialogHeader> | |||
| <CreateAgentForm hideModal={hideModal} onOk={onOk}></CreateAgentForm> | |||
| <DialogFooter> | |||
| <LoadingButton type="submit" form={TagRenameId} loading={loading}> | |||
| <ButtonLoading type="submit" form={TagRenameId} loading={loading}> | |||
| {t('common.save')} | |||
| </LoadingButton> | |||
| </ButtonLoading> | |||
| </DialogFooter> | |||
| </DialogContent> | |||
| </Dialog> | |||
| @@ -1,19 +1,22 @@ | |||
| import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { Card, CardContent } from '@/components/ui/card'; | |||
| import { useSetModalState } from '@/hooks/common-hooks'; | |||
| import { IFlowTemplate } from '@/interfaces/database/flow'; | |||
| import { useCallback } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| interface IProps { | |||
| data: IFlowTemplate; | |||
| showModal(record: IFlowTemplate): void; | |||
| } | |||
| export function TemplateCard({ | |||
| data, | |||
| showModal, | |||
| }: IProps & Pick<ReturnType<typeof useSetModalState>, 'showModal'>) { | |||
| export function TemplateCard({ data, showModal }: IProps) { | |||
| const { t } = useTranslation(); | |||
| const handleClick = useCallback(() => { | |||
| showModal(data); | |||
| }, [data, showModal]); | |||
| return ( | |||
| <Card className="bg-colors-background-inverse-weak border-colors-outline-neutral-standard group relative"> | |||
| <CardContent className="p-4 "> | |||
| @@ -35,7 +38,7 @@ export function TemplateCard({ | |||
| <Button | |||
| variant="tertiary" | |||
| className="absolute bottom-4 right-4 left-4 hidden justify-end group-hover:block text-center" | |||
| onClick={showModal} | |||
| onClick={handleClick} | |||
| > | |||
| {t('flow.useTemplate')} | |||
| </Button> | |||