mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-27 18:56:31 +08:00
feat(agent): add Querit Contents tool (#18156)
### Summary Add Querit Contents as a built-in page content tool for RAGFlow Agents and Canvas workflows. Querit Contents crawls one or more known URLs and returns their page content and optional metadata. It complements the existing Querit Search tool: Search discovers relevant pages, while Contents retrieves pages already selected by an Agent or workflow. This integration supports two usage modes: - A standalone `QueritContents` node in Canvas workflows. - An embedded content tool available to RAGFlow Agents.
This commit is contained in:
@@ -74,6 +74,7 @@ export const SVGIconMap = {
|
||||
[Operator.KeenableSearch]: 'keenable',
|
||||
[Operator.TavilyExtract]: 'tavily',
|
||||
[Operator.TavilySearch]: 'tavily',
|
||||
[Operator.QueritContents]: 'querit',
|
||||
[Operator.QueritSearch]: 'querit',
|
||||
[Operator.Wikipedia]: 'wikipedia',
|
||||
[Operator.YahooFinance]: 'yahoo-finance',
|
||||
@@ -123,7 +124,7 @@ const OperatorIcon = ({ name, className }: IProps) => {
|
||||
const svgIcon = SVGIconMap[name as keyof typeof SVGIconMap];
|
||||
const LucideIcon = LucideIconMap[name as keyof typeof LucideIconMap];
|
||||
|
||||
if (name === Operator.QueritSearch) {
|
||||
if (name === Operator.QueritContents || name === Operator.QueritSearch) {
|
||||
return (
|
||||
<img
|
||||
src={queritLogo}
|
||||
|
||||
@@ -111,6 +111,7 @@ export enum Operator {
|
||||
Tool = 'Tool',
|
||||
TavilySearch = 'TavilySearch',
|
||||
TavilyExtract = 'TavilyExtract',
|
||||
QueritContents = 'QueritContents',
|
||||
QueritSearch = 'QueritSearch',
|
||||
UserFillUp = 'UserFillUp',
|
||||
StringTransform = 'StringTransform',
|
||||
|
||||
@@ -2583,6 +2583,15 @@ Best for: Documents with flowing, contextually connected content — such as boo
|
||||
queritLanguageInclude: 'Languages to include',
|
||||
queritLanguageIncludeTip: 'Return results in the specified languages.',
|
||||
queritListPlaceholder: 'Enter a value',
|
||||
queritContentsUrls: 'URLs',
|
||||
queritContentsUrlsTip:
|
||||
'Enter between one and ten absolute HTTP or HTTPS URLs, separated by commas.',
|
||||
queritContentsTimeout: 'Crawl timeout',
|
||||
queritContentsTimeoutTip:
|
||||
'Set the per-page crawl timeout from 1 to 60 seconds.',
|
||||
queritContentsMetadata: 'Include page metadata',
|
||||
queritContentsMetadataTip:
|
||||
'Return page title, publication time, site name, and site icon when available.',
|
||||
Days: 'Days',
|
||||
comma: 'Comma',
|
||||
semicolon: 'Semicolon',
|
||||
@@ -3241,6 +3250,9 @@ This process aggregates variables from multiple branches into a single variable
|
||||
queritSearch: 'Querit search',
|
||||
queritSearchDescription:
|
||||
'Search the web with Querit and return source-backed results for agents.',
|
||||
queritContents: 'Querit contents',
|
||||
queritContentsDescription:
|
||||
'Crawl web pages with Querit and return their contents.',
|
||||
tavilyExtract: 'Tavily extract',
|
||||
tavilyExtractDescription: 'Tavily Extract',
|
||||
log: 'Log',
|
||||
@@ -3274,6 +3286,7 @@ This process aggregates variables from multiple branches into a single variable
|
||||
textProcessing: 'Tidying up text',
|
||||
tavilySearch: 'Searching the web',
|
||||
queritSearch: 'Searching the web with Querit',
|
||||
queritContents: 'Reading web pages with Querit',
|
||||
tavilyExtract: 'Reading the page',
|
||||
exeSQL: 'Querying database',
|
||||
google: 'Searching the web',
|
||||
|
||||
@@ -2218,6 +2218,15 @@ NER:使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系
|
||||
queritLanguageInclude: '包含的语言',
|
||||
queritLanguageIncludeTip: '返回使用指定语言的结果。',
|
||||
queritListPlaceholder: '请输入内容',
|
||||
queritContentsUrls: '网页地址',
|
||||
queritContentsUrlsTip:
|
||||
'请输入 1 到 10 个完整的 HTTP 或 HTTPS 地址,并使用英文逗号分隔。',
|
||||
queritContentsTimeout: '抓取超时',
|
||||
queritContentsTimeoutTip:
|
||||
'设置每个页面的抓取超时时间,范围为 1 到 60 秒。',
|
||||
queritContentsMetadata: '包含页面元数据',
|
||||
queritContentsMetadataTip:
|
||||
'在可用时返回页面标题、发布时间、站点名称和站点图标。',
|
||||
days: '天数',
|
||||
comma: '逗号',
|
||||
semicolon: '分号',
|
||||
@@ -2838,6 +2847,8 @@ NER:使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系
|
||||
queritSearch: 'Querit 搜索',
|
||||
queritSearchDescription:
|
||||
'使用 Querit 搜索网络,并为智能体返回带来源的搜索结果。',
|
||||
queritContents: 'Querit 网页内容',
|
||||
queritContentsDescription: '使用 Querit 抓取网页并返回页面内容。',
|
||||
tavilyExtract: 'Tavily 提取',
|
||||
tavilyExtractDescription: 'Tavily 提取',
|
||||
log: '日志',
|
||||
@@ -2859,6 +2870,7 @@ NER:使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系
|
||||
textProcessing: '整理文字',
|
||||
tavilySearch: '正在网上搜索',
|
||||
queritSearch: '正在使用 Querit 搜索网络',
|
||||
queritContents: '正在使用 Querit 读取网页',
|
||||
tavilyExtract: '读取网页内容',
|
||||
exeSQL: '查询数据库',
|
||||
google: '正在网上搜索',
|
||||
|
||||
@@ -109,6 +109,7 @@ export function AccordionOperators({
|
||||
operators={[
|
||||
Operator.TavilySearch,
|
||||
Operator.TavilyExtract,
|
||||
Operator.QueritContents,
|
||||
Operator.QueritSearch,
|
||||
Operator.ExeSQL,
|
||||
Operator.Google,
|
||||
|
||||
@@ -592,6 +592,26 @@ export const initialQueritValues = {
|
||||
},
|
||||
};
|
||||
|
||||
export enum QueritContentFormat {
|
||||
Text = 'text',
|
||||
Markdown = 'markdown',
|
||||
Html = 'html',
|
||||
}
|
||||
|
||||
export const initialQueritContentsValues = {
|
||||
api_key: '',
|
||||
urls: '',
|
||||
format: QueritContentFormat.Markdown,
|
||||
crawl_timeout: 10,
|
||||
extras_meta: false,
|
||||
outputs: {
|
||||
json: {
|
||||
value: {},
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export enum TavilyExtractDepth {
|
||||
Basic = 'basic',
|
||||
Advanced = 'advanced',
|
||||
@@ -742,6 +762,7 @@ export const RestrictedUpstreamMap = {
|
||||
[Operator.WaitingDialogue]: [Operator.Begin],
|
||||
[Operator.Agent]: [Operator.Begin],
|
||||
[Operator.TavilySearch]: [Operator.Begin],
|
||||
[Operator.QueritContents]: [Operator.Begin],
|
||||
[Operator.QueritSearch]: [Operator.Begin],
|
||||
[Operator.TavilyExtract]: [Operator.Begin],
|
||||
[Operator.StringTransform]: [Operator.Begin],
|
||||
@@ -798,6 +819,7 @@ export const NodeMap = {
|
||||
[Operator.Agent]: 'agentNode',
|
||||
[Operator.Tool]: 'toolNode',
|
||||
[Operator.TavilySearch]: 'ragNode',
|
||||
[Operator.QueritContents]: 'ragNode',
|
||||
[Operator.QueritSearch]: 'ragNode',
|
||||
[Operator.UserFillUp]: 'ragNode',
|
||||
[Operator.StringTransform]: 'ragNode',
|
||||
|
||||
@@ -26,6 +26,7 @@ import LoopForm from '../form/loop-form';
|
||||
import MessageForm from '../form/message-form';
|
||||
import ParserForm from '../form/parser-form';
|
||||
import PubMedForm from '../form/pubmed-form';
|
||||
import QueritContentsForm from '../form/querit-contents-form';
|
||||
import QueritForm from '../form/querit-form';
|
||||
import BGPTForm from '../form/bgpt-form';
|
||||
import RetrievalForm from '../form/retrieval-form/next';
|
||||
@@ -146,6 +147,9 @@ export const FormConfigMap = {
|
||||
[Operator.TavilySearch]: {
|
||||
component: TavilyForm,
|
||||
},
|
||||
[Operator.QueritContents]: {
|
||||
component: QueritContentsForm,
|
||||
},
|
||||
[Operator.QueritSearch]: {
|
||||
component: QueritForm,
|
||||
},
|
||||
|
||||
@@ -23,6 +23,7 @@ const Menus = [
|
||||
list: [
|
||||
Operator.TavilySearch,
|
||||
Operator.TavilyExtract,
|
||||
Operator.QueritContents,
|
||||
Operator.QueritSearch,
|
||||
Operator.Google,
|
||||
// Operator.Bing,
|
||||
|
||||
138
web/src/pages/agent/form/querit-contents-form/index.tsx
Normal file
138
web/src/pages/agent/form/querit-contents-form/index.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
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 { Switch } from '@/components/ui/switch';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
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 {
|
||||
QueritContentFormat,
|
||||
initialQueritContentsValues,
|
||||
} 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 { ApiKeyField } from '../components/api-key-field';
|
||||
import { FormWrapper } from '../components/form-wrapper';
|
||||
import { Output } from '../components/output';
|
||||
import { PromptEditor } from '../components/prompt-editor';
|
||||
|
||||
const outputList = buildOutputList(initialQueritContentsValues.outputs);
|
||||
|
||||
const FormSchema = z.object({
|
||||
api_key: z.string(),
|
||||
urls: z.string().min(1),
|
||||
format: z.enum([
|
||||
QueritContentFormat.Text,
|
||||
QueritContentFormat.Markdown,
|
||||
QueritContentFormat.Html,
|
||||
]),
|
||||
crawl_timeout: z.coerce.number().int().min(1).max(60),
|
||||
extras_meta: z.boolean(),
|
||||
});
|
||||
|
||||
function QueritContentsForm({ node }: INextOperatorForm) {
|
||||
const { t } = useTranslate('flow');
|
||||
const values = useFormValues(initialQueritContentsValues, node);
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
defaultValues: values,
|
||||
resolver: zodResolver(FormSchema),
|
||||
mode: 'onChange',
|
||||
});
|
||||
|
||||
useWatchFormChange(node?.id, form);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<FormWrapper>
|
||||
<FormContainer>
|
||||
<ApiKeyField />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="urls"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel tooltip={t('queritContentsUrlsTip')}>
|
||||
{t('queritContentsUrls')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<PromptEditor
|
||||
{...field}
|
||||
multiLine={false}
|
||||
showToolbar={false}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="format"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('format')}</FormLabel>
|
||||
<FormControl>
|
||||
<RAGFlowSelect
|
||||
{...field}
|
||||
options={buildOptions(QueritContentFormat)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="crawl_timeout"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel tooltip={t('queritContentsTimeoutTip')}>
|
||||
{t('queritContentsTimeout')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" min={1} max={60} step={1} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="extras_meta"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between">
|
||||
<FormLabel tooltip={t('queritContentsMetadataTip')}>
|
||||
{t('queritContentsMetadata')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</FormContainer>
|
||||
</FormWrapper>
|
||||
<div className="p-5">
|
||||
<Output list={outputList} />
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(QueritContentsForm);
|
||||
@@ -14,7 +14,7 @@ import QueritForm from './querit-form';
|
||||
import BGPTForm from './bgpt-form';
|
||||
import RetrievalForm from './retrieval-form';
|
||||
import SearXNGForm from './searxng-form';
|
||||
import TavilyForm from './tavily-form';
|
||||
import ApiKeyToolForm from './tavily-form';
|
||||
import WenCaiForm from './wencai-form';
|
||||
import WikipediaForm from './wikipedia-form';
|
||||
import YahooFinanceForm from './yahoo-finance-form';
|
||||
@@ -35,8 +35,9 @@ export const ToolFormConfigMap = {
|
||||
[Operator.YahooFinance]: YahooFinanceForm,
|
||||
[Operator.Crawler]: CrawlerForm,
|
||||
[Operator.Email]: EmailForm,
|
||||
[Operator.TavilySearch]: TavilyForm,
|
||||
[Operator.TavilyExtract]: TavilyForm,
|
||||
[Operator.TavilySearch]: ApiKeyToolForm,
|
||||
[Operator.TavilyExtract]: ApiKeyToolForm,
|
||||
[Operator.QueritContents]: ApiKeyToolForm,
|
||||
[Operator.QueritSearch]: QueritForm,
|
||||
[Operator.WenCai]: WenCaiForm,
|
||||
[Operator.SearXNG]: SearXNGForm,
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
initialParserValues,
|
||||
initialPubMedValues,
|
||||
initialBGPTValues,
|
||||
initialQueritContentsValues,
|
||||
initialQueritValues,
|
||||
initialRetrievalValues,
|
||||
initialRewriteQuestionValues,
|
||||
@@ -168,6 +169,7 @@ export const useInitializeOperatorParams = () => {
|
||||
[Operator.Agent]: { ...initialAgentValues, llm_id: llmId },
|
||||
[Operator.Tool]: {},
|
||||
[Operator.TavilySearch]: initialTavilyValues,
|
||||
[Operator.QueritContents]: initialQueritContentsValues,
|
||||
[Operator.QueritSearch]: initialQueritValues,
|
||||
[Operator.KeenableSearch]: initialKeenableValues,
|
||||
[Operator.UserFillUp]: initialUserFillUpValues,
|
||||
|
||||
@@ -23,6 +23,8 @@ export function useAgentToolInitialValues() {
|
||||
};
|
||||
case Operator.QueritSearch:
|
||||
return getQueritAgentInitialValues(initialValues);
|
||||
case Operator.QueritContents:
|
||||
return { api_key: '' };
|
||||
case Operator.ExeSQL:
|
||||
return omit(initialValues, 'sql');
|
||||
case Operator.Bing:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
jest.mock('@/constants/agent', () => ({
|
||||
Operator: {
|
||||
QueritContents: 'QueritContents',
|
||||
QueritSearch: 'QueritSearch',
|
||||
},
|
||||
}));
|
||||
@@ -15,6 +16,13 @@ describe('getToolOperatorName', () => {
|
||||
},
|
||||
);
|
||||
|
||||
it.each(['QueritContents', 'querit_contents'])(
|
||||
'maps the Querit Contents timeline name %p to its operator',
|
||||
(toolName) => {
|
||||
expect(getToolOperatorName(toolName)).toBe(Operator.QueritContents);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([undefined, null, ''])(
|
||||
'returns an empty name for the missing value %p',
|
||||
(toolName) => {
|
||||
|
||||
@@ -9,6 +9,9 @@ export function getToolOperatorName(toolName?: string | null) {
|
||||
if (normalizedName === Operator.QueritSearch.toLowerCase()) {
|
||||
return Operator.QueritSearch;
|
||||
}
|
||||
if (normalizedName === Operator.QueritContents.toLowerCase()) {
|
||||
return Operator.QueritContents;
|
||||
}
|
||||
|
||||
return toolName
|
||||
.split('_')
|
||||
|
||||
@@ -29,6 +29,7 @@ type IToolIcon =
|
||||
| Operator.BGPT
|
||||
| Operator.TavilyExtract
|
||||
| Operator.TavilySearch
|
||||
| Operator.QueritContents
|
||||
| Operator.QueritSearch
|
||||
| Operator.KeenableSearch
|
||||
| Operator.Wikipedia
|
||||
|
||||
@@ -5,6 +5,7 @@ jest.mock('@/constants/agent', () => ({
|
||||
Google: 'Google',
|
||||
KeenableSearch: 'KeenableSearch',
|
||||
BGPT: 'Bing',
|
||||
QueritContents: 'QueritContents',
|
||||
QueritSearch: 'QueritSearch',
|
||||
},
|
||||
}));
|
||||
@@ -44,28 +45,31 @@ describe('clearSensitiveFields', () => {
|
||||
expect(dsl.tools[0].params.api_key).toBe('querit-secret');
|
||||
});
|
||||
|
||||
it.each(['querit', 'querit_search', 'queritsearch'])(
|
||||
'clears a Querit API key for the %s registry alias',
|
||||
(componentName) => {
|
||||
const dsl = {
|
||||
tools: [
|
||||
{
|
||||
component_name: componentName,
|
||||
params: {
|
||||
api_key: 'querit-secret',
|
||||
count: 5,
|
||||
},
|
||||
it.each([
|
||||
'querit',
|
||||
'querit_contents',
|
||||
'queritcontents',
|
||||
'querit_search',
|
||||
'queritsearch',
|
||||
])('clears a Querit API key for the %s registry alias', (componentName) => {
|
||||
const dsl = {
|
||||
tools: [
|
||||
{
|
||||
component_name: componentName,
|
||||
params: {
|
||||
api_key: 'querit-secret',
|
||||
count: 5,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const sanitized = clearSensitiveFields(dsl);
|
||||
const sanitized = clearSensitiveFields(dsl);
|
||||
|
||||
expect(sanitized.tools[0].params.api_key).toBe('');
|
||||
expect(sanitized.tools[0].params.count).toBe(5);
|
||||
expect(dsl.tools[0].params.api_key).toBe('querit-secret');
|
||||
},
|
||||
);
|
||||
expect(sanitized.tools[0].params.api_key).toBe('');
|
||||
expect(sanitized.tools[0].params.count).toBe(5);
|
||||
expect(dsl.tools[0].params.api_key).toBe('querit-secret');
|
||||
});
|
||||
|
||||
it('clears a standalone Querit Canvas key from graph and components', () => {
|
||||
const dsl = {
|
||||
|
||||
@@ -7,6 +7,7 @@ const apiKeyOperators = [
|
||||
Operator.Google,
|
||||
Operator.KeenableSearch,
|
||||
Operator.BGPT,
|
||||
Operator.QueritContents,
|
||||
Operator.QueritSearch,
|
||||
];
|
||||
|
||||
@@ -15,7 +16,7 @@ function isQueritOperator(value: unknown) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ['querit', 'queritsearch'].includes(
|
||||
return ['querit', 'queritcontents', 'queritsearch'].includes(
|
||||
value.replace(/_/g, '').toLowerCase(),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user