Feat: Add TavilyExtract operator #3221 (#8899)

### What problem does this PR solve?

Feat: Add TavilyExtract operator #3221

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-07-17 17:33:01 +08:00
committed by GitHub
parent ecdb1701df
commit 71efd8d765
19 changed files with 389 additions and 780 deletions

View File

@ -18,6 +18,7 @@ const Menus = [
label: 'Search',
list: [
Operator.TavilySearch,
Operator.TavilyExtract,
Operator.Google,
Operator.Bing,
Operator.DuckDuckGo,
@ -41,7 +42,6 @@ const Menus = [
Operator.GitHub,
Operator.ExeSQL,
Operator.Invoke,
Operator.Crawler,
Operator.Code,
Operator.Retrieval,
],

View File

@ -1,6 +1,5 @@
import { LargeModelFormField } from '@/components/large-model-form-field';
import NumberInput from '@/components/originui/number-input';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { TopNFormField } from '@/components/top-n-item';
import { ButtonLoading } from '@/components/ui/button';
import {
Form,
@ -10,7 +9,7 @@ import {
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input, NumberInput } from '@/components/ui/input';
import { Input } from '@/components/ui/input';
import { useTranslate } from '@/hooks/common-hooks';
import { zodResolver } from '@hookform/resolvers/zod';
import { memo } from 'react';
@ -31,7 +30,6 @@ export function ExeSQLFormWidgets({ loading }: { loading: boolean }) {
return (
<>
<LargeModelFormField></LargeModelFormField>
<FormField
control={form.control}
name="db_type"
@ -94,7 +92,7 @@ export function ExeSQLFormWidgets({ loading }: { loading: boolean }) {
<FormItem>
<FormLabel>{t('port')}</FormLabel>
<FormControl>
<NumberInput {...field}></NumberInput>
<NumberInput {...field} className="w-full"></NumberInput>
</FormControl>
<FormMessage />
</FormItem>
@ -113,20 +111,21 @@ export function ExeSQLFormWidgets({ loading }: { loading: boolean }) {
</FormItem>
)}
/>
<FormField
control={form.control}
name="loop"
name="max_records"
render={({ field }) => (
<FormItem>
<FormLabel tooltip={t('loopTip')}>{t('loop')}</FormLabel>
<FormLabel>{t('maxRecords')}</FormLabel>
<FormControl>
<NumberInput {...field}></NumberInput>
<NumberInput {...field} className="w-full"></NumberInput>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<TopNFormField max={1000}></TopNFormField>
<div className="flex justify-end">
<ButtonLoading loading={loading} type="submit">
Test
@ -151,7 +150,7 @@ function ExeSQLForm({ node }: INextOperatorForm) {
return (
<Form {...form}>
<FormWrapper onSubmit={form.handleSubmit(onSubmit)}>
<QueryVariable></QueryVariable>
<QueryVariable name="sql"></QueryVariable>
<ExeSQLFormWidgets loading={loading}></ExeSQLFormWidgets>
</FormWrapper>
</Form>

View File

@ -3,15 +3,14 @@ import { useCallback } from 'react';
import { z } from 'zod';
export const ExeSQLFormSchema = {
llm_id: z.string().min(1),
sql: z.string(),
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(),
max_records: z.number(),
};
export const FormSchema = z.object({

View File

@ -0,0 +1,115 @@
import { FormContainer } from '@/components/form-container';
import {
Form,
FormControl,
FormField,
FormItem,
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 { memo } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import {
TavilyExtractDepth,
TavilyExtractFormat,
initialTavilyExtractValues,
} from '../../constant';
import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
import { buildOutputList } from '../../utils/build-output-list';
import { FormWrapper } from '../components/form-wrapper';
import { Output } from '../components/output';
import { TavilyApiKeyField, TavilyFormSchema } from '../tavily-form';
const outputList = buildOutputList(initialTavilyExtractValues.outputs);
function TavilyExtractForm({ node }: INextOperatorForm) {
const values = useFormValues(initialTavilyExtractValues, node);
const FormSchema = z.object({
...TavilyFormSchema,
urls: z.string(),
extract_depth: z.enum([
TavilyExtractDepth.Advanced,
TavilyExtractDepth.Basic,
]),
format: z.enum([TavilyExtractFormat.Text, TavilyExtractFormat.Markdown]),
});
const form = useForm<z.infer<typeof FormSchema>>({
defaultValues: values,
resolver: zodResolver(FormSchema),
});
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper>
<FormContainer>
<TavilyApiKeyField></TavilyApiKeyField>
</FormContainer>
<FormContainer>
<FormField
control={form.control}
name="urls"
render={({ field }) => (
<FormItem>
<FormLabel>URL</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="extract_depth"
render={({ field }) => (
<FormItem>
<FormLabel>Extract Depth</FormLabel>
<FormControl>
<RAGFlowSelect
placeholder="shadcn"
{...field}
options={buildOptions(TavilyExtractDepth)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="format"
render={({ field }) => (
<FormItem>
<FormLabel>Format</FormLabel>
<FormControl>
<RAGFlowSelect
placeholder="shadcn"
{...field}
options={buildOptions(TavilyExtractFormat)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</FormContainer>
</FormWrapper>
<div className="p-5">
<Output list={outputList}></Output>
</div>
</Form>
);
}
export default memo(TavilyExtractForm);

View File

@ -13,7 +13,7 @@ import { Switch } from '@/components/ui/switch';
import { buildOptions } from '@/utils/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { memo, useMemo } from 'react';
import { useForm } from 'react-hook-form';
import { useForm, useFormContext } from 'react-hook-form';
import { z } from 'zod';
import {
TavilySearchDepth,
@ -21,17 +21,41 @@ import {
initialTavilyValues,
} from '../../constant';
import { INextOperatorForm } from '../../interface';
import { FormWrapper } from '../components/form-wrapper';
import { Output, OutputType } from '../components/output';
import { QueryVariable } from '../components/query-variable';
import { DynamicDomain } from './dynamic-domain';
import { useValues } from './use-values';
import { useWatchFormChange } from './use-watch-change';
export function TavilyApiKeyField() {
const form = useFormContext();
return (
<FormField
control={form.control}
name="api_key"
render={({ field }) => (
<FormItem>
<FormLabel>Api Key</FormLabel>
<FormControl>
<Input type="password" {...field}></Input>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
}
export const TavilyFormSchema = {
api_key: z.string(),
};
function TavilyForm({ node }: INextOperatorForm) {
const values = useValues(node);
const FormSchema = z.object({
api_key: z.string(),
...TavilyFormSchema,
query: z.string(),
search_depth: z.enum([TavilySearchDepth.Advanced, TavilySearchDepth.Basic]),
topic: z.enum([TavilyTopic.News, TavilyTopic.General]),
@ -64,27 +88,9 @@ function TavilyForm({ node }: INextOperatorForm) {
return (
<Form {...form}>
<form
className="space-y-5 px-5 "
autoComplete="off"
onSubmit={(e) => {
e.preventDefault();
}}
>
<FormWrapper>
<FormContainer>
<FormField
control={form.control}
name="api_key"
render={({ field }) => (
<FormItem>
<FormLabel>Api Key</FormLabel>
<FormControl>
<Input type="password" {...field}></Input>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<TavilyApiKeyField></TavilyApiKeyField>
</FormContainer>
<FormContainer>
<QueryVariable></QueryVariable>
@ -221,7 +227,7 @@ function TavilyForm({ node }: INextOperatorForm) {
label={'Exclude Domains'}
></DynamicDomain>
</FormContainer>
</form>
</FormWrapper>
<div className="p-5">
<Output list={outputList}></Output>
</div>

View File

@ -9,7 +9,7 @@ export function useWatchFormChange(id?: string, form?: UseFormReturn<any>) {
useEffect(() => {
// Manually triggered form updates are synchronized to the canvas
if (id && form?.formState.isDirty) {
if (id) {
values = form?.getValues();
let nextValues: any = {
...values,

View File

@ -34,4 +34,5 @@ export const ToolFormConfigMap = {
[Operator.Crawler]: CrawlerForm,
[Operator.Email]: EmailForm,
[Operator.TavilySearch]: TavilyForm,
[Operator.TavilyExtract]: TavilyForm,
};

View File

@ -1,25 +1,17 @@
import { FormContainer } from '@/components/form-container';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
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 { TavilyApiKeyField, TavilyFormSchema } from '../../tavily-form';
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 FormSchema = z.object(TavilyFormSchema);
const form = useForm<z.infer<typeof FormSchema>>({
defaultValues: values,
@ -30,29 +22,11 @@ const TavilyForm = () => {
return (
<Form {...form}>
<form
className="space-y-5 px-5 "
autoComplete="off"
onSubmit={(e) => {
e.preventDefault();
}}
>
<FormWrapper>
<FormContainer>
<FormField
control={form.control}
name="api_key"
render={({ field }) => (
<FormItem>
<FormLabel>Api Key</FormLabel>
<FormControl>
<Input type="password" {...field}></Input>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<TavilyApiKeyField></TavilyApiKeyField>
</FormContainer>
</form>
</FormWrapper>
</Form>
);
};