### What problem does this PR solve? Feat: Add Tavily operator #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.20.0
| @@ -88,7 +88,7 @@ function AccordionOperators() { | |||
| <AccordionItem value="item-5"> | |||
| <AccordionTrigger className="text-xl">Tools</AccordionTrigger> | |||
| <AccordionContent className="flex flex-col gap-4 text-balance"> | |||
| <OperatorItemList operators={[]}></OperatorItemList> | |||
| <OperatorItemList operators={[Operator.Tavily]}></OperatorItemList> | |||
| </AccordionContent> | |||
| </AccordionItem> | |||
| </Accordion> | |||
| @@ -387,7 +387,7 @@ const initialQueryBaseValues = { | |||
| }; | |||
| export const initialRetrievalValues = { | |||
| query: '', | |||
| query: AgentGlobals.SysQuery, | |||
| top_n: 8, | |||
| top_k: 1024, | |||
| kb_ids: [], | |||
| @@ -686,6 +686,41 @@ export const initialAgentValues = { | |||
| }, | |||
| }; | |||
| export enum TavilySearchDepth { | |||
| Basic = 'basic', | |||
| Advanced = 'advanced', | |||
| } | |||
| export enum TavilyTopic { | |||
| News = 'news', | |||
| General = 'general', | |||
| } | |||
| export const initialTavilyValues = { | |||
| api_key: '', | |||
| query: AgentGlobals.SysQuery, | |||
| search_depth: TavilySearchDepth.Basic, | |||
| topic: TavilyTopic.General, | |||
| max_results: 5, | |||
| days: 7, | |||
| include_answer: false, | |||
| include_raw_content: true, | |||
| include_images: false, | |||
| include_image_descriptions: false, | |||
| include_domains: [], | |||
| exclude_domains: [], | |||
| outputs: { | |||
| formalized_content: { | |||
| value: '', | |||
| type: 'string', | |||
| }, | |||
| json: { | |||
| value: {}, | |||
| type: 'Object', | |||
| }, | |||
| }, | |||
| }; | |||
| export const CategorizeAnchorPointPositions = [ | |||
| { top: 1, right: 34 }, | |||
| { top: 8, right: 18 }, | |||
| @@ -813,6 +848,7 @@ export const NodeMap = { | |||
| [Operator.WaitingDialogue]: 'ragNode', | |||
| [Operator.Agent]: 'agentNode', | |||
| [Operator.Tool]: 'toolNode', | |||
| [Operator.Tavily]: 'ragNode', | |||
| }; | |||
| export const LanguageOptions = [ | |||
| @@ -33,6 +33,7 @@ import RelevantForm from '../form/relevant-form'; | |||
| import RetrievalForm from '../form/retrieval-form/next'; | |||
| import RewriteQuestionForm from '../form/rewrite-question-form'; | |||
| import SwitchForm from '../form/switch-form'; | |||
| import TavilyForm from '../form/tavily-form'; | |||
| import TemplateForm from '../form/template-form'; | |||
| import ToolForm from '../form/tool-form'; | |||
| import TuShareForm from '../form/tushare-form'; | |||
| @@ -375,6 +376,11 @@ export function useFormConfigMap() { | |||
| defaultValues: {}, | |||
| schema: z.object({}), | |||
| }, | |||
| [Operator.Tavily]: { | |||
| component: TavilyForm, | |||
| defaultValues: {}, | |||
| schema: z.object({}), | |||
| }, | |||
| }; | |||
| return FormConfigMap; | |||
| @@ -36,7 +36,7 @@ export function AgentTools() { | |||
| <ToolCard key={x}> | |||
| {x} | |||
| <div className="flex items-center gap-2 text-text-sub-title"> | |||
| <PencilLine className="size-4 cursor-pointer" /> | |||
| <PencilLine className="size-4 cursor-pointer" data-tool={x} /> | |||
| <X | |||
| className="size-4 cursor-pointer" | |||
| onClick={deleteNodeTool(x)} | |||
| @@ -102,6 +102,7 @@ const AgentForm = ({ node }: INextOperatorForm) => { | |||
| name={`prompts`} | |||
| render={({ field }) => ( | |||
| <FormItem className="flex-1"> | |||
| <FormLabel>User Prompt</FormLabel> | |||
| <FormControl> | |||
| <section> | |||
| <PromptEditor {...field} showToolbar={false}></PromptEditor> | |||
| @@ -15,20 +15,26 @@ import { zodResolver } from '@hookform/resolvers/zod'; | |||
| import { useMemo } from 'react'; | |||
| import { useForm } from 'react-hook-form'; | |||
| import { z } from 'zod'; | |||
| import { | |||
| TavilySearchDepth, | |||
| TavilyTopic, | |||
| initialTavilyValues, | |||
| } from '../../constant'; | |||
| import { INextOperatorForm } from '../../interface'; | |||
| import { Output, OutputType } from '../components/output'; | |||
| import { QueryVariable } from '../components/query-variable'; | |||
| import { DynamicDomain } from './dynamic-domain'; | |||
| import { SearchDepth, Topic, defaultValues, useValues } from './use-values'; | |||
| import { useValues } from './use-values'; | |||
| import { useWatchFormChange } from './use-watch-change'; | |||
| const TavilyForm = () => { | |||
| const values = useValues(); | |||
| const TavilyForm = ({ node }: INextOperatorForm) => { | |||
| const values = useValues(node); | |||
| const FormSchema = z.object({ | |||
| api_key: z.string(), | |||
| query: z.string(), | |||
| search_depth: z.enum([SearchDepth.Advanced, SearchDepth.Basic]), | |||
| topic: z.enum([Topic.News, Topic.General]), | |||
| search_depth: z.enum([TavilySearchDepth.Advanced, TavilySearchDepth.Basic]), | |||
| topic: z.enum([TavilyTopic.News, TavilyTopic.General]), | |||
| max_results: z.coerce.number(), | |||
| days: z.coerce.number(), | |||
| include_answer: z.boolean(), | |||
| @@ -45,7 +51,7 @@ const TavilyForm = () => { | |||
| }); | |||
| const outputList = useMemo(() => { | |||
| return Object.entries(defaultValues.outputs).reduce<OutputType[]>( | |||
| return Object.entries(initialTavilyValues.outputs).reduce<OutputType[]>( | |||
| (pre, [key, val]) => { | |||
| pre.push({ title: key, type: val.type }); | |||
| return pre; | |||
| @@ -54,7 +60,7 @@ const TavilyForm = () => { | |||
| ); | |||
| }, []); | |||
| useWatchFormChange(form); | |||
| useWatchFormChange(node?.id, form); | |||
| return ( | |||
| <Form {...form}> | |||
| @@ -92,7 +98,7 @@ const TavilyForm = () => { | |||
| <RAGFlowSelect | |||
| placeholder="shadcn" | |||
| {...field} | |||
| options={buildOptions(SearchDepth)} | |||
| options={buildOptions(TavilySearchDepth)} | |||
| /> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| @@ -104,12 +110,12 @@ const TavilyForm = () => { | |||
| name="topic" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>Topic</FormLabel> | |||
| <FormLabel>TavilyTopic</FormLabel> | |||
| <FormControl> | |||
| <RAGFlowSelect | |||
| placeholder="shadcn" | |||
| {...field} | |||
| options={buildOptions(Topic)} | |||
| options={buildOptions(TavilyTopic)} | |||
| /> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| @@ -1,59 +1,15 @@ | |||
| import { AgentGlobals } from '@/constants/agent'; | |||
| import { RAGFlowNodeType } from '@/interfaces/database/agent'; | |||
| import { isEmpty } from 'lodash'; | |||
| import { useMemo } from 'react'; | |||
| import useGraphStore from '../../store'; | |||
| import { convertToObjectArray, getAgentNodeTools } from '../../utils'; | |||
| export enum SearchDepth { | |||
| Basic = 'basic', | |||
| Advanced = 'advanced', | |||
| } | |||
| export enum Topic { | |||
| News = 'news', | |||
| General = 'general', | |||
| } | |||
| export const defaultValues = { | |||
| api_key: '', | |||
| query: AgentGlobals.SysQuery, | |||
| 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: [], | |||
| outputs: { | |||
| formalized_content: { | |||
| value: '', | |||
| type: 'string', | |||
| }, | |||
| json: { | |||
| value: {}, | |||
| type: 'Object', | |||
| }, | |||
| }, | |||
| }; | |||
| export function useValues() { | |||
| const { clickedToolId, clickedNodeId, findUpstreamNodeById } = useGraphStore( | |||
| (state) => state, | |||
| ); | |||
| import { initialTavilyValues } from '../../constant'; | |||
| import { convertToObjectArray } from '../../utils'; | |||
| export function useValues(node?: RAGFlowNodeType) { | |||
| const values = useMemo(() => { | |||
| const agentNode = findUpstreamNodeById(clickedNodeId); | |||
| const tools = getAgentNodeTools(agentNode); | |||
| const formData = tools.find( | |||
| (x) => x.component_name === clickedToolId, | |||
| )?.params; | |||
| const formData = node?.data?.form; | |||
| if (isEmpty(formData)) { | |||
| return defaultValues; | |||
| return initialTavilyValues; | |||
| } | |||
| return { | |||
| @@ -61,7 +17,7 @@ export function useValues() { | |||
| include_domains: convertToObjectArray(formData.include_domains), | |||
| exclude_domains: convertToObjectArray(formData.exclude_domains), | |||
| }; | |||
| }, [clickedNodeId, clickedToolId, findUpstreamNodeById]); | |||
| }, [node?.data?.form]); | |||
| return values; | |||
| } | |||
| @@ -1,41 +1,23 @@ | |||
| import { useEffect } from 'react'; | |||
| import { UseFormReturn, useWatch } from 'react-hook-form'; | |||
| import useGraphStore from '../../store'; | |||
| import { convertToStringArray, getAgentNodeTools } from '../../utils'; | |||
| import { convertToStringArray } from '../../utils'; | |||
| export function useWatchFormChange(form?: UseFormReturn<any>) { | |||
| export function useWatchFormChange(id?: string, form?: UseFormReturn<any>) { | |||
| let values = useWatch({ control: form?.control }); | |||
| const { clickedToolId, clickedNodeId, findUpstreamNodeById, updateNodeForm } = | |||
| useGraphStore((state) => state); | |||
| const updateNodeForm = useGraphStore((state) => state.updateNodeForm); | |||
| useEffect(() => { | |||
| const agentNode = findUpstreamNodeById(clickedNodeId); | |||
| // Manually triggered form updates are synchronized to the canvas | |||
| if (agentNode && form?.formState.isDirty) { | |||
| const agentNodeId = agentNode?.id; | |||
| const tools = getAgentNodeTools(agentNode); | |||
| if (id && form?.formState.isDirty) { | |||
| values = form?.getValues(); | |||
| const nextTools = tools.map((x) => { | |||
| if (x.component_name === clickedToolId) { | |||
| return { | |||
| ...x, | |||
| params: { | |||
| ...values, | |||
| include_domains: convertToStringArray(values.include_domains), | |||
| exclude_domains: convertToStringArray(values.exclude_domains), | |||
| }, | |||
| }; | |||
| } | |||
| return x; | |||
| }); | |||
| const nextValues = { | |||
| ...(agentNode?.data?.form ?? {}), | |||
| tools: nextTools, | |||
| let nextValues: any = { | |||
| ...values, | |||
| include_domains: convertToStringArray(values.include_domains), | |||
| exclude_domains: convertToStringArray(values.exclude_domains), | |||
| }; | |||
| updateNodeForm(agentNodeId, nextValues); | |||
| updateNodeForm(id, nextValues); | |||
| } | |||
| }, [form?.formState.isDirty, updateNodeForm, values]); | |||
| }, [form?.formState.isDirty, id, updateNodeForm, values]); | |||
| } | |||
| @@ -13,9 +13,9 @@ 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'; | |||
| import TavilyForm from './tavily-form'; | |||
| export const ToolFormConfigMap = { | |||
| [Operator.Retrieval]: RetrievalForm, | |||
| @@ -0,0 +1,60 @@ | |||
| import { FormContainer } from '@/components/form-container'; | |||
| import { | |||
| Form, | |||
| FormControl, | |||
| FormField, | |||
| FormItem, | |||
| FormLabel, | |||
| FormMessage, | |||
| } from '@/components/ui/form'; | |||
| import { Input } from '@/components/ui/input'; | |||
| import { zodResolver } from '@hookform/resolvers/zod'; | |||
| import { useForm } from 'react-hook-form'; | |||
| import { z } from 'zod'; | |||
| import { useValues } from '../use-values'; | |||
| import { useWatchFormChange } from '../use-watch-change'; | |||
| const TavilyForm = () => { | |||
| const values = useValues(); | |||
| const FormSchema = z.object({ | |||
| api_key: z.string(), | |||
| }); | |||
| const form = useForm<z.infer<typeof FormSchema>>({ | |||
| defaultValues: values, | |||
| resolver: zodResolver(FormSchema), | |||
| }); | |||
| useWatchFormChange(form); | |||
| return ( | |||
| <Form {...form}> | |||
| <form | |||
| className="space-y-5 px-5 " | |||
| autoComplete="off" | |||
| onSubmit={(e) => { | |||
| e.preventDefault(); | |||
| }} | |||
| > | |||
| <FormContainer> | |||
| <FormField | |||
| control={form.control} | |||
| name="api_key" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>Api Key</FormLabel> | |||
| <FormControl> | |||
| <Input type="password" {...field}></Input> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| </FormContainer> | |||
| </form> | |||
| </Form> | |||
| ); | |||
| }; | |||
| export default TavilyForm; | |||
| @@ -0,0 +1,43 @@ | |||
| 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', | |||
| } | |||
| export const defaultValues = { | |||
| api_key: '', | |||
| }; | |||
| export function useValues() { | |||
| const { clickedToolId, clickedNodeId, findUpstreamNodeById } = useGraphStore( | |||
| (state) => state, | |||
| ); | |||
| const values = useMemo(() => { | |||
| const agentNode = findUpstreamNodeById(clickedNodeId); | |||
| const tools = getAgentNodeTools(agentNode); | |||
| const formData = tools.find( | |||
| (x) => x.component_name === clickedToolId, | |||
| )?.params; | |||
| if (isEmpty(formData)) { | |||
| return defaultValues; | |||
| } | |||
| return { | |||
| ...formData, | |||
| }; | |||
| }, [clickedNodeId, clickedToolId, findUpstreamNodeById]); | |||
| return values; | |||
| } | |||
| @@ -0,0 +1,39 @@ | |||
| import { useEffect } from 'react'; | |||
| import { UseFormReturn, useWatch } from 'react-hook-form'; | |||
| import useGraphStore from '../../store'; | |||
| import { getAgentNodeTools } from '../../utils'; | |||
| export function useWatchFormChange(form?: UseFormReturn<any>) { | |||
| let values = useWatch({ control: form?.control }); | |||
| const { clickedToolId, clickedNodeId, findUpstreamNodeById, updateNodeForm } = | |||
| useGraphStore((state) => state); | |||
| useEffect(() => { | |||
| const agentNode = findUpstreamNodeById(clickedNodeId); | |||
| // Manually triggered form updates are synchronized to the canvas | |||
| if (agentNode && form?.formState.isDirty) { | |||
| const agentNodeId = agentNode?.id; | |||
| const tools = getAgentNodeTools(agentNode); | |||
| values = form?.getValues(); | |||
| const nextTools = tools.map((x) => { | |||
| if (x.component_name === clickedToolId) { | |||
| return { | |||
| ...x, | |||
| params: { | |||
| ...values, | |||
| }, | |||
| }; | |||
| } | |||
| return x; | |||
| }); | |||
| const nextValues = { | |||
| ...(agentNode?.data?.form ?? {}), | |||
| tools: nextTools, | |||
| }; | |||
| updateNodeForm(agentNodeId, nextValues); | |||
| } | |||
| }, [form?.formState.isDirty, updateNodeForm, values]); | |||
| } | |||
| @@ -64,6 +64,7 @@ import { | |||
| initialRetrievalValues, | |||
| initialRewriteQuestionValues, | |||
| initialSwitchValues, | |||
| initialTavilyValues, | |||
| initialTemplateValues, | |||
| initialTuShareValues, | |||
| initialWaitingDialogueValues, | |||
| @@ -147,6 +148,7 @@ export const useInitializeOperatorParams = () => { | |||
| [Operator.Code]: initialCodeValues, | |||
| [Operator.WaitingDialogue]: initialWaitingDialogueValues, | |||
| [Operator.Agent]: { ...initialAgentValues, llm_id: llmId }, | |||
| [Operator.Tavily]: initialTavilyValues, | |||
| }; | |||
| }, [llmId]); | |||
| @@ -39,6 +39,7 @@ import { | |||
| initialRetrievalValues, | |||
| initialRewriteQuestionValues, | |||
| initialSwitchValues, | |||
| initialTavilyValues, | |||
| initialTemplateValues, | |||
| initialTuShareValues, | |||
| initialWaitingDialogueValues, | |||
| @@ -104,6 +105,7 @@ export const useInitializeOperatorParams = () => { | |||
| [Operator.WaitingDialogue]: initialWaitingDialogueValues, | |||
| [Operator.Agent]: { ...initialAgentValues, llm_id: llmId }, | |||
| [Operator.Tool]: {}, | |||
| [Operator.Tavily]: initialTavilyValues, | |||
| }; | |||
| }, [llmId]); | |||