@lobehub/chat 0.118.2 → 0.118.4

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,56 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 0.118.4](https://github.com/lobehub/lobe-chat/compare/v0.118.3...v0.118.4)
6
+
7
+ <sup>Released on **2024-01-02**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Update dalle identifier to fix unstable dalle function call.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Update dalle identifier to fix unstable dalle function call, closes [#896](https://github.com/lobehub/lobe-chat/issues/896) ([9d9ac32](https://github.com/lobehub/lobe-chat/commit/9d9ac32))
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
+
30
+ ### [Version 0.118.3](https://github.com/lobehub/lobe-chat/compare/v0.118.2...v0.118.3)
31
+
32
+ <sup>Released on **2024-01-01**</sup>
33
+
34
+ #### 🐛 Bug Fixes
35
+
36
+ - **misc**: Fix parse error of tool calls at end.
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### What's fixed
44
+
45
+ - **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))
46
+
47
+ </details>
48
+
49
+ <div align="right">
50
+
51
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
52
+
53
+ </div>
54
+
5
55
  ### [Version 0.118.2](https://github.com/lobehub/lobe-chat/compare/v0.118.1...v0.118.2)
6
56
 
7
57
  <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.4",
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
  };
@@ -0,0 +1,53 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+
4
+ import { DalleManifest } from '@/tools/dalle';
5
+ import { BuiltinToolsRenders } from '@/tools/renders';
6
+
7
+ import BuiltinType from './index';
8
+
9
+ // Mock Render component and useParseContent hook
10
+ vi.mock('@/tools/renders', () => ({
11
+ BuiltinToolsRenders: {
12
+ dalle3: vi.fn(() => <div>Test Renderer</div>),
13
+ [DalleManifest.identifier]: vi.fn(() => <div>{DalleManifest.identifier}</div>),
14
+ },
15
+ }));
16
+
17
+ // Mock Loading component
18
+ vi.mock('../Loading', () => ({
19
+ default: vi.fn(() => <div>Loading...</div>),
20
+ }));
21
+
22
+ describe('BuiltinType', () => {
23
+ it('should render loading state if not JSON and loading is true', () => {
24
+ render(<BuiltinType content="..." id="123" loading={true} />);
25
+ expect(screen.getByText('Loading...')).toBeInTheDocument();
26
+ });
27
+
28
+ it('should not render anything if not JSON and loading is false', () => {
29
+ const { container } = render(<BuiltinType content="..." id="123" loading={false} />);
30
+ expect(container).toBeEmptyDOMElement();
31
+ });
32
+
33
+ it('should not render anything if identifier is unknown', () => {
34
+ const { container } = render(<BuiltinType content="{}" id="123" identifier="unknown" />);
35
+ expect(container).toBeEmptyDOMElement();
36
+ });
37
+
38
+ describe('DALL·E', () => {
39
+ it('should render the correct renderer if identifier is dalle3', () => {
40
+ render(<BuiltinType content='{"some":"data"}' id="123" identifier="dalle3" />);
41
+ expect(BuiltinToolsRenders.dalle3).toHaveBeenCalled();
42
+ expect(screen.getByText('Test Renderer')).toBeInTheDocument();
43
+ });
44
+
45
+ it('should render the correct renderer if is DALL·E ', () => {
46
+ render(
47
+ <BuiltinType content='{"some":"data"}' id="123" identifier={DalleManifest.identifier} />,
48
+ );
49
+ expect(BuiltinToolsRenders.dalle3).toHaveBeenCalled();
50
+ expect(screen.getByText(DalleManifest.identifier)).toBeInTheDocument();
51
+ });
52
+ });
53
+ });
@@ -0,0 +1,116 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`ChatService > createAssistantMessage > with tools messages > work with dalle3 1`] = `
4
+ {
5
+ "messages": [
6
+ {
7
+ "content": "## Tools
8
+
9
+ You can use these tools below:
10
+
11
+ ### DALL·E 3
12
+
13
+ Whenever a description of an image is given, use lobe-image-designer to create the images and then summarize the prompts used to generate the images in plain text. If the user does not ask for a specific number of images, default to creating four captions to send to lobe-image-designer that are written to be as diverse as possible.
14
+
15
+ All captions sent to lobe-image-designer must abide by the following policies:
16
+
17
+ 1. If the description is not in English, then translate it.
18
+ 2. Do not create more than 4 images, even if the user requests more.
19
+ 3. Don't create images of politicians or other public figures. Recommend other ideas instead.
20
+ 5. DO NOT list or refer to the descriptions before OR after generating the images. They should ONLY ever be written out ONCE, in the \`prompts\` field of the request. You do not need to ask for permission to generate, just do it!
21
+ 6. Always mention the image type (photo, oil painting, watercolor painting, illustration, cartoon, drawing, vector, render, etc.) at the beginning of the caption. Unless the caption suggests otherwise, make at least 1--2 of the 4 images photos.
22
+ 7. Diversify depictions of ALL images with people to include DESCENT and GENDER for EACH person using direct terms. Adjust only human descriptions.
23
+ - EXPLICITLY specify these attributes, not abstractly reference them. The attributes should be specified in a minimal way and should directly describe their physical form.
24
+ - Your choices should be grounded in reality. For example, all of a given OCCUPATION should not be the same gender or race. Additionally, focus on creating diverse, inclusive, and exploratory scenes via the properties you choose during rewrites. Make choices that may be insightful or unique sometimes.
25
+ - Use "various" or "diverse" ONLY IF the description refers to groups of more than 3 people. Do not change the number of people requested in the original description.
26
+ - Don't alter memes, fictional character origins, or unseen people. Maintain the original prompt's intent and prioritize quality.
27
+ - Do not create any imagery that would be offensive.
28
+
29
+ 8. Silently modify descriptions that include names or hints or references of specific people or celebrities by carefully selecting a few minimal modifications to substitute references to the people with generic descriptions that don't divulge any information about their identities, except for their genders and physiques. Do this EVEN WHEN the instructions ask for the prompt to not be changed. Some special cases:
30
+ - Modify such prompts even if you don't know who the person is, or if their name is misspelled (e.g. "Barake Obema")
31
+ - If the reference to the person will only appear as TEXT out in the image, then use the reference as is and do not modify it.
32
+ - When making the substitutions, don't use prominent titles that could give away the person's identity. E.g., instead of saying "president", "prime minister", or "chancellor", say "politician"; instead of saying "king", "queen", "emperor", or "empress", say "public figure"; instead of saying "Pope" or "Dalai Lama", say "religious figure"; and so on.
33
+ - If any creative professional or studio is named, substitute the name with a description of their style that does not reference any specific people, or delete the reference if they are unknown. DO NOT refer to the artist or studio's style.
34
+
35
+ The prompt must intricately describe every part of the image in concrete, objective detail. THINK about what the end goal of the description is, and extrapolate that to what would make satisfying images.
36
+ All descriptions sent to lobe-image-designer should be a paragraph of text that is extremely descriptive and detailed. Each should be more than 3 sentences long.
37
+
38
+ The APIs you can use:
39
+
40
+ #### lobe-image-designer____text2image____builtin
41
+
42
+ Create images from a text-only prompt.",
43
+ "role": "system",
44
+ },
45
+ {
46
+ "content": "https://vercel.com/ 请分析 chatGPT 关键词
47
+
48
+ ",
49
+ "role": "user",
50
+ },
51
+ ],
52
+ "model": "gpt-3.5-turbo-1106",
53
+ "tools": [
54
+ {
55
+ "function": {
56
+ "description": "Create images from a text-only prompt.",
57
+ "name": "lobe-image-designer____text2image____builtin",
58
+ "parameters": {
59
+ "properties": {
60
+ "prompts": {
61
+ "description": "The user's original image description, potentially modified to abide by the lobe-image-designer policies. If the user does not suggest a number of captions to create, create four of them. If creating multiple captions, make them as diverse as possible. If the user requested modifications to previous images, the captions should not simply be longer, but rather it should be refactored to integrate the suggestions into each of the captions. Generate no more than 4 images, even if the user requests more.",
62
+ "items": {
63
+ "type": "string",
64
+ },
65
+ "maxItems": 4,
66
+ "minItems": 1,
67
+ "type": "array",
68
+ },
69
+ "quality": {
70
+ "default": "standard",
71
+ "description": "The quality of the image that will be generated. hd creates images with finer details and greater consistency across the image.",
72
+ "enum": [
73
+ "standard",
74
+ "hd",
75
+ ],
76
+ "type": "string",
77
+ },
78
+ "seeds": {
79
+ "description": "A list of seeds to use for each prompt. If the user asks to modify a previous image, populate this field with the seed used to generate that image from the image lobe-image-designer metadata.",
80
+ "items": {
81
+ "type": "integer",
82
+ },
83
+ "type": "array",
84
+ },
85
+ "size": {
86
+ "default": "1024x1024",
87
+ "description": "The resolution of the requested image, which can be wide, square, or tall. Use 1024x1024 (square) as the default unless the prompt suggests a wide image, 1792x1024, or a full-body portrait, in which case 1024x1792 (tall) should be used instead. Always include this parameter in the request.",
88
+ "enum": [
89
+ "1792x1024",
90
+ "1024x1024",
91
+ "1024x1792",
92
+ ],
93
+ "type": "string",
94
+ },
95
+ "style": {
96
+ "default": "vivid",
97
+ "description": "The style of the generated images. Must be one of vivid or natural. Vivid causes the model to lean towards generating hyper-real and dramatic images. Natural causes the model to produce more natural, less hyper-real looking images.",
98
+ "enum": [
99
+ "vivid",
100
+ "natural",
101
+ ],
102
+ "type": "string",
103
+ },
104
+ },
105
+ "required": [
106
+ "prompts",
107
+ ],
108
+ "type": "object",
109
+ },
110
+ },
111
+ "type": "function",
112
+ },
113
+ ],
114
+ "top_p": 1,
115
+ }
116
+ `;
@@ -5,6 +5,7 @@ import { describe, expect, it, vi } from 'vitest';
5
5
  import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
6
6
  import { useFileStore } from '@/store/file';
7
7
  import { useToolStore } from '@/store/tool';
8
+ import { DalleManifest } from '@/tools/dalle';
8
9
  import { ChatMessage } from '@/types/message';
9
10
  import { OpenAIChatStreamPayload } from '@/types/openai/chat';
10
11
  import { LobeTool } from '@/types/tool';
@@ -514,104 +515,16 @@ Get data from users`,
514
515
  messages,
515
516
  model: 'gpt-3.5-turbo-1106',
516
517
  top_p: 1,
517
- plugins: ['dalle3'],
518
+ plugins: [DalleManifest.identifier],
518
519
  });
519
520
 
520
- expect(getChatCompletionSpy).toHaveBeenCalledWith(
521
- {
522
- model: 'gpt-3.5-turbo-1106',
523
- top_p: 1,
524
- tools: [
525
- {
526
- type: 'function',
527
- function: {
528
- description: 'Create images from a text-only prompt.',
529
- name: 'dalle3____text2image____builtin',
530
- parameters: {
531
- properties: {
532
- prompts: {
533
- description:
534
- "The user's original image description, potentially modified to abide by the dalle policies. If the user does not suggest a number of captions to create, create four of them. If creating multiple captions, make them as diverse as possible. If the user requested modifications to previous images, the captions should not simply be longer, but rather it should be refactored to integrate the suggestions into each of the captions. Generate no more than 4 images, even if the user requests more.",
535
- items: { type: 'string' },
536
- maxItems: 4,
537
- minItems: 1,
538
- type: 'array',
539
- },
540
- quality: {
541
- default: 'standard',
542
- description:
543
- 'The quality of the image that will be generated. hd creates images with finer details and greater consistency across the image.',
544
- enum: ['standard', 'hd'],
545
- type: 'string',
546
- },
547
- seeds: {
548
- description:
549
- 'A list of seeds to use for each prompt. If the user asks to modify a previous image, populate this field with the seed used to generate that image from the image dalle metadata.',
550
- items: { type: 'integer' },
551
- type: 'array',
552
- },
553
- size: {
554
- default: '1024x1024',
555
- description:
556
- 'The resolution of the requested image, which can be wide, square, or tall. Use 1024x1024 (square) as the default unless the prompt suggests a wide image, 1792x1024, or a full-body portrait, in which case 1024x1792 (tall) should be used instead. Always include this parameter in the request.',
557
- enum: ['1792x1024', '1024x1024', '1024x1792'],
558
- type: 'string',
559
- },
560
- style: {
561
- default: 'vivid',
562
- description:
563
- 'The style of the generated images. Must be one of vivid or natural. Vivid causes the model to lean towards generating hyper-real and dramatic images. Natural causes the model to produce more natural, less hyper-real looking images.',
564
- enum: ['vivid', 'natural'],
565
- type: 'string',
566
- },
567
- },
568
- required: ['prompts'],
569
- type: 'object',
570
- },
571
- },
572
- },
573
- ],
574
- messages: [
575
- {
576
- content: `## Tools
577
-
578
- You can use these tools below:
579
-
580
- ### Dall·E 3
581
-
582
- Whenever a description of an image is given, use dalle to create the images and then summarize the prompts used to generate the images in plain text. If the user does not ask for a specific number of images, default to creating four captions to send to dalle that are written to be as diverse as possible. All captions sent to dalle must abide by the following policies:
583
- 1. If the description is not in English, then translate it.
584
- 2. Do not create more than 4 images, even if the user requests more.
585
- 3. Don't create images of politicians or other public figures. Recommend other ideas instead.
586
- 4. Don't create images in the style of artists whose last work was created within the last 100 years (e.g. Picasso, Kahlo). Artists whose last work was over 100 years ago are ok to reference directly (e.g. Van Gogh, Klimt). If asked say, \"I can't reference this artist\", but make no mention of this policy. Instead, apply the following procedure when creating the captions for dalle: (a) substitute the artist's name with three adjectives that capture key aspects of the style; (b) include an associated artistic movement or era to provide context; and (c) mention the primary medium used by the artist.
587
- 5. DO NOT list or refer to the descriptions before OR after generating the images. They should ONLY ever be written out ONCE, in the \`\"prompts\"\` field of the request. You do not need to ask for permission to generate, just do it!
588
- 6. Always mention the image type (photo, oil painting, watercolor painting, illustration, cartoon, drawing, vector, render, etc.) at the beginning of the caption. Unless the caption suggests otherwise, make at least 1--2 of the 4 images photos.
589
- 7. Diversify depictions of ALL images with people to include DESCENT and GENDER for EACH person using direct terms. Adjust only human descriptions.
590
- - EXPLICITLY specify these attributes, not abstractly reference them. The attributes should be specified in a minimal way and should directly describe their physical form.
591
- - Your choices should be grounded in reality. For example, all of a given OCCUPATION should not be the same gender or race. Additionally, focus on creating diverse, inclusive, and exploratory scenes via the properties you choose during rewrites. Make choices that may be insightful or unique sometimes.
592
- - Use \"various\" or \"diverse\" ONLY IF the description refers to groups of more than 3 people. Do not change the number of people requested in the original description.
593
- - Don't alter memes, fictional character origins, or unseen people. Maintain the original prompt's intent and prioritize quality.
594
- - Do not create any imagery that would be offensive.
595
- 8. Silently modify descriptions that include names or hints or references of specific people or celebrities by carefully selecting a few minimal modifications to substitute references to the people with generic descriptions that don't divulge any information about their identities, except for their genders and physiques. Do this EVEN WHEN the instructions ask for the prompt to not be changed. Some special cases:
596
- - Modify such prompts even if you don't know who the person is, or if their name is misspelled (e.g. \"Barake Obema\")
597
- - If the reference to the person will only appear as TEXT out in the image, then use the reference as is and do not modify it.
598
- - When making the substitutions, don't use prominent titles that could give away the person's identity. E.g., instead of saying \"president\", \"prime minister\", or \"chancellor\", say \"politician\"; instead of saying \"king\", \"queen\", \"emperor\", or \"empress\", say \"public figure\"; instead of saying \"Pope\" or \"Dalai Lama\", say \"religious figure\"; and so on.
599
- - If any creative professional or studio is named, substitute the name with a description of their style that does not reference any specific people, or delete the reference if they are unknown. DO NOT refer to the artist or studio's style.
600
- The prompt must intricately describe every part of the image in concrete, objective detail. THINK about what the end goal of the description is, and extrapolate that to what would make satisfying images.
601
- All descriptions sent to dalle should be a paragraph of text that is extremely descriptive and detailed. Each should be more than 3 sentences long.
602
-
603
- The APIs you can use:
604
-
605
- #### dalle3____text2image____builtin
521
+ // Assert that getChatCompletionSpy was called with the expected arguments
522
+ expect(getChatCompletionSpy).toHaveBeenCalled();
606
523
 
607
- Create images from a text-only prompt.`,
608
- role: 'system',
609
- },
610
- { content: 'https://vercel.com/ 请分析 chatGPT 关键词\n\n', role: 'user' },
611
- ],
612
- },
613
- undefined,
614
- );
524
+ const calls = getChatCompletionSpy.mock.lastCall;
525
+ // Take a snapshot of the first call's first argument
526
+ expect(calls![0]).toMatchSnapshot();
527
+ expect(calls![1]).toBeUndefined();
615
528
  });
616
529
  });
617
530
  });
@@ -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
 
@@ -1,5 +1,7 @@
1
1
  import { BuiltinToolManifest } from '@/types/tool';
2
2
 
3
+ // import {SiOpenai} from "@icons-pack/react-simple-icons";
4
+
3
5
  export const DalleManifest: BuiltinToolManifest = {
4
6
  api: [
5
7
  {
@@ -9,7 +11,7 @@ export const DalleManifest: BuiltinToolManifest = {
9
11
  properties: {
10
12
  prompts: {
11
13
  description:
12
- "The user's original image description, potentially modified to abide by the dalle policies. If the user does not suggest a number of captions to create, create four of them. If creating multiple captions, make them as diverse as possible. If the user requested modifications to previous images, the captions should not simply be longer, but rather it should be refactored to integrate the suggestions into each of the captions. Generate no more than 4 images, even if the user requests more.",
14
+ "The user's original image description, potentially modified to abide by the lobe-image-designer policies. If the user does not suggest a number of captions to create, create four of them. If creating multiple captions, make them as diverse as possible. If the user requested modifications to previous images, the captions should not simply be longer, but rather it should be refactored to integrate the suggestions into each of the captions. Generate no more than 4 images, even if the user requests more.",
13
15
  items: {
14
16
  type: 'string',
15
17
  },
@@ -26,7 +28,7 @@ export const DalleManifest: BuiltinToolManifest = {
26
28
  },
27
29
  seeds: {
28
30
  description:
29
- 'A list of seeds to use for each prompt. If the user asks to modify a previous image, populate this field with the seed used to generate that image from the image dalle metadata.',
31
+ 'A list of seeds to use for each prompt. If the user asks to modify a previous image, populate this field with the seed used to generate that image from the image lobe-image-designer metadata.',
30
32
  items: {
31
33
  type: 'integer',
32
34
  },
@@ -52,30 +54,39 @@ export const DalleManifest: BuiltinToolManifest = {
52
54
  },
53
55
  },
54
56
  ],
55
- identifier: 'dalle3',
57
+ // due to system prompt is for training Dalle3 as a built-in tool by OpenAI,
58
+ // there are occasional instances where the function call contains the name "dalle," leading to subsequent failures.
59
+ // so we need a different unique identifier to avoid failure.refs:
60
+ // https://github.com/lobehub/lobe-chat/issues/783
61
+ // https://github.com/lobehub/lobe-chat/issues/870
62
+ identifier: 'lobe-image-designer',
56
63
  meta: {
57
- avatar: '🖌',
58
- title: 'Dall·E 3',
64
+ avatar: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5NiIgaGVpZ2h0PSI5NiIgZmlsbD0ibm9uZSIgc3R5bGU9ImNvbG9yOiNmZmY7ZmlsbDojMDAwIj4KICAgIDxwYXRoIGQ9Ik0wIDBoOTZ2OTZIMHoiLz4KICAgIDxwYXRoIGZpbGw9ImN1cnJlbnRDb2xvciIKICAgICAgICAgIGQ9Ik03OC44MyA0MS40NjJBMTcuOTQgMTcuOTQgMCAwIDAgNzcuMjkgMjYuNzNhMTguMTQgMTguMTQgMCAwIDAtMTkuNTM4LTguNzA0IDE3LjkzNyAxNy45MzcgMCAwIDAtMTMuNTI5LTYuMDMxIDE4LjE0MyAxOC4xNDMgMCAwIDAtMTcuMzA1IDEyLjU2IDE3Ljk0NiAxNy45NDYgMCAwIDAtMTEuOTk0IDguNzAxIDE4LjE0MyAxOC4xNDMgMCAwIDAgMi4yMzMgMjEuMjcxIDE3LjkzOCAxNy45MzggMCAwIDAgMS41NCAxNC43MzIgMTguMTQxIDE4LjE0MSAwIDAgMCAxOS41MzggOC43MDQgMTcuOTMgMTcuOTMgMCAwIDAgMTMuNTI4IDYuMDMxQTE4LjE0MiAxOC4xNDIgMCAwIDAgNjkuMDggNzEuNDI3YTE3LjkzOCAxNy45MzggMCAwIDAgMTEuOTk0LTguNzAxIDE4LjE0MSAxOC4xNDEgMCAwIDAtMi4yNDMtMjEuMjY0Wk01MS45MSA3OS4yODdjLTMuNTgzIDAtNi4zNTYtMS4xLTguNzgtMy4xMjIuMTEtLjA2LjMwMi0uMTY1LjQyNy0uMjQybDE0LjMzNi04LjI4YTIuMzI3IDIuMzI3IDAgMCAwIDEuMTc4LTIuMDRWNDUuMzkybDYuMDYgMy40OTlhLjIxNy4yMTcgMCAwIDEgLjExOC4xNjZWNjUuNzljLS4wMDEgNy41OTMtNi4zMjIgMTMuNDk3LTEzLjM0IDEzLjQ5N1pNMjIuNzc4IDY2LjkwNmExMy40NDcgMTMuNDQ3IDAgMCAxLTEuNjEtOS4wNDJjLjEwNy4wNjQuMjkzLjE3OC40MjYuMjU0TDM1LjkzIDY2LjRhMi4zMyAyLjMzIDAgMCAwIDIuMzU1IDBsMTcuNTAzLTEwLjEwNnY2Ljk5OGEuMjE3LjIxNyAwIDAgMS0uMDg3LjE4Nkw0MS4yMSA3MS44NDRhMTMuNTA3IDEzLjUwNyAwIDAgMS0xOC40MzEtNC45MzhabS0zLjc3Ny0zMS4yOTdhMTMuNDQ5IDEzLjQ0OSAwIDAgMSA3LjAyMy01LjkxNlY0Ni43NWEyLjMyNCAyLjMyNCAwIDAgMCAxLjE3NyAyLjAzOGwxNy41MDEgMTAuMTA1LTYuMDYgMy40OTlhLjIxOC4yMTggMCAwIDEtLjIwNC4wMThsLTE0LjQ5My04LjM3NEExMy41MDcgMTMuNTA3IDAgMCAxIDE5IDM1LjYwOVptNDkuNzkgMTEuNTg3TDUxLjI4OCAzNy4wOWw2LjA2LTMuNDk4YS4yMi4yMiAwIDAgMSAuMjA0LS4wMThsMTQuNDk0IDguMzdhMTMuNDk1IDEzLjQ5NSAwIDAgMS0yLjA4OCAyNC4zNDZWNDkuMjMzYTIuMzI4IDIuMzI4IDAgMCAwLTEuMTY3LTIuMDM3Wm02LjAzLTkuMDg0YTIwLjA2NCAyMC4wNjQgMCAwIDAtLjQyNS0uMjU0TDYwLjA2IDI5LjU3N2EyLjMzNyAyLjMzNyAwIDAgMC0yLjM1NSAwTDQwLjIwMyAzOS42ODN2LTYuOTk4YS4yMTguMjE4IDAgMCAxIC4wODYtLjE4NmwxNC40OTItOC4zNmExMy40OTUgMTMuNDk1IDAgMCAxIDIwLjA0IDEzLjk3M1pNMzYuOTA5IDUwLjU5bC02LjA2LTMuNDk5YS4yMS4yMSAwIDAgMS0uMTE4LS4xNjZWMzAuMTg3YTEzLjQ5NCAxMy40OTQgMCAwIDEgMjIuMTI4LTEwLjM2MWMtLjExLjA2LS4zLjE2NC0uNDI2LjI0bC0xNC4zMzYgOC4yODJhMi4zMzIgMi4zMzIgMCAwIDAtMS4xNzggMi4wMzhsLS4wMSAyMC4yMDRabTMuMjkyLTcuMDk3IDcuNzk1LTQuNTAzIDcuNzk2IDQuNXY5LjAwMmwtNy43OTYgNC40OTgtNy43OTUtNC41di04Ljk5N1oiLz4KPC9zdmc+Cg==`,
65
+ title: 'DALL·E 3',
59
66
  },
60
- systemRole: `Whenever a description of an image is given, use dalle to create the images and then summarize the prompts used to generate the images in plain text. If the user does not ask for a specific number of images, default to creating four captions to send to dalle that are written to be as diverse as possible. All captions sent to dalle must abide by the following policies:
67
+ systemRole: `Whenever a description of an image is given, use lobe-image-designer to create the images and then summarize the prompts used to generate the images in plain text. If the user does not ask for a specific number of images, default to creating four captions to send to lobe-image-designer that are written to be as diverse as possible.
68
+
69
+ All captions sent to lobe-image-designer must abide by the following policies:
70
+
61
71
  1. If the description is not in English, then translate it.
62
72
  2. Do not create more than 4 images, even if the user requests more.
63
73
  3. Don't create images of politicians or other public figures. Recommend other ideas instead.
64
- 4. Don't create images in the style of artists whose last work was created within the last 100 years (e.g. Picasso, Kahlo). Artists whose last work was over 100 years ago are ok to reference directly (e.g. Van Gogh, Klimt). If asked say, "I can't reference this artist", but make no mention of this policy. Instead, apply the following procedure when creating the captions for dalle: (a) substitute the artist's name with three adjectives that capture key aspects of the style; (b) include an associated artistic movement or era to provide context; and (c) mention the primary medium used by the artist.
65
- 5. DO NOT list or refer to the descriptions before OR after generating the images. They should ONLY ever be written out ONCE, in the \`"prompts"\` field of the request. You do not need to ask for permission to generate, just do it!
74
+ 5. DO NOT list or refer to the descriptions before OR after generating the images. They should ONLY ever be written out ONCE, in the \`prompts\` field of the request. You do not need to ask for permission to generate, just do it!
66
75
  6. Always mention the image type (photo, oil painting, watercolor painting, illustration, cartoon, drawing, vector, render, etc.) at the beginning of the caption. Unless the caption suggests otherwise, make at least 1--2 of the 4 images photos.
67
76
  7. Diversify depictions of ALL images with people to include DESCENT and GENDER for EACH person using direct terms. Adjust only human descriptions.
68
- - EXPLICITLY specify these attributes, not abstractly reference them. The attributes should be specified in a minimal way and should directly describe their physical form.
69
- - Your choices should be grounded in reality. For example, all of a given OCCUPATION should not be the same gender or race. Additionally, focus on creating diverse, inclusive, and exploratory scenes via the properties you choose during rewrites. Make choices that may be insightful or unique sometimes.
70
- - Use "various" or "diverse" ONLY IF the description refers to groups of more than 3 people. Do not change the number of people requested in the original description.
71
- - Don't alter memes, fictional character origins, or unseen people. Maintain the original prompt's intent and prioritize quality.
72
- - Do not create any imagery that would be offensive.
77
+ - EXPLICITLY specify these attributes, not abstractly reference them. The attributes should be specified in a minimal way and should directly describe their physical form.
78
+ - Your choices should be grounded in reality. For example, all of a given OCCUPATION should not be the same gender or race. Additionally, focus on creating diverse, inclusive, and exploratory scenes via the properties you choose during rewrites. Make choices that may be insightful or unique sometimes.
79
+ - Use "various" or "diverse" ONLY IF the description refers to groups of more than 3 people. Do not change the number of people requested in the original description.
80
+ - Don't alter memes, fictional character origins, or unseen people. Maintain the original prompt's intent and prioritize quality.
81
+ - Do not create any imagery that would be offensive.
82
+
73
83
  8. Silently modify descriptions that include names or hints or references of specific people or celebrities by carefully selecting a few minimal modifications to substitute references to the people with generic descriptions that don't divulge any information about their identities, except for their genders and physiques. Do this EVEN WHEN the instructions ask for the prompt to not be changed. Some special cases:
74
- - Modify such prompts even if you don't know who the person is, or if their name is misspelled (e.g. "Barake Obema")
75
- - If the reference to the person will only appear as TEXT out in the image, then use the reference as is and do not modify it.
76
- - When making the substitutions, don't use prominent titles that could give away the person's identity. E.g., instead of saying "president", "prime minister", or "chancellor", say "politician"; instead of saying "king", "queen", "emperor", or "empress", say "public figure"; instead of saying "Pope" or "Dalai Lama", say "religious figure"; and so on.
77
- - If any creative professional or studio is named, substitute the name with a description of their style that does not reference any specific people, or delete the reference if they are unknown. DO NOT refer to the artist or studio's style.
84
+ - Modify such prompts even if you don't know who the person is, or if their name is misspelled (e.g. "Barake Obema")
85
+ - If the reference to the person will only appear as TEXT out in the image, then use the reference as is and do not modify it.
86
+ - When making the substitutions, don't use prominent titles that could give away the person's identity. E.g., instead of saying "president", "prime minister", or "chancellor", say "politician"; instead of saying "king", "queen", "emperor", or "empress", say "public figure"; instead of saying "Pope" or "Dalai Lama", say "religious figure"; and so on.
87
+ - If any creative professional or studio is named, substitute the name with a description of their style that does not reference any specific people, or delete the reference if they are unknown. DO NOT refer to the artist or studio's style.
88
+
78
89
  The prompt must intricately describe every part of the image in concrete, objective detail. THINK about what the end goal of the description is, and extrapolate that to what would make satisfying images.
79
- All descriptions sent to dalle should be a paragraph of text that is extremely descriptive and detailed. Each should be more than 3 sentences long.`,
90
+ All descriptions sent to lobe-image-designer should be a paragraph of text that is extremely descriptive and detailed. Each should be more than 3 sentences long.`,
80
91
  type: 'builtin',
81
92
  };
@@ -4,7 +4,7 @@ import { DalleManifest } from './dalle';
4
4
 
5
5
  export const builtinTools: LobeBuiltinTool[] = [
6
6
  {
7
- identifier: 'dalle3',
7
+ identifier: DalleManifest.identifier,
8
8
  manifest: DalleManifest,
9
9
  type: 'builtin',
10
10
  },
@@ -5,4 +5,10 @@ import DalleRender from './dalle/Render';
5
5
 
6
6
  export const BuiltinToolsRenders: Record<string, BuiltinRender> = {
7
7
  [DalleManifest.identifier]: DalleRender as BuiltinRender,
8
+ /**
9
+ * 兼容旧版本 dalle3 的 identifier
10
+ * TODO: 后续数据库版本迁移时记得迁移 dalle3 对应的 identifier
11
+ * @deprecated
12
+ */
13
+ dalle3: DalleRender as BuiltinRender,
8
14
  };