@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
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#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.2",
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
+ });
@@ -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
- // it('should auto-create topic and switch to it if enabled and threshold is reached', async () => {
230
- // const { result } = renderHook(() => useChatStore());
231
- // const message = 'Test message';
232
- // const autoCreateTopicThreshold = 5;
233
- // const enableAutoCreateTopic = true;
234
- //
235
- // // Mock state with the necessary settings
236
- // useChatStore.setState({
237
- // ...mockState,
238
- // messages: Array(autoCreateTopicThreshold).fill({}), // Fill with dummy messages to reach threshold
239
- // });
240
- //
241
- // // Mock messageService.create to resolve with a message id
242
- // (messageService.create as vi.Mock).mockResolvedValue('new-message-id');
243
- //
244
- // await act(async () => {
245
- // await result.current.sendMessage(message);
246
- // });
247
- //
248
- // expect(result.current.saveToTopic).toHaveBeenCalled();
249
- // expect(result.current.switchTopic).toHaveBeenCalled();
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": {"name": "plugin-identifier____recommendClothes____standalone", "arguments": "{\n "mood": "",\n "gender": "man"\n}"}}
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