### What problem does this PR solve? Feat: Synchronize the data of the tavily form to the canvas node #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.19.1
| @@ -85,6 +85,7 @@ export enum Operator { | |||
| WaitingDialogue = 'WaitingDialogue', | |||
| Agent = 'Agent', | |||
| Tool = 'Tool', | |||
| Tavily = 'Tavily', | |||
| } | |||
| export const SwitchLogicOperatorOptions = ['and', 'or']; | |||
| @@ -249,6 +250,7 @@ export const operatorMap: Record< | |||
| [Operator.Code]: { backgroundColor: '#4c5458' }, | |||
| [Operator.WaitingDialogue]: { backgroundColor: '#a5d65c' }, | |||
| [Operator.Agent]: { backgroundColor: '#a5d65c' }, | |||
| [Operator.Tavily]: { backgroundColor: '#a5d65c' }, | |||
| }; | |||
| export const componentMenuList = [ | |||
| @@ -9,13 +9,14 @@ import { | |||
| CommandList, | |||
| } from '@/components/ui/command'; | |||
| import { cn } from '@/lib/utils'; | |||
| import { Operator } from '@/pages/flow/constant'; | |||
| import { Operator } from '@/pages/agent/constant'; | |||
| import { useCallback, useEffect, useState } from 'react'; | |||
| const Menus = [ | |||
| { | |||
| label: 'Search', | |||
| list: [ | |||
| Operator.Tavily, | |||
| Operator.Google, | |||
| Operator.Bing, | |||
| Operator.DuckDuckGo, | |||
| @@ -1,54 +0,0 @@ | |||
| import KnowledgeBaseItem from '@/components/knowledge-base-item'; | |||
| import Rerank from '@/components/rerank'; | |||
| import SimilaritySlider from '@/components/similarity-slider'; | |||
| import TopNItem from '@/components/top-n-item'; | |||
| import { useTranslate } from '@/hooks/common-hooks'; | |||
| import type { FormProps } from 'antd'; | |||
| import { Form, Input } from 'antd'; | |||
| import { IOperatorForm } from '../../interface'; | |||
| import DynamicInputVariable from '../components/dynamic-input-variable'; | |||
| type FieldType = { | |||
| top_n?: number; | |||
| }; | |||
| const onFinish: FormProps<FieldType>['onFinish'] = (values) => { | |||
| console.log('Success:', values); | |||
| }; | |||
| const onFinishFailed: FormProps<FieldType>['onFinishFailed'] = (errorInfo) => { | |||
| console.log('Failed:', errorInfo); | |||
| }; | |||
| const RetrievalForm = ({ onValuesChange, form, node }: IOperatorForm) => { | |||
| const { t } = useTranslate('flow'); | |||
| return ( | |||
| <Form | |||
| name="basic" | |||
| onFinish={onFinish} | |||
| onFinishFailed={onFinishFailed} | |||
| autoComplete="off" | |||
| onValuesChange={onValuesChange} | |||
| form={form} | |||
| layout={'vertical'} | |||
| > | |||
| <DynamicInputVariable node={node}></DynamicInputVariable> | |||
| <SimilaritySlider | |||
| isTooltipShown | |||
| vectorSimilarityWeightName="keywords_similarity_weight" | |||
| ></SimilaritySlider> | |||
| <TopNItem></TopNItem> | |||
| <Rerank></Rerank> | |||
| <KnowledgeBaseItem></KnowledgeBaseItem> | |||
| <Form.Item | |||
| name={'empty_response'} | |||
| label={t('emptyResponse', { keyPrefix: 'chat' })} | |||
| tooltip={t('emptyResponseTip', { keyPrefix: 'chat' })} | |||
| > | |||
| <Input.TextArea placeholder="" rows={4} /> | |||
| </Form.Item> | |||
| </Form> | |||
| ); | |||
| }; | |||
| export default RetrievalForm; | |||
| @@ -7,19 +7,31 @@ import { | |||
| FormLabel, | |||
| FormMessage, | |||
| } from '@/components/ui/form'; | |||
| import { Input } from '@/components/ui/input'; | |||
| import { RAGFlowSelect } from '@/components/ui/select'; | |||
| import { buildOptions } from '@/utils/form'; | |||
| import { zodResolver } from '@hookform/resolvers/zod'; | |||
| import { useForm } from 'react-hook-form'; | |||
| import { z } from 'zod'; | |||
| import { INextOperatorForm } from '../../interface'; | |||
| import { useValues } from './use-values'; | |||
| import { QueryVariable } from '../components/query-variable'; | |||
| import { SearchDepth, Topic, useValues } from './use-values'; | |||
| import { useWatchFormChange } from './use-watch-change'; | |||
| const TavilyForm = ({ node }: INextOperatorForm) => { | |||
| const values = useValues(node); | |||
| const TavilyForm = () => { | |||
| const values = useValues(); | |||
| const FormSchema = z.object({ | |||
| query: z.string(), | |||
| search_depth: z.enum([SearchDepth.Advanced, SearchDepth.Basic]), | |||
| topic: z.enum([Topic.News, Topic.General]), | |||
| max_results: z.coerce.number(), | |||
| days: z.coerce.number(), | |||
| include_answer: z.boolean(), | |||
| include_raw_content: z.boolean(), | |||
| include_images: z.boolean(), | |||
| include_image_descriptions: z.boolean(), | |||
| include_domains: z.array(z.string()), | |||
| exclude_domains: z.array(z.string()), | |||
| }); | |||
| const form = useForm({ | |||
| @@ -27,7 +39,7 @@ const TavilyForm = ({ node }: INextOperatorForm) => { | |||
| resolver: zodResolver(FormSchema), | |||
| }); | |||
| useWatchFormChange(node?.id, form); | |||
| useWatchFormChange(form); | |||
| return ( | |||
| <Form {...form}> | |||
| @@ -39,14 +51,50 @@ const TavilyForm = ({ node }: INextOperatorForm) => { | |||
| }} | |||
| > | |||
| <FormContainer> | |||
| <QueryVariable></QueryVariable> | |||
| <FormField | |||
| control={form.control} | |||
| name="search_depth" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>Search Depth</FormLabel> | |||
| <FormControl> | |||
| <RAGFlowSelect | |||
| placeholder="shadcn" | |||
| {...field} | |||
| options={buildOptions(SearchDepth)} | |||
| /> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <FormField | |||
| control={form.control} | |||
| name="topic" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>Topic</FormLabel> | |||
| <FormControl> | |||
| <RAGFlowSelect | |||
| placeholder="shadcn" | |||
| {...field} | |||
| options={buildOptions(Topic)} | |||
| /> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <FormField | |||
| control={form.control} | |||
| name="query" | |||
| name="max_results" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>Username</FormLabel> | |||
| <FormLabel>Max Results</FormLabel> | |||
| <FormControl> | |||
| <RAGFlowSelect placeholder="shadcn" {...field} options={[]} /> | |||
| <Input type={'number'} {...field}></Input> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| @@ -1,14 +1,44 @@ | |||
| import { RAGFlowNodeType } from '@/interfaces/database/flow'; | |||
| import { isEmpty } from 'lodash'; | |||
| import { useMemo } from 'react'; | |||
| import useGraphStore from '../../store'; | |||
| import { getAgentNodeTools } from '../../utils'; | |||
| export enum SearchDepth { | |||
| Basic = 'basic', | |||
| Advanced = 'advanced', | |||
| } | |||
| export enum Topic { | |||
| News = 'news', | |||
| General = 'general', | |||
| } | |||
| const defaultValues = { | |||
| content: [], | |||
| query: '', | |||
| search_depth: SearchDepth.Basic, | |||
| topic: Topic.General, | |||
| max_results: 5, | |||
| days: 7, | |||
| include_answer: false, | |||
| include_raw_content: true, | |||
| include_images: false, | |||
| include_image_descriptions: false, | |||
| include_domains: [], | |||
| exclude_domains: [], | |||
| }; | |||
| export function useValues(node?: RAGFlowNodeType) { | |||
| export function useValues() { | |||
| const { clickedToolId, clickedNodeId, findUpstreamNodeById } = useGraphStore( | |||
| (state) => state, | |||
| ); | |||
| const values = useMemo(() => { | |||
| const formData = node?.data?.form; | |||
| const agentNode = findUpstreamNodeById(clickedNodeId); | |||
| const tools = getAgentNodeTools(agentNode); | |||
| const formData = tools.find( | |||
| (x) => x.component_name === clickedToolId, | |||
| )?.params; | |||
| if (isEmpty(formData)) { | |||
| return defaultValues; | |||
| @@ -17,7 +47,7 @@ export function useValues(node?: RAGFlowNodeType) { | |||
| return { | |||
| ...formData, | |||
| }; | |||
| }, [node]); | |||
| }, [clickedNodeId, clickedToolId, findUpstreamNodeById]); | |||
| return values; | |||
| } | |||
| @@ -1,22 +1,34 @@ | |||
| import { useEffect } from 'react'; | |||
| import { UseFormReturn, useWatch } from 'react-hook-form'; | |||
| import useGraphStore from '../../store'; | |||
| import { getAgentNodeTools } from '../../utils'; | |||
| export function useWatchFormChange(id?: string, form?: UseFormReturn) { | |||
| export function useWatchFormChange(form?: UseFormReturn<any>) { | |||
| let values = useWatch({ control: form?.control }); | |||
| const updateNodeForm = useGraphStore((state) => state.updateNodeForm); | |||
| const { clickedToolId, clickedNodeId, findUpstreamNodeById, updateNodeForm } = | |||
| useGraphStore((state) => state); | |||
| useEffect(() => { | |||
| const agentNode = findUpstreamNodeById(clickedNodeId); | |||
| // Manually triggered form updates are synchronized to the canvas | |||
| if (id && form?.formState.isDirty) { | |||
| if (agentNode && form?.formState.isDirty) { | |||
| const agentNodeId = agentNode?.id; | |||
| const tools = getAgentNodeTools(agentNode); | |||
| values = form?.getValues(); | |||
| let nextValues: any = values; | |||
| const nextTools = tools.map((x) => { | |||
| if (x.component_name === clickedToolId) { | |||
| return { ...x, params: { ...values } }; | |||
| } | |||
| return x; | |||
| }); | |||
| nextValues = { | |||
| ...values, | |||
| const nextValues = { | |||
| ...(agentNode?.data?.form ?? {}), | |||
| tools: nextTools, | |||
| }; | |||
| updateNodeForm(id, nextValues); | |||
| updateNodeForm(agentNodeId, nextValues); | |||
| } | |||
| }, [form?.formState.isDirty, id, updateNodeForm, values]); | |||
| }, [form?.formState.isDirty, updateNodeForm, values]); | |||
| } | |||
| @@ -13,6 +13,7 @@ import GoogleForm from '../google-form'; | |||
| import GoogleScholarForm from '../google-scholar-form'; | |||
| import PubMedForm from '../pubmed-form'; | |||
| import RetrievalForm from '../retrieval-form/next'; | |||
| import TavilyForm from '../tavily-form'; | |||
| import WikipediaForm from '../wikipedia-form'; | |||
| import YahooFinanceForm from '../yahoo-finance-form'; | |||
| @@ -33,4 +34,5 @@ export const ToolFormConfigMap = { | |||
| [Operator.YahooFinance]: YahooFinanceForm, | |||
| [Operator.Crawler]: CrawlerForm, | |||
| [Operator.Email]: EmailForm, | |||
| [Operator.Tavily]: TavilyForm, | |||
| }; | |||
| @@ -75,6 +75,7 @@ export type RFState = { | |||
| generateNodeName: (name: string) => string; | |||
| setClickedNodeId: (id?: string) => void; | |||
| setClickedToolId: (id?: string) => void; | |||
| findUpstreamNodeById: (id?: string | null) => RAGFlowNodeType | undefined; | |||
| }; | |||
| // this is our useStore hook that we can use in our components to get parts of the store and call actions | |||
| @@ -471,6 +472,11 @@ const useGraphStore = create<RFState>()( | |||
| setClickedToolId: (id?: string) => { | |||
| set({ clickedToolId: id }); | |||
| }, | |||
| findUpstreamNodeById: (id) => { | |||
| const { edges, getNode } = get(); | |||
| const edge = edges.find((x) => x.target === id); | |||
| return getNode(edge?.source); | |||
| }, | |||
| })), | |||
| { name: 'graph', trace: true }, | |||
| ), | |||
| @@ -1,4 +1,5 @@ | |||
| import { | |||
| IAgentForm, | |||
| ICategorizeItem, | |||
| ICategorizeItemResult, | |||
| } from '@/interfaces/database/agent'; | |||
| @@ -460,3 +461,8 @@ export const buildCategorizeObjectFromList = (list: Array<ICategorizeItem>) => { | |||
| return pre; | |||
| }, {}); | |||
| }; | |||
| export function getAgentNodeTools(agentNode?: RAGFlowNodeType) { | |||
| const tools: IAgentForm['tools'] = get(agentNode, 'data.form.tools', []); | |||
| return tools; | |||
| } | |||
| @@ -26,3 +26,7 @@ export const removeUselessFieldsFromValues = (values: any, prefix?: string) => { | |||
| return nextValues; | |||
| }; | |||
| export function buildOptions(data: Record<string, any>) { | |||
| return Object.values(data).map((val) => ({ label: val, value: val })); | |||
| } | |||