plugin-ai conversation add web search switch (#7580)

* feat: plugin ai add web search switch in chat box

* feat: plugin-ai dashscope add web search support
This commit is contained in:
Drol
2025-10-13 15:31:50 +08:00
committed by GitHub
parent a42d557e44
commit 0c2e3aae56
17 changed files with 223 additions and 9 deletions
@@ -0,0 +1,32 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import React, { useState } from 'react';
import { Button, Tooltip } from 'antd';
import { GlobalOutlined } from '@ant-design/icons';
import { useChatConversationOptions } from './hooks/useChatConversationOptions';
import { useT } from '../../locale';
export const SearchSwitch: React.FC = () => {
const t = useT();
const { loading, webSearch, updateWebSearch } = useChatConversationOptions();
const switchChecked = () => {
updateWebSearch(!webSearch);
};
return webSearch ? (
<Tooltip title={t('Disable search')} arrow={false}>
<Button color="primary" variant="filled" icon={<GlobalOutlined />} onClick={switchChecked} loading={loading} />
</Tooltip>
) : (
<Tooltip title={t('Enable search')} arrow={false}>
<Button type="text" icon={<GlobalOutlined />} onClick={switchChecked} loading={loading} />
</Tooltip>
);
};
@@ -26,6 +26,7 @@ const useSendMessage = () => {
const editingMessageId = useChatBoxStore.use.editingMessageId();
const currentConversation = useChatConversationsStore.use.currentConversation();
const webSearch = useChatConversationsStore.use.webSearch();
const attachments = useChatMessagesStore.use.attachments();
const contextItems = useChatMessagesStore.use.contextItems();
@@ -50,6 +51,7 @@ const useSendMessage = () => {
workContext: contextItems,
editingMessageId: isEditingMessage ? editingMessageId : undefined,
skillSettings,
webSearch,
});
if (isEditingMessage) {
@@ -14,6 +14,7 @@ import { AddContextButton } from '../AddContextButton';
import { useChatMessagesStore } from './stores/chat-messages';
import { useChatBoxStore } from './stores/chat-box';
import _ from 'lodash';
import { SearchSwitch } from './SearchSwitch';
export const SenderFooter: React.FC<{
components: any;
@@ -60,6 +61,7 @@ export const SenderFooter: React.FC<{
/>
<Divider type="vertical" />
<Upload />
{currentEmployee?.webSearch && <SearchSwitch />}
</Flex>
<Flex align="center">
{loading ? (
@@ -179,6 +179,7 @@ export const useChatBoxActions = () => {
attachments,
workContext,
skillSettings,
webSearch: false,
});
}
return;
@@ -0,0 +1,63 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { useAPIClient } from '@nocobase/client';
import { useChatConversationsStore } from '../stores/chat-conversations';
import { useEffect, useState } from 'react';
export const useChatConversationOptions = () => {
const api = useAPIClient();
const sessionId = useChatConversationsStore.use.currentConversation();
const webSearch = useChatConversationsStore.use.webSearch();
const setWebSearch = useChatConversationsStore.use.setWebSearch();
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!sessionId) {
return;
}
setLoading(true);
api
.resource('aiConversations')
.get({
filterByTk: sessionId,
})
.then(({ data }) => {
const { conversationSettings } = data?.data?.options ?? {};
const { webSearch } = conversationSettings ?? { webSearch: true };
setWebSearch(webSearch);
})
.finally(() => {
setLoading(false);
})
.catch(() => {});
}, [api, sessionId, setWebSearch]);
const updateWebSearch = async (webSearch: boolean) => {
if (sessionId) {
setLoading(true);
await api.resource('aiConversations').updateOptions({
filterByTk: sessionId,
values: {
conversationSettings: { webSearch },
},
});
setWebSearch(webSearch);
setLoading(false);
} else {
setWebSearch(webSearch);
}
};
return {
loading,
webSearch,
updateWebSearch,
};
};
@@ -208,6 +208,7 @@ export const useChatMessageActions = () => {
editingMessageId,
onConversationCreate,
skillSettings,
webSearch,
}: SendOptions & {
onConversationCreate?: (sessionId: string) => void;
}) => {
@@ -240,7 +241,7 @@ export const useChatMessageActions = () => {
if (!sessionId) {
const createRes = await api.resource('aiConversations').create({
values: { aiEmployee, systemMessage, skillSettings },
values: { aiEmployee, systemMessage, skillSettings, conversationSettings: { webSearch } },
});
const conversation = createRes?.data?.data;
if (!conversation) return;
@@ -15,18 +15,21 @@ interface ChatConversationsState {
currentConversation?: string;
conversations: Conversation[];
keyword: string;
webSearch: boolean;
}
interface ChatConversationsActions {
setCurrentConversation: (id: string | undefined) => void;
setKeyword: (keyword: string) => void;
setConversations: (conversations: Conversation[] | ((prev: Conversation[]) => Conversation[])) => void;
setWebSearch: (webSearch: boolean) => void;
}
const store = create<ChatConversationsState & ChatConversationsActions>((set) => ({
currentConversation: undefined,
conversations: [],
keyword: '',
webSearch: true,
setCurrentConversation: (id) => set({ currentConversation: id }),
setKeyword: (keyword) => set({ keyword }),
@@ -34,6 +37,7 @@ const store = create<ChatConversationsState & ChatConversationsActions>((set) =>
set((state) => ({
conversations: typeof conversations === 'function' ? conversations(state.conversations) : conversations,
})),
setWebSearch: (webSearch) => set({ webSearch }),
}));
export const useChatConversationsStore = createSelectors(store);
@@ -30,6 +30,7 @@ export type AIEmployee = {
skills?: { name: string }[];
};
builtIn?: boolean;
webSearch?: boolean;
};
export type SkillSettings = {
@@ -43,6 +44,16 @@ export type Conversation = {
aiEmployee: AIEmployee;
};
export type ConversationOptions = {
sessionId: string;
options: {
conversationSetting?: ConversationSetting;
};
};
export type ConversationSetting = {
webSearch?: boolean;
};
export type ContextItem = {
type: string;
uid: string;
@@ -106,6 +117,7 @@ export type SendOptions = {
workContext: ContextItem[];
editingMessageId?: string;
skillSettings?: SkillSettings;
webSearch?: boolean;
};
export type ResendOptions = {
@@ -11,7 +11,7 @@ import React from 'react';
import { SchemaComponent } from '@nocobase/client';
import { tval } from '@nocobase/utils/client';
import { namespace, useT } from '../../locale';
import { Collapse } from 'antd';
import { Collapse, Switch } from 'antd';
import { WorkflowVariableRawTextArea } from '@nocobase/plugin-workflow/client';
import { ModelSelect } from '../components/ModelSelect';
@@ -147,7 +147,7 @@ const Options: React.FC = () => {
export const ModelSettingsForm: React.FC = () => {
return (
<SchemaComponent
components={{ Options, WorkflowVariableRawTextArea, ModelSelect }}
components={{ Options, WorkflowVariableRawTextArea, ModelSelect, Switch }}
schema={{
type: 'void',
properties: {
@@ -158,6 +158,21 @@ export const ModelSettingsForm: React.FC = () => {
'x-decorator': 'FormItem',
'x-component': 'ModelSelect',
},
builtIn: {
title: tval('Built in tools', { ns: namespace }),
type: 'object',
properties: {
webSearch: {
type: 'boolean',
title: tval('Web search', { ns: namespace }),
'x-decorator': 'FormItem',
'x-decorator-props': {
layout: 'horizontal',
},
'x-component': 'Switch',
},
},
},
options: {
type: 'void',
'x-component': 'Options',
@@ -179,5 +179,7 @@
"Auto usage": "Auto usage",
"Use skills": "Use skills",
"Cite {{count}} pieces of information as references": "Cite {{count}} pieces of information as references",
"references {{index}}": "references {{index}}"
"references {{index}}": "references {{index}}",
"Enable search": "Enable search",
"Disable search": "Disable search"
}
@@ -180,5 +180,7 @@
"Auto usage": "自动使用",
"Use skills": "使用技能",
"Cite {{count}} pieces of information as references": "引用 {{count}} 篇资料作为参考",
"references {{index}}": "参考资料 {{index}}"
"references {{index}}": "参考资料 {{index}}",
"Enable search": "启用搜索",
"Disable search": "禁用搜索"
}
@@ -48,6 +48,7 @@ export class AIEmployee {
private systemMessage: string;
private aiChatConversation: AIChatConversation;
private skillSettings?: Record<string, any>;
private webSearch?: boolean;
constructor(
ctx: Context,
@@ -55,6 +56,7 @@ export class AIEmployee {
sessionId: string,
systemMessage?: string,
skillSettings?: Record<string, any>,
webSearch?: boolean,
) {
this.employee = employee;
this.ctx = ctx;
@@ -68,6 +70,7 @@ export class AIEmployee {
const locale = this.ctx.getCurrentLocale();
const builtInManager = this.plugin.builtInManager;
builtInManager.setupBuiltInInfo(locale, this.employee as unknown as AIEmployeeType);
this.webSearch = webSearch;
}
async getLLMService() {
@@ -77,6 +80,14 @@ export class AIEmployee {
throw new Error('LLM service not configured');
}
if (modelSettings?.builtIn?.webSearch === true) {
if (this.webSearch !== false) {
modelSettings.builtIn.webSearch = true;
} else {
modelSettings.builtIn.webSearch = false;
}
}
const service = await this.db.getRepository('llmServices').findOne({
filter: {
name: modelSettings.llmService,
@@ -31,11 +31,21 @@ export class DashscopeProvider extends LLMProvider {
if (responseFormat === 'json_schema' && schema) {
responseFormatOptions['json_schema'] = schema;
}
const additionalArgs = {};
if (this.modelOptions?.builtIn?.webSearch === true) {
// enable platform's web search ability
// ref: https://bailian.console.aliyun.com/?tab=doc#/doc/?type=model&url=2867560
additionalArgs['enable_search'] = true;
additionalArgs['search_options'] = { forced_search: true };
}
return new ChatOpenAI({
apiKey,
...this.modelOptions,
modelKwargs: {
response_format: responseFormatOptions,
...additionalArgs,
},
configuration: {
baseURL: baseURL || this.baseURL,
@@ -53,9 +53,9 @@ export abstract class LLMProvider {
if (!this.isToolConflict() && context.tools?.length) {
tools.push(...context.tools);
}
chain = chain.bindTools(tools);
chain = chain.bindTools?.(tools);
} else if (context.tools?.length) {
chain = chain.bindTools(context.tools);
chain = chain.bindTools?.(context.tools);
}
if (context.structuredOutput) {
@@ -125,7 +125,7 @@ class AIChatConversationImpl implements AIChatConversation {
}
return {
messages,
tools: options?.tools,
tools: options.tools,
};
}
@@ -58,7 +58,7 @@ export default {
async create(ctx: Context, next: Next) {
const userId = ctx.auth?.user.id;
const { aiEmployee, systemMessage, skillSettings } = ctx.action.params.values || {};
const { aiEmployee, systemMessage, skillSettings, conversationSettings } = ctx.action.params.values || {};
const employee = await getAIEmployee(ctx, aiEmployee.username);
if (!employee) {
ctx.throw(400, 'AI employee not found');
@@ -72,6 +72,7 @@ export default {
options: {
systemMessage,
skillSettings,
conversationSettings,
},
},
});
@@ -95,6 +96,57 @@ export default {
await next();
},
async updateOptions(ctx: Context, next: Next) {
const userId = ctx.auth?.user.id;
if (!userId) {
return ctx.throw(403);
}
const { filterByTk: sessionId } = ctx.action.params;
if (!sessionId) {
return ctx.throw(400, 'invalid sessionId');
}
const { systemMessage, skillSettings, conversationSettings } = ctx.action.params.values || {};
if (!systemMessage && !skillSettings && !conversationSettings) {
return ctx.throw(400, 'invalid options');
}
const conversation = await ctx.db.getRepository('aiConversations').findOne({
filter: {
sessionId,
userId,
},
});
if (!conversation) {
ctx.throw(400, 'invalid sessionId');
}
const options = conversation.options ?? {};
if (systemMessage) {
options['systemMessage'] = systemMessage;
}
if (skillSettings) {
options['skillSettings'] = skillSettings;
}
if (conversationSettings) {
options['conversationSettings'] = conversationSettings;
}
ctx.body = await ctx.db.getRepository('aiConversations').update({
filter: {
userId,
sessionId,
},
values: {
options,
},
});
await next();
},
async destroy(ctx: Context, next: Next) {
const userId = ctx.auth?.user.id;
if (!userId) {
@@ -299,6 +351,7 @@ export default {
sessionId,
conversation.options?.systemMessage,
conversation.options?.skillSettings,
conversation.options?.conversationSettings?.webSearch,
);
await aiEmployee.cancelToolCall();
await aiEmployee.processMessages(messages, editingMessageId);
@@ -363,6 +416,7 @@ export default {
sessionId,
conversation.options?.systemMessage,
conversation.options?.skillSettings,
conversation.options?.conversationSettings?.webSearch,
);
await aiEmployee.resendMessages(messageId);
} catch (err) {
@@ -466,6 +520,7 @@ export default {
sessionId,
conversation.options?.systemMessage,
conversation.options?.skillSettings,
conversation.options?.conversationSettings?.webSearch,
);
await aiEmployee.callTool(message.messageId, false);
} catch (err) {
@@ -524,6 +579,7 @@ export default {
sessionId,
conversation.options?.systemMessage,
conversation.options?.skillSettings,
conversation.options?.conversationSettings?.webSearch,
);
await aiEmployee.confirmToolCall(message.messageId, toolCallIds);
} catch (err) {
@@ -100,6 +100,7 @@ export const listByUser = async (ctx: Context, next: Next) => {
},
skillSettings: row.skillSettings,
builtIn: row.builtIn,
webSearch: row.modelSettings?.builtIn?.webSearch ?? false,
}));
await next();
};