@lobehub/chat 0.156.2 → 0.157.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 +25 -0
- package/package.json +3 -2
- package/src/config/modelProviders/anthropic.ts +3 -0
- package/src/config/modelProviders/google.ts +3 -0
- package/src/config/modelProviders/groq.ts +5 -1
- package/src/config/modelProviders/minimax.ts +10 -7
- package/src/config/modelProviders/mistral.ts +1 -0
- package/src/config/modelProviders/moonshot.ts +3 -0
- package/src/config/modelProviders/zhipu.ts +2 -6
- package/src/config/server/provider.ts +1 -1
- package/src/database/client/core/db.ts +32 -0
- package/src/database/client/core/schemas.ts +9 -0
- package/src/database/client/models/__tests__/message.test.ts +2 -2
- package/src/database/client/schemas/message.ts +8 -1
- package/src/features/AgentSetting/store/action.ts +15 -6
- package/src/features/Conversation/Actions/Tool.tsx +16 -0
- package/src/features/Conversation/Actions/index.ts +2 -2
- package/src/features/Conversation/Messages/Assistant/ToolCalls/index.tsx +78 -0
- package/src/features/Conversation/Messages/Assistant/ToolCalls/style.ts +25 -0
- package/src/features/Conversation/Messages/Assistant/index.tsx +47 -0
- package/src/features/Conversation/Messages/Default.tsx +4 -1
- package/src/features/Conversation/{Plugins → Messages/Tool}/Inspector/index.tsx +34 -35
- package/src/features/Conversation/Messages/Tool/index.tsx +44 -0
- package/src/features/Conversation/Messages/index.ts +3 -2
- package/src/features/Conversation/Plugins/Render/StandaloneType/Iframe.tsx +1 -1
- package/src/features/Conversation/components/SkeletonList.tsx +2 -2
- package/src/features/Conversation/index.tsx +2 -3
- package/src/libs/agent-runtime/BaseAI.ts +2 -9
- package/src/libs/agent-runtime/anthropic/index.test.ts +195 -0
- package/src/libs/agent-runtime/anthropic/index.ts +71 -15
- package/src/libs/agent-runtime/azureOpenai/index.ts +6 -5
- package/src/libs/agent-runtime/bedrock/index.ts +24 -18
- package/src/libs/agent-runtime/google/index.test.ts +154 -0
- package/src/libs/agent-runtime/google/index.ts +91 -10
- package/src/libs/agent-runtime/groq/index.test.ts +41 -72
- package/src/libs/agent-runtime/groq/index.ts +7 -0
- package/src/libs/agent-runtime/minimax/index.test.ts +2 -2
- package/src/libs/agent-runtime/minimax/index.ts +14 -37
- package/src/libs/agent-runtime/mistral/index.test.ts +0 -53
- package/src/libs/agent-runtime/mistral/index.ts +1 -0
- package/src/libs/agent-runtime/moonshot/index.test.ts +1 -71
- package/src/libs/agent-runtime/ollama/index.test.ts +197 -0
- package/src/libs/agent-runtime/ollama/index.ts +3 -3
- package/src/libs/agent-runtime/openai/index.test.ts +0 -53
- package/src/libs/agent-runtime/openrouter/index.test.ts +1 -53
- package/src/libs/agent-runtime/perplexity/index.test.ts +0 -71
- package/src/libs/agent-runtime/perplexity/index.ts +2 -3
- package/src/libs/agent-runtime/togetherai/__snapshots__/index.test.ts.snap +886 -0
- package/src/libs/agent-runtime/togetherai/fixtures/models.json +8111 -0
- package/src/libs/agent-runtime/togetherai/index.test.ts +16 -54
- package/src/libs/agent-runtime/types/chat.ts +19 -3
- package/src/libs/agent-runtime/utils/anthropicHelpers.test.ts +120 -1
- package/src/libs/agent-runtime/utils/anthropicHelpers.ts +67 -4
- package/src/libs/agent-runtime/utils/debugStream.test.ts +70 -0
- package/src/libs/agent-runtime/utils/debugStream.ts +39 -9
- package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.test.ts +521 -0
- package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.ts +76 -5
- package/src/libs/agent-runtime/utils/response.ts +12 -0
- package/src/libs/agent-runtime/utils/streams/anthropic.test.ts +197 -0
- package/src/libs/agent-runtime/utils/streams/anthropic.ts +91 -0
- package/src/libs/agent-runtime/utils/streams/bedrock/claude.ts +21 -0
- package/src/libs/agent-runtime/utils/streams/bedrock/common.ts +32 -0
- package/src/libs/agent-runtime/utils/streams/bedrock/index.ts +3 -0
- package/src/libs/agent-runtime/utils/streams/bedrock/llama.test.ts +196 -0
- package/src/libs/agent-runtime/utils/streams/bedrock/llama.ts +51 -0
- package/src/libs/agent-runtime/utils/streams/google-ai.test.ts +97 -0
- package/src/libs/agent-runtime/utils/streams/google-ai.ts +68 -0
- package/src/libs/agent-runtime/utils/streams/index.ts +7 -0
- package/src/libs/agent-runtime/utils/streams/minimax.ts +39 -0
- package/src/libs/agent-runtime/utils/streams/ollama.test.ts +77 -0
- package/src/libs/agent-runtime/utils/streams/ollama.ts +38 -0
- package/src/libs/agent-runtime/utils/streams/openai.test.ts +263 -0
- package/src/libs/agent-runtime/utils/streams/openai.ts +79 -0
- package/src/libs/agent-runtime/utils/streams/protocol.ts +100 -0
- package/src/libs/agent-runtime/zeroone/index.test.ts +1 -53
- package/src/libs/agent-runtime/zhipu/index.test.ts +1 -1
- package/src/libs/agent-runtime/zhipu/index.ts +3 -2
- package/src/locales/default/plugin.ts +3 -4
- package/src/migrations/FromV4ToV5/fixtures/from-v1-to-v5-output.json +245 -0
- package/src/migrations/FromV4ToV5/fixtures/function-input-v4.json +96 -0
- package/src/migrations/FromV4ToV5/fixtures/function-output-v5.json +120 -0
- package/src/migrations/FromV4ToV5/index.ts +58 -0
- package/src/migrations/FromV4ToV5/migrations.test.ts +49 -0
- package/src/migrations/FromV4ToV5/types/v4.ts +21 -0
- package/src/migrations/FromV4ToV5/types/v5.ts +27 -0
- package/src/migrations/index.ts +8 -1
- package/src/services/__tests__/chat.test.ts +10 -20
- package/src/services/chat.ts +78 -65
- package/src/store/chat/slices/enchance/action.ts +15 -10
- package/src/store/chat/slices/message/action.test.ts +36 -86
- package/src/store/chat/slices/message/action.ts +70 -79
- package/src/store/chat/slices/message/reducer.ts +18 -1
- package/src/store/chat/slices/message/selectors.test.ts +38 -68
- package/src/store/chat/slices/message/selectors.ts +1 -22
- package/src/store/chat/slices/plugin/action.test.ts +147 -203
- package/src/store/chat/slices/plugin/action.ts +96 -82
- package/src/store/chat/slices/share/action.test.ts +3 -3
- package/src/store/chat/slices/share/action.ts +1 -1
- package/src/store/chat/slices/topic/action.ts +7 -2
- package/src/store/tool/selectors/tool.ts +6 -24
- package/src/store/tool/slices/builtin/action.test.ts +90 -0
- package/src/types/llm.ts +1 -1
- package/src/types/message/index.ts +9 -4
- package/src/types/message/tools.ts +57 -0
- package/src/types/openai/chat.ts +6 -0
- package/src/utils/fetch.test.ts +245 -1
- package/src/utils/fetch.ts +120 -44
- package/src/utils/toolCall.ts +21 -0
- package/src/features/Conversation/Messages/Assistant.tsx +0 -26
- package/src/features/Conversation/Messages/Function.tsx +0 -35
- package/src/libs/agent-runtime/ollama/stream.ts +0 -31
- /package/src/features/Conversation/{Plugins → Messages/Tool}/Inspector/PluginResultJSON.tsx +0 -0
- /package/src/features/Conversation/{Plugins → Messages/Tool}/Inspector/Settings.tsx +0 -0
- /package/src/features/Conversation/{Plugins → Messages/Tool}/Inspector/style.ts +0 -0
package/src/utils/fetch.test.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
|
2
|
+
import { FetchEventSourceInit } from '@microsoft/fetch-event-source';
|
|
1
3
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
4
|
|
|
3
5
|
import { ErrorResponse } from '@/types/fetch';
|
|
4
6
|
|
|
5
|
-
import { getMessageError } from './fetch';
|
|
7
|
+
import { fetchSSE, getMessageError, parseToolCalls } from './fetch';
|
|
6
8
|
|
|
7
9
|
// 模拟 i18next
|
|
8
10
|
vi.mock('i18next', () => ({
|
|
@@ -39,6 +41,10 @@ const createMockResponse = (body: any, ok: boolean, status: number = 200) => ({
|
|
|
39
41
|
},
|
|
40
42
|
});
|
|
41
43
|
|
|
44
|
+
vi.mock('@microsoft/fetch-event-source', () => ({
|
|
45
|
+
fetchEventSource: vi.fn(),
|
|
46
|
+
}));
|
|
47
|
+
|
|
42
48
|
// 在每次测试后清理所有模拟
|
|
43
49
|
afterEach(() => {
|
|
44
50
|
vi.restoreAllMocks();
|
|
@@ -77,3 +83,241 @@ describe('getMessageError', () => {
|
|
|
77
83
|
expect(mockResponse.json).toHaveBeenCalled();
|
|
78
84
|
});
|
|
79
85
|
});
|
|
86
|
+
|
|
87
|
+
describe('parseToolCalls', () => {
|
|
88
|
+
it('should create add new item', () => {
|
|
89
|
+
const chunk = [
|
|
90
|
+
{ index: 0, id: '1', type: 'function', function: { name: 'func', arguments: '' } },
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
const result = parseToolCalls([], chunk);
|
|
94
|
+
expect(result).toEqual([
|
|
95
|
+
{ id: '1', type: 'function', function: { name: 'func', arguments: '' } },
|
|
96
|
+
]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should update arguments if there is a toolCall', () => {
|
|
100
|
+
const origin = [{ id: '1', type: 'function', function: { name: 'func', arguments: '' } }];
|
|
101
|
+
|
|
102
|
+
const chunk1 = [{ index: 0, function: { arguments: '{"lo' } }];
|
|
103
|
+
|
|
104
|
+
const result1 = parseToolCalls(origin, chunk1);
|
|
105
|
+
expect(result1).toEqual([
|
|
106
|
+
{ id: '1', type: 'function', function: { name: 'func', arguments: '{"lo' } },
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
const chunk2 = [{ index: 0, function: { arguments: 'cation\\": \\"Hangzhou\\"}' } }];
|
|
110
|
+
const result2 = parseToolCalls(result1, chunk2);
|
|
111
|
+
|
|
112
|
+
expect(result2).toEqual([
|
|
113
|
+
{
|
|
114
|
+
id: '1',
|
|
115
|
+
type: 'function',
|
|
116
|
+
function: { name: 'func', arguments: '{"location\\": \\"Hangzhou\\"}' },
|
|
117
|
+
},
|
|
118
|
+
]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should add a new tool call if the index is different', () => {
|
|
122
|
+
const origin = [
|
|
123
|
+
{
|
|
124
|
+
id: '1',
|
|
125
|
+
type: 'function',
|
|
126
|
+
function: { name: 'func', arguments: '{"location\\": \\"Hangzhou\\"}' },
|
|
127
|
+
},
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
const chunk = [
|
|
131
|
+
{
|
|
132
|
+
index: 1,
|
|
133
|
+
id: '2',
|
|
134
|
+
type: 'function',
|
|
135
|
+
function: { name: 'func', arguments: '' },
|
|
136
|
+
},
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
const result1 = parseToolCalls(origin, chunk);
|
|
140
|
+
expect(result1).toEqual([
|
|
141
|
+
{
|
|
142
|
+
id: '1',
|
|
143
|
+
type: 'function',
|
|
144
|
+
function: { name: 'func', arguments: '{"location\\": \\"Hangzhou\\"}' },
|
|
145
|
+
},
|
|
146
|
+
{ id: '2', type: 'function', function: { name: 'func', arguments: '' } },
|
|
147
|
+
]);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should update correct arguments if there are multi tool calls', () => {
|
|
151
|
+
const origin = [
|
|
152
|
+
{
|
|
153
|
+
id: '1',
|
|
154
|
+
type: 'function',
|
|
155
|
+
function: { name: 'func', arguments: '{"location\\": \\"Hangzhou\\"}' },
|
|
156
|
+
},
|
|
157
|
+
{ id: '2', type: 'function', function: { name: 'func', arguments: '' } },
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
const chunk = [{ index: 1, function: { arguments: '{"location\\": \\"Beijing\\"}' } }];
|
|
161
|
+
|
|
162
|
+
const result1 = parseToolCalls(origin, chunk);
|
|
163
|
+
expect(result1).toEqual([
|
|
164
|
+
{
|
|
165
|
+
id: '1',
|
|
166
|
+
type: 'function',
|
|
167
|
+
function: { name: 'func', arguments: '{"location\\": \\"Hangzhou\\"}' },
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: '2',
|
|
171
|
+
type: 'function',
|
|
172
|
+
function: { name: 'func', arguments: '{"location\\": \\"Beijing\\"}' },
|
|
173
|
+
},
|
|
174
|
+
]);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('fetchSSE', () => {
|
|
179
|
+
it('should handle text event correctly', async () => {
|
|
180
|
+
const mockOnMessageHandle = vi.fn();
|
|
181
|
+
const mockOnFinish = vi.fn();
|
|
182
|
+
|
|
183
|
+
(fetchEventSource as any).mockImplementationOnce(
|
|
184
|
+
(url: string, options: FetchEventSourceInit) => {
|
|
185
|
+
options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
|
|
186
|
+
options.onmessage!({ event: 'text', data: JSON.stringify('Hello') } as any);
|
|
187
|
+
options.onmessage!({ event: 'text', data: JSON.stringify(' World') } as any);
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
await fetchSSE('/', { onMessageHandle: mockOnMessageHandle, onFinish: mockOnFinish });
|
|
192
|
+
|
|
193
|
+
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, { text: 'Hello', type: 'text' });
|
|
194
|
+
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, { text: ' World', type: 'text' });
|
|
195
|
+
expect(mockOnFinish).toHaveBeenCalledWith('Hello World', {
|
|
196
|
+
observationId: null,
|
|
197
|
+
toolCalls: undefined,
|
|
198
|
+
traceId: null,
|
|
199
|
+
type: 'done',
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should handle tool_calls event correctly', async () => {
|
|
204
|
+
const mockOnMessageHandle = vi.fn();
|
|
205
|
+
const mockOnFinish = vi.fn();
|
|
206
|
+
|
|
207
|
+
(fetchEventSource as any).mockImplementationOnce(
|
|
208
|
+
(url: string, options: FetchEventSourceInit) => {
|
|
209
|
+
options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
|
|
210
|
+
options.onmessage!({
|
|
211
|
+
event: 'tool_calls',
|
|
212
|
+
data: JSON.stringify([
|
|
213
|
+
{ index: 0, id: '1', type: 'function', function: { name: 'func1', arguments: 'arg1' } },
|
|
214
|
+
]),
|
|
215
|
+
} as any);
|
|
216
|
+
options.onmessage!({
|
|
217
|
+
event: 'tool_calls',
|
|
218
|
+
data: JSON.stringify([
|
|
219
|
+
{ index: 1, id: '2', type: 'function', function: { name: 'func2', arguments: 'arg2' } },
|
|
220
|
+
]),
|
|
221
|
+
} as any);
|
|
222
|
+
},
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
await fetchSSE('/', { onMessageHandle: mockOnMessageHandle, onFinish: mockOnFinish });
|
|
226
|
+
|
|
227
|
+
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, {
|
|
228
|
+
tool_calls: [{ id: '1', type: 'function', function: { name: 'func1', arguments: 'arg1' } }],
|
|
229
|
+
type: 'tool_calls',
|
|
230
|
+
});
|
|
231
|
+
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, {
|
|
232
|
+
tool_calls: [
|
|
233
|
+
{ id: '1', type: 'function', function: { name: 'func1', arguments: 'arg1' } },
|
|
234
|
+
{ id: '2', type: 'function', function: { name: 'func2', arguments: 'arg2' } },
|
|
235
|
+
],
|
|
236
|
+
type: 'tool_calls',
|
|
237
|
+
});
|
|
238
|
+
expect(mockOnFinish).toHaveBeenCalledWith('', {
|
|
239
|
+
observationId: null,
|
|
240
|
+
toolCalls: [
|
|
241
|
+
{ id: '1', type: 'function', function: { name: 'func1', arguments: 'arg1' } },
|
|
242
|
+
{ id: '2', type: 'function', function: { name: 'func2', arguments: 'arg2' } },
|
|
243
|
+
],
|
|
244
|
+
traceId: null,
|
|
245
|
+
type: 'done',
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should call onAbort when AbortError is thrown', async () => {
|
|
250
|
+
const mockOnAbort = vi.fn();
|
|
251
|
+
|
|
252
|
+
(fetchEventSource as any).mockImplementationOnce(
|
|
253
|
+
(url: string, options: FetchEventSourceInit) => {
|
|
254
|
+
options.onmessage!({ event: 'text', data: JSON.stringify('Hello') } as any);
|
|
255
|
+
options.onerror!({ name: 'AbortError' });
|
|
256
|
+
},
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
await fetchSSE('/', { onAbort: mockOnAbort });
|
|
260
|
+
|
|
261
|
+
expect(mockOnAbort).toHaveBeenCalledWith('Hello');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('should call onErrorHandle when other error is thrown', async () => {
|
|
265
|
+
const mockOnErrorHandle = vi.fn();
|
|
266
|
+
const mockError = new Error('Unknown error');
|
|
267
|
+
|
|
268
|
+
(fetchEventSource as any).mockImplementationOnce(
|
|
269
|
+
(url: string, options: FetchEventSourceInit) => {
|
|
270
|
+
options.onerror!(mockError);
|
|
271
|
+
},
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
await fetchSSE('/', { onErrorHandle: mockOnErrorHandle });
|
|
275
|
+
|
|
276
|
+
expect(mockOnErrorHandle).not.toHaveBeenCalled();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should call onErrorHandle when response is not ok', async () => {
|
|
280
|
+
const mockOnErrorHandle = vi.fn();
|
|
281
|
+
|
|
282
|
+
(fetchEventSource as any).mockImplementationOnce(
|
|
283
|
+
(url: string, options: FetchEventSourceInit) => {
|
|
284
|
+
const res = new Response(JSON.stringify({ errorType: 'SomeError' }), {
|
|
285
|
+
status: 400,
|
|
286
|
+
statusText: 'Error',
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
options.onopen!(res as any);
|
|
290
|
+
},
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
await fetchSSE('/', { onErrorHandle: mockOnErrorHandle });
|
|
294
|
+
|
|
295
|
+
expect(mockOnErrorHandle).toHaveBeenCalledWith({
|
|
296
|
+
body: undefined,
|
|
297
|
+
message: 'translated_response.SomeError',
|
|
298
|
+
type: 'SomeError',
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should call onMessageHandle with full text if no message event', async () => {
|
|
303
|
+
const mockOnMessageHandle = vi.fn();
|
|
304
|
+
const mockOnFinish = vi.fn();
|
|
305
|
+
|
|
306
|
+
(fetchEventSource as any).mockImplementationOnce(
|
|
307
|
+
(url: string, options: FetchEventSourceInit) => {
|
|
308
|
+
const res = new Response('Hello World', { status: 200, statusText: 'OK' });
|
|
309
|
+
options.onopen!(res as any);
|
|
310
|
+
},
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
await fetchSSE('/', { onMessageHandle: mockOnMessageHandle, onFinish: mockOnFinish });
|
|
314
|
+
|
|
315
|
+
expect(mockOnMessageHandle).toHaveBeenCalledWith({ text: 'Hello World', type: 'text' });
|
|
316
|
+
expect(mockOnFinish).toHaveBeenCalledWith('Hello World', {
|
|
317
|
+
observationId: null,
|
|
318
|
+
toolCalls: undefined,
|
|
319
|
+
traceId: null,
|
|
320
|
+
type: 'done',
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
});
|
package/src/utils/fetch.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
|
1
2
|
import { t } from 'i18next';
|
|
3
|
+
import { produce } from 'immer';
|
|
2
4
|
|
|
3
5
|
import { LOBE_CHAT_OBSERVATION_ID, LOBE_CHAT_TRACE_ID } from '@/const/trace';
|
|
4
6
|
import { ErrorResponse, ErrorType } from '@/types/fetch';
|
|
5
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
ChatMessageError,
|
|
9
|
+
MessageToolCall,
|
|
10
|
+
MessageToolCallChunk,
|
|
11
|
+
MessageToolCallSchema,
|
|
12
|
+
} from '@/types/message';
|
|
6
13
|
|
|
7
14
|
export const getMessageError = async (response: Response) => {
|
|
8
15
|
let chatMessageError: ChatMessageError;
|
|
@@ -32,68 +39,137 @@ export type OnFinishHandler = (
|
|
|
32
39
|
text: string,
|
|
33
40
|
context: {
|
|
34
41
|
observationId?: string | null;
|
|
42
|
+
toolCalls?: MessageToolCall[];
|
|
35
43
|
traceId?: string | null;
|
|
36
44
|
type?: SSEFinishType;
|
|
37
45
|
},
|
|
38
46
|
) => Promise<void>;
|
|
39
47
|
|
|
48
|
+
export interface MessageTextChunk {
|
|
49
|
+
text: string;
|
|
50
|
+
type: 'text';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface MessageToolCallsChunk {
|
|
54
|
+
tool_calls: MessageToolCall[];
|
|
55
|
+
type: 'tool_calls';
|
|
56
|
+
}
|
|
57
|
+
|
|
40
58
|
export interface FetchSSEOptions {
|
|
59
|
+
fetcher?: typeof fetch;
|
|
41
60
|
onAbort?: (text: string) => Promise<void>;
|
|
42
61
|
onErrorHandle?: (error: ChatMessageError) => void;
|
|
43
62
|
onFinish?: OnFinishHandler;
|
|
44
|
-
onMessageHandle?: (
|
|
63
|
+
onMessageHandle?: (chunk: MessageTextChunk | MessageToolCallsChunk) => void;
|
|
45
64
|
}
|
|
46
65
|
|
|
66
|
+
export const parseToolCalls = (origin: MessageToolCall[], value: MessageToolCallChunk[]) =>
|
|
67
|
+
produce(origin, (draft) => {
|
|
68
|
+
if (draft.length === 0) {
|
|
69
|
+
draft.push(...value.map((item) => MessageToolCallSchema.parse(item)));
|
|
70
|
+
} else {
|
|
71
|
+
value.forEach(({ index, ...item }) => {
|
|
72
|
+
if (!draft?.[index]) {
|
|
73
|
+
draft?.splice(index, 0, MessageToolCallSchema.parse(item));
|
|
74
|
+
} else {
|
|
75
|
+
if (item.function?.arguments) {
|
|
76
|
+
draft[index].function.arguments += item.function.arguments;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
47
83
|
/**
|
|
48
84
|
* Fetch data using stream method
|
|
49
85
|
*/
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// 如果不 ok 说明有请求错误
|
|
54
|
-
if (!response.ok) {
|
|
55
|
-
const chatMessageError = await getMessageError(response);
|
|
56
|
-
|
|
57
|
-
options.onErrorHandle?.(chatMessageError);
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const returnRes = response.clone();
|
|
62
|
-
|
|
63
|
-
const data = response.body;
|
|
64
|
-
|
|
65
|
-
if (!data) return;
|
|
86
|
+
// eslint-disable-next-line no-undef
|
|
87
|
+
export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptions = {}) => {
|
|
66
88
|
let output = '';
|
|
67
|
-
|
|
68
|
-
|
|
89
|
+
let toolCalls: undefined | MessageToolCall[];
|
|
90
|
+
let triggerOnMessageHandler = false;
|
|
69
91
|
|
|
70
|
-
let done = false;
|
|
71
92
|
let finishedType: SSEFinishType = 'done';
|
|
93
|
+
let response!: Response;
|
|
72
94
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
95
|
+
try {
|
|
96
|
+
await fetchEventSource(url, {
|
|
97
|
+
body: options.body,
|
|
98
|
+
fetch: options?.fetcher,
|
|
99
|
+
headers: options.headers as Record<string, string>,
|
|
100
|
+
method: options.method,
|
|
101
|
+
onerror: (error) => {
|
|
102
|
+
if ((error as TypeError).name === 'AbortError') {
|
|
103
|
+
finishedType = 'abort';
|
|
104
|
+
options?.onAbort?.(output);
|
|
105
|
+
} else {
|
|
106
|
+
finishedType = 'error';
|
|
107
|
+
console.error(error);
|
|
108
|
+
}
|
|
109
|
+
throw new Error('Fetch error');
|
|
110
|
+
// options.onErrorHandle()
|
|
111
|
+
},
|
|
112
|
+
onmessage: (ev) => {
|
|
113
|
+
triggerOnMessageHandler = true;
|
|
114
|
+
let data;
|
|
115
|
+
try {
|
|
116
|
+
data = JSON.parse(ev.data);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
console.warn('parse error, fallback to stream', e);
|
|
119
|
+
options.onMessageHandle?.({ text: data, type: 'text' });
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
switch (ev.event) {
|
|
124
|
+
case 'text': {
|
|
125
|
+
output += data;
|
|
126
|
+
options.onMessageHandle?.({ text: data, type: 'text' });
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
case 'tool_calls': {
|
|
131
|
+
if (!toolCalls) {
|
|
132
|
+
toolCalls = [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
toolCalls = parseToolCalls(toolCalls, data);
|
|
136
|
+
|
|
137
|
+
options.onMessageHandle?.({
|
|
138
|
+
tool_calls: toolCalls,
|
|
139
|
+
type: 'tool_calls',
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
onopen: async (res) => {
|
|
145
|
+
response = res.clone();
|
|
146
|
+
|
|
147
|
+
// 如果不 ok 说明有请求错误
|
|
148
|
+
if (!response.ok) {
|
|
149
|
+
const chatMessageError = await getMessageError(res);
|
|
150
|
+
|
|
151
|
+
options.onErrorHandle?.(chatMessageError);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
signal: options.signal,
|
|
157
|
+
});
|
|
158
|
+
} catch {}
|
|
159
|
+
|
|
160
|
+
// only call onFinish when response is available
|
|
161
|
+
// so like abort, we don't need to call onFinish
|
|
162
|
+
if (response) {
|
|
163
|
+
// if there is no onMessageHandler, we should call onHandleMessage first
|
|
164
|
+
if (!triggerOnMessageHandler) {
|
|
165
|
+
output = await response.clone().text();
|
|
166
|
+
options.onMessageHandle?.({ text: output, type: 'text' });
|
|
91
167
|
}
|
|
92
|
-
}
|
|
93
168
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
169
|
+
const traceId = response.headers.get(LOBE_CHAT_TRACE_ID);
|
|
170
|
+
const observationId = response.headers.get(LOBE_CHAT_OBSERVATION_ID);
|
|
171
|
+
await options?.onFinish?.(output, { observationId, toolCalls, traceId, type: finishedType });
|
|
172
|
+
}
|
|
97
173
|
|
|
98
|
-
return
|
|
174
|
+
return response;
|
|
99
175
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Md5 } from 'ts-md5';
|
|
2
|
+
|
|
3
|
+
import { PLUGIN_SCHEMA_API_MD5_PREFIX, PLUGIN_SCHEMA_SEPARATOR } from '@/const/plugin';
|
|
4
|
+
|
|
5
|
+
export const genToolCallingName = (identifier: string, name: string, type: string = 'default') => {
|
|
6
|
+
const pluginType = type && type !== 'default' ? `${PLUGIN_SCHEMA_SEPARATOR + type}` : '';
|
|
7
|
+
|
|
8
|
+
// 将插件的 identifier 作为前缀,避免重复
|
|
9
|
+
let apiName = identifier + PLUGIN_SCHEMA_SEPARATOR + name + pluginType;
|
|
10
|
+
|
|
11
|
+
// OpenAI GPT function_call name can't be longer than 64 characters
|
|
12
|
+
// So we need to use md5 to shorten the name
|
|
13
|
+
// and then find the correct apiName in response by md5
|
|
14
|
+
if (apiName.length >= 64) {
|
|
15
|
+
const md5Content = PLUGIN_SCHEMA_API_MD5_PREFIX + Md5.hashStr(name).toString();
|
|
16
|
+
|
|
17
|
+
apiName = identifier + PLUGIN_SCHEMA_SEPARATOR + md5Content + pluginType;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return apiName;
|
|
21
|
+
};
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { ReactNode, memo } from 'react';
|
|
2
|
-
|
|
3
|
-
import { isFunctionMessageAtStart } from '@/const/message';
|
|
4
|
-
import { useChatStore } from '@/store/chat';
|
|
5
|
-
import { chatSelectors } from '@/store/chat/selectors';
|
|
6
|
-
import { ChatMessage } from '@/types/message';
|
|
7
|
-
|
|
8
|
-
import Inspector from '../Plugins/Inspector';
|
|
9
|
-
import { DefaultMessage } from './Default';
|
|
10
|
-
|
|
11
|
-
export const AssistantMessage = memo<
|
|
12
|
-
ChatMessage & {
|
|
13
|
-
editableContent: ReactNode;
|
|
14
|
-
}
|
|
15
|
-
>(({ id, plugin, content, ...props }) => {
|
|
16
|
-
const fcProps = useChatStore(chatSelectors.getFunctionMessageProps({ content, id, plugin }));
|
|
17
|
-
|
|
18
|
-
if (!isFunctionMessageAtStart(content))
|
|
19
|
-
return <DefaultMessage content={content} id={id} {...props} />;
|
|
20
|
-
|
|
21
|
-
return (
|
|
22
|
-
<div id={id}>
|
|
23
|
-
<Inspector {...fcProps} />
|
|
24
|
-
</div>
|
|
25
|
-
);
|
|
26
|
-
});
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import isEqual from 'fast-deep-equal';
|
|
2
|
-
import { memo, useState } from 'react';
|
|
3
|
-
import { Flexbox } from 'react-layout-kit';
|
|
4
|
-
|
|
5
|
-
import { useChatStore } from '@/store/chat';
|
|
6
|
-
import { chatSelectors } from '@/store/chat/selectors';
|
|
7
|
-
import { ChatMessage } from '@/types/message';
|
|
8
|
-
|
|
9
|
-
import Inspector from '../Plugins/Inspector';
|
|
10
|
-
import PluginRender from '../Plugins/Render';
|
|
11
|
-
|
|
12
|
-
export const FunctionMessage = memo<ChatMessage>(({ id, content, plugin }) => {
|
|
13
|
-
const fcProps = useChatStore(
|
|
14
|
-
chatSelectors.getFunctionMessageProps({ content, id, plugin }),
|
|
15
|
-
isEqual,
|
|
16
|
-
);
|
|
17
|
-
|
|
18
|
-
const [showRender, setShow] = useState(true);
|
|
19
|
-
|
|
20
|
-
return (
|
|
21
|
-
<Flexbox gap={12} id={id} width={'100%'}>
|
|
22
|
-
<Inspector showRender={showRender} {...fcProps} setShow={setShow} />
|
|
23
|
-
{showRender && (
|
|
24
|
-
<PluginRender
|
|
25
|
-
content={content}
|
|
26
|
-
id={id}
|
|
27
|
-
identifier={plugin?.identifier}
|
|
28
|
-
loading={fcProps.loading}
|
|
29
|
-
payload={fcProps.command}
|
|
30
|
-
type={fcProps.type}
|
|
31
|
-
/>
|
|
32
|
-
)}
|
|
33
|
-
</Flexbox>
|
|
34
|
-
);
|
|
35
|
-
});
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
// copy from https://github.com/vercel/ai/discussions/539#discussioncomment-8193721
|
|
2
|
-
// and I have remove the unnecessary code
|
|
3
|
-
import {
|
|
4
|
-
type AIStreamCallbacksAndOptions,
|
|
5
|
-
createCallbacksTransformer,
|
|
6
|
-
createStreamDataTransformer,
|
|
7
|
-
readableFromAsyncIterable,
|
|
8
|
-
} from 'ai';
|
|
9
|
-
import { ChatResponse } from 'ollama/browser';
|
|
10
|
-
|
|
11
|
-
// A modified version of the streamable function specifically for chat messages
|
|
12
|
-
const chatStreamable = async function* (stream: AsyncIterable<ChatResponse>) {
|
|
13
|
-
for await (const response of stream) {
|
|
14
|
-
if (response.message) {
|
|
15
|
-
yield response.message;
|
|
16
|
-
}
|
|
17
|
-
if (response.done) {
|
|
18
|
-
// Additional final response data can be handled here if necessary
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export const OllamaStream = (
|
|
25
|
-
res: AsyncIterable<ChatResponse>,
|
|
26
|
-
cb?: AIStreamCallbacksAndOptions,
|
|
27
|
-
): ReadableStream<string> => {
|
|
28
|
-
return readableFromAsyncIterable(chatStreamable(res))
|
|
29
|
-
.pipeThrough(createCallbacksTransformer(cb) as any)
|
|
30
|
-
.pipeThrough(createStreamDataTransformer(cb?.experimental_streamData));
|
|
31
|
-
};
|
|
File without changes
|
|
File without changes
|
|
File without changes
|