mirror of
https://github.com/nocobase/nocobase.git
synced 2026-10-28 03:04:56 +08:00
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:
@@ -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 ? (
|
||||
|
||||
+1
@@ -179,6 +179,7 @@ export const useChatBoxActions = () => {
|
||||
attachments,
|
||||
workContext,
|
||||
skillSettings,
|
||||
webSearch: false,
|
||||
});
|
||||
}
|
||||
return;
|
||||
|
||||
+63
@@ -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,
|
||||
};
|
||||
};
|
||||
+2
-1
@@ -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;
|
||||
|
||||
+4
@@ -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 = {
|
||||
|
||||
+17
-2
@@ -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();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user