@lobehub/chat 0.157.0 → 0.157.1
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 +25 -0
- package/package.json +30 -30
- package/src/app/api/chat/[provider]/route.test.ts +2 -2
- package/src/app/api/chat/[provider]/route.ts +1 -1
- package/src/app/api/chat/models/[provider]/route.ts +1 -1
- package/src/app/api/config.test.ts +1 -51
- package/src/app/api/openai/createBizOpenAI/auth.test.ts +52 -0
- package/src/app/api/openai/createBizOpenAI/index.ts +1 -1
- package/src/app/api/plugin/gateway/route.ts +1 -1
- package/src/app/api/text-to-image/[provider]/route.ts +61 -0
- package/src/components/GalleyGrid/index.tsx +2 -2
- package/src/database/client/schemas/message.ts +2 -0
- package/src/features/Conversation/Actions/Assistant.tsx +3 -2
- package/src/features/Conversation/Actions/Tool.tsx +23 -11
- package/src/features/Conversation/Messages/Assistant/ToolCalls/index.tsx +9 -14
- package/src/features/Conversation/Messages/Assistant/index.tsx +7 -3
- package/src/features/Conversation/Messages/Tool/Inspector/index.tsx +1 -1
- package/src/features/Conversation/Plugins/Render/index.tsx +11 -2
- package/src/hooks/useTokenCount.test.ts +38 -0
- package/src/hooks/useTokenCount.ts +1 -2
- package/src/libs/agent-runtime/AgentRuntime.ts +9 -1
- package/src/libs/agent-runtime/BaseAI.ts +3 -0
- package/src/libs/agent-runtime/types/index.ts +1 -0
- package/src/libs/agent-runtime/types/textToImage.ts +34 -0
- package/src/libs/agent-runtime/utils/createError.ts +1 -0
- package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.ts +51 -0
- package/src/locales/default/tool.ts +1 -0
- package/src/services/_url.ts +1 -1
- package/src/services/{imageGeneration.ts → textToImage.ts} +11 -2
- package/src/store/chat/initialState.ts +1 -1
- package/src/store/chat/selectors.ts +1 -1
- package/src/store/chat/slices/{tool → builtinTool}/action.test.ts +1 -1
- package/src/store/chat/slices/{tool → builtinTool}/action.ts +16 -4
- package/src/store/chat/slices/enchance/action.ts +10 -11
- package/src/store/chat/slices/message/action.ts +30 -92
- package/src/store/chat/slices/message/initialState.ts +5 -0
- package/src/store/chat/slices/message/selectors.ts +8 -0
- package/src/store/chat/slices/plugin/action.test.ts +1 -1
- package/src/store/chat/slices/plugin/action.ts +95 -80
- package/src/store/chat/store.ts +2 -2
- package/src/store/tool/slices/store/action.test.ts +6 -2
- package/src/store/tool/slices/store/action.ts +3 -1
- package/src/tools/dalle/Render/Item/Error.tsx +50 -0
- package/src/tools/dalle/Render/Item/Image.tsx +44 -0
- package/src/tools/dalle/Render/{Item.tsx → Item/index.tsx} +20 -29
- package/src/utils/fetch.test.ts +208 -3
- package/src/utils/fetch.ts +242 -19
- package/src/app/api/openai/images/createImageGeneration.ts +0 -26
- package/src/app/api/openai/images/route.ts +0 -16
- package/src/features/Conversation/Actions/Function.tsx +0 -17
- /package/src/app/api/{chat → middleware}/auth/index.test.ts +0 -0
- /package/src/app/api/{chat → middleware}/auth/index.ts +0 -0
- /package/src/app/api/{chat → middleware}/auth/utils.ts +0 -0
- /package/src/app/api/{auth.ts → openai/createBizOpenAI/auth.ts} +0 -0
- /package/src/store/chat/slices/{tool → builtinTool}/initialState.ts +0 -0
- /package/src/store/chat/slices/{tool → builtinTool}/selectors.ts +0 -0
- /package/src/tools/dalle/Render/{EditMode.tsx → Item/EditMode.tsx} +0 -0
package/src/utils/fetch.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
|
2
2
|
import { FetchEventSourceInit } from '@microsoft/fetch-event-source';
|
|
3
3
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { ZodError } from 'zod';
|
|
4
5
|
|
|
5
6
|
import { ErrorResponse } from '@/types/fetch';
|
|
6
7
|
|
|
@@ -82,6 +83,16 @@ describe('getMessageError', () => {
|
|
|
82
83
|
});
|
|
83
84
|
expect(mockResponse.json).toHaveBeenCalled();
|
|
84
85
|
});
|
|
86
|
+
|
|
87
|
+
it('should handle timeout error correctly', async () => {
|
|
88
|
+
const mockResponse = createMockResponse(undefined, false, 504);
|
|
89
|
+
const error = await getMessageError(mockResponse as any);
|
|
90
|
+
|
|
91
|
+
expect(error).toEqual({
|
|
92
|
+
message: 'translated_response.504',
|
|
93
|
+
type: 504,
|
|
94
|
+
});
|
|
95
|
+
});
|
|
85
96
|
});
|
|
86
97
|
|
|
87
98
|
describe('parseToolCalls', () => {
|
|
@@ -173,6 +184,34 @@ describe('parseToolCalls', () => {
|
|
|
173
184
|
},
|
|
174
185
|
]);
|
|
175
186
|
});
|
|
187
|
+
|
|
188
|
+
it('should throw error if incomplete tool calls data', () => {
|
|
189
|
+
const origin = [
|
|
190
|
+
{
|
|
191
|
+
id: '1',
|
|
192
|
+
type: 'function',
|
|
193
|
+
function: { name: 'func', arguments: '{"location\\": \\"Hangzhou\\"}' },
|
|
194
|
+
},
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
const chunk = [{ index: 1, id: '2', type: 'function' }];
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
parseToolCalls(origin, chunk as any);
|
|
201
|
+
} catch (e) {
|
|
202
|
+
expect(e).toEqual(
|
|
203
|
+
new ZodError([
|
|
204
|
+
{
|
|
205
|
+
code: 'invalid_type',
|
|
206
|
+
expected: 'object',
|
|
207
|
+
received: 'undefined',
|
|
208
|
+
path: ['function'],
|
|
209
|
+
message: 'Required',
|
|
210
|
+
},
|
|
211
|
+
]),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
176
215
|
});
|
|
177
216
|
|
|
178
217
|
describe('fetchSSE', () => {
|
|
@@ -188,7 +227,11 @@ describe('fetchSSE', () => {
|
|
|
188
227
|
},
|
|
189
228
|
);
|
|
190
229
|
|
|
191
|
-
await fetchSSE('/', {
|
|
230
|
+
await fetchSSE('/', {
|
|
231
|
+
onMessageHandle: mockOnMessageHandle,
|
|
232
|
+
onFinish: mockOnFinish,
|
|
233
|
+
smoothing: false,
|
|
234
|
+
});
|
|
192
235
|
|
|
193
236
|
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, { text: 'Hello', type: 'text' });
|
|
194
237
|
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, { text: ' World', type: 'text' });
|
|
@@ -222,7 +265,11 @@ describe('fetchSSE', () => {
|
|
|
222
265
|
},
|
|
223
266
|
);
|
|
224
267
|
|
|
225
|
-
await fetchSSE('/', {
|
|
268
|
+
await fetchSSE('/', {
|
|
269
|
+
onMessageHandle: mockOnMessageHandle,
|
|
270
|
+
onFinish: mockOnFinish,
|
|
271
|
+
smoothing: false,
|
|
272
|
+
});
|
|
226
273
|
|
|
227
274
|
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, {
|
|
228
275
|
tool_calls: [{ id: '1', type: 'function', function: { name: 'func1', arguments: 'arg1' } }],
|
|
@@ -256,7 +303,7 @@ describe('fetchSSE', () => {
|
|
|
256
303
|
},
|
|
257
304
|
);
|
|
258
305
|
|
|
259
|
-
await fetchSSE('/', { onAbort: mockOnAbort });
|
|
306
|
+
await fetchSSE('/', { onAbort: mockOnAbort, smoothing: false });
|
|
260
307
|
|
|
261
308
|
expect(mockOnAbort).toHaveBeenCalledWith('Hello');
|
|
262
309
|
});
|
|
@@ -320,4 +367,162 @@ describe('fetchSSE', () => {
|
|
|
320
367
|
type: 'done',
|
|
321
368
|
});
|
|
322
369
|
});
|
|
370
|
+
|
|
371
|
+
it('should handle text event with smoothing correctly', async () => {
|
|
372
|
+
const mockOnMessageHandle = vi.fn();
|
|
373
|
+
const mockOnFinish = vi.fn();
|
|
374
|
+
|
|
375
|
+
(fetchEventSource as any).mockImplementationOnce(
|
|
376
|
+
(url: string, options: FetchEventSourceInit) => {
|
|
377
|
+
options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
|
|
378
|
+
options.onmessage!({ event: 'text', data: JSON.stringify('Hello') } as any);
|
|
379
|
+
options.onmessage!({ event: 'text', data: JSON.stringify(' World') } as any);
|
|
380
|
+
},
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
await fetchSSE('/', {
|
|
384
|
+
onMessageHandle: mockOnMessageHandle,
|
|
385
|
+
onFinish: mockOnFinish,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, { text: 'He', type: 'text' });
|
|
389
|
+
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, { text: 'llo World', type: 'text' });
|
|
390
|
+
// more assertions for each character...
|
|
391
|
+
expect(mockOnFinish).toHaveBeenCalledWith('Hello World', {
|
|
392
|
+
observationId: null,
|
|
393
|
+
toolCalls: undefined,
|
|
394
|
+
traceId: null,
|
|
395
|
+
type: 'done',
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('should handle tool_calls event with smoothing correctly', async () => {
|
|
400
|
+
const mockOnMessageHandle = vi.fn();
|
|
401
|
+
const mockOnFinish = vi.fn();
|
|
402
|
+
|
|
403
|
+
(fetchEventSource as any).mockImplementationOnce(
|
|
404
|
+
(url: string, options: FetchEventSourceInit) => {
|
|
405
|
+
options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
|
|
406
|
+
options.onmessage!({
|
|
407
|
+
event: 'tool_calls',
|
|
408
|
+
data: JSON.stringify([
|
|
409
|
+
{ index: 0, id: '1', type: 'function', function: { name: 'func1', arguments: 'a' } },
|
|
410
|
+
]),
|
|
411
|
+
} as any);
|
|
412
|
+
options.onmessage!({
|
|
413
|
+
event: 'tool_calls',
|
|
414
|
+
data: JSON.stringify([
|
|
415
|
+
{ index: 0, function: { arguments: 'rg1' } },
|
|
416
|
+
{ index: 1, id: '2', type: 'function', function: { name: 'func2', arguments: 'a' } },
|
|
417
|
+
]),
|
|
418
|
+
} as any);
|
|
419
|
+
options.onmessage!({
|
|
420
|
+
event: 'tool_calls',
|
|
421
|
+
data: JSON.stringify([{ index: 1, function: { arguments: 'rg2' } }]),
|
|
422
|
+
} as any);
|
|
423
|
+
},
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
await fetchSSE('/', {
|
|
427
|
+
onMessageHandle: mockOnMessageHandle,
|
|
428
|
+
onFinish: mockOnFinish,
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// TODO: need to check whether the `aarg1` is correct
|
|
432
|
+
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, {
|
|
433
|
+
isAnimationActives: [true],
|
|
434
|
+
tool_calls: [
|
|
435
|
+
{ id: '1', type: 'function', function: { name: 'func1', arguments: 'aarg1' } },
|
|
436
|
+
{ function: { arguments: 'aarg2', name: 'func2' }, id: '2', type: 'function' },
|
|
437
|
+
],
|
|
438
|
+
type: 'tool_calls',
|
|
439
|
+
});
|
|
440
|
+
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, {
|
|
441
|
+
isAnimationActives: [true, true],
|
|
442
|
+
tool_calls: [
|
|
443
|
+
{ id: '1', type: 'function', function: { name: 'func1', arguments: 'aarg1' } },
|
|
444
|
+
{ id: '2', type: 'function', function: { name: 'func2', arguments: 'aarg2' } },
|
|
445
|
+
],
|
|
446
|
+
type: 'tool_calls',
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// more assertions for each character...
|
|
450
|
+
expect(mockOnFinish).toHaveBeenCalledWith('', {
|
|
451
|
+
observationId: null,
|
|
452
|
+
toolCalls: [
|
|
453
|
+
{ id: '1', type: 'function', function: { name: 'func1', arguments: 'arg1' } },
|
|
454
|
+
{ id: '2', type: 'function', function: { name: 'func2', arguments: 'arg2' } },
|
|
455
|
+
],
|
|
456
|
+
traceId: null,
|
|
457
|
+
type: 'done',
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('should handle request interruption and resumption correctly', async () => {
|
|
462
|
+
const mockOnMessageHandle = vi.fn();
|
|
463
|
+
const mockOnFinish = vi.fn();
|
|
464
|
+
const abortController = new AbortController();
|
|
465
|
+
|
|
466
|
+
(fetchEventSource as any).mockImplementationOnce(
|
|
467
|
+
(url: string, options: FetchEventSourceInit) => {
|
|
468
|
+
options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
|
|
469
|
+
options.onmessage!({ event: 'text', data: JSON.stringify('Hello') } as any);
|
|
470
|
+
abortController.abort();
|
|
471
|
+
options.onmessage!({ event: 'text', data: JSON.stringify(' World') } as any);
|
|
472
|
+
},
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
await fetchSSE('/', {
|
|
476
|
+
onMessageHandle: mockOnMessageHandle,
|
|
477
|
+
onFinish: mockOnFinish,
|
|
478
|
+
signal: abortController.signal,
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, { text: 'He', type: 'text' });
|
|
482
|
+
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, { text: 'llo World', type: 'text' });
|
|
483
|
+
|
|
484
|
+
expect(mockOnFinish).toHaveBeenCalledWith('Hello World', {
|
|
485
|
+
type: 'done',
|
|
486
|
+
observationId: null,
|
|
487
|
+
traceId: null,
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('should call onFinish with correct parameters for different finish types', async () => {
|
|
492
|
+
const mockOnFinish = vi.fn();
|
|
493
|
+
|
|
494
|
+
(fetchEventSource as any).mockImplementationOnce(
|
|
495
|
+
(url: string, options: FetchEventSourceInit) => {
|
|
496
|
+
options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
|
|
497
|
+
options.onmessage!({ event: 'text', data: JSON.stringify('Hello') } as any);
|
|
498
|
+
options.onerror!({ name: 'AbortError' });
|
|
499
|
+
},
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
await fetchSSE('/', { onFinish: mockOnFinish, smoothing: false });
|
|
503
|
+
|
|
504
|
+
expect(mockOnFinish).toHaveBeenCalledWith('Hello', {
|
|
505
|
+
observationId: null,
|
|
506
|
+
toolCalls: undefined,
|
|
507
|
+
traceId: null,
|
|
508
|
+
type: 'abort',
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
(fetchEventSource as any).mockImplementationOnce(
|
|
512
|
+
(url: string, options: FetchEventSourceInit) => {
|
|
513
|
+
options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
|
|
514
|
+
options.onmessage!({ event: 'text', data: JSON.stringify('Hello') } as any);
|
|
515
|
+
options.onerror!(new Error('Unknown error'));
|
|
516
|
+
},
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
await fetchSSE('/', { onFinish: mockOnFinish, smoothing: false });
|
|
520
|
+
|
|
521
|
+
expect(mockOnFinish).toHaveBeenCalledWith('Hello', {
|
|
522
|
+
observationId: null,
|
|
523
|
+
toolCalls: undefined,
|
|
524
|
+
traceId: null,
|
|
525
|
+
type: 'error',
|
|
526
|
+
});
|
|
527
|
+
});
|
|
323
528
|
});
|
package/src/utils/fetch.ts
CHANGED
|
@@ -51,6 +51,7 @@ export interface MessageTextChunk {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
interface MessageToolCallsChunk {
|
|
54
|
+
isAnimationActives?: boolean[];
|
|
54
55
|
tool_calls: MessageToolCall[];
|
|
55
56
|
type: 'tool_calls';
|
|
56
57
|
}
|
|
@@ -61,24 +62,201 @@ export interface FetchSSEOptions {
|
|
|
61
62
|
onErrorHandle?: (error: ChatMessageError) => void;
|
|
62
63
|
onFinish?: OnFinishHandler;
|
|
63
64
|
onMessageHandle?: (chunk: MessageTextChunk | MessageToolCallsChunk) => void;
|
|
65
|
+
smoothing?: boolean;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
export const parseToolCalls = (origin: MessageToolCall[], value: MessageToolCallChunk[]) =>
|
|
67
69
|
produce(origin, (draft) => {
|
|
70
|
+
// if there is no origin, we should parse all the value and set it to draft
|
|
68
71
|
if (draft.length === 0) {
|
|
69
72
|
draft.push(...value.map((item) => MessageToolCallSchema.parse(item)));
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// if there is origin, we should merge the value to the origin
|
|
77
|
+
value.forEach(({ index, ...item }) => {
|
|
78
|
+
if (!draft?.[index]) {
|
|
79
|
+
// if not, we should insert it to the draft
|
|
80
|
+
draft?.splice(index, 0, MessageToolCallSchema.parse(item));
|
|
81
|
+
} else {
|
|
82
|
+
// if it is already in the draft, we should merge the arguments to the draft
|
|
83
|
+
if (item.function?.arguments) {
|
|
84
|
+
draft[index].function.arguments += item.function.arguments;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const createSmoothMessage = (params: { onTextUpdate: (delta: string, text: string) => void }) => {
|
|
91
|
+
let buffer = '';
|
|
92
|
+
// why use queue: https://shareg.pt/GLBrjpK
|
|
93
|
+
let outputQueue: string[] = [];
|
|
94
|
+
|
|
95
|
+
// eslint-disable-next-line no-undef
|
|
96
|
+
let animationTimeoutId: NodeJS.Timeout | null = null;
|
|
97
|
+
let isAnimationActive = false;
|
|
98
|
+
|
|
99
|
+
// when you need to stop the animation, call this function
|
|
100
|
+
const stopAnimation = () => {
|
|
101
|
+
isAnimationActive = false;
|
|
102
|
+
if (animationTimeoutId !== null) {
|
|
103
|
+
clearTimeout(animationTimeoutId);
|
|
104
|
+
animationTimeoutId = null;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// define startAnimation function to display the text in buffer smooth
|
|
109
|
+
// when you need to start the animation, call this function
|
|
110
|
+
const startAnimation = (speed = 2) =>
|
|
111
|
+
new Promise<void>((resolve) => {
|
|
112
|
+
if (isAnimationActive) {
|
|
113
|
+
resolve();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
isAnimationActive = true;
|
|
118
|
+
|
|
119
|
+
const updateText = () => {
|
|
120
|
+
// 如果动画已经不再激活,则停止更新文本
|
|
121
|
+
if (!isAnimationActive) {
|
|
122
|
+
clearTimeout(animationTimeoutId!);
|
|
123
|
+
animationTimeoutId = null;
|
|
124
|
+
resolve();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 如果还有文本没有显示
|
|
128
|
+
// 检查队列中是否有字符待显示
|
|
129
|
+
if (outputQueue.length > 0) {
|
|
130
|
+
// 从队列中获取前两个字符(如果存在)
|
|
131
|
+
const charsToAdd = outputQueue.splice(0, speed).join('');
|
|
132
|
+
buffer += charsToAdd;
|
|
133
|
+
|
|
134
|
+
// 更新消息内容,这里可能需要结合实际情况调整
|
|
135
|
+
params.onTextUpdate(charsToAdd, buffer);
|
|
136
|
+
|
|
137
|
+
// 设置下一个字符的延迟
|
|
138
|
+
animationTimeoutId = setTimeout(updateText, 16); // 16 毫秒的延迟模拟打字机效果
|
|
74
139
|
} else {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
140
|
+
// 当所有字符都显示完毕时,清除动画状态
|
|
141
|
+
isAnimationActive = false;
|
|
142
|
+
animationTimeoutId = null;
|
|
143
|
+
resolve();
|
|
78
144
|
}
|
|
79
|
-
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
updateText();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const pushToQueue = (text: string) => {
|
|
151
|
+
outputQueue.push(...text.split(''));
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
isAnimationActive,
|
|
156
|
+
isTokenRemain: () => outputQueue.length > 0,
|
|
157
|
+
pushToQueue,
|
|
158
|
+
startAnimation,
|
|
159
|
+
stopAnimation,
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const createSmoothToolCalls = (params: {
|
|
164
|
+
onToolCallsUpdate: (toolCalls: MessageToolCall[], isAnimationActives: boolean[]) => void;
|
|
165
|
+
}) => {
|
|
166
|
+
let toolCallsBuffer: MessageToolCall[] = [];
|
|
167
|
+
|
|
168
|
+
// 为每个 tool_call 维护一个输出队列和动画控制器
|
|
169
|
+
|
|
170
|
+
// eslint-disable-next-line no-undef
|
|
171
|
+
const animationTimeoutIds: (NodeJS.Timeout | null)[] = [];
|
|
172
|
+
const outputQueues: string[][] = [];
|
|
173
|
+
const isAnimationActives: boolean[] = [];
|
|
174
|
+
|
|
175
|
+
const stopAnimation = (index: number) => {
|
|
176
|
+
isAnimationActives[index] = false;
|
|
177
|
+
if (animationTimeoutIds[index] !== null) {
|
|
178
|
+
clearTimeout(animationTimeoutIds[index]!);
|
|
179
|
+
animationTimeoutIds[index] = null;
|
|
80
180
|
}
|
|
81
|
-
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const startAnimation = (index: number, speed = 2) =>
|
|
184
|
+
new Promise<void>((resolve) => {
|
|
185
|
+
if (isAnimationActives[index]) {
|
|
186
|
+
resolve();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
isAnimationActives[index] = true;
|
|
191
|
+
|
|
192
|
+
const updateToolCall = () => {
|
|
193
|
+
if (!isAnimationActives[index]) {
|
|
194
|
+
resolve();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (outputQueues[index].length > 0) {
|
|
198
|
+
const charsToAdd = outputQueues[index].splice(0, speed).join('');
|
|
199
|
+
|
|
200
|
+
const toolCallToUpdate = toolCallsBuffer[index];
|
|
201
|
+
|
|
202
|
+
if (toolCallToUpdate) {
|
|
203
|
+
toolCallToUpdate.function.arguments += charsToAdd;
|
|
204
|
+
|
|
205
|
+
// 触发 ui 更新
|
|
206
|
+
params.onToolCallsUpdate(toolCallsBuffer, [...isAnimationActives]);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
animationTimeoutIds[index] = setTimeout(updateToolCall, 16);
|
|
210
|
+
} else {
|
|
211
|
+
isAnimationActives[index] = false;
|
|
212
|
+
animationTimeoutIds[index] = null;
|
|
213
|
+
resolve();
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
updateToolCall();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const pushToQueue = (toolCallChunks: MessageToolCallChunk[]) => {
|
|
221
|
+
toolCallChunks.forEach((chunk) => {
|
|
222
|
+
// init the tool call buffer and output queue
|
|
223
|
+
if (!toolCallsBuffer[chunk.index]) {
|
|
224
|
+
toolCallsBuffer[chunk.index] = MessageToolCallSchema.parse(chunk);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!outputQueues[chunk.index]) {
|
|
228
|
+
outputQueues[chunk.index] = [];
|
|
229
|
+
isAnimationActives[chunk.index] = false;
|
|
230
|
+
animationTimeoutIds[chunk.index] = null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
outputQueues[chunk.index].push(...(chunk.function?.arguments || '').split(''));
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const startAnimations = async (speed = 2) => {
|
|
238
|
+
const pools = toolCallsBuffer.map(async (_, index) => {
|
|
239
|
+
if (outputQueues[index].length > 0 && !isAnimationActives[index]) {
|
|
240
|
+
await startAnimation(index, speed);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
await Promise.all(pools);
|
|
245
|
+
};
|
|
246
|
+
const stopAnimations = () => {
|
|
247
|
+
toolCallsBuffer.forEach((_, index) => {
|
|
248
|
+
stopAnimation(index);
|
|
249
|
+
});
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
isAnimationActives,
|
|
254
|
+
isTokenRemain: () => outputQueues.some((token) => token.length > 0),
|
|
255
|
+
pushToQueue,
|
|
256
|
+
startAnimations,
|
|
257
|
+
stopAnimations,
|
|
258
|
+
};
|
|
259
|
+
};
|
|
82
260
|
|
|
83
261
|
/**
|
|
84
262
|
* Fetch data using stream method
|
|
@@ -92,6 +270,21 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
|
|
|
92
270
|
let finishedType: SSEFinishType = 'done';
|
|
93
271
|
let response!: Response;
|
|
94
272
|
|
|
273
|
+
const { smoothing = true } = options;
|
|
274
|
+
|
|
275
|
+
const textController = createSmoothMessage({
|
|
276
|
+
onTextUpdate: (delta, text) => {
|
|
277
|
+
output = text;
|
|
278
|
+
options.onMessageHandle?.({ text: delta, type: 'text' });
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const toolCallsController = createSmoothToolCalls({
|
|
283
|
+
onToolCallsUpdate: (toolCalls, isAnimationActives) => {
|
|
284
|
+
options.onMessageHandle?.({ isAnimationActives, tool_calls: toolCalls, type: 'tool_calls' });
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
|
|
95
288
|
try {
|
|
96
289
|
await fetchEventSource(url, {
|
|
97
290
|
body: options.body,
|
|
@@ -102,6 +295,7 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
|
|
|
102
295
|
if ((error as TypeError).name === 'AbortError') {
|
|
103
296
|
finishedType = 'abort';
|
|
104
297
|
options?.onAbort?.(output);
|
|
298
|
+
textController.stopAnimation();
|
|
105
299
|
} else {
|
|
106
300
|
finishedType = 'error';
|
|
107
301
|
console.error(error);
|
|
@@ -122,22 +316,39 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
|
|
|
122
316
|
|
|
123
317
|
switch (ev.event) {
|
|
124
318
|
case 'text': {
|
|
125
|
-
|
|
126
|
-
|
|
319
|
+
if (smoothing) {
|
|
320
|
+
textController.pushToQueue(data);
|
|
321
|
+
|
|
322
|
+
if (!textController.isAnimationActive) textController.startAnimation();
|
|
323
|
+
} else {
|
|
324
|
+
output += data;
|
|
325
|
+
options.onMessageHandle?.({ text: data, type: 'text' });
|
|
326
|
+
}
|
|
327
|
+
|
|
127
328
|
break;
|
|
128
329
|
}
|
|
129
330
|
|
|
130
331
|
case 'tool_calls': {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
332
|
+
// get finial
|
|
333
|
+
// if there is no tool calls, we should initialize the tool calls
|
|
334
|
+
if (!toolCalls) toolCalls = [];
|
|
135
335
|
toolCalls = parseToolCalls(toolCalls, data);
|
|
136
336
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
337
|
+
if (smoothing) {
|
|
338
|
+
// make the tool calls smooth
|
|
339
|
+
|
|
340
|
+
// push the tool calls to the smooth queue
|
|
341
|
+
toolCallsController.pushToQueue(data);
|
|
342
|
+
// if there is no animation active, we should start the animation
|
|
343
|
+
if (toolCallsController.isAnimationActives.some((value) => !value)) {
|
|
344
|
+
toolCallsController.startAnimations();
|
|
345
|
+
}
|
|
346
|
+
} else {
|
|
347
|
+
options.onMessageHandle?.({
|
|
348
|
+
tool_calls: toolCalls,
|
|
349
|
+
type: 'tool_calls',
|
|
350
|
+
});
|
|
351
|
+
}
|
|
141
352
|
}
|
|
142
353
|
}
|
|
143
354
|
},
|
|
@@ -160,6 +371,9 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
|
|
|
160
371
|
// only call onFinish when response is available
|
|
161
372
|
// so like abort, we don't need to call onFinish
|
|
162
373
|
if (response) {
|
|
374
|
+
textController.stopAnimation();
|
|
375
|
+
toolCallsController.stopAnimations();
|
|
376
|
+
|
|
163
377
|
// if there is no onMessageHandler, we should call onHandleMessage first
|
|
164
378
|
if (!triggerOnMessageHandler) {
|
|
165
379
|
output = await response.clone().text();
|
|
@@ -168,6 +382,15 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
|
|
|
168
382
|
|
|
169
383
|
const traceId = response.headers.get(LOBE_CHAT_TRACE_ID);
|
|
170
384
|
const observationId = response.headers.get(LOBE_CHAT_OBSERVATION_ID);
|
|
385
|
+
|
|
386
|
+
if (textController.isTokenRemain()) {
|
|
387
|
+
await textController.startAnimation(15);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (toolCallsController.isTokenRemain()) {
|
|
391
|
+
await toolCallsController.startAnimations(15);
|
|
392
|
+
}
|
|
393
|
+
|
|
171
394
|
await options?.onFinish?.(output, { observationId, toolCalls, traceId, type: finishedType });
|
|
172
395
|
}
|
|
173
396
|
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import OpenAI from 'openai';
|
|
2
|
-
|
|
3
|
-
import { OpenAIImagePayload } from '@/types/openai/image';
|
|
4
|
-
|
|
5
|
-
export const createImageGeneration = async ({
|
|
6
|
-
openai,
|
|
7
|
-
payload,
|
|
8
|
-
}: {
|
|
9
|
-
openai: OpenAI;
|
|
10
|
-
payload: OpenAIImagePayload;
|
|
11
|
-
}) => {
|
|
12
|
-
const res = await openai.images.generate({ ...payload, response_format: 'url' });
|
|
13
|
-
|
|
14
|
-
const urls = res.data.map((o) => o.url) as string[];
|
|
15
|
-
|
|
16
|
-
return new Response(JSON.stringify(urls));
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
// const mockImages = [
|
|
20
|
-
// 'https://github-production-user-asset-6210df.s3.amazonaws.com/28616219/292159272-032d5c8b-20be-48d9-8dbb-f2491f231bac.png',
|
|
21
|
-
// 'https://github-production-user-asset-6210df.s3.amazonaws.com/28616219/292159798-cad89421-20c5-44b0-a337-fcbb857f1f70.png',
|
|
22
|
-
// 'https://github-production-user-asset-6210df.s3.amazonaws.com/28616219/292160015-2263156f-d41f-48ae-9c2c-d96799b9d2b8.png',
|
|
23
|
-
// 'https://github-production-user-asset-6210df.s3.amazonaws.com/28616219/292160229-592d112f-5dfd-47d7-98d3-44bc09dc91f7.png',
|
|
24
|
-
// ];
|
|
25
|
-
// export const createImageGeneration = async () =>
|
|
26
|
-
// new Response(JSON.stringify([mockImages[Math.round(Math.random() * 3)]]));
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { OpenAIImagePayload } from '@/types/openai/image';
|
|
2
|
-
|
|
3
|
-
import { createBizOpenAI } from '../createBizOpenAI';
|
|
4
|
-
import { createImageGeneration } from './createImageGeneration';
|
|
5
|
-
|
|
6
|
-
export const runtime = 'edge';
|
|
7
|
-
|
|
8
|
-
export const POST = async (req: Request) => {
|
|
9
|
-
const payload = (await req.json()) as OpenAIImagePayload;
|
|
10
|
-
|
|
11
|
-
const openaiOrErrResponse = createBizOpenAI(req);
|
|
12
|
-
// if resOrOpenAI is a Response, it means there is an error,just return it
|
|
13
|
-
if (openaiOrErrResponse instanceof Response) return openaiOrErrResponse;
|
|
14
|
-
|
|
15
|
-
return createImageGeneration({ openai: openaiOrErrResponse, payload });
|
|
16
|
-
};
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { ActionIconGroup } from '@lobehub/ui';
|
|
2
|
-
import { memo } from 'react';
|
|
3
|
-
|
|
4
|
-
import { useChatListActionsBar } from '../hooks/useChatListActionsBar';
|
|
5
|
-
import { RenderAction } from '../types';
|
|
6
|
-
|
|
7
|
-
export const FunctionActionsBar: RenderAction = memo(({ onActionClick }) => {
|
|
8
|
-
const { regenerate, delAndRegenerate, del } = useChatListActionsBar();
|
|
9
|
-
return (
|
|
10
|
-
<ActionIconGroup
|
|
11
|
-
dropdownMenu={[regenerate, delAndRegenerate, del]}
|
|
12
|
-
items={[regenerate]}
|
|
13
|
-
onActionClick={onActionClick}
|
|
14
|
-
type="ghost"
|
|
15
|
-
/>
|
|
16
|
-
);
|
|
17
|
-
});
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|