@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 +0 -1
- package/CHANGELOG.md +42 -0
- package/package.json +1 -1
- package/src/app/(main)/profile/[[...slugs]]/Client.tsx +1 -0
- package/src/app/webapi/tokenizer/index.test.ts +32 -0
- package/src/app/webapi/tokenizer/route.ts +8 -0
- package/src/chains/answerWithContext.ts +6 -7
- package/src/database/server/models/session.ts +1 -1
- package/src/hooks/useTokenCount.ts +19 -7
- package/src/layout/AuthProvider/Clerk/useAppearance.ts +1 -0
- package/src/server/context.ts +3 -1
- package/src/server/routers/edge/index.ts +1 -1
- package/src/services/upload.ts +1 -1
- package/src/services/user/client.ts +1 -1
- package/src/types/files/upload.ts +1 -1
- package/src/types/worker.d.ts +7 -0
- package/src/utils/fetch/__tests__/fetchSSE.test.ts +4 -4
- package/src/utils/fetch/fetchSSE.ts +9 -5
- package/src/utils/tokenizer/client.ts +35 -0
- package/src/utils/tokenizer/index.ts +15 -0
- package/src/utils/tokenizer/server.ts +11 -0
- package/src/utils/tokenizer/tokenizer.worker.ts +14 -0
- package/src/utils/tokenizer.ts +0 -5
package/.i18nrc.js
CHANGED
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
|
+
[](#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
|
+
[](#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.
|
|
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",
|
|
@@ -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
|
+
});
|
|
@@ -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
|
-
|
|
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 {
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
encodeAsync(
|
|
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
|
-
|
|
24
|
+
|
|
25
|
+
// 清理函数
|
|
26
|
+
return () => {
|
|
27
|
+
debouncedEncode.cancel();
|
|
28
|
+
};
|
|
29
|
+
}, [input, debouncedEncode]);
|
|
18
30
|
|
|
19
31
|
return value;
|
|
20
32
|
};
|
package/src/server/context.ts
CHANGED
|
@@ -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 });
|
package/src/services/upload.ts
CHANGED
|
@@ -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
|
|
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
|
}
|
|
@@ -139,8 +139,8 @@ describe('fetchSSE', () => {
|
|
|
139
139
|
onFinish: mockOnFinish,
|
|
140
140
|
});
|
|
141
141
|
|
|
142
|
-
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, { text: '
|
|
143
|
-
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, { 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: '
|
|
236
|
-
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, { 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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
372
|
+
await textController.startAnimation(END_ANIMATION_SPEED);
|
|
369
373
|
}
|
|
370
374
|
|
|
371
375
|
if (toolCallsController.isTokenRemain()) {
|
|
372
|
-
await toolCallsController.startAnimations(
|
|
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
|
+
});
|