feat: 支持插件列表 与 基于 Serpapi 的搜索引擎插件 (#12)

This commit is contained in:
Arvin Xu
2023-07-23 18:08:03 +08:00
committed by GitHub
parent c263a60c17
commit d89e06f294
19 changed files with 274 additions and 26 deletions

View File

@@ -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",

View File

@@ -35,6 +35,9 @@ export default {
'newAgent': '新建助手',
'noDescription': '暂无描述',
'ok': '确定',
'plugin-realtimeWeather': '实时天气预报',
'plugin-searchEngine': '搜索引擎',
'pluginList': '插件列表',
'profile': '助手身份',
'reset': '重置',
'searchAgentPlaceholder': '搜索助手和对话...',

View File

@@ -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);
}

View File

@@ -1,3 +0,0 @@
import getWeather from './weather';
export const plugins = [getWeather];

View 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;

View File

@@ -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>
);
};

View File

@@ -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
View File

@@ -0,0 +1,6 @@
import searchEngine from './searchEngine';
import getWeather from './weather';
const pluginList = [getWeather, searchEngine];
export default pluginList;

View 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;

View 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;

View File

@@ -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;

View File

@@ -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,
),

View File

@@ -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());

View File

@@ -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);

View File

@@ -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,
),

View File

@@ -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

View File

@@ -39,6 +39,10 @@ export interface LobeAgentConfig {
* 语言模型参数
*/
params: LLMParams;
/**
* 启用的插件
*/
plugins?: string[];
/**
* 系统角色
*/

1
src/utils/env.ts Normal file
View File

@@ -0,0 +1 @@
export const isDev = process.env.NODE_ENV === 'development';