@lobehub/chat 0.118.2 → 0.118.3
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,31 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
### [Version 0.118.3](https://github.com/lobehub/lobe-chat/compare/v0.118.2...v0.118.3)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2024-01-01**</sup>
|
|
8
|
+
|
|
9
|
+
#### 🐛 Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **misc**: Fix parse error of tool calls at end.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's fixed
|
|
19
|
+
|
|
20
|
+
- **misc**: Fix parse error of tool calls at end, closes [#893](https://github.com/lobehub/lobe-chat/issues/893) ([f369b6e](https://github.com/lobehub/lobe-chat/commit/f369b6e))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
5
30
|
### [Version 0.118.2](https://github.com/lobehub/lobe-chat/compare/v0.118.1...v0.118.2)
|
|
6
31
|
|
|
7
32
|
<sup>Released on **2023-12-31**</sup>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/chat",
|
|
3
|
-
"version": "0.118.
|
|
3
|
+
"version": "0.118.3",
|
|
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,55 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { testFunctionMessageAtEnd } from './message';
|
|
4
|
+
|
|
5
|
+
describe('testFunctionMessageAtEnd', () => {
|
|
6
|
+
it('should extract tool_calls JSON when present', () => {
|
|
7
|
+
const content = 'Some content before {"tool_calls": [{"tool": "example", "args": ["arg1"]}]}';
|
|
8
|
+
const result = testFunctionMessageAtEnd(content);
|
|
9
|
+
expect(result).toEqual({
|
|
10
|
+
content: '{"tool_calls": [{"tool": "example", "args": ["arg1"]}]}',
|
|
11
|
+
valid: true,
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should extract tool_calls JSON when there are space at end', () => {
|
|
16
|
+
const content =
|
|
17
|
+
'Some content before {"tool_calls": [{"tool": "example", "args": ["arg1"]}]} ';
|
|
18
|
+
const result = testFunctionMessageAtEnd(content);
|
|
19
|
+
expect(result).toEqual({
|
|
20
|
+
content: '{"tool_calls": [{"tool": "example", "args": ["arg1"]}]}',
|
|
21
|
+
valid: true,
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should not extract tool_calls JSON when in middle', () => {
|
|
26
|
+
const content =
|
|
27
|
+
'Some content before {"tool_calls": [{"tool": "example", "args": ["arg1"]}]}, here are some end content';
|
|
28
|
+
const result = testFunctionMessageAtEnd(content);
|
|
29
|
+
expect(result).toEqual({ content: '', valid: false });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should return an empty string and valid false when JSON is not present', () => {
|
|
33
|
+
const content = 'Some content without the JSON structure';
|
|
34
|
+
const result = testFunctionMessageAtEnd(content);
|
|
35
|
+
expect(result).toEqual({ content: '', valid: false });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should return an empty string and valid false when content is empty', () => {
|
|
39
|
+
const content = '';
|
|
40
|
+
const result = testFunctionMessageAtEnd(content);
|
|
41
|
+
|
|
42
|
+
expect(result).toEqual({ content: '', valid: false });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should handle null or undefined content gracefully', () => {
|
|
46
|
+
const nullContent = null;
|
|
47
|
+
const undefinedContent = undefined;
|
|
48
|
+
|
|
49
|
+
const resultWithNull = testFunctionMessageAtEnd(nullContent as any);
|
|
50
|
+
const resultWithUndefined = testFunctionMessageAtEnd(undefinedContent as any);
|
|
51
|
+
|
|
52
|
+
expect(resultWithNull).toEqual({ content: '', valid: false });
|
|
53
|
+
expect(resultWithUndefined).toEqual({ content: '', valid: false });
|
|
54
|
+
});
|
|
55
|
+
});
|
package/src/const/message.ts
CHANGED
|
@@ -8,8 +8,8 @@ export const isFunctionMessageAtStart = (content: string) => {
|
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
export const testFunctionMessageAtEnd = (content: string) => {
|
|
11
|
-
const regExp = /{"tool_calls":.*?}
|
|
12
|
-
const match = content.match(regExp);
|
|
11
|
+
const regExp = /{"tool_calls":.*?]}$/;
|
|
12
|
+
const match = content?.trim().match(regExp);
|
|
13
13
|
|
|
14
|
-
return { content: match ? match[0] : '', valid: match };
|
|
14
|
+
return { content: match ? match[0] : '', valid: !!match };
|
|
15
15
|
};
|
|
@@ -3,10 +3,12 @@ import useSWR, { mutate } from 'swr';
|
|
|
3
3
|
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
4
|
|
|
5
5
|
import { LOADING_FLAT } from '@/const/message';
|
|
6
|
+
import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
|
|
6
7
|
import { chatService } from '@/services/chat';
|
|
7
8
|
import { messageService } from '@/services/message';
|
|
8
9
|
import { topicService } from '@/services/topic';
|
|
9
10
|
import { chatSelectors } from '@/store/chat/selectors';
|
|
11
|
+
import { agentSelectors } from '@/store/session/selectors';
|
|
10
12
|
import { ChatMessage } from '@/types/message';
|
|
11
13
|
|
|
12
14
|
import { useChatStore } from '../../store';
|
|
@@ -41,6 +43,12 @@ vi.mock('@/store/chat/selectors', () => ({
|
|
|
41
43
|
},
|
|
42
44
|
}));
|
|
43
45
|
|
|
46
|
+
vi.mock('@/store/session/selectors', () => ({
|
|
47
|
+
agentSelectors: {
|
|
48
|
+
currentAgentConfig: vi.fn(),
|
|
49
|
+
},
|
|
50
|
+
}));
|
|
51
|
+
|
|
44
52
|
const realCoreProcessMessage = useChatStore.getState().coreProcessMessage;
|
|
45
53
|
const realRefreshMessages = useChatStore.getState().refreshMessages;
|
|
46
54
|
// Mock state
|
|
@@ -57,6 +65,8 @@ const mockState = {
|
|
|
57
65
|
beforeEach(() => {
|
|
58
66
|
vi.clearAllMocks();
|
|
59
67
|
useChatStore.setState(mockState, false);
|
|
68
|
+
|
|
69
|
+
(agentSelectors.currentAgentConfig as Mock).mockImplementation(() => DEFAULT_AGENT_CONFIG);
|
|
60
70
|
});
|
|
61
71
|
afterEach(() => {
|
|
62
72
|
vi.restoreAllMocks();
|
|
@@ -226,29 +236,126 @@ describe('chatMessage actions', () => {
|
|
|
226
236
|
expect(result.current.coreProcessMessage).toHaveBeenCalled();
|
|
227
237
|
});
|
|
228
238
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
239
|
+
describe('auto-create topic', () => {
|
|
240
|
+
it('should not auto-create topic if enableAutoCreateTopic is false', async () => {
|
|
241
|
+
const { result } = renderHook(() => useChatStore());
|
|
242
|
+
const message = 'Test message';
|
|
243
|
+
const autoCreateTopicThreshold = 5;
|
|
244
|
+
const enableAutoCreateTopic = false;
|
|
245
|
+
|
|
246
|
+
// Mock messageService.create to resolve with a message id
|
|
247
|
+
(messageService.create as Mock).mockResolvedValue('new-message-id');
|
|
248
|
+
|
|
249
|
+
// Mock agent config to simulate auto-create topic behavior
|
|
250
|
+
(agentSelectors.currentAgentConfig as Mock).mockImplementation(() => ({
|
|
251
|
+
autoCreateTopicThreshold,
|
|
252
|
+
enableAutoCreateTopic,
|
|
253
|
+
}));
|
|
254
|
+
|
|
255
|
+
// Mock the currentChats selector to return a list that does not reach the threshold
|
|
256
|
+
(chatSelectors.currentChats as Mock).mockReturnValue(
|
|
257
|
+
Array.from({ length: autoCreateTopicThreshold + 1 }, (_, i) => ({ id: `msg-${i}` })),
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// Mock saveToTopic and switchTopic to simulate not being called
|
|
261
|
+
const saveToTopicMock = vi.fn();
|
|
262
|
+
const switchTopicMock = vi.fn();
|
|
263
|
+
|
|
264
|
+
await act(async () => {
|
|
265
|
+
useChatStore.setState({
|
|
266
|
+
...mockState,
|
|
267
|
+
activeTopicId: undefined,
|
|
268
|
+
saveToTopic: saveToTopicMock,
|
|
269
|
+
switchTopic: switchTopicMock,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
await result.current.sendMessage(message);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
expect(saveToTopicMock).not.toHaveBeenCalled();
|
|
276
|
+
expect(switchTopicMock).not.toHaveBeenCalled();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should auto-create topic and switch to it if enabled and threshold is reached', async () => {
|
|
280
|
+
const { result } = renderHook(() => useChatStore());
|
|
281
|
+
const message = 'Test message';
|
|
282
|
+
const autoCreateTopicThreshold = 5;
|
|
283
|
+
const enableAutoCreateTopic = true;
|
|
284
|
+
|
|
285
|
+
// Mock agent config to simulate auto-create topic behavior
|
|
286
|
+
(agentSelectors.currentAgentConfig as Mock).mockImplementation(() => ({
|
|
287
|
+
autoCreateTopicThreshold,
|
|
288
|
+
enableAutoCreateTopic,
|
|
289
|
+
}));
|
|
290
|
+
|
|
291
|
+
// Mock messageService.create to resolve with a message id
|
|
292
|
+
(messageService.create as Mock).mockResolvedValue('new-message-id');
|
|
293
|
+
|
|
294
|
+
// Mock the currentChats selector to return a list that reaches the threshold
|
|
295
|
+
(chatSelectors.currentChats as Mock).mockReturnValue(
|
|
296
|
+
Array.from({ length: autoCreateTopicThreshold }, (_, i) => ({ id: `msg-${i}` })),
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
// Mock saveToTopic to resolve with a topic id and switchTopic to switch to the new topic
|
|
300
|
+
const saveToTopicMock = vi.fn(() => Promise.resolve('new-topic-id'));
|
|
301
|
+
const switchTopicMock = vi.fn();
|
|
302
|
+
|
|
303
|
+
act(() => {
|
|
304
|
+
useChatStore.setState({
|
|
305
|
+
...mockState,
|
|
306
|
+
activeTopicId: undefined,
|
|
307
|
+
saveToTopic: saveToTopicMock,
|
|
308
|
+
switchTopic: switchTopicMock,
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
await act(async () => {
|
|
313
|
+
await result.current.sendMessage(message);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
expect(saveToTopicMock).toHaveBeenCalled();
|
|
317
|
+
expect(switchTopicMock).toHaveBeenCalledWith('new-topic-id');
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('should not auto-create topic if autoCreateTopicThreshold is not reached', async () => {
|
|
321
|
+
const { result } = renderHook(() => useChatStore());
|
|
322
|
+
const message = 'Test message';
|
|
323
|
+
const autoCreateTopicThreshold = 5;
|
|
324
|
+
const enableAutoCreateTopic = true;
|
|
325
|
+
|
|
326
|
+
// Mock messageService.create to resolve with a message id
|
|
327
|
+
(messageService.create as Mock).mockResolvedValue('new-message-id');
|
|
328
|
+
|
|
329
|
+
// Mock agent config to simulate auto-create topic behavior
|
|
330
|
+
(agentSelectors.currentAgentConfig as Mock).mockImplementation(() => ({
|
|
331
|
+
autoCreateTopicThreshold,
|
|
332
|
+
enableAutoCreateTopic,
|
|
333
|
+
}));
|
|
334
|
+
|
|
335
|
+
// Mock the currentChats selector to return a list that does not reach the threshold
|
|
336
|
+
(chatSelectors.currentChats as Mock).mockReturnValue(
|
|
337
|
+
Array.from({ length: autoCreateTopicThreshold - 1 }, (_, i) => ({ id: `msg-${i}` })),
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
// Mock saveToTopic and switchTopic to simulate not being called
|
|
341
|
+
const saveToTopicMock = vi.fn();
|
|
342
|
+
const switchTopicMock = vi.fn();
|
|
343
|
+
|
|
344
|
+
await act(async () => {
|
|
345
|
+
useChatStore.setState({
|
|
346
|
+
...mockState,
|
|
347
|
+
activeTopicId: undefined,
|
|
348
|
+
saveToTopic: saveToTopicMock,
|
|
349
|
+
switchTopic: switchTopicMock,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
await result.current.sendMessage(message);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
expect(saveToTopicMock).not.toHaveBeenCalled();
|
|
356
|
+
expect(switchTopicMock).not.toHaveBeenCalled();
|
|
357
|
+
});
|
|
358
|
+
});
|
|
252
359
|
});
|
|
253
360
|
|
|
254
361
|
describe('resendMessage action', () => {
|
|
@@ -483,4 +590,80 @@ describe('chatMessage actions', () => {
|
|
|
483
590
|
});
|
|
484
591
|
});
|
|
485
592
|
});
|
|
593
|
+
|
|
594
|
+
describe('fetchAIChatMessage', () => {
|
|
595
|
+
it('should fetch AI chat message and return content', async () => {
|
|
596
|
+
const { result } = renderHook(() => useChatStore());
|
|
597
|
+
const messages = [{ id: 'message-id', content: 'Hello', role: 'user' }] as ChatMessage[];
|
|
598
|
+
const assistantMessageId = 'assistant-message-id';
|
|
599
|
+
const aiResponse = 'Hello, human!';
|
|
600
|
+
|
|
601
|
+
// Mock chatService.createAssistantMessage to resolve with AI response
|
|
602
|
+
(chatService.createAssistantMessage as Mock).mockResolvedValue(new Response(aiResponse));
|
|
603
|
+
|
|
604
|
+
await act(async () => {
|
|
605
|
+
const response = await result.current.fetchAIChatMessage(messages, assistantMessageId);
|
|
606
|
+
expect(response.content).toEqual(aiResponse);
|
|
607
|
+
expect(response.isFunctionCall).toEqual(false);
|
|
608
|
+
expect(response.functionCallAtEnd).toEqual(false);
|
|
609
|
+
expect(response.functionCallContent).toEqual('');
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
it('should handle function call message at start of AI response', async () => {
|
|
614
|
+
const { result } = renderHook(() => useChatStore());
|
|
615
|
+
const messages = [{ id: 'message-id', content: 'Hello', role: 'user' }] as ChatMessage[];
|
|
616
|
+
const assistantMessageId = 'assistant-message-id';
|
|
617
|
+
const aiResponse =
|
|
618
|
+
'{"tool_calls":[{"id":"call_sbca","type":"function","function":{"name":"pluginName____apiName","arguments":{"key":"value"}}}]}';
|
|
619
|
+
|
|
620
|
+
// Mock chatService.createAssistantMessage to resolve with AI response containing function call
|
|
621
|
+
(chatService.createAssistantMessage as Mock).mockResolvedValue(new Response(aiResponse));
|
|
622
|
+
|
|
623
|
+
await act(async () => {
|
|
624
|
+
const response = await result.current.fetchAIChatMessage(messages, assistantMessageId);
|
|
625
|
+
expect(response.content).toEqual(aiResponse);
|
|
626
|
+
expect(response.isFunctionCall).toEqual(true);
|
|
627
|
+
expect(response.functionCallAtEnd).toEqual(false);
|
|
628
|
+
expect(response.functionCallContent).toEqual('');
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
it('should handle function message at end of AI response', async () => {
|
|
633
|
+
const { result } = renderHook(() => useChatStore());
|
|
634
|
+
const messages = [{ id: 'message-id', content: 'Hello', role: 'user' }] as ChatMessage[];
|
|
635
|
+
const assistantMessageId = 'assistant-message-id';
|
|
636
|
+
const aiResponse =
|
|
637
|
+
'Hello, human! {"tool_calls":[{"id":"call_sbca","type":"function","function":{"name":"pluginName____apiName","arguments":{"key":"value"}}}]}';
|
|
638
|
+
|
|
639
|
+
// Mock chatService.createAssistantMessage to resolve with AI response containing function call at end
|
|
640
|
+
(chatService.createAssistantMessage as Mock).mockResolvedValue(new Response(aiResponse));
|
|
641
|
+
|
|
642
|
+
await act(async () => {
|
|
643
|
+
const response = await result.current.fetchAIChatMessage(messages, assistantMessageId);
|
|
644
|
+
expect(response.content).toEqual(aiResponse);
|
|
645
|
+
expect(response.isFunctionCall).toEqual(true);
|
|
646
|
+
expect(response.functionCallAtEnd).toEqual(true);
|
|
647
|
+
expect(response.functionCallContent).toEqual(
|
|
648
|
+
'{"tool_calls":[{"id":"call_sbca","type":"function","function":{"name":"pluginName____apiName","arguments":{"key":"value"}}}]}',
|
|
649
|
+
);
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it('should handle errors during AI response fetching', async () => {
|
|
654
|
+
const { result } = renderHook(() => useChatStore());
|
|
655
|
+
const messages = [{ id: 'message-id', content: 'Hello', role: 'user' }] as ChatMessage[];
|
|
656
|
+
const assistantMessageId = 'assistant-message-id';
|
|
657
|
+
|
|
658
|
+
// Mock chatService.createAssistantMessage to reject with an error
|
|
659
|
+
const errorMessage = 'Error fetching AI response';
|
|
660
|
+
(chatService.createAssistantMessage as Mock).mockRejectedValue(new Error(errorMessage));
|
|
661
|
+
|
|
662
|
+
await act(async () => {
|
|
663
|
+
await expect(
|
|
664
|
+
result.current.fetchAIChatMessage(messages, assistantMessageId),
|
|
665
|
+
).rejects.toThrow(errorMessage);
|
|
666
|
+
});
|
|
667
|
+
});
|
|
668
|
+
});
|
|
486
669
|
});
|
|
@@ -382,7 +382,7 @@ export const chatMessage: StateCreator<
|
|
|
382
382
|
toggleChatLoading(false, undefined, n('generateMessage(end)') as string);
|
|
383
383
|
|
|
384
384
|
// also exist message like this:
|
|
385
|
-
// 请稍等,我帮您查询一下。{"tool_calls":
|
|
385
|
+
// 请稍等,我帮您查询一下。{"tool_calls":[{"id":"call_sbca","type":"function","function":{"name":"pluginName____apiName","arguments":{"key":"value"}}}]}
|
|
386
386
|
if (!isFunctionCall) {
|
|
387
387
|
const { content, valid } = testFunctionMessageAtEnd(output);
|
|
388
388
|
|