mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
✨ feat: 支持插件列表 与 基于 Serpapi 的搜索引擎插件 (#12)
This commit is contained in:
@@ -82,11 +82,13 @@
|
||||
"next": "13.4.7",
|
||||
"openai-edge": "^1",
|
||||
"polished": "^4",
|
||||
"query-string": "^8",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-hotkeys-hook": "^4",
|
||||
"react-i18next": "^13",
|
||||
"react-layout-kit": "^1",
|
||||
"serpapi": "^1",
|
||||
"swr": "^2",
|
||||
"ts-md5": "^1",
|
||||
"uuid": "^9",
|
||||
|
||||
@@ -35,6 +35,9 @@ export default {
|
||||
'newAgent': '新建助手',
|
||||
'noDescription': '暂无描述',
|
||||
'ok': '确定',
|
||||
'plugin-realtimeWeather': '实时天气预报',
|
||||
'plugin-searchEngine': '搜索引擎',
|
||||
'pluginList': '插件列表',
|
||||
'profile': '助手身份',
|
||||
'reset': '重置',
|
||||
'searchAgentPlaceholder': '搜索助手和对话...',
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ChatCompletionFunctions, ChatCompletionRequestMessage } from 'openai-ed
|
||||
|
||||
import { OpenAIStreamPayload } from '@/types/openai';
|
||||
|
||||
import { plugins } from './plugins';
|
||||
import pluginList from '../../plugins';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
@@ -18,12 +18,28 @@ const config = new Configuration({
|
||||
|
||||
const openai = new OpenAIApi(config, isDev && OPENAI_PROXY_URL ? OPENAI_PROXY_URL : undefined);
|
||||
|
||||
const functions: ChatCompletionFunctions[] = plugins.map((f) => f.schema);
|
||||
|
||||
export default async function handler(req: Request) {
|
||||
// Extract the `messages` from the body of the request
|
||||
const { messages, ...params } = (await req.json()) as OpenAIStreamPayload;
|
||||
const {
|
||||
messages,
|
||||
plugins: enabledPlugins,
|
||||
...params
|
||||
} = (await req.json()) as OpenAIStreamPayload;
|
||||
|
||||
// ============ 1. 前置处理 functions ============ //
|
||||
|
||||
const filterFunctions: ChatCompletionFunctions[] = pluginList
|
||||
.filter((p) => {
|
||||
// 如果不存在 enabledPlugins,那么全部不启用
|
||||
if (!enabledPlugins) return false;
|
||||
|
||||
// 如果存在 enabledPlugins,那么只启用 enabledPlugins 中的插件
|
||||
return enabledPlugins.includes(p.name);
|
||||
})
|
||||
.map((f) => f.schema);
|
||||
|
||||
const functions = filterFunctions.length === 0 ? undefined : filterFunctions;
|
||||
|
||||
// ============ 2. 前置处理 messages ============ //
|
||||
const formatMessages = messages.map((m) => ({ content: m.content, role: m.role }));
|
||||
|
||||
const response = await openai.createChatCompletion({
|
||||
@@ -35,7 +51,9 @@ export default async function handler(req: Request) {
|
||||
|
||||
const stream = OpenAIStream(response, {
|
||||
experimental_onFunctionCall: async ({ name, arguments: args }, createFunctionCallMessages) => {
|
||||
const func = plugins.find((f) => f.name === name);
|
||||
console.log(`执行 functionCall [${name}]`, 'args:', args);
|
||||
|
||||
const func = pluginList.find((f) => f.name === name);
|
||||
|
||||
if (func) {
|
||||
const result = await func.runner(args as any);
|
||||
@@ -51,5 +69,6 @@ export default async function handler(req: Request) {
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return new StreamingTextResponse(stream);
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import getWeather from './weather';
|
||||
|
||||
export const plugins = [getWeather];
|
||||
46
src/pages/chat/[id]/edit/AgentConfig/Plugin.tsx
Normal file
46
src/pages/chat/[id]/edit/AgentConfig/Plugin.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Avatar } from '@lobehub/ui';
|
||||
import { List, Switch, Tag } from 'antd';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import pluginList from '@/plugins';
|
||||
import { agentSelectors, useSessionStore } from '@/store/session';
|
||||
|
||||
const PluginList = () => {
|
||||
const { t } = useTranslation('common');
|
||||
|
||||
const config = useSessionStore(agentSelectors.currentAgentConfigSafe, isEqual);
|
||||
|
||||
const [toggleAgentPlugin] = useSessionStore((s) => [s.toggleAgentPlugin], shallow);
|
||||
|
||||
return (
|
||||
<List
|
||||
bordered
|
||||
dataSource={pluginList}
|
||||
renderItem={(item) => (
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
avatar={<Avatar avatar={item.avatar} />}
|
||||
description={item.schema.description}
|
||||
title={
|
||||
<Flexbox align={'center'} gap={8} horizontal>
|
||||
{t(`plugin-${item.name}` as any)} <Tag>{item.name}</Tag>
|
||||
</Flexbox>
|
||||
}
|
||||
/>
|
||||
<Switch
|
||||
checked={!config.plugins ? false : config.plugins.includes(item.name)}
|
||||
onChange={() => {
|
||||
toggleAgentPlugin(item.name);
|
||||
}}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
size={'large'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginList;
|
||||
@@ -9,6 +9,7 @@ import { LanguageModel } from '@/types/llm';
|
||||
|
||||
import { FormItem } from '../FormItem';
|
||||
import { useStyles } from '../style';
|
||||
import Plugin from './Plugin';
|
||||
import Prompt from './Prompt';
|
||||
|
||||
const AgentConfig = () => {
|
||||
@@ -58,8 +59,6 @@ const AgentConfig = () => {
|
||||
</FormItem>
|
||||
<Prompt />
|
||||
<Collapse
|
||||
activeKey={['advanceSettings']}
|
||||
bordered={false}
|
||||
className={styles.title}
|
||||
expandIconPosition={'end'}
|
||||
items={[
|
||||
@@ -95,7 +94,19 @@ const AgentConfig = () => {
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Flexbox>{' '}
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
distribution={'space-between'}
|
||||
horizontal
|
||||
paddingBlock={12}
|
||||
style={{
|
||||
borderBottom: `1px solid ${theme.colorBorder}`,
|
||||
}}
|
||||
>
|
||||
<Flexbox className={styles.profile}> {t('pluginList')}</Flexbox>
|
||||
</Flexbox>
|
||||
<Plugin />
|
||||
</Flexbox>
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -30,13 +30,5 @@ export const useStyles = createStyles(({ css, token }) => ({
|
||||
title: css`
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
|
||||
.ant-collapse-header {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.ant-collapse-content-box {
|
||||
padding: 0 !important;
|
||||
}
|
||||
`,
|
||||
}));
|
||||
|
||||
6
src/plugins/index.ts
Normal file
6
src/plugins/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import searchEngine from './searchEngine';
|
||||
import getWeather from './weather';
|
||||
|
||||
const pluginList = [getWeather, searchEngine];
|
||||
|
||||
export default pluginList;
|
||||
27
src/plugins/searchEngine/index.ts
Normal file
27
src/plugins/searchEngine/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { ChatCompletionFunctions } from 'openai-edge/types/api';
|
||||
|
||||
import runner from './runner';
|
||||
|
||||
const schema: ChatCompletionFunctions = {
|
||||
description: '查询搜索引擎获取信息',
|
||||
name: 'searchEngine',
|
||||
parameters: {
|
||||
properties: {
|
||||
keywords: {
|
||||
description: '关键词',
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['keywords'],
|
||||
type: 'object',
|
||||
},
|
||||
};
|
||||
|
||||
const searchEngine = {
|
||||
avatar: '🔍',
|
||||
name: 'searchEngine',
|
||||
runner,
|
||||
schema,
|
||||
};
|
||||
|
||||
export default searchEngine;
|
||||
106
src/plugins/searchEngine/runner.ts
Normal file
106
src/plugins/searchEngine/runner.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import querystring from 'query-string';
|
||||
|
||||
const BASE_URL = 'https://serpapi.com/search';
|
||||
|
||||
const API_KEY = process.env.SERPAI_API_KEY;
|
||||
|
||||
export type OrganicResults = OrganicResult[];
|
||||
|
||||
export interface OrganicResult {
|
||||
about_page_link: string;
|
||||
about_page_serpapi_link: string;
|
||||
about_this_result: AboutThisResult;
|
||||
cached_page_link?: string;
|
||||
date?: string;
|
||||
displayed_link: string;
|
||||
favicon?: string;
|
||||
link: string;
|
||||
position: number;
|
||||
related_results?: RelatedResult[];
|
||||
rich_snippet?: RichSnippet;
|
||||
snippet: string;
|
||||
snippet_highlighted_words?: string[];
|
||||
source: string;
|
||||
thumbnail?: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface AboutThisResult {
|
||||
languages: string[];
|
||||
regions: string[];
|
||||
source: Source;
|
||||
}
|
||||
|
||||
export interface Source {
|
||||
description: string;
|
||||
icon: string;
|
||||
security?: string;
|
||||
source_info_link?: string;
|
||||
}
|
||||
|
||||
export interface RelatedResult {
|
||||
about_page_link: string;
|
||||
about_page_serpapi_link: string;
|
||||
about_this_result: AboutThisResult2;
|
||||
cached_page_link: string;
|
||||
date: string;
|
||||
displayed_link: string;
|
||||
link: string;
|
||||
position: number;
|
||||
snippet: string;
|
||||
snippet_highlighted_words: string[];
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface AboutThisResult2 {
|
||||
languages: string[];
|
||||
regions: string[];
|
||||
source: Source2;
|
||||
}
|
||||
|
||||
export interface Source2 {
|
||||
description: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export interface RichSnippet {
|
||||
top: Top;
|
||||
}
|
||||
|
||||
export interface Top {
|
||||
detected_extensions: DetectedExtensions;
|
||||
extensions: string[];
|
||||
}
|
||||
|
||||
export interface DetectedExtensions {
|
||||
month_ago: number;
|
||||
}
|
||||
|
||||
const fetchResult = async (keywords: string) => {
|
||||
const params = {
|
||||
api_key: API_KEY,
|
||||
gl: 'cn',
|
||||
google_domain: 'google.com',
|
||||
hl: 'zh-cn',
|
||||
location: 'China',
|
||||
q: keywords,
|
||||
};
|
||||
|
||||
const query = querystring.stringify(params);
|
||||
|
||||
const res = await fetch(`${BASE_URL}?${query}`);
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
const results = data.organic_results as OrganicResults;
|
||||
|
||||
return results.map((r) => ({
|
||||
content: r.snippet,
|
||||
date: r.date,
|
||||
link: r.link,
|
||||
source: r.source,
|
||||
title: r.title,
|
||||
}));
|
||||
};
|
||||
|
||||
export default fetchResult;
|
||||
@@ -2,7 +2,7 @@ import runner from './runner';
|
||||
|
||||
const schema = {
|
||||
description: '获取当前天气情况',
|
||||
name: 'fetchWeather',
|
||||
name: 'realtimeWeather',
|
||||
parameters: {
|
||||
properties: {
|
||||
city: {
|
||||
@@ -15,6 +15,6 @@ const schema = {
|
||||
},
|
||||
};
|
||||
|
||||
const getWeather = { name: 'fetchWeather', runner, schema };
|
||||
const getWeather = { avatar: '☂️', name: 'realtimeWeather', runner, schema };
|
||||
|
||||
export default getWeather;
|
||||
@@ -1,6 +1,8 @@
|
||||
import { create } from 'zustand';
|
||||
import { PersistOptions, devtools, persist } from 'zustand/middleware';
|
||||
|
||||
import { isDev } from '@/utils/env';
|
||||
|
||||
import { SessionStore, createStore } from './store';
|
||||
|
||||
type SessionPersist = Pick<SessionStore, 'sessions'>;
|
||||
@@ -23,7 +25,7 @@ const persistOptions: PersistOptions<SessionStore, SessionPersist> = {
|
||||
export const useSessionStore = create<SessionStore>()(
|
||||
persist(
|
||||
devtools(createStore, {
|
||||
name: LOBE_CHAT,
|
||||
name: LOBE_CHAT + (isDev ? '_DEV' : ''),
|
||||
}),
|
||||
persistOptions,
|
||||
),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { produce } from 'immer';
|
||||
import { StateCreator } from 'zustand/vanilla';
|
||||
|
||||
import { promptPickEmoji, promptSummaryAgentName, promptSummaryDescription } from '@/prompts/agent';
|
||||
@@ -43,12 +44,13 @@ export interface AgentAction {
|
||||
* @returns 任意类型的返回值
|
||||
*/
|
||||
internalUpdateAgentMeta: (id: string) => any;
|
||||
toggleAgentPlugin: (pluginId: string) => void;
|
||||
|
||||
/**
|
||||
* 切换配置
|
||||
* @param showPanel - 是否显示面板,默认为 true
|
||||
*/
|
||||
toggleConfig: (showPanel?: boolean) => void;
|
||||
|
||||
/**
|
||||
* 更新代理配置
|
||||
* @param config - 部分 LobeAgentConfig 的配置
|
||||
@@ -191,6 +193,27 @@ export const createAgentSlice: StateCreator<
|
||||
};
|
||||
},
|
||||
|
||||
toggleAgentPlugin: (id: string) => {
|
||||
const { activeId } = get();
|
||||
const session = sessionSelectors.currentSession(get());
|
||||
if (!activeId || !session) return;
|
||||
|
||||
const config = produce(session.config, (draft) => {
|
||||
if (draft.plugins === undefined) {
|
||||
draft.plugins = [id];
|
||||
} else {
|
||||
const plugins = draft.plugins;
|
||||
if (plugins.includes(id)) {
|
||||
plugins.splice(plugins.indexOf(id), 1);
|
||||
} else {
|
||||
plugins.push(id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
get().dispatchSession({ config, id: activeId, type: 'updateSessionConfig' });
|
||||
},
|
||||
|
||||
toggleConfig: (newValue) => {
|
||||
const showAgentSettings = typeof newValue === 'boolean' ? newValue : !get().showAgentSettings;
|
||||
|
||||
@@ -204,6 +227,7 @@ export const createAgentSlice: StateCreator<
|
||||
|
||||
get().dispatchSession({ config, id: activeId, type: 'updateSessionConfig' });
|
||||
},
|
||||
|
||||
updateAgentMeta: (meta) => {
|
||||
const { activeId } = get();
|
||||
const session = sessionSelectors.currentSession(get());
|
||||
|
||||
@@ -104,7 +104,8 @@ export const createChatSlice: StateCreator<
|
||||
set({ chatLoading: true });
|
||||
const config = agentSelectors.currentAgentConfigSafe(get());
|
||||
|
||||
const fetcher = () => fetchChatModel({ messages, model: config.model, ...config.params });
|
||||
const fetcher = () =>
|
||||
fetchChatModel({ messages, model: config.model, ...config.params, plugins: config.plugins });
|
||||
|
||||
await fetchSSE(fetcher, options);
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { create } from 'zustand';
|
||||
import { type PersistOptions, devtools, persist } from 'zustand/middleware';
|
||||
|
||||
import { isDev } from '@/utils/env';
|
||||
|
||||
import { type SettingsStore, createStore } from './store';
|
||||
|
||||
export const LOBE_SETTINGS = 'LOBE_SETTINGS';
|
||||
@@ -13,7 +15,7 @@ const persistOptions: PersistOptions<SettingsStore> = {
|
||||
export const useSettings = create<SettingsStore>()(
|
||||
persist(
|
||||
devtools(createStore, {
|
||||
name: LOBE_SETTINGS,
|
||||
name: LOBE_SETTINGS + (isDev ? '_DEV' : ''),
|
||||
}),
|
||||
persistOptions,
|
||||
),
|
||||
|
||||
@@ -39,6 +39,10 @@ export interface OpenAIStreamPayload {
|
||||
* @title 返回的文本数量
|
||||
*/
|
||||
n?: number;
|
||||
/**
|
||||
* 开启的插件列表
|
||||
*/
|
||||
plugins?: string[];
|
||||
/**
|
||||
* @title 控制生成文本中的惩罚系数,用于减少主题的变化
|
||||
* @default 0
|
||||
@@ -54,6 +58,7 @@ export interface OpenAIStreamPayload {
|
||||
* @default 0.5
|
||||
*/
|
||||
temperature: number;
|
||||
|
||||
/**
|
||||
* @title 控制生成文本中最高概率的单个令牌
|
||||
* @default 1
|
||||
|
||||
@@ -39,6 +39,10 @@ export interface LobeAgentConfig {
|
||||
* 语言模型参数
|
||||
*/
|
||||
params: LLMParams;
|
||||
/**
|
||||
* 启用的插件
|
||||
*/
|
||||
plugins?: string[];
|
||||
/**
|
||||
* 系统角色
|
||||
*/
|
||||
|
||||
1
src/utils/env.ts
Normal file
1
src/utils/env.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const isDev = process.env.NODE_ENV === 'development';
|
||||
Reference in New Issue
Block a user