@lobehub/chat 1.15.8 → 1.15.10

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/.i18nrc.js CHANGED
@@ -24,7 +24,6 @@ module.exports = defineConfig({
24
24
  ],
25
25
  temperature: 0,
26
26
  modelName: 'gpt-4o-mini',
27
- splitToken: 2048,
28
27
  experimental: {
29
28
  jsonMode: true,
30
29
  },
package/CHANGELOG.md CHANGED
@@ -2,6 +2,48 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 1.15.10](https://github.com/lobehub/lobe-chat/compare/v1.15.9...v1.15.10)
6
+
7
+ <sup>Released on **2024-09-03**</sup>
8
+
9
+ <br/>
10
+
11
+ <details>
12
+ <summary><kbd>Improvements and Fixes</kbd></summary>
13
+
14
+ </details>
15
+
16
+ <div align="right">
17
+
18
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
19
+
20
+ </div>
21
+
22
+ ### [Version 1.15.9](https://github.com/lobehub/lobe-chat/compare/v1.15.8...v1.15.9)
23
+
24
+ <sup>Released on **2024-09-03**</sup>
25
+
26
+ #### 🐛 Bug Fixes
27
+
28
+ - **misc**: Fix speed and rag prompt.
29
+
30
+ <br/>
31
+
32
+ <details>
33
+ <summary><kbd>Improvements and Fixes</kbd></summary>
34
+
35
+ #### What's fixed
36
+
37
+ - **misc**: Fix speed and rag prompt, closes [#3751](https://github.com/lobehub/lobe-chat/issues/3751) ([dce200c](https://github.com/lobehub/lobe-chat/commit/dce200c))
38
+
39
+ </details>
40
+
41
+ <div align="right">
42
+
43
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
44
+
45
+ </div>
46
+
5
47
  ### [Version 1.15.8](https://github.com/lobehub/lobe-chat/compare/v1.15.7...v1.15.8)
6
48
 
7
49
  <sup>Released on **2024-09-03**</sup>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.15.8",
3
+ "version": "1.15.10",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -56,6 +56,7 @@ export const useStyles = createStyles(
56
56
  border-radius: unset;
57
57
  `,
58
58
  }) as Partial<{
59
+ // eslint-disable-next-line unused-imports/no-unused-vars
59
60
  [k in keyof ElementsConfig]: any;
60
61
  }>,
61
62
  );
@@ -0,0 +1,32 @@
1
+ // @vitest-environment edge-runtime
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { POST } from './route';
5
+
6
+ describe('tokenizer Route', () => {
7
+ it('count hello world', async () => {
8
+ const txt = 'Hello, world!';
9
+ const request = new Request('https://test.com', {
10
+ method: 'POST',
11
+ body: txt,
12
+ });
13
+
14
+ const response = await POST(request);
15
+
16
+ const data = await response.json();
17
+ expect(data.count).toEqual(4);
18
+ });
19
+
20
+ it('count Chinese', async () => {
21
+ const txt = '今天天气真好';
22
+ const request = new Request('https://test.com', {
23
+ method: 'POST',
24
+ body: txt,
25
+ });
26
+
27
+ const response = await POST(request);
28
+
29
+ const data = await response.json();
30
+ expect(data.count).toEqual(5);
31
+ });
32
+ });
@@ -0,0 +1,8 @@
1
+ import { encode } from 'gpt-tokenizer/encoding/o200k_base';
2
+ import { NextResponse } from 'next/server';
3
+
4
+ export const POST = async (req: Request) => {
5
+ const str = await req.text();
6
+
7
+ return NextResponse.json({ count: encode(str).length });
8
+ };
@@ -11,7 +11,7 @@ export const chainAnswerWithContext = ({
11
11
  }): Partial<ChatStreamPayload> => ({
12
12
  messages: [
13
13
  {
14
- content: `You are a helpful assistant good answering questions related to ${knowledge.join('/')}. And you'll be provided with a question and several passages that might be relevant. And your task is to provide answer based on the question and passages.
14
+ content: `You are aslo a helpful assistant good answering questions related to ${knowledge.join('/')}. And you'll be provided with a question and several passages that might be relevant. And currently your task is to provide answer based on the question and passages.
15
15
 
16
16
  Note that passages might not be relevant to the question, please only use the passages that are relevant. Or if there is no relevant passage, please answer using your knowledge.
17
17
 
@@ -21,13 +21,12 @@ The provided passages as context:
21
21
 
22
22
  <Context>
23
23
  ${context.join('\n')}
24
- </Context>`,
25
- role: 'system',
26
- },
27
- {
28
- content: `The question to answer:
24
+ </Context>
25
+
26
+ The question to answer is:
29
27
 
30
- ${question}`,
28
+ ${question}
29
+ `,
31
30
  role: 'user',
32
31
  },
33
32
  ],
@@ -170,7 +170,7 @@ export class SessionModel {
170
170
 
171
171
  if (!result) return;
172
172
 
173
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
173
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars,unused-imports/no-unused-vars
174
174
  const { agent, clientId, ...session } = result;
175
175
  const sessionId = this.genId();
176
176
 
@@ -1,20 +1,32 @@
1
- import { startTransition, useEffect, useState } from 'react';
1
+ import { debounce } from 'lodash-es';
2
+ import { startTransition, useCallback, useEffect, useState } from 'react';
2
3
 
3
4
  import { encodeAsync } from '@/utils/tokenizer';
4
5
 
5
6
  export const useTokenCount = (input: string = '') => {
6
7
  const [value, setNum] = useState(0);
7
8
 
8
- useEffect(() => {
9
- startTransition(() => {
10
- encodeAsync(input || '')
9
+ const debouncedEncode = useCallback(
10
+ debounce((text: string) => {
11
+ encodeAsync(text)
11
12
  .then(setNum)
12
13
  .catch(() => {
13
- // 兜底采用字符数
14
- setNum(input.length);
14
+ setNum(text.length);
15
15
  });
16
+ }, 300),
17
+ [],
18
+ );
19
+
20
+ useEffect(() => {
21
+ startTransition(() => {
22
+ debouncedEncode(input || '');
16
23
  });
17
- }, [input]);
24
+
25
+ // 清理函数
26
+ return () => {
27
+ debouncedEncode.cancel();
28
+ };
29
+ }, [input, debouncedEncode]);
18
30
 
19
31
  return value;
20
32
  };
@@ -89,6 +89,7 @@ export const useStyles = createStyles(
89
89
  order: -1;
90
90
  `,
91
91
  }) as Partial<{
92
+ // eslint-disable-next-line unused-imports/no-unused-vars
92
93
  [k in keyof ElementsConfig]: any;
93
94
  }>,
94
95
  );
@@ -62,7 +62,9 @@ export const createContext = async (request: NextRequest): Promise<Context> => {
62
62
  userId = session.user.id;
63
63
  }
64
64
  return createContextInner({ authorizationHeader: authorization, nextAuth: auth, userId });
65
- } catch {}
65
+ } catch (e) {
66
+ console.error('next auth err', e);
67
+ }
66
68
  }
67
69
 
68
70
  return createContextInner({ authorizationHeader: authorization, userId });
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file contains the root router of Lobe Chat tRPC-backend
2
+ * This file contains the edge router of Lobe Chat tRPC-backend
3
3
  */
4
4
  import { publicProcedure, router } from '@/libs/trpc';
5
5
 
@@ -35,7 +35,7 @@ class UploadService {
35
35
  // so make it as 99.9 and let users think it's still uploading
36
36
  progress: progress === 100 ? 99.9 : progress,
37
37
  restTime: (event.total - event.loaded) / speedInByte,
38
- speed: speedInByte / 1024,
38
+ speed: speedInByte,
39
39
  });
40
40
  }
41
41
  });
@@ -50,7 +50,7 @@ export class ClientService implements IUserService {
50
50
  await this.preferenceStorage.saveToLocalStorage(preference);
51
51
  }
52
52
 
53
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
53
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars,unused-imports/no-unused-vars
54
54
  async updateGuide(guide: Partial<UserGuide>) {
55
55
  throw new Error('Method not implemented.');
56
56
  }
@@ -9,7 +9,7 @@ export interface FileUploadState {
9
9
  */
10
10
  restTime: number;
11
11
  /**
12
- * upload speed in KB/s
12
+ * upload speed in Byte/s
13
13
  */
14
14
  speed: number;
15
15
  }
@@ -0,0 +1,7 @@
1
+ declare module '*.worker.ts' {
2
+ class WebpackWorker extends Worker {
3
+ constructor();
4
+ }
5
+
6
+ export default WebpackWorker;
7
+ }
@@ -139,8 +139,8 @@ describe('fetchSSE', () => {
139
139
  onFinish: mockOnFinish,
140
140
  });
141
141
 
142
- expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, { text: 'He', type: 'text' });
143
- expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, { text: 'llo World', type: 'text' });
142
+ expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, { text: 'Hell', type: 'text' });
143
+ expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, { text: 'o World', type: 'text' });
144
144
  // more assertions for each character...
145
145
  expect(mockOnFinish).toHaveBeenCalledWith('Hello World', {
146
146
  observationId: null,
@@ -232,8 +232,8 @@ describe('fetchSSE', () => {
232
232
  signal: abortController.signal,
233
233
  });
234
234
 
235
- expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, { text: 'He', type: 'text' });
236
- expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, { text: 'llo World', type: 'text' });
235
+ expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, { text: 'Hell', type: 'text' });
236
+ expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, { text: 'o World', type: 'text' });
237
237
 
238
238
  expect(mockOnFinish).toHaveBeenCalledWith('Hello World', {
239
239
  type: 'done',
@@ -44,6 +44,10 @@ export interface FetchSSEOptions {
44
44
  smoothing?: boolean;
45
45
  }
46
46
 
47
+ const START_ANIMATION_SPEED = 4;
48
+
49
+ const END_ANIMATION_SPEED = 15;
50
+
47
51
  const createSmoothMessage = (params: { onTextUpdate: (delta: string, text: string) => void }) => {
48
52
  let buffer = '';
49
53
  // why use queue: https://shareg.pt/GLBrjpK
@@ -64,7 +68,7 @@ const createSmoothMessage = (params: { onTextUpdate: (delta: string, text: strin
64
68
 
65
69
  // define startAnimation function to display the text in buffer smooth
66
70
  // when you need to start the animation, call this function
67
- const startAnimation = (speed = 2) =>
71
+ const startAnimation = (speed = START_ANIMATION_SPEED) =>
68
72
  new Promise<void>((resolve) => {
69
73
  if (isAnimationActive) {
70
74
  resolve();
@@ -137,7 +141,7 @@ const createSmoothToolCalls = (params: {
137
141
  }
138
142
  };
139
143
 
140
- const startAnimation = (index: number, speed = 2) =>
144
+ const startAnimation = (index: number, speed = START_ANIMATION_SPEED) =>
141
145
  new Promise<void>((resolve) => {
142
146
  if (isAnimationActives[index]) {
143
147
  resolve();
@@ -191,7 +195,7 @@ const createSmoothToolCalls = (params: {
191
195
  });
192
196
  };
193
197
 
194
- const startAnimations = async (speed = 2) => {
198
+ const startAnimations = async (speed = START_ANIMATION_SPEED) => {
195
199
  const pools = toolCallsBuffer.map(async (_, index) => {
196
200
  if (outputQueues[index].length > 0 && !isAnimationActives[index]) {
197
201
  await startAnimation(index, speed);
@@ -365,11 +369,11 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
365
369
  const observationId = response.headers.get(LOBE_CHAT_OBSERVATION_ID);
366
370
 
367
371
  if (textController.isTokenRemain()) {
368
- await textController.startAnimation(15);
372
+ await textController.startAnimation(END_ANIMATION_SPEED);
369
373
  }
370
374
 
371
375
  if (toolCallsController.isTokenRemain()) {
372
- await toolCallsController.startAnimations(15);
376
+ await toolCallsController.startAnimations(END_ANIMATION_SPEED);
373
377
  }
374
378
 
375
379
  await options?.onFinish?.(output, { observationId, toolCalls, traceId, type: finishedType });
@@ -0,0 +1,35 @@
1
+ let worker: Worker | null = null;
2
+
3
+ const getWorker = () => {
4
+ if (!worker && typeof Worker !== 'undefined') {
5
+ worker = new Worker(new URL('tokenizer.worker.ts', import.meta.url));
6
+ }
7
+ return worker;
8
+ };
9
+
10
+ export const clientEncodeAsync = (str: string): Promise<number> =>
11
+ new Promise((resolve, reject) => {
12
+ const worker = getWorker();
13
+
14
+ if (!worker) {
15
+ // 如果 WebWorker 不可用,回退到字符串计算
16
+ resolve(str.length);
17
+ return;
18
+ }
19
+
20
+ const id = Date.now().toString();
21
+
22
+ const handleMessage = (event: MessageEvent) => {
23
+ if (event.data.id === id) {
24
+ worker.removeEventListener('message', handleMessage);
25
+ if (event.data.error) {
26
+ reject(new Error(event.data.error));
27
+ } else {
28
+ resolve(event.data.result);
29
+ }
30
+ }
31
+ };
32
+
33
+ worker.addEventListener('message', handleMessage);
34
+ worker.postMessage({ id, str });
35
+ });
@@ -0,0 +1,15 @@
1
+ export const encodeAsync = async (str: string): Promise<number> => {
2
+ if (str.length === 0) return 0;
3
+
4
+ // 50_000 is the limit of the client
5
+ // if the string is longer than 100_000, we will use the server
6
+ if (str.length <= 50_000) {
7
+ const { clientEncodeAsync } = await import('./client');
8
+
9
+ return await clientEncodeAsync(str);
10
+ } else {
11
+ const { serverEncodeAsync } = await import('./server');
12
+
13
+ return await serverEncodeAsync(str);
14
+ }
15
+ };
@@ -0,0 +1,11 @@
1
+ export const serverEncodeAsync = async (str: string): Promise<number> => {
2
+ try {
3
+ const res = await fetch('/webapi/tokenizer', { body: str, method: 'POST' });
4
+ const data = await res.json();
5
+
6
+ return data.count;
7
+ } catch (e) {
8
+ console.error('serverEncodeAsync:', e);
9
+ return str.length;
10
+ }
11
+ };
@@ -0,0 +1,14 @@
1
+ addEventListener('message', async (event) => {
2
+ const { id, str } = event.data;
3
+ try {
4
+ const { encode } = await import('gpt-tokenizer');
5
+
6
+ console.time('client tokenizer');
7
+ const tokenCount = encode(str).length;
8
+ console.timeEnd('client tokenizer');
9
+
10
+ postMessage({ id, result: tokenCount });
11
+ } catch (error) {
12
+ postMessage({ error: (error as Error).message, id });
13
+ }
14
+ });
@@ -1,5 +0,0 @@
1
- export const encodeAsync = async (str: string) => {
2
- const { encode } = await import('gpt-tokenizer');
3
-
4
- return encode(str).length;
5
- };