@lobehub/chat 0.10.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,74 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 0.12.0](https://github.com/lobehub/lobe-chat/compare/v0.11.0...v0.12.0)
6
+
7
+ <sup>Released on **2023-07-23**</sup>
8
+
9
+ #### ✨ Features
10
+
11
+ - **misc**: 支持插件列表 与 基于 Serpapi 的搜索引擎插件.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's improved
19
+
20
+ - **misc**: 支持插件列表 与 基于 Serpapi 的搜索引擎插件, closes [#12](https://github.com/lobehub/lobe-chat/issues/12) ([d89e06f](https://github.com/lobehub/lobe-chat/commit/d89e06f))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
30
+ ## [Version 0.11.0](https://github.com/lobehub/lobe-chat/compare/v0.10.2...v0.11.0)
31
+
32
+ <sup>Released on **2023-07-23**</sup>
33
+
34
+ #### ♻ Code Refactoring
35
+
36
+ - **misc**: Remove langchain, 优化代码.
37
+
38
+ #### ✨ Features
39
+
40
+ - **misc**: 支持查询天气.
41
+
42
+ #### 💄 Styles
43
+
44
+ - **misc**: Update manifest, 增加国际化文案.
45
+
46
+ <br/>
47
+
48
+ <details>
49
+ <summary><kbd>Improvements and Fixes</kbd></summary>
50
+
51
+ #### Code refactoring
52
+
53
+ - **misc**: Remove langchain ([7b0f96c](https://github.com/lobehub/lobe-chat/commit/7b0f96c))
54
+ - **misc**: 优化代码 ([6a8f7df](https://github.com/lobehub/lobe-chat/commit/6a8f7df))
55
+
56
+ #### What's improved
57
+
58
+ - **misc**: 支持查询天气 ([34bf285](https://github.com/lobehub/lobe-chat/commit/34bf285))
59
+
60
+ #### Styles
61
+
62
+ - **misc**: Update manifest ([ea9e8de](https://github.com/lobehub/lobe-chat/commit/ea9e8de))
63
+ - **misc**: 增加国际化文案 ([f5e8d7c](https://github.com/lobehub/lobe-chat/commit/f5e8d7c))
64
+
65
+ </details>
66
+
67
+ <div align="right">
68
+
69
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
70
+
71
+ </div>
72
+
5
73
  ### [Version 0.10.2](https://github.com/lobehub/lobe-chat/compare/v0.10.1...v0.10.2)
6
74
 
7
75
  <sup>Released on **2023-07-23**</sup>
package/README.md CHANGED
@@ -8,9 +8,7 @@
8
8
 
9
9
  <h1>Lobe Chat</h1>
10
10
 
11
- Lobe Chat is an open-source chatbot client using LangChain
12
-
13
- Typescript and Next.js
11
+ Lobe Chat is an open-source chatbot client using OpenAI and Next.js
14
12
 
15
13
  [Changelog](./CHANGELOG.md) · [Report Bug][issues-url] · [Request Feature][issues-url]
16
14
 
package/next.config.mjs CHANGED
@@ -1,13 +1,14 @@
1
1
  import nextPWA from 'next-pwa';
2
2
 
3
+ const isProd = process.env.NODE_ENV === 'production';
4
+ const API_END_PORT_URL = process.env.API_END_PORT_URL || '';
5
+
3
6
  const withPWA = nextPWA({
4
7
  dest: 'public',
5
8
  register: true,
6
9
  skipWaiting: true,
7
- disable: process.env.NODE_ENV === 'development',
8
10
  });
9
11
 
10
- const API_END_PORT_URL = process.env.API_END_PORT_URL || '';
11
12
  /** @type {import('next').NextConfig} */
12
13
  const nextConfig = {
13
14
  reactStrictMode: true,
@@ -37,4 +38,4 @@ const nextConfig = {
37
38
  },
38
39
  };
39
40
 
40
- export default withPWA(nextConfig);
41
+ export default isProd ? withPWA(nextConfig) : nextConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.10.2",
3
+ "version": "0.12.0",
4
4
  "description": "Lobe Chat is an open-source chatbot client using LangChain, Typescript and Next.js",
5
5
  "keywords": [
6
6
  "chatbot",
@@ -76,18 +76,19 @@
76
76
  "gpt-tokenizer": "^2",
77
77
  "i18next": "^23",
78
78
  "immer": "^10",
79
- "langchain": "latest",
80
79
  "lodash-es": "^4",
81
80
  "lucide-react": "latest",
82
81
  "nanoid": "^4",
83
82
  "next": "13.4.7",
84
83
  "openai-edge": "^1",
85
84
  "polished": "^4",
85
+ "query-string": "^8",
86
86
  "react": "^18",
87
87
  "react-dom": "^18",
88
88
  "react-hotkeys-hook": "^4",
89
89
  "react-i18next": "^13",
90
90
  "react-layout-kit": "^1",
91
+ "serpapi": "^1",
91
92
  "swr": "^2",
92
93
  "ts-md5": "^1",
93
94
  "uuid": "^9",
@@ -5,20 +5,17 @@
5
5
  {
6
6
  "src": "icons/icon-72x72.png",
7
7
  "sizes": "72x72",
8
- "type": "image/png",
9
- "purpose": "any maskable"
8
+ "type": "image/png"
10
9
  },
11
10
  {
12
11
  "src": "icons/icon-96x96.png",
13
12
  "sizes": "96x96",
14
- "type": "image/png",
15
- "purpose": "any maskable"
13
+ "type": "image/png"
16
14
  },
17
15
  {
18
16
  "src": "icons/icon-192x192.png",
19
17
  "sizes": "192x192",
20
- "type": "image/png",
21
- "purpose": "any maskable"
18
+ "type": "image/png"
22
19
  }
23
20
  ],
24
21
  "name": "LobeChat",
@@ -4,6 +4,7 @@ export default {
4
4
  'agentAvatar': '头像',
5
5
  'agentDescription': '描述',
6
6
  'agentDescriptionPlaceholder': '请输入描述',
7
+ 'agentMaxToken': '会话最大长度',
7
8
  'agentModel': '模型',
8
9
  'agentName': '名称',
9
10
  'agentNamePlaceholder': '请输入名称',
@@ -34,6 +35,9 @@ export default {
34
35
  'newAgent': '新建助手',
35
36
  'noDescription': '暂无描述',
36
37
  'ok': '确定',
38
+ 'plugin-realtimeWeather': '实时天气预报',
39
+ 'plugin-searchEngine': '搜索引擎',
40
+ 'pluginList': '插件列表',
37
41
  'profile': '助手身份',
38
42
  'reset': '重置',
39
43
  'searchAgentPlaceholder': '搜索助手和对话...',
@@ -3,11 +3,9 @@ import type { AppProps } from 'next/app';
3
3
 
4
4
  import Layout from '@/layout';
5
5
 
6
- export default ({ Component, pageProps }: AppProps) => {
7
- return (
8
- <Layout>
9
- <Component {...pageProps} />
10
- <Analytics />
11
- </Layout>
12
- );
13
- };
6
+ export default ({ Component, pageProps }: AppProps) => (
7
+ <Layout>
8
+ <Component {...pageProps} />
9
+ <Analytics />
10
+ </Layout>
11
+ );
@@ -1,8 +1,13 @@
1
1
  import { OpenAIStream, StreamingTextResponse } from 'ai';
2
2
  import { Configuration, OpenAIApi } from 'openai-edge';
3
+ import { ChatCompletionFunctions, ChatCompletionRequestMessage } from 'openai-edge/types/api';
3
4
 
4
5
  import { OpenAIStreamPayload } from '@/types/openai';
5
6
 
7
+ import pluginList from '../../plugins';
8
+
9
+ export const runtime = 'edge';
10
+
6
11
  const isDev = process.env.NODE_ENV === 'development';
7
12
  const OPENAI_PROXY_URL = process.env.OPENAI_PROXY_URL;
8
13
 
@@ -13,19 +18,57 @@ const config = new Configuration({
13
18
 
14
19
  const openai = new OpenAIApi(config, isDev && OPENAI_PROXY_URL ? OPENAI_PROXY_URL : undefined);
15
20
 
16
- export const runtime = 'edge';
17
-
18
21
  export default async function handler(req: Request) {
19
- // Extract the `messages` from the body of the request
20
- const { messages, ...params } = (await req.json()) as OpenAIStreamPayload;
22
+ const {
23
+ messages,
24
+ plugins: enabledPlugins,
25
+ ...params
26
+ } = (await req.json()) as OpenAIStreamPayload;
27
+
28
+ // ============ 1. 前置处理 functions ============ //
29
+
30
+ const filterFunctions: ChatCompletionFunctions[] = pluginList
31
+ .filter((p) => {
32
+ // 如果不存在 enabledPlugins,那么全部不启用
33
+ if (!enabledPlugins) return false;
34
+
35
+ // 如果存在 enabledPlugins,那么只启用 enabledPlugins 中的插件
36
+ return enabledPlugins.includes(p.name);
37
+ })
38
+ .map((f) => f.schema);
39
+
40
+ const functions = filterFunctions.length === 0 ? undefined : filterFunctions;
41
+
42
+ // ============ 2. 前置处理 messages ============ //
43
+ const formatMessages = messages.map((m) => ({ content: m.content, role: m.role }));
21
44
 
22
- console.log(params);
23
45
  const response = await openai.createChatCompletion({
46
+ functions,
47
+ messages: formatMessages,
24
48
  stream: true,
25
49
  ...params,
26
- messages: messages.map((m) => ({ content: m.content, role: m.role })),
27
50
  });
28
51
 
29
- const stream = OpenAIStream(response);
52
+ const stream = OpenAIStream(response, {
53
+ experimental_onFunctionCall: async ({ name, arguments: args }, createFunctionCallMessages) => {
54
+ console.log(`执行 functionCall [${name}]`, 'args:', args);
55
+
56
+ const func = pluginList.find((f) => f.name === name);
57
+
58
+ if (func) {
59
+ const result = await func.runner(args as any);
60
+
61
+ const newMessages = createFunctionCallMessages(result) as ChatCompletionRequestMessage[];
62
+
63
+ return openai.createChatCompletion({
64
+ functions,
65
+ messages: [...formatMessages, ...newMessages],
66
+ stream: true,
67
+ ...params,
68
+ });
69
+ }
70
+ },
71
+ });
72
+
30
73
  return new StreamingTextResponse(stream);
31
74
  }
@@ -3,6 +3,7 @@ import { createStyles } from 'antd-style';
3
3
  import isEqual from 'fast-deep-equal';
4
4
  import { LucideBrain, LucideThermometer, WholeWord } from 'lucide-react';
5
5
  import { memo } from 'react';
6
+ import { useTranslation } from 'react-i18next';
6
7
  import { Center, Flexbox } from 'react-layout-kit';
7
8
  import { shallow } from 'zustand/shallow';
8
9
 
@@ -31,26 +32,26 @@ const ReadMode = memo(() => {
31
32
  const title = useSessionStore(agentSelectors.currentAgentTitle, shallow);
32
33
  const model = useSessionStore(agentSelectors.currentAgentModel, shallow);
33
34
 
35
+ const { t } = useTranslation('common');
34
36
  return (
35
- <Center gap={12} padding={'32px 16px'} style={{ marginTop: 8 }}>
37
+ <Center gap={12} paddingBlock={16} style={{ marginTop: 8 }}>
36
38
  <Avatar avatar={avatar} size={100} />
37
39
  <Flexbox className={styles.title}>{title || DEFAULT_TITLE}</Flexbox>
38
40
  <Flexbox className={styles.model}>{model}</Flexbox>
39
41
  <Flexbox className={styles.desc}>{session.meta.description}</Flexbox>
40
42
 
41
43
  <Flexbox flex={1} gap={12} width={'100%'}>
42
- <ConfigCell icon={LucideBrain} label={'提示词'} />
43
-
44
+ <ConfigCell icon={LucideBrain} label={t('agentPrompt')} />
44
45
  <ConfigCellGroup
45
46
  items={[
46
47
  {
47
48
  icon: LucideThermometer,
48
- label: '温度',
49
+ label: t('modelTemperature'),
49
50
  value: session.config.params.temperature,
50
51
  },
51
52
  {
52
53
  icon: WholeWord,
53
- label: '会话最大长度',
54
+ label: t('agentMaxToken'),
54
55
  value: session.config.params.max_tokens,
55
56
  },
56
57
  ]}
@@ -0,0 +1,46 @@
1
+ import { Avatar } from '@lobehub/ui';
2
+ import { List, Switch, Tag } from 'antd';
3
+ import isEqual from 'fast-deep-equal';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { Flexbox } from 'react-layout-kit';
6
+ import { shallow } from 'zustand/shallow';
7
+
8
+ import pluginList from '@/plugins';
9
+ import { agentSelectors, useSessionStore } from '@/store/session';
10
+
11
+ const PluginList = () => {
12
+ const { t } = useTranslation('common');
13
+
14
+ const config = useSessionStore(agentSelectors.currentAgentConfigSafe, isEqual);
15
+
16
+ const [toggleAgentPlugin] = useSessionStore((s) => [s.toggleAgentPlugin], shallow);
17
+
18
+ return (
19
+ <List
20
+ bordered
21
+ dataSource={pluginList}
22
+ renderItem={(item) => (
23
+ <List.Item>
24
+ <List.Item.Meta
25
+ avatar={<Avatar avatar={item.avatar} />}
26
+ description={item.schema.description}
27
+ title={
28
+ <Flexbox align={'center'} gap={8} horizontal>
29
+ {t(`plugin-${item.name}` as any)} <Tag>{item.name}</Tag>
30
+ </Flexbox>
31
+ }
32
+ />
33
+ <Switch
34
+ checked={!config.plugins ? false : config.plugins.includes(item.name)}
35
+ onChange={() => {
36
+ toggleAgentPlugin(item.name);
37
+ }}
38
+ />
39
+ </List.Item>
40
+ )}
41
+ size={'large'}
42
+ />
43
+ );
44
+ };
45
+
46
+ export default PluginList;
@@ -9,6 +9,7 @@ import { LanguageModel } from '@/types/llm';
9
9
 
10
10
  import { FormItem } from '../FormItem';
11
11
  import { useStyles } from '../style';
12
+ import Plugin from './Plugin';
12
13
  import Prompt from './Prompt';
13
14
 
14
15
  const AgentConfig = () => {
@@ -58,8 +59,6 @@ const AgentConfig = () => {
58
59
  </FormItem>
59
60
  <Prompt />
60
61
  <Collapse
61
- activeKey={['advanceSettings']}
62
- bordered={false}
63
62
  className={styles.title}
64
63
  expandIconPosition={'end'}
65
64
  items={[
@@ -95,7 +94,19 @@ const AgentConfig = () => {
95
94
  },
96
95
  ]}
97
96
  />
98
- </Flexbox>{' '}
97
+ <Flexbox
98
+ align={'center'}
99
+ distribution={'space-between'}
100
+ horizontal
101
+ paddingBlock={12}
102
+ style={{
103
+ borderBottom: `1px solid ${theme.colorBorder}`,
104
+ }}
105
+ >
106
+ <Flexbox className={styles.profile}> {t('pluginList')}</Flexbox>
107
+ </Flexbox>
108
+ <Plugin />
109
+ </Flexbox>
99
110
  </ConfigProvider>
100
111
  );
101
112
  };
@@ -30,13 +30,5 @@ export const useStyles = createStyles(({ css, token }) => ({
30
30
  title: css`
31
31
  font-size: 16px;
32
32
  font-weight: 500;
33
-
34
- .ant-collapse-header {
35
- padding: 0 !important;
36
- }
37
-
38
- .ant-collapse-content-box {
39
- padding: 0 !important;
40
- }
41
33
  `,
42
34
  }));
@@ -0,0 +1,6 @@
1
+ import searchEngine from './searchEngine';
2
+ import getWeather from './weather';
3
+
4
+ const pluginList = [getWeather, searchEngine];
5
+
6
+ export default pluginList;
@@ -0,0 +1,27 @@
1
+ import { ChatCompletionFunctions } from 'openai-edge/types/api';
2
+
3
+ import runner from './runner';
4
+
5
+ const schema: ChatCompletionFunctions = {
6
+ description: '查询搜索引擎获取信息',
7
+ name: 'searchEngine',
8
+ parameters: {
9
+ properties: {
10
+ keywords: {
11
+ description: '关键词',
12
+ type: 'string',
13
+ },
14
+ },
15
+ required: ['keywords'],
16
+ type: 'object',
17
+ },
18
+ };
19
+
20
+ const searchEngine = {
21
+ avatar: '🔍',
22
+ name: 'searchEngine',
23
+ runner,
24
+ schema,
25
+ };
26
+
27
+ export default searchEngine;
@@ -0,0 +1,106 @@
1
+ import querystring from 'query-string';
2
+
3
+ const BASE_URL = 'https://serpapi.com/search';
4
+
5
+ const API_KEY = process.env.SERPAI_API_KEY;
6
+
7
+ export type OrganicResults = OrganicResult[];
8
+
9
+ export interface OrganicResult {
10
+ about_page_link: string;
11
+ about_page_serpapi_link: string;
12
+ about_this_result: AboutThisResult;
13
+ cached_page_link?: string;
14
+ date?: string;
15
+ displayed_link: string;
16
+ favicon?: string;
17
+ link: string;
18
+ position: number;
19
+ related_results?: RelatedResult[];
20
+ rich_snippet?: RichSnippet;
21
+ snippet: string;
22
+ snippet_highlighted_words?: string[];
23
+ source: string;
24
+ thumbnail?: string;
25
+ title: string;
26
+ }
27
+
28
+ export interface AboutThisResult {
29
+ languages: string[];
30
+ regions: string[];
31
+ source: Source;
32
+ }
33
+
34
+ export interface Source {
35
+ description: string;
36
+ icon: string;
37
+ security?: string;
38
+ source_info_link?: string;
39
+ }
40
+
41
+ export interface RelatedResult {
42
+ about_page_link: string;
43
+ about_page_serpapi_link: string;
44
+ about_this_result: AboutThisResult2;
45
+ cached_page_link: string;
46
+ date: string;
47
+ displayed_link: string;
48
+ link: string;
49
+ position: number;
50
+ snippet: string;
51
+ snippet_highlighted_words: string[];
52
+ title: string;
53
+ }
54
+
55
+ export interface AboutThisResult2 {
56
+ languages: string[];
57
+ regions: string[];
58
+ source: Source2;
59
+ }
60
+
61
+ export interface Source2 {
62
+ description: string;
63
+ icon: string;
64
+ }
65
+
66
+ export interface RichSnippet {
67
+ top: Top;
68
+ }
69
+
70
+ export interface Top {
71
+ detected_extensions: DetectedExtensions;
72
+ extensions: string[];
73
+ }
74
+
75
+ export interface DetectedExtensions {
76
+ month_ago: number;
77
+ }
78
+
79
+ const fetchResult = async (keywords: string) => {
80
+ const params = {
81
+ api_key: API_KEY,
82
+ gl: 'cn',
83
+ google_domain: 'google.com',
84
+ hl: 'zh-cn',
85
+ location: 'China',
86
+ q: keywords,
87
+ };
88
+
89
+ const query = querystring.stringify(params);
90
+
91
+ const res = await fetch(`${BASE_URL}?${query}`);
92
+
93
+ const data = await res.json();
94
+
95
+ const results = data.organic_results as OrganicResults;
96
+
97
+ return results.map((r) => ({
98
+ content: r.snippet,
99
+ date: r.date,
100
+ link: r.link,
101
+ source: r.source,
102
+ title: r.title,
103
+ }));
104
+ };
105
+
106
+ export default fetchResult;
@@ -0,0 +1,20 @@
1
+ import runner from './runner';
2
+
3
+ const schema = {
4
+ description: '获取当前天气情况',
5
+ name: 'realtimeWeather',
6
+ parameters: {
7
+ properties: {
8
+ city: {
9
+ description: '城市名称',
10
+ type: 'string',
11
+ },
12
+ },
13
+ required: ['city'],
14
+ type: 'object',
15
+ },
16
+ };
17
+
18
+ const getWeather = { avatar: '☂️', name: 'realtimeWeather', runner, schema };
19
+
20
+ export default getWeather;
@@ -0,0 +1,30 @@
1
+ const weatherBaseURL = 'https://restapi.amap.com/v3/weather/weatherInfo';
2
+
3
+ const citySearchURL = 'https://restapi.amap.com/v3/config/district';
4
+
5
+ const KEY = process.env.GAODE_WEATHER_KEY;
6
+
7
+ interface WeatherParams {
8
+ city: string;
9
+ extensions?: 'base' | 'all';
10
+ }
11
+
12
+ const fetchCityCode = async (keywords: string): Promise<string> => {
13
+ const URL = `${citySearchURL}?keywords=${keywords}&subdistrict=0&extensions=base&key=${KEY}`;
14
+ const res = await fetch(URL);
15
+
16
+ const data = await res.json();
17
+
18
+ return data.districts[0].adcode;
19
+ };
20
+
21
+ const fetchWeather = async ({ city, extensions = 'all' }: WeatherParams) => {
22
+ const cityCode = await fetchCityCode(city);
23
+
24
+ const URL = `${weatherBaseURL}?city=${cityCode}&extensions=${extensions}&key=${KEY}`;
25
+ const res = await fetch(URL);
26
+
27
+ return await res.json();
28
+ };
29
+
30
+ export default fetchWeather;
@@ -3,6 +3,5 @@ const isDev = process.env.NODE_ENV === 'development';
3
3
  const prefix = isDev ? '-dev' : '';
4
4
 
5
5
  export const URLS = {
6
- chain: '/api/chain' + prefix,
7
6
  openai: '/api/openai' + prefix,
8
7
  };
@@ -1,6 +1,8 @@
1
1
  import { create } from 'zustand';
2
2
  import { PersistOptions, devtools, persist } from 'zustand/middleware';
3
3
 
4
+ import { isDev } from '@/utils/env';
5
+
4
6
  import { SessionStore, createStore } from './store';
5
7
 
6
8
  type SessionPersist = Pick<SessionStore, 'sessions'>;
@@ -23,7 +25,7 @@ const persistOptions: PersistOptions<SessionStore, SessionPersist> = {
23
25
  export const useSessionStore = create<SessionStore>()(
24
26
  persist(
25
27
  devtools(createStore, {
26
- name: LOBE_CHAT,
28
+ name: LOBE_CHAT + (isDev ? '_DEV' : ''),
27
29
  }),
28
30
  persistOptions,
29
31
  ),
@@ -1,3 +1,4 @@
1
+ import { produce } from 'immer';
1
2
  import { StateCreator } from 'zustand/vanilla';
2
3
 
3
4
  import { promptPickEmoji, promptSummaryAgentName, promptSummaryDescription } from '@/prompts/agent';
@@ -43,12 +44,13 @@ export interface AgentAction {
43
44
  * @returns 任意类型的返回值
44
45
  */
45
46
  internalUpdateAgentMeta: (id: string) => any;
47
+ toggleAgentPlugin: (pluginId: string) => void;
48
+
46
49
  /**
47
50
  * 切换配置
48
51
  * @param showPanel - 是否显示面板,默认为 true
49
52
  */
50
53
  toggleConfig: (showPanel?: boolean) => void;
51
-
52
54
  /**
53
55
  * 更新代理配置
54
56
  * @param config - 部分 LobeAgentConfig 的配置
@@ -191,6 +193,27 @@ export const createAgentSlice: StateCreator<
191
193
  };
192
194
  },
193
195
 
196
+ toggleAgentPlugin: (id: string) => {
197
+ const { activeId } = get();
198
+ const session = sessionSelectors.currentSession(get());
199
+ if (!activeId || !session) return;
200
+
201
+ const config = produce(session.config, (draft) => {
202
+ if (draft.plugins === undefined) {
203
+ draft.plugins = [id];
204
+ } else {
205
+ const plugins = draft.plugins;
206
+ if (plugins.includes(id)) {
207
+ plugins.splice(plugins.indexOf(id), 1);
208
+ } else {
209
+ plugins.push(id);
210
+ }
211
+ }
212
+ });
213
+
214
+ get().dispatchSession({ config, id: activeId, type: 'updateSessionConfig' });
215
+ },
216
+
194
217
  toggleConfig: (newValue) => {
195
218
  const showAgentSettings = typeof newValue === 'boolean' ? newValue : !get().showAgentSettings;
196
219
 
@@ -204,6 +227,7 @@ export const createAgentSlice: StateCreator<
204
227
 
205
228
  get().dispatchSession({ config, id: activeId, type: 'updateSessionConfig' });
206
229
  },
230
+
207
231
  updateAgentMeta: (meta) => {
208
232
  const { activeId } = get();
209
233
  const session = sessionSelectors.currentSession(get());
@@ -104,7 +104,8 @@ export const createChatSlice: StateCreator<
104
104
  set({ chatLoading: true });
105
105
  const config = agentSelectors.currentAgentConfigSafe(get());
106
106
 
107
- const fetcher = () => fetchChatModel({ messages, model: config.model, ...config.params });
107
+ const fetcher = () =>
108
+ fetchChatModel({ messages, model: config.model, ...config.params, plugins: config.plugins });
108
109
 
109
110
  await fetchSSE(fetcher, options);
110
111
 
@@ -1,6 +1,8 @@
1
1
  import { create } from 'zustand';
2
2
  import { type PersistOptions, devtools, persist } from 'zustand/middleware';
3
3
 
4
+ import { isDev } from '@/utils/env';
5
+
4
6
  import { type SettingsStore, createStore } from './store';
5
7
 
6
8
  export const LOBE_SETTINGS = 'LOBE_SETTINGS';
@@ -13,7 +15,7 @@ const persistOptions: PersistOptions<SettingsStore> = {
13
15
  export const useSettings = create<SettingsStore>()(
14
16
  persist(
15
17
  devtools(createStore, {
16
- name: LOBE_SETTINGS,
18
+ name: LOBE_SETTINGS + (isDev ? '_DEV' : ''),
17
19
  }),
18
20
  persistOptions,
19
21
  ),
@@ -39,6 +39,10 @@ export interface OpenAIStreamPayload {
39
39
  * @title 返回的文本数量
40
40
  */
41
41
  n?: number;
42
+ /**
43
+ * 开启的插件列表
44
+ */
45
+ plugins?: string[];
42
46
  /**
43
47
  * @title 控制生成文本中的惩罚系数,用于减少主题的变化
44
48
  * @default 0
@@ -54,6 +58,7 @@ export interface OpenAIStreamPayload {
54
58
  * @default 0.5
55
59
  */
56
60
  temperature: number;
61
+
57
62
  /**
58
63
  * @title 控制生成文本中最高概率的单个令牌
59
64
  * @default 1
@@ -39,6 +39,10 @@ export interface LobeAgentConfig {
39
39
  * 语言模型参数
40
40
  */
41
41
  params: LLMParams;
42
+ /**
43
+ * 启用的插件
44
+ */
45
+ plugins?: string[];
42
46
  /**
43
47
  * 系统角色
44
48
  */
@@ -0,0 +1 @@
1
+ export const isDev = process.env.NODE_ENV === 'development';
@@ -1,95 +0,0 @@
1
- import { LLMChain } from 'langchain/chains';
2
- import { ChatOpenAI } from 'langchain/chat_models/openai';
3
- import {
4
- AIMessagePromptTemplate,
5
- ChatPromptTemplate,
6
- HumanMessagePromptTemplate,
7
- SystemMessagePromptTemplate,
8
- } from 'langchain/prompts';
9
-
10
- import { LangChainParams } from '@/types/langchain';
11
-
12
- const isDev = process.env.NODE_ENV === 'development';
13
- const OPENAI_PROXY_URL = process.env.OPENAI_PROXY_URL;
14
-
15
- export function LangChainStream(payload: LangChainParams) {
16
- const { prompts, vars, llm } = payload;
17
-
18
- // 将 payload 中的消息转换为 ChatOpenAI 所需的 HumanChatMessage、SystemChatMessage 和 AIChatMessage 类型
19
- const chatPrompt = ChatPromptTemplate.fromPromptMessages(
20
- prompts.map((m) => {
21
- switch (m.role) {
22
- default:
23
- case 'user': {
24
- return HumanMessagePromptTemplate.fromTemplate(m.content);
25
- }
26
- case 'system': {
27
- return SystemMessagePromptTemplate.fromTemplate(m.content);
28
- }
29
-
30
- case 'assistant': {
31
- return AIMessagePromptTemplate.fromTemplate(m.content);
32
- }
33
- }
34
- }),
35
- );
36
-
37
- // 使用 TextEncoder 将字符串转换为字节数组,以便在 ReadableStream 中发送
38
- const encoder = new TextEncoder();
39
-
40
- // 初始化换行符计数器
41
-
42
- return new ReadableStream({
43
- async start(controller) {
44
- let newlineCounter = 0;
45
-
46
- const chat = new ChatOpenAI(
47
- {
48
- streaming: true,
49
- ...llm,
50
-
51
- callbacks: [
52
- {
53
- handleLLMNewToken(token) {
54
- // 如果 message 是换行符,且 newlineCounter 小于 2,那么跳过该换行符
55
- if (newlineCounter < 2 && token === '\n') {
56
- return;
57
- }
58
-
59
- // 将 message 编码为字节并添加到流中
60
- const queue = encoder.encode(token);
61
- controller.enqueue(queue);
62
- newlineCounter++;
63
- },
64
- },
65
- ],
66
- // 暂时设定不重试 ,后续看是否需要支持重试
67
- maxRetries: 0,
68
- },
69
- isDev && OPENAI_PROXY_URL ? { basePath: OPENAI_PROXY_URL } : undefined,
70
- );
71
-
72
- const chain = new LLMChain({
73
- callbacks: [
74
- {
75
- handleChainError(err: Error): Promise<void> | void {
76
- console.log(err.message);
77
- },
78
- },
79
- ],
80
- llm: chat,
81
- prompt: chatPrompt,
82
- verbose: true,
83
- });
84
- try {
85
- // 使用转换后的聊天消息作为输入开始聊天
86
- await chain.call(vars);
87
- // 完成后,关闭流
88
- controller.close();
89
- } catch (error) {
90
- // 如果在执行过程中发生错误,向流发送错误
91
- controller.error(error);
92
- }
93
- },
94
- });
95
- }
@@ -1,17 +0,0 @@
1
- import { LangChainParams } from '@/types/langchain';
2
-
3
- import { LangChainStream } from './LangChainStream';
4
-
5
- if (!process.env.OPENAI_API_KEY) {
6
- throw new Error('Missing env var from OpenAI');
7
- }
8
-
9
- export const config = {
10
- runtime: 'edge',
11
- };
12
-
13
- export default async function handler(request: Request) {
14
- const payload = (await request.json()) as LangChainParams;
15
-
16
- return new Response(LangChainStream(payload));
17
- }
@@ -1,18 +0,0 @@
1
- import { URLS } from '@/services/url';
2
- import { LangChainParams } from '@/types/langchain';
3
- import { fetchAIFactory } from '@/utils/fetch';
4
-
5
- /**
6
- * 专门用于 FlowChain 的 fetch
7
- */
8
- export const fetchLangChain = fetchAIFactory(
9
- (params: LangChainParams, signal?: AbortSignal | undefined) =>
10
- fetch(URLS.chain, {
11
- body: JSON.stringify(params),
12
- headers: {
13
- 'Content-Type': 'application/json',
14
- },
15
- method: 'POST',
16
- signal,
17
- }),
18
- );
@@ -1,34 +0,0 @@
1
- import { ChatMessage } from '@lobehub/ui';
2
-
3
- export interface LangChainParams {
4
- llm: {
5
- /**
6
- * 控制生成文本中的惩罚系数,用于减少重复性
7
- */
8
- frequency_penalty?: number;
9
- /**
10
- * 生成文本的最大长度
11
- */
12
- max_tokens?: number;
13
- model: string;
14
- /**
15
- * 控制生成文本中的惩罚系数,用于减少主题的变化
16
- */
17
- presence_penalty?: number;
18
- /**
19
- * 生成文本的随机度量,用于控制文本的创造性和多样性
20
- * @default 0.6
21
- */
22
- temperature: number;
23
- /**
24
- * 控制生成文本中最高概率的单个令牌
25
- */
26
- top_p?: number;
27
- };
28
-
29
- /**
30
- * 聊天信息列表
31
- */
32
- prompts: ChatMessage[];
33
- vars: Record<string, string>;
34
- }