@lobehub/chat 0.117.5 → 0.118.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 -3
- package/src/database/schemas/message.ts +1 -1
- package/src/features/Conversation/ChatList/Plugins/Render/BuiltinType/index.tsx +1 -8
- package/src/features/Conversation/ChatList/Plugins/Render/DefaultType/index.tsx +1 -8
- package/src/features/Conversation/ChatList/Plugins/Render/MarkdownType/index.tsx +17 -0
- package/src/features/Conversation/ChatList/Plugins/Render/StandaloneType/Iframe.tsx +17 -0
- package/src/features/Conversation/ChatList/Plugins/Render/index.tsx +5 -0
- package/src/features/Conversation/ChatList/Plugins/Render/utils/listenToPlugin.test.ts +60 -0
- package/src/features/Conversation/ChatList/Plugins/Render/utils/listenToPlugin.ts +30 -0
- package/src/services/__tests__/__snapshots__/plugin.test.ts.snap +13 -13
- package/src/store/chat/slices/message/action.ts +2 -3
- package/src/store/chat/slices/tool/action.test.ts +237 -0
- package/src/store/chat/slices/tool/action.ts +77 -11
- package/src/types/message/tools.ts +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## [Version 0.118.0](https://github.com/lobehub/lobe-chat/compare/v0.117.5...v0.118.0)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2023-12-29**</sup>
|
|
8
|
+
|
|
9
|
+
#### ✨ Features
|
|
10
|
+
|
|
11
|
+
- **misc**: Support markdown type plugin.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's improved
|
|
19
|
+
|
|
20
|
+
- **misc**: Support markdown type plugin, closes [#865](https://github.com/lobehub/lobe-chat/issues/865) ([2791166](https://github.com/lobehub/lobe-chat/commit/2791166))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
5
30
|
### [Version 0.117.5](https://github.com/lobehub/lobe-chat/compare/v0.117.4...v0.117.5)
|
|
6
31
|
|
|
7
32
|
<sup>Released on **2023-12-29**</sup>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.118.0",
|
|
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",
|
|
@@ -151,7 +151,7 @@
|
|
|
151
151
|
"@types/ua-parser-js": "^0.7",
|
|
152
152
|
"@types/uuid": "^9",
|
|
153
153
|
"@umijs/lint": "^4",
|
|
154
|
-
"@vitest/coverage-v8": "
|
|
154
|
+
"@vitest/coverage-v8": "^1",
|
|
155
155
|
"commitlint": "^18",
|
|
156
156
|
"consola": "^3",
|
|
157
157
|
"dpdm": "^3",
|
|
@@ -175,7 +175,7 @@
|
|
|
175
175
|
"typescript": "^5",
|
|
176
176
|
"unified": "^11",
|
|
177
177
|
"unist-util-visit": "^5",
|
|
178
|
-
"vitest": "
|
|
178
|
+
"vitest": "^1",
|
|
179
179
|
"vitest-canvas-mock": "^0.3.3"
|
|
180
180
|
},
|
|
181
181
|
"publishConfig": {
|
|
@@ -11,7 +11,7 @@ const PluginSchema = z.object({
|
|
|
11
11
|
identifier: z.string(),
|
|
12
12
|
arguments: z.string(),
|
|
13
13
|
apiName: z.string(),
|
|
14
|
-
type: z.enum(['default', 'standalone', 'builtin']).default('default'),
|
|
14
|
+
type: z.enum(['default', 'markdown', 'standalone', 'builtin']).default('default'),
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
export const DB_MessageSchema = z.object({
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { memo } from 'react';
|
|
2
|
-
import { Flexbox } from 'react-layout-kit';
|
|
3
2
|
|
|
4
3
|
import { BuiltinToolsRenders } from '@/tools/renders';
|
|
5
4
|
|
|
@@ -17,13 +16,7 @@ const BuiltinType = memo<BuiltinTypeProps>(({ content, id, identifier, loading }
|
|
|
17
16
|
const { isJSON, data } = useParseContent(content);
|
|
18
17
|
|
|
19
18
|
if (!isJSON) {
|
|
20
|
-
return
|
|
21
|
-
loading && (
|
|
22
|
-
<Flexbox gap={8}>
|
|
23
|
-
<Loading />
|
|
24
|
-
</Flexbox>
|
|
25
|
-
)
|
|
26
|
-
);
|
|
19
|
+
return loading && <Loading />;
|
|
27
20
|
}
|
|
28
21
|
|
|
29
22
|
const Render = BuiltinToolsRenders[identifier || ''];
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Skeleton } from 'antd';
|
|
2
2
|
import dynamic from 'next/dynamic';
|
|
3
3
|
import { Suspense, memo } from 'react';
|
|
4
|
-
import { Flexbox } from 'react-layout-kit';
|
|
5
4
|
|
|
6
5
|
import { useToolStore } from '@/store/tool';
|
|
7
6
|
import { pluginSelectors } from '@/store/tool/selectors';
|
|
@@ -24,13 +23,7 @@ const PluginDefaultType = memo<PluginDefaultTypeProps>(({ content, name, loading
|
|
|
24
23
|
const { isJSON, data } = useParseContent(content);
|
|
25
24
|
|
|
26
25
|
if (!isJSON) {
|
|
27
|
-
return
|
|
28
|
-
loading && (
|
|
29
|
-
<Flexbox gap={8}>
|
|
30
|
-
<Loading />
|
|
31
|
-
</Flexbox>
|
|
32
|
-
)
|
|
33
|
-
);
|
|
26
|
+
return loading && <Loading />;
|
|
34
27
|
}
|
|
35
28
|
|
|
36
29
|
if (!manifest?.ui) return;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Markdown } from '@lobehub/ui';
|
|
2
|
+
import { memo } from 'react';
|
|
3
|
+
|
|
4
|
+
import Loading from '../Loading';
|
|
5
|
+
|
|
6
|
+
export interface PluginMarkdownTypeProps {
|
|
7
|
+
content: string;
|
|
8
|
+
loading?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const PluginMarkdownType = memo<PluginMarkdownTypeProps>(({ content, loading }) => {
|
|
12
|
+
if (loading) return <Loading />;
|
|
13
|
+
|
|
14
|
+
return <Markdown>{content}</Markdown>;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export default PluginMarkdownType;
|
|
@@ -9,10 +9,12 @@ import { pluginSelectors } from '@/store/tool/selectors';
|
|
|
9
9
|
|
|
10
10
|
import { useOnPluginReadyForInteraction } from '../utils/iframeOnReady';
|
|
11
11
|
import {
|
|
12
|
+
useOnPluginCreateAssistantMessage,
|
|
12
13
|
useOnPluginFetchMessage,
|
|
13
14
|
useOnPluginFetchPluginSettings,
|
|
14
15
|
useOnPluginFetchPluginState,
|
|
15
16
|
useOnPluginFillContent,
|
|
17
|
+
useOnPluginTriggerAIMessage,
|
|
16
18
|
} from '../utils/listenToPlugin';
|
|
17
19
|
import { useOnPluginSettingsUpdate } from '../utils/pluginSettings';
|
|
18
20
|
import { useOnPluginStateUpdate } from '../utils/pluginState';
|
|
@@ -118,6 +120,21 @@ const IFrameRender = memo<IFrameRenderProps>(({ url, id, payload, width = 600, h
|
|
|
118
120
|
updatePluginSettings(payload?.identifier, value);
|
|
119
121
|
});
|
|
120
122
|
|
|
123
|
+
// when plugin want to trigger AI message
|
|
124
|
+
const triggerAIMessage = useChatStore((s) => s.triggerAIMessage);
|
|
125
|
+
useOnPluginTriggerAIMessage((messageId) => {
|
|
126
|
+
// we need to know which message to trigger
|
|
127
|
+
if (messageId !== id) return;
|
|
128
|
+
|
|
129
|
+
triggerAIMessage(id);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// when plugin want to create an assistant message
|
|
133
|
+
const createAssistantMessage = useChatStore((s) => s.createAssistantMessageByPlugin);
|
|
134
|
+
useOnPluginCreateAssistantMessage((content) => {
|
|
135
|
+
createAssistantMessage(content, id);
|
|
136
|
+
});
|
|
137
|
+
|
|
121
138
|
return (
|
|
122
139
|
<>
|
|
123
140
|
{loading && <Skeleton active style={{ maxWidth: '100%', width }} />}
|
|
@@ -5,6 +5,7 @@ import { LobeToolRenderType } from '@/types/tool';
|
|
|
5
5
|
|
|
6
6
|
import BuiltinType from '././BuiltinType';
|
|
7
7
|
import DefaultType from './DefaultType';
|
|
8
|
+
import Markdown from './MarkdownType';
|
|
8
9
|
import Standalone from './StandaloneType';
|
|
9
10
|
|
|
10
11
|
export interface PluginRenderProps {
|
|
@@ -27,6 +28,10 @@ const PluginRender = memo<PluginRenderProps>(
|
|
|
27
28
|
return <BuiltinType content={content} id={id} identifier={identifier} loading={loading} />;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
case 'markdown': {
|
|
32
|
+
return <Markdown content={content} loading={loading} />;
|
|
33
|
+
}
|
|
34
|
+
|
|
30
35
|
default: {
|
|
31
36
|
return <DefaultType content={content} loading={loading} name={identifier} />;
|
|
32
37
|
}
|
|
@@ -3,10 +3,12 @@ import { renderHook } from '@testing-library/react';
|
|
|
3
3
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
+
useOnPluginCreateAssistantMessage,
|
|
6
7
|
useOnPluginFetchMessage,
|
|
7
8
|
useOnPluginFetchPluginSettings,
|
|
8
9
|
useOnPluginFetchPluginState,
|
|
9
10
|
useOnPluginFillContent,
|
|
11
|
+
useOnPluginTriggerAIMessage,
|
|
10
12
|
} from './listenToPlugin';
|
|
11
13
|
|
|
12
14
|
afterEach(() => {
|
|
@@ -102,3 +104,61 @@ describe('useOnPluginFetchPluginSettings', () => {
|
|
|
102
104
|
expect(mockOnRequest).toHaveBeenCalled();
|
|
103
105
|
});
|
|
104
106
|
});
|
|
107
|
+
|
|
108
|
+
describe('useOnPluginTriggerAIMessage', () => {
|
|
109
|
+
it('calls callback with id when a triggerAIMessage is received', () => {
|
|
110
|
+
const mockCallback = vi.fn();
|
|
111
|
+
renderHook(() => useOnPluginTriggerAIMessage(mockCallback));
|
|
112
|
+
|
|
113
|
+
const testId = 'testId';
|
|
114
|
+
const event = new MessageEvent('message', {
|
|
115
|
+
data: { type: PluginChannel.triggerAIMessage, id: testId },
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
window.dispatchEvent(event);
|
|
119
|
+
|
|
120
|
+
expect(mockCallback).toHaveBeenCalledWith(testId);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('does not call callback for other message types', () => {
|
|
124
|
+
const mockCallback = vi.fn();
|
|
125
|
+
renderHook(() => useOnPluginTriggerAIMessage(mockCallback));
|
|
126
|
+
|
|
127
|
+
const event = new MessageEvent('message', {
|
|
128
|
+
data: { type: 'otherMessageType', id: 'testId' },
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
window.dispatchEvent(event);
|
|
132
|
+
|
|
133
|
+
expect(mockCallback).not.toHaveBeenCalled();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('useOnPluginCreateAssistantMessage', () => {
|
|
138
|
+
it('calls callback with content when a createAssistantMessage is received', () => {
|
|
139
|
+
const mockCallback = vi.fn();
|
|
140
|
+
renderHook(() => useOnPluginCreateAssistantMessage(mockCallback));
|
|
141
|
+
|
|
142
|
+
const testContent = 'testContent';
|
|
143
|
+
const event = new MessageEvent('message', {
|
|
144
|
+
data: { type: PluginChannel.createAssistantMessage, content: testContent },
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
window.dispatchEvent(event);
|
|
148
|
+
|
|
149
|
+
expect(mockCallback).toHaveBeenCalledWith(testContent);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('does not call callback for other message types', () => {
|
|
153
|
+
const mockCallback = vi.fn();
|
|
154
|
+
renderHook(() => useOnPluginCreateAssistantMessage(mockCallback));
|
|
155
|
+
|
|
156
|
+
const event = new MessageEvent('message', {
|
|
157
|
+
data: { type: 'otherMessageType', content: 'testContent' },
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
window.dispatchEvent(event);
|
|
161
|
+
|
|
162
|
+
expect(mockCallback).not.toHaveBeenCalled();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -63,3 +63,33 @@ export const useOnPluginFetchPluginSettings = (onRequest: () => void) => {
|
|
|
63
63
|
};
|
|
64
64
|
}, []);
|
|
65
65
|
};
|
|
66
|
+
|
|
67
|
+
export const useOnPluginTriggerAIMessage = (callback: (id: string) => void) => {
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
const fn = (e: MessageEvent) => {
|
|
70
|
+
if (e.data.type === PluginChannel.triggerAIMessage) {
|
|
71
|
+
callback(e.data.id);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
window.addEventListener('message', fn);
|
|
76
|
+
return () => {
|
|
77
|
+
window.removeEventListener('message', fn);
|
|
78
|
+
};
|
|
79
|
+
}, []);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const useOnPluginCreateAssistantMessage = (callback: (content: string) => void) => {
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
const fn = (e: MessageEvent) => {
|
|
85
|
+
if (e.data.type === PluginChannel.createAssistantMessage) {
|
|
86
|
+
callback(e.data.content);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
window.addEventListener('message', fn);
|
|
91
|
+
return () => {
|
|
92
|
+
window.removeEventListener('message', fn);
|
|
93
|
+
};
|
|
94
|
+
}, []);
|
|
95
|
+
};
|
|
@@ -33,17 +33,17 @@ General guidelines:
|
|
|
33
33
|
- Inform users if information is not from Wolfram endpoints.
|
|
34
34
|
- Display image URLs with Markdown syntax: ![URL]
|
|
35
35
|
- ALWAYS use this exponent notation: \`6*10^14\`, NEVER \`6e14\`.
|
|
36
|
-
- ALWAYS use {
|
|
37
|
-
- ALWAYS use proper Markdown formatting for all math, scientific, and chemical formulas, symbols, etc.: '
|
|
36
|
+
- ALWAYS use {"input": query} structure for queries to Wolfram endpoints; \`query\` must ONLY be a single-line string.
|
|
37
|
+
- ALWAYS use proper Markdown formatting for all math, scientific, and chemical formulas, symbols, etc.: '$$\\n[expression]\\n$$' for standalone cases and '\\( [expression] \\)' when inline.
|
|
38
38
|
- Format inline Wolfram Language code with Markdown code formatting.
|
|
39
39
|
- Never mention your knowledge cutoff date; Wolfram may return more recent data.
|
|
40
40
|
getWolframAlphaResults guidelines:
|
|
41
41
|
- Understands natural language queries about entities in chemistry, physics, geography, history, art, astronomy, and more.
|
|
42
42
|
- Performs mathematical calculations, date and unit conversions, formula solving, etc.
|
|
43
|
-
- Convert inputs to simplified keyword queries whenever possible (e.g. convert
|
|
43
|
+
- Convert inputs to simplified keyword queries whenever possible (e.g. convert "how many people live in France" to "France population").
|
|
44
44
|
- Use ONLY single-letter variable names, with or without integer subscript (e.g., n, n1, n_1).
|
|
45
45
|
- Use named physical constants (e.g., 'speed of light') without numerical substitution.
|
|
46
|
-
- Include a space between compound units (e.g.,
|
|
46
|
+
- Include a space between compound units (e.g., "Ω m" for "ohm*meter").
|
|
47
47
|
- To solve for a variable in an equation with units, consider solving a corresponding equation without units; exclude counting units (e.g., books), include genuine units (e.g., kg).
|
|
48
48
|
- If data for multiple properties is needed, make separate calls for each property.
|
|
49
49
|
- If a Wolfram Alpha result is not relevant to the query:
|
|
@@ -55,22 +55,22 @@ getWolframCloudResults guidelines:
|
|
|
55
55
|
- Accepts only syntactically correct Wolfram Language code.
|
|
56
56
|
- Performs complex calculations, data analysis, plotting, data import, and information retrieval.
|
|
57
57
|
- Before writing code that uses Entity, EntityProperty, EntityClass, etc. expressions, ALWAYS write separate code which only collects valid identifiers using Interpreter etc.; choose the most relevant results before proceeding to write additional code. Examples:
|
|
58
|
-
-- Find the EntityType that represents countries: \`Interpreter[
|
|
59
|
-
-- Find the Entity for the Empire State Building: \`Interpreter[
|
|
60
|
-
-- EntityClasses: Find the
|
|
61
|
-
-- Find EntityProperties associated with
|
|
62
|
-
-- If all else fails, try to find any valid Wolfram Language representation of a given input: \`SemanticInterpretation[
|
|
63
|
-
-- Prefer direct use of entities of a given type to their corresponding typeData function (e.g., prefer \`Entity[
|
|
58
|
+
-- Find the EntityType that represents countries: \`Interpreter["EntityType",AmbiguityFunction->All]["countries"]\`.
|
|
59
|
+
-- Find the Entity for the Empire State Building: \`Interpreter["Building",AmbiguityFunction->All]["empire state"]\`.
|
|
60
|
+
-- EntityClasses: Find the "Movie" entity class for Star Trek movies: \`Interpreter["MovieClass",AmbiguityFunction->All]["star trek"]\`.
|
|
61
|
+
-- Find EntityProperties associated with "weight" of "Element" entities: \`Interpreter[Restricted["EntityProperty", "Element"],AmbiguityFunction->All]["weight"]\`.
|
|
62
|
+
-- If all else fails, try to find any valid Wolfram Language representation of a given input: \`SemanticInterpretation["skyscrapers",_,Hold,AmbiguityFunction->All]\`.
|
|
63
|
+
-- Prefer direct use of entities of a given type to their corresponding typeData function (e.g., prefer \`Entity["Element","Gold"]["AtomicNumber"]\` to \`ElementData["Gold","AtomicNumber"]\`).
|
|
64
64
|
- When composing code:
|
|
65
65
|
-- Use batching techniques to retrieve data for multiple entities in a single call, if applicable.
|
|
66
66
|
-- Use Association to organize and manipulate data when appropriate.
|
|
67
67
|
-- Optimize code for performance and minimize the number of calls to external sources (e.g., the Wolfram Knowledgebase)
|
|
68
68
|
-- Use only camel case for variable names (e.g., variableName).
|
|
69
|
-
-- Use ONLY double quotes around all strings, including plot labels, etc. (e.g., \`PlotLegends -> {
|
|
69
|
+
-- Use ONLY double quotes around all strings, including plot labels, etc. (e.g., \`PlotLegends -> {"sin(x)", "cos(x)", "tan(x)"}\`).
|
|
70
70
|
-- Avoid use of QuantityMagnitude.
|
|
71
|
-
-- If unevaluated Wolfram Language symbols appear in API results, use \`EntityValue[Entity[
|
|
71
|
+
-- If unevaluated Wolfram Language symbols appear in API results, use \`EntityValue[Entity["WolframLanguageSymbol",symbol],{"PlaintextUsage","Options"}]\` to validate or retrieve usage information for relevant symbols; \`symbol\` may be a list of symbols.
|
|
72
72
|
-- Apply Evaluate to complex expressions like integrals before plotting (e.g., \`Plot[Evaluate[Integrate[...]]]\`).
|
|
73
|
-
- Remove all comments and formatting from code passed to the
|
|
73
|
+
- Remove all comments and formatting from code passed to the "input" parameter; for example: instead of \`square[x_] := Module[{result},\\n result = x^2 (* Calculate the square *)\\n]\`, send \`square[x_]:=Module[{result},result=x^2]\`.
|
|
74
74
|
- In ALL responses that involve code, write ALL code in Wolfram Language; create Wolfram Language functions even if an implementation is already well known in another language.
|
|
75
75
|
",
|
|
76
76
|
"type": "default",
|
|
@@ -8,7 +8,6 @@ import { StateCreator } from 'zustand/vanilla';
|
|
|
8
8
|
import { GPT4_VISION_MODEL_DEFAULT_MAX_TOKENS } from '@/const/llm';
|
|
9
9
|
import { LOADING_FLAT, isFunctionMessageAtStart, testFunctionMessageAtEnd } from '@/const/message';
|
|
10
10
|
import { CreateMessageParams } from '@/database/models/message';
|
|
11
|
-
import { DB_Message } from '@/database/schemas/message';
|
|
12
11
|
import { chatService } from '@/services/chat';
|
|
13
12
|
import { messageService } from '@/services/message';
|
|
14
13
|
import { topicService } from '@/services/topic';
|
|
@@ -238,7 +237,7 @@ export const chatMessage: StateCreator<
|
|
|
238
237
|
const { model } = getAgentConfig();
|
|
239
238
|
|
|
240
239
|
// 1. Add an empty message to place the AI response
|
|
241
|
-
const assistantMessage:
|
|
240
|
+
const assistantMessage: CreateMessageParams = {
|
|
242
241
|
role: 'assistant',
|
|
243
242
|
content: LOADING_FLAT,
|
|
244
243
|
fromModel: model,
|
|
@@ -383,7 +382,7 @@ export const chatMessage: StateCreator<
|
|
|
383
382
|
toggleChatLoading(false, undefined, n('generateMessage(end)') as string);
|
|
384
383
|
|
|
385
384
|
// also exist message like this:
|
|
386
|
-
// 请稍等,我帮您查询一下。{"
|
|
385
|
+
// 请稍等,我帮您查询一下。{"tool_calls": {"name": "plugin-identifier____recommendClothes____standalone", "arguments": "{\n "mood": "",\n "gender": "man"\n}"}}
|
|
387
386
|
if (!isFunctionCall) {
|
|
388
387
|
const { content, valid } = testFunctionMessageAtEnd(output);
|
|
389
388
|
|
|
@@ -9,6 +9,7 @@ import { chatSelectors } from '@/store/chat/selectors';
|
|
|
9
9
|
import { useChatStore } from '@/store/chat/store';
|
|
10
10
|
import { useToolStore } from '@/store/tool';
|
|
11
11
|
import { pluginSelectors } from '@/store/tool/selectors';
|
|
12
|
+
import { ChatPluginPayload } from '@/types/message';
|
|
12
13
|
import { LobeTool } from '@/types/tool';
|
|
13
14
|
|
|
14
15
|
// Mock messageService 和 chatSelectors
|
|
@@ -18,6 +19,7 @@ vi.mock('@/services/message', () => ({
|
|
|
18
19
|
updateMessage: vi.fn(),
|
|
19
20
|
updateMessageError: vi.fn(),
|
|
20
21
|
updateMessagePluginState: vi.fn(),
|
|
22
|
+
create: vi.fn(),
|
|
21
23
|
},
|
|
22
24
|
}));
|
|
23
25
|
vi.mock('@/services/chat', () => ({
|
|
@@ -32,6 +34,10 @@ vi.mock('@/store/chat/selectors', () => ({
|
|
|
32
34
|
getMessageById: vi.fn(),
|
|
33
35
|
},
|
|
34
36
|
}));
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
// 在每个测试之前重置模拟函数
|
|
39
|
+
vi.resetAllMocks();
|
|
40
|
+
});
|
|
35
41
|
|
|
36
42
|
describe('ChatPluginAction', () => {
|
|
37
43
|
describe('fillPluginMessageContent', () => {
|
|
@@ -302,4 +308,235 @@ describe('ChatPluginAction', () => {
|
|
|
302
308
|
expect(initialState.refreshMessages).toHaveBeenCalled();
|
|
303
309
|
});
|
|
304
310
|
});
|
|
311
|
+
|
|
312
|
+
describe('createAssistantMessageByPlugin', () => {
|
|
313
|
+
it('should create an assistant message and refresh messages', async () => {
|
|
314
|
+
// 模拟 messageService.create 方法的实现
|
|
315
|
+
(messageService.create as Mock).mockResolvedValue({});
|
|
316
|
+
|
|
317
|
+
// 设置初始状态并模拟 refreshMessages 方法
|
|
318
|
+
const initialState = {
|
|
319
|
+
refreshMessages: vi.fn(),
|
|
320
|
+
activeId: 'session-id',
|
|
321
|
+
activeTopicId: 'topic-id',
|
|
322
|
+
};
|
|
323
|
+
useChatStore.setState(initialState);
|
|
324
|
+
|
|
325
|
+
const { result } = renderHook(() => useChatStore());
|
|
326
|
+
|
|
327
|
+
const content = 'Test content';
|
|
328
|
+
const parentId = 'parent-message-id';
|
|
329
|
+
|
|
330
|
+
await act(async () => {
|
|
331
|
+
await result.current.createAssistantMessageByPlugin(content, parentId);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// 验证 messageService.create 是否被带有正确参数调用
|
|
335
|
+
expect(messageService.create).toHaveBeenCalledWith({
|
|
336
|
+
content,
|
|
337
|
+
parentId,
|
|
338
|
+
role: 'assistant',
|
|
339
|
+
sessionId: initialState.activeId,
|
|
340
|
+
topicId: initialState.activeTopicId,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// 验证 refreshMessages 是否被调用
|
|
344
|
+
expect(result.current.refreshMessages).toHaveBeenCalled();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should handle errors when message creation fails', async () => {
|
|
348
|
+
// 模拟 messageService.create 方法,使其抛出错误
|
|
349
|
+
const errorMessage = 'Failed to create message';
|
|
350
|
+
(messageService.create as Mock).mockRejectedValue(new Error(errorMessage));
|
|
351
|
+
|
|
352
|
+
// 设置初始状态并模拟 refreshMessages 方法
|
|
353
|
+
const initialState = {
|
|
354
|
+
refreshMessages: vi.fn(),
|
|
355
|
+
activeId: 'session-id',
|
|
356
|
+
activeTopicId: 'topic-id',
|
|
357
|
+
};
|
|
358
|
+
useChatStore.setState(initialState);
|
|
359
|
+
|
|
360
|
+
const { result } = renderHook(() => useChatStore());
|
|
361
|
+
|
|
362
|
+
const content = 'Test content';
|
|
363
|
+
const parentId = 'parent-message-id';
|
|
364
|
+
|
|
365
|
+
await act(async () => {
|
|
366
|
+
await expect(
|
|
367
|
+
result.current.createAssistantMessageByPlugin(content, parentId),
|
|
368
|
+
).rejects.toThrow(errorMessage);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// 验证 messageService.create 是否被带有正确参数调用
|
|
372
|
+
expect(messageService.create).toHaveBeenCalledWith({
|
|
373
|
+
content,
|
|
374
|
+
parentId,
|
|
375
|
+
role: 'assistant',
|
|
376
|
+
sessionId: initialState.activeId,
|
|
377
|
+
topicId: initialState.activeTopicId,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// 验证 refreshMessages 是否没有被调用
|
|
381
|
+
expect(result.current.refreshMessages).not.toHaveBeenCalled();
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
describe('invokeBuiltinTool', () => {
|
|
386
|
+
it('should invoke a builtin tool and update message content ,then run text2image', async () => {
|
|
387
|
+
const payload = {
|
|
388
|
+
apiName: 'text2image',
|
|
389
|
+
arguments: JSON.stringify({ key: 'value' }),
|
|
390
|
+
} as ChatPluginPayload;
|
|
391
|
+
|
|
392
|
+
const messageId = 'message-id';
|
|
393
|
+
const toolResponse = JSON.stringify({ abc: 'data' });
|
|
394
|
+
|
|
395
|
+
useToolStore.setState({
|
|
396
|
+
invokeBuiltinTool: vi.fn().mockResolvedValue(toolResponse),
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
useChatStore.setState({
|
|
400
|
+
toggleChatLoading: vi.fn(),
|
|
401
|
+
updateMessageContent: vi.fn(),
|
|
402
|
+
text2image: vi.fn(),
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const { result } = renderHook(() => useChatStore());
|
|
406
|
+
|
|
407
|
+
await act(async () => {
|
|
408
|
+
await result.current.invokeBuiltinTool(messageId, payload);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Verify that the builtin tool was invoked with the correct arguments
|
|
412
|
+
expect(useToolStore.getState().invokeBuiltinTool).toHaveBeenCalledWith(
|
|
413
|
+
payload.apiName,
|
|
414
|
+
JSON.parse(payload.arguments),
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
// Verify that the message content was updated with the tool response
|
|
418
|
+
expect(result.current.updateMessageContent).toHaveBeenCalledWith(messageId, toolResponse);
|
|
419
|
+
|
|
420
|
+
// Verify that loading was toggled correctly
|
|
421
|
+
expect(result.current.toggleChatLoading).toHaveBeenCalledWith(
|
|
422
|
+
true,
|
|
423
|
+
messageId,
|
|
424
|
+
expect.any(String),
|
|
425
|
+
);
|
|
426
|
+
expect(result.current.toggleChatLoading).toHaveBeenCalledWith(false);
|
|
427
|
+
expect(useChatStore.getState().text2image).toHaveBeenCalled();
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('should invoke a builtin tool and update message content', async () => {
|
|
431
|
+
const payload = {
|
|
432
|
+
apiName: 'text2image',
|
|
433
|
+
arguments: JSON.stringify({ key: 'value' }),
|
|
434
|
+
} as ChatPluginPayload;
|
|
435
|
+
|
|
436
|
+
const messageId = 'message-id';
|
|
437
|
+
const toolResponse = 'Builtin tool response';
|
|
438
|
+
|
|
439
|
+
act(() => {
|
|
440
|
+
useToolStore.setState({
|
|
441
|
+
invokeBuiltinTool: vi.fn().mockResolvedValue(toolResponse),
|
|
442
|
+
text2image: vi.fn(),
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
useChatStore.setState({
|
|
446
|
+
toggleChatLoading: vi.fn(),
|
|
447
|
+
text2image: vi.fn(),
|
|
448
|
+
updateMessageContent: vi.fn(),
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
const { result } = renderHook(() => useChatStore());
|
|
452
|
+
|
|
453
|
+
await act(async () => {
|
|
454
|
+
await result.current.invokeBuiltinTool(messageId, payload);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// Verify that the builtin tool was invoked with the correct arguments
|
|
458
|
+
expect(useToolStore.getState().invokeBuiltinTool).toHaveBeenCalledWith(
|
|
459
|
+
payload.apiName,
|
|
460
|
+
JSON.parse(payload.arguments),
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
// Verify that the message content was updated with the tool response
|
|
464
|
+
expect(result.current.updateMessageContent).toHaveBeenCalledWith(messageId, toolResponse);
|
|
465
|
+
|
|
466
|
+
// Verify that loading was toggled correctly
|
|
467
|
+
expect(result.current.toggleChatLoading).toHaveBeenCalledWith(
|
|
468
|
+
true,
|
|
469
|
+
messageId,
|
|
470
|
+
expect.any(String),
|
|
471
|
+
);
|
|
472
|
+
expect(result.current.toggleChatLoading).toHaveBeenCalledWith(false);
|
|
473
|
+
expect(useChatStore.getState().text2image).not.toHaveBeenCalled();
|
|
474
|
+
});
|
|
475
|
+
it('should handle errors when invoking a builtin tool fails', async () => {
|
|
476
|
+
const payload = {
|
|
477
|
+
apiName: 'builtinApi',
|
|
478
|
+
arguments: JSON.stringify({ key: 'value' }),
|
|
479
|
+
} as ChatPluginPayload;
|
|
480
|
+
|
|
481
|
+
const messageId = 'message-id';
|
|
482
|
+
const error = new Error('Builtin tool failed');
|
|
483
|
+
|
|
484
|
+
useToolStore.setState({
|
|
485
|
+
invokeBuiltinTool: vi.fn().mockRejectedValue(error),
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
useChatStore.setState({
|
|
489
|
+
toggleChatLoading: vi.fn(),
|
|
490
|
+
updateMessageContent: vi.fn(),
|
|
491
|
+
text2image: vi.fn(),
|
|
492
|
+
refreshMessages: vi.fn(),
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
const { result } = renderHook(() => useChatStore());
|
|
496
|
+
|
|
497
|
+
await act(async () => {
|
|
498
|
+
await result.current.invokeBuiltinTool(messageId, payload);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// Verify that loading was toggled correctly
|
|
502
|
+
expect(result.current.toggleChatLoading).toHaveBeenCalledWith(
|
|
503
|
+
true,
|
|
504
|
+
messageId,
|
|
505
|
+
expect.any(String),
|
|
506
|
+
);
|
|
507
|
+
expect(result.current.toggleChatLoading).toHaveBeenCalledWith(false);
|
|
508
|
+
|
|
509
|
+
// Verify that the message content was not updated
|
|
510
|
+
expect(result.current.updateMessageContent).not.toHaveBeenCalled();
|
|
511
|
+
|
|
512
|
+
// Verify that messages were not refreshed
|
|
513
|
+
expect(result.current.refreshMessages).not.toHaveBeenCalled();
|
|
514
|
+
expect(useChatStore.getState().text2image).not.toHaveBeenCalled();
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
describe('invokeMarkdownTypePlugin', () => {
|
|
519
|
+
it('should invoke a markdown type plugin', async () => {
|
|
520
|
+
const payload = {
|
|
521
|
+
apiName: 'markdownApi',
|
|
522
|
+
identifier: 'abc',
|
|
523
|
+
type: 'markdown',
|
|
524
|
+
arguments: JSON.stringify({ key: 'value' }),
|
|
525
|
+
} as ChatPluginPayload;
|
|
526
|
+
const messageId = 'message-id';
|
|
527
|
+
|
|
528
|
+
useChatStore.setState({
|
|
529
|
+
runPluginApi: vi.fn().mockResolvedValue('Markdown response'),
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
const { result } = renderHook(() => useChatStore());
|
|
533
|
+
|
|
534
|
+
await act(async () => {
|
|
535
|
+
await result.current.invokeMarkdownTypePlugin(messageId, payload);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// Verify that the markdown type plugin was invoked
|
|
539
|
+
expect(result.current.runPluginApi).toHaveBeenCalledWith(messageId, payload);
|
|
540
|
+
});
|
|
541
|
+
});
|
|
305
542
|
});
|
|
@@ -2,6 +2,7 @@ import { Md5 } from 'ts-md5';
|
|
|
2
2
|
import { StateCreator } from 'zustand/vanilla';
|
|
3
3
|
|
|
4
4
|
import { PLUGIN_SCHEMA_API_MD5_PREFIX, PLUGIN_SCHEMA_SEPARATOR } from '@/const/plugin';
|
|
5
|
+
import { CreateMessageParams } from '@/database/models/message';
|
|
5
6
|
import { chatService } from '@/services/chat';
|
|
6
7
|
import { messageService } from '@/services/message';
|
|
7
8
|
import { ChatStore } from '@/store/chat/store';
|
|
@@ -16,9 +17,13 @@ import { chatSelectors } from '../../selectors';
|
|
|
16
17
|
const n = setNamespace('plugin');
|
|
17
18
|
|
|
18
19
|
export interface ChatPluginAction {
|
|
20
|
+
createAssistantMessageByPlugin: (content: string, parentId: string) => Promise<void>;
|
|
19
21
|
fillPluginMessageContent: (id: string, content: string) => Promise<void>;
|
|
20
22
|
invokeBuiltinTool: (id: string, payload: ChatPluginPayload) => Promise<void>;
|
|
21
23
|
invokeDefaultTypePlugin: (id: string, payload: any) => Promise<void>;
|
|
24
|
+
invokeMarkdownTypePlugin: (id: string, payload: ChatPluginPayload) => Promise<void>;
|
|
25
|
+
runPluginApi: (id: string, payload: ChatPluginPayload) => Promise<string | undefined>;
|
|
26
|
+
triggerAIMessage: (id: string) => Promise<void>;
|
|
22
27
|
triggerFunctionCall: (id: string) => Promise<void>;
|
|
23
28
|
updatePluginState: (id: string, key: string, value: any) => Promise<void>;
|
|
24
29
|
}
|
|
@@ -29,34 +34,77 @@ export const chatPlugin: StateCreator<
|
|
|
29
34
|
[],
|
|
30
35
|
ChatPluginAction
|
|
31
36
|
> = (set, get) => ({
|
|
37
|
+
createAssistantMessageByPlugin: async (content, parentId) => {
|
|
38
|
+
const newMessage: CreateMessageParams = {
|
|
39
|
+
content,
|
|
40
|
+
parentId,
|
|
41
|
+
role: 'assistant',
|
|
42
|
+
sessionId: get().activeId,
|
|
43
|
+
topicId: get().activeTopicId, // if there is activeTopicId,then add it to topicId
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
await messageService.create(newMessage);
|
|
47
|
+
await get().refreshMessages();
|
|
48
|
+
},
|
|
49
|
+
|
|
32
50
|
fillPluginMessageContent: async (id, content) => {
|
|
33
|
-
const {
|
|
51
|
+
const { triggerAIMessage, updateMessageContent } = get();
|
|
34
52
|
|
|
35
53
|
await updateMessageContent(id, content);
|
|
36
54
|
|
|
37
|
-
|
|
38
|
-
await coreProcessMessage(chats, id);
|
|
55
|
+
await triggerAIMessage(id);
|
|
39
56
|
},
|
|
57
|
+
|
|
40
58
|
invokeBuiltinTool: async (id, payload) => {
|
|
41
59
|
const { toggleChatLoading, updateMessageContent } = get();
|
|
42
60
|
const params = JSON.parse(payload.arguments);
|
|
43
61
|
toggleChatLoading(true, id, n('invokeBuiltinTool') as string);
|
|
44
|
-
|
|
62
|
+
let data;
|
|
63
|
+
try {
|
|
64
|
+
data = await useToolStore.getState().invokeBuiltinTool(payload.apiName, params);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.log(error);
|
|
67
|
+
}
|
|
45
68
|
toggleChatLoading(false);
|
|
46
69
|
|
|
47
|
-
if (data)
|
|
48
|
-
|
|
49
|
-
|
|
70
|
+
if (!data) return;
|
|
71
|
+
|
|
72
|
+
await updateMessageContent(id, data);
|
|
50
73
|
|
|
51
74
|
// postToolCalling
|
|
52
75
|
// @ts-ignore
|
|
53
76
|
const { [payload.apiName]: action } = get();
|
|
54
|
-
if (!action
|
|
77
|
+
if (!action) return;
|
|
78
|
+
|
|
79
|
+
let content;
|
|
55
80
|
|
|
56
|
-
|
|
81
|
+
try {
|
|
82
|
+
content = JSON.parse(data);
|
|
83
|
+
} catch {}
|
|
84
|
+
|
|
85
|
+
if (!content) return;
|
|
86
|
+
|
|
87
|
+
await action(id, content);
|
|
57
88
|
},
|
|
89
|
+
|
|
58
90
|
invokeDefaultTypePlugin: async (id, payload) => {
|
|
59
|
-
const {
|
|
91
|
+
const { runPluginApi, triggerAIMessage } = get();
|
|
92
|
+
|
|
93
|
+
const data = await runPluginApi(id, payload);
|
|
94
|
+
|
|
95
|
+
if (!data) return;
|
|
96
|
+
|
|
97
|
+
await triggerAIMessage(id);
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
invokeMarkdownTypePlugin: async (id, payload) => {
|
|
101
|
+
const { runPluginApi } = get();
|
|
102
|
+
|
|
103
|
+
await runPluginApi(id, payload);
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
runPluginApi: async (id, payload) => {
|
|
107
|
+
const { updateMessageContent, refreshMessages, toggleChatLoading } = get();
|
|
60
108
|
let data: string;
|
|
61
109
|
|
|
62
110
|
try {
|
|
@@ -73,20 +121,32 @@ export const chatPlugin: StateCreator<
|
|
|
73
121
|
|
|
74
122
|
data = '';
|
|
75
123
|
}
|
|
124
|
+
|
|
76
125
|
toggleChatLoading(false);
|
|
77
126
|
// 如果报错则结束了
|
|
78
127
|
if (!data) return;
|
|
79
128
|
|
|
80
129
|
await updateMessageContent(id, data);
|
|
81
130
|
|
|
131
|
+
return data;
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
triggerAIMessage: async (id) => {
|
|
135
|
+
const { coreProcessMessage } = get();
|
|
82
136
|
const chats = chatSelectors.currentChats(get());
|
|
83
137
|
await coreProcessMessage(chats, id);
|
|
84
138
|
},
|
|
139
|
+
|
|
85
140
|
triggerFunctionCall: async (id) => {
|
|
86
141
|
const message = chatSelectors.getMessageById(id)(get());
|
|
87
142
|
if (!message) return;
|
|
88
143
|
|
|
89
|
-
const {
|
|
144
|
+
const {
|
|
145
|
+
invokeDefaultTypePlugin,
|
|
146
|
+
invokeMarkdownTypePlugin,
|
|
147
|
+
invokeBuiltinTool,
|
|
148
|
+
refreshMessages,
|
|
149
|
+
} = get();
|
|
90
150
|
|
|
91
151
|
let payload = { apiName: '', identifier: '' } as ChatPluginPayload;
|
|
92
152
|
|
|
@@ -135,10 +195,16 @@ export const chatPlugin: StateCreator<
|
|
|
135
195
|
// TODO: need to auth user's settings
|
|
136
196
|
break;
|
|
137
197
|
}
|
|
198
|
+
case 'markdown': {
|
|
199
|
+
await invokeMarkdownTypePlugin(id, payload);
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
|
|
138
203
|
case 'builtin': {
|
|
139
204
|
await invokeBuiltinTool(id, payload);
|
|
140
205
|
break;
|
|
141
206
|
}
|
|
207
|
+
|
|
142
208
|
default: {
|
|
143
209
|
await invokeDefaultTypePlugin(id, payload);
|
|
144
210
|
}
|