@lobehub/chat 1.0.12 → 1.0.13
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 +4 -3
- package/src/features/Conversation/Error/ErrorJsonViewer.tsx +1 -1
- package/src/features/Conversation/Messages/Assistant/ToolCalls/index.tsx +3 -2
- package/src/features/Conversation/Messages/Tool/index.tsx +3 -5
- package/src/features/Conversation/Messages/components/Arguments.tsx +20 -0
- package/src/features/Conversation/Messages/hooks/useYamlArguments.ts +11 -0
- package/src/libs/agent-runtime/anthropic/index.test.ts +1 -31
- package/src/libs/agent-runtime/anthropic/index.ts +9 -63
- package/src/libs/agent-runtime/openai/index.test.ts +7 -7
- package/src/libs/agent-runtime/openai/index.ts +0 -6
- package/src/libs/agent-runtime/utils/anthropicHelpers.ts +2 -2
- package/src/libs/agent-runtime/utils/streams/anthropic.test.ts +102 -28
- package/src/libs/agent-runtime/utils/streams/anthropic.ts +29 -12
- package/src/libs/agent-runtime/utils/streams/protocol.ts +6 -1
- package/src/services/message/server.ts +4 -0
- package/src/services/message/type.ts +1 -1
- package/src/store/chat/slices/message/action.ts +48 -48
- package/src/store/chat/slices/message/initialState.ts +2 -0
- package/src/store/chat/slices/message/selectors.ts +3 -0
- package/src/store/chat/slices/plugin/action.test.ts +15 -15
- package/src/store/chat/slices/plugin/action.ts +128 -115
- package/src/store/chat/utils/index.ts +19 -0
- package/src/types/message/index.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
### [Version 1.0.13](https://github.com/lobehub/lobe-chat/compare/v1.0.12...v1.0.13)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2024-06-19**</sup>
|
|
8
|
+
|
|
9
|
+
#### 🐛 Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **misc**: Fix and improve tool calling.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's fixed
|
|
19
|
+
|
|
20
|
+
- **misc**: Fix and improve tool calling, closes [#2941](https://github.com/lobehub/lobe-chat/issues/2941) ([ea06536](https://github.com/lobehub/lobe-chat/commit/ea06536))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
5
30
|
### [Version 1.0.12](https://github.com/lobehub/lobe-chat/compare/v1.0.11...v1.0.12)
|
|
6
31
|
|
|
7
32
|
<sup>Released on **2024-06-19**</sup>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/chat",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.13",
|
|
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",
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
},
|
|
93
93
|
"dependencies": {
|
|
94
94
|
"@ant-design/icons": "^5.3.7",
|
|
95
|
-
"@anthropic-ai/sdk": "^0.
|
|
95
|
+
"@anthropic-ai/sdk": "^0.23.0",
|
|
96
96
|
"@auth/core": "0.28.0",
|
|
97
97
|
"@aws-sdk/client-bedrock-runtime": "^3.583.0",
|
|
98
98
|
"@aws-sdk/client-s3": "^3.583.0",
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
"@lobehub/chat-plugins-gateway": "^1.9.0",
|
|
110
110
|
"@lobehub/icons": "^1.23.0",
|
|
111
111
|
"@lobehub/tts": "^1.24.1",
|
|
112
|
-
"@lobehub/ui": "^1.143.
|
|
112
|
+
"@lobehub/ui": "^1.143.5",
|
|
113
113
|
"@microsoft/fetch-event-source": "^2.0.1",
|
|
114
114
|
"@neondatabase/serverless": "^0.9.3",
|
|
115
115
|
"@next/third-parties": "^14.2.3",
|
|
@@ -154,6 +154,7 @@
|
|
|
154
154
|
"nuqs": "^1.17.4",
|
|
155
155
|
"ollama": "^0.5.1",
|
|
156
156
|
"openai": "^4.47.1",
|
|
157
|
+
"partial-json": "^0.1.7",
|
|
157
158
|
"pg": "^8.11.5",
|
|
158
159
|
"pino": "^9.1.0",
|
|
159
160
|
"polished": "^4.3.1",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Avatar,
|
|
1
|
+
import { Avatar, Icon } from '@lobehub/ui';
|
|
2
2
|
import isEqual from 'fast-deep-equal';
|
|
3
3
|
import { Loader2, LucideChevronDown, LucideChevronRight, LucideToyBrick } from 'lucide-react';
|
|
4
4
|
import { CSSProperties, memo, useState } from 'react';
|
|
@@ -10,6 +10,7 @@ import { chatSelectors } from '@/store/chat/selectors';
|
|
|
10
10
|
import { pluginHelpers, useToolStore } from '@/store/tool';
|
|
11
11
|
import { toolSelectors } from '@/store/tool/selectors';
|
|
12
12
|
|
|
13
|
+
import Arguments from '../../components/Arguments';
|
|
13
14
|
import { useStyles } from './style';
|
|
14
15
|
|
|
15
16
|
export interface InspectorProps {
|
|
@@ -64,7 +65,7 @@ const CallItem = memo<InspectorProps>(
|
|
|
64
65
|
</Flexbox>
|
|
65
66
|
<Icon icon={open ? LucideChevronDown : LucideChevronRight} />
|
|
66
67
|
</Flexbox>
|
|
67
|
-
{(open || loading) && <
|
|
68
|
+
{(open || loading) && <Arguments arguments={requestArgs} />}
|
|
68
69
|
</Flexbox>
|
|
69
70
|
);
|
|
70
71
|
},
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Snippet } from '@lobehub/ui';
|
|
2
1
|
import { memo, useState } from 'react';
|
|
3
2
|
import { Flexbox } from 'react-layout-kit';
|
|
4
3
|
|
|
@@ -7,10 +6,11 @@ import { chatSelectors } from '@/store/chat/selectors';
|
|
|
7
6
|
import { ChatMessage } from '@/types/message';
|
|
8
7
|
|
|
9
8
|
import PluginRender from '../../Plugins/Render';
|
|
9
|
+
import Arguments from '../components/Arguments';
|
|
10
10
|
import Inspector from './Inspector';
|
|
11
11
|
|
|
12
12
|
export const ToolMessage = memo<ChatMessage>(({ id, content, plugin }) => {
|
|
13
|
-
const loading = useChatStore(chatSelectors.
|
|
13
|
+
const loading = useChatStore(chatSelectors.isPluginApiInvoking(id));
|
|
14
14
|
|
|
15
15
|
const [showRender, setShow] = useState(plugin?.type !== 'default');
|
|
16
16
|
|
|
@@ -35,9 +35,7 @@ export const ToolMessage = memo<ChatMessage>(({ id, content, plugin }) => {
|
|
|
35
35
|
type={plugin?.type}
|
|
36
36
|
/>
|
|
37
37
|
) : (
|
|
38
|
-
<
|
|
39
|
-
<Snippet>{plugin?.arguments || ''}</Snippet>
|
|
40
|
-
</Flexbox>
|
|
38
|
+
<Arguments arguments={plugin?.arguments} />
|
|
41
39
|
)}
|
|
42
40
|
</Flexbox>
|
|
43
41
|
);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Highlighter } from '@lobehub/ui';
|
|
2
|
+
import { memo } from 'react';
|
|
3
|
+
|
|
4
|
+
import { useYamlArguments } from '../hooks/useYamlArguments';
|
|
5
|
+
|
|
6
|
+
export interface ArgumentsProps {
|
|
7
|
+
arguments?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const Arguments = memo<ArgumentsProps>(({ arguments: args = '' }) => {
|
|
11
|
+
const yaml = useYamlArguments(args);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<Highlighter language={'yaml'} showLanguage={false}>
|
|
15
|
+
{yaml}
|
|
16
|
+
</Highlighter>
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export default Arguments;
|
|
@@ -22,10 +22,6 @@ beforeEach(() => {
|
|
|
22
22
|
|
|
23
23
|
// 使用 vi.spyOn 来模拟 chat.completions.create 方法
|
|
24
24
|
vi.spyOn(instance['client'].messages, 'create').mockReturnValue(new ReadableStream() as any);
|
|
25
|
-
|
|
26
|
-
vi.spyOn(instance['client'].beta.tools.messages, 'create').mockReturnValue({
|
|
27
|
-
content: [],
|
|
28
|
-
} as any);
|
|
29
25
|
});
|
|
30
26
|
|
|
31
27
|
afterEach(() => {
|
|
@@ -260,35 +256,9 @@ describe('LobeAnthropicAI', () => {
|
|
|
260
256
|
});
|
|
261
257
|
|
|
262
258
|
// Assert
|
|
263
|
-
expect(instance['client'].
|
|
259
|
+
expect(instance['client'].messages.create).toHaveBeenCalled();
|
|
264
260
|
expect(spyOn).toHaveBeenCalledWith(tools);
|
|
265
261
|
});
|
|
266
|
-
|
|
267
|
-
it('should handle text and tool_use content correctly in transformResponseToStream', async () => {
|
|
268
|
-
// Arrange
|
|
269
|
-
const mockResponse = {
|
|
270
|
-
content: [
|
|
271
|
-
{ type: 'text', text: 'Hello' },
|
|
272
|
-
{ type: 'tool_use', id: 'tool1', name: 'tool1', input: 'input1' },
|
|
273
|
-
],
|
|
274
|
-
};
|
|
275
|
-
// @ts-ignore
|
|
276
|
-
vi.spyOn(instance, 'transformResponseToStream').mockReturnValue(new ReadableStream());
|
|
277
|
-
vi.spyOn(instance['client'].beta.tools.messages, 'create').mockResolvedValue(
|
|
278
|
-
mockResponse as any,
|
|
279
|
-
);
|
|
280
|
-
|
|
281
|
-
// Act
|
|
282
|
-
await instance.chat({
|
|
283
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
|
284
|
-
model: 'claude-3-haiku-20240307',
|
|
285
|
-
temperature: 0,
|
|
286
|
-
tools: [{ function: { name: 'tool1', description: 'desc1' }, type: 'function' }],
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
// Assert
|
|
290
|
-
expect(instance['transformResponseToStream']).toHaveBeenCalledWith(mockResponse);
|
|
291
|
-
});
|
|
292
262
|
});
|
|
293
263
|
|
|
294
264
|
describe('Error', () => {
|
|
@@ -30,41 +30,20 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
|
|
|
30
30
|
async chat(payload: ChatStreamPayload, options?: ChatCompetitionOptions) {
|
|
31
31
|
try {
|
|
32
32
|
const anthropicPayload = this.buildAnthropicPayload(payload);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
{
|
|
39
|
-
signal: options?.signal,
|
|
40
|
-
},
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
const [prod, debug] = response.tee();
|
|
44
|
-
|
|
45
|
-
if (process.env.DEBUG_ANTHROPIC_CHAT_COMPLETION === '1') {
|
|
46
|
-
debugStream(debug.toReadableStream()).catch(console.error);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return StreamingResponse(AnthropicStream(prod, options?.callback), {
|
|
50
|
-
headers: options?.headers,
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// or we should call the tool API
|
|
55
|
-
const response = await this.client.beta.tools.messages.create(
|
|
56
|
-
{ ...anthropicPayload, stream: false },
|
|
57
|
-
{ signal: options?.signal },
|
|
33
|
+
const response = await this.client.messages.create(
|
|
34
|
+
{ ...anthropicPayload, stream: true },
|
|
35
|
+
{
|
|
36
|
+
signal: options?.signal,
|
|
37
|
+
},
|
|
58
38
|
);
|
|
59
39
|
|
|
40
|
+
const [prod, debug] = response.tee();
|
|
41
|
+
|
|
60
42
|
if (process.env.DEBUG_ANTHROPIC_CHAT_COMPLETION === '1') {
|
|
61
|
-
|
|
62
|
-
console.log(JSON.stringify(response) + '\n');
|
|
43
|
+
debugStream(debug.toReadableStream()).catch(console.error);
|
|
63
44
|
}
|
|
64
45
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return StreamingResponse(AnthropicStream(stream, options?.callback), {
|
|
46
|
+
return StreamingResponse(AnthropicStream(prod, options?.callback), {
|
|
68
47
|
headers: options?.headers,
|
|
69
48
|
});
|
|
70
49
|
} catch (error) {
|
|
@@ -118,43 +97,10 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
|
|
|
118
97
|
model,
|
|
119
98
|
system: system_message?.content as string,
|
|
120
99
|
temperature,
|
|
121
|
-
// TODO: Anthropic sdk don't have tools interface currently
|
|
122
|
-
// @ts-ignore
|
|
123
100
|
tools: buildAnthropicTools(tools),
|
|
124
101
|
top_p,
|
|
125
102
|
} satisfies Anthropic.MessageCreateParams;
|
|
126
103
|
}
|
|
127
|
-
|
|
128
|
-
private transformResponseToStream = (response: Anthropic.Beta.Tools.ToolsBetaMessage) => {
|
|
129
|
-
return new ReadableStream<Anthropic.MessageStreamEvent>({
|
|
130
|
-
start(controller) {
|
|
131
|
-
response.content.forEach((content) => {
|
|
132
|
-
switch (content.type) {
|
|
133
|
-
case 'text': {
|
|
134
|
-
controller.enqueue({
|
|
135
|
-
delta: { text: content.text, type: 'text_delta' },
|
|
136
|
-
type: 'content_block_delta',
|
|
137
|
-
} as Anthropic.ContentBlockDeltaEvent);
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
140
|
-
case 'tool_use': {
|
|
141
|
-
controller.enqueue({
|
|
142
|
-
delta: {
|
|
143
|
-
tool_use: { id: content.id, input: content.input, name: content.name },
|
|
144
|
-
type: 'tool_use',
|
|
145
|
-
},
|
|
146
|
-
type: 'content_block_delta',
|
|
147
|
-
} as any);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
controller.enqueue({ type: 'message_stop' } as Anthropic.MessageStopEvent);
|
|
153
|
-
|
|
154
|
-
controller.close();
|
|
155
|
-
},
|
|
156
|
-
});
|
|
157
|
-
};
|
|
158
104
|
}
|
|
159
105
|
|
|
160
106
|
export default LobeAnthropicAI;
|
|
@@ -49,7 +49,7 @@ describe('LobeOpenAI', () => {
|
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
describe('Error', () => {
|
|
52
|
-
it('should return
|
|
52
|
+
it('should return ProviderBizError with an openai error response when OpenAI.APIError is thrown', async () => {
|
|
53
53
|
// Arrange
|
|
54
54
|
const apiError = new OpenAI.APIError(
|
|
55
55
|
400,
|
|
@@ -79,7 +79,7 @@ describe('LobeOpenAI', () => {
|
|
|
79
79
|
error: { message: 'Bad Request' },
|
|
80
80
|
status: 400,
|
|
81
81
|
},
|
|
82
|
-
errorType: '
|
|
82
|
+
errorType: 'ProviderBizError',
|
|
83
83
|
provider: 'openai',
|
|
84
84
|
});
|
|
85
85
|
}
|
|
@@ -89,11 +89,11 @@ describe('LobeOpenAI', () => {
|
|
|
89
89
|
try {
|
|
90
90
|
new LobeOpenAI({});
|
|
91
91
|
} catch (e) {
|
|
92
|
-
expect(e).toEqual({ errorType: '
|
|
92
|
+
expect(e).toEqual({ errorType: 'InvalidProviderAPIKey' });
|
|
93
93
|
}
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
-
it('should return
|
|
96
|
+
it('should return ProviderBizError with the cause when OpenAI.APIError is thrown with cause', async () => {
|
|
97
97
|
// Arrange
|
|
98
98
|
const errorInfo = {
|
|
99
99
|
stack: 'abc',
|
|
@@ -119,13 +119,13 @@ describe('LobeOpenAI', () => {
|
|
|
119
119
|
cause: { message: 'api is undefined' },
|
|
120
120
|
stack: 'abc',
|
|
121
121
|
},
|
|
122
|
-
errorType: '
|
|
122
|
+
errorType: 'ProviderBizError',
|
|
123
123
|
provider: 'openai',
|
|
124
124
|
});
|
|
125
125
|
}
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
-
it('should return
|
|
128
|
+
it('should return ProviderBizError with an cause response with desensitize Url', async () => {
|
|
129
129
|
// Arrange
|
|
130
130
|
const errorInfo = {
|
|
131
131
|
stack: 'abc',
|
|
@@ -155,7 +155,7 @@ describe('LobeOpenAI', () => {
|
|
|
155
155
|
cause: { message: 'api is undefined' },
|
|
156
156
|
stack: 'abc',
|
|
157
157
|
},
|
|
158
|
-
errorType: '
|
|
158
|
+
errorType: 'ProviderBizError',
|
|
159
159
|
provider: 'openai',
|
|
160
160
|
});
|
|
161
161
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { AgentRuntimeErrorType } from '../error';
|
|
2
1
|
import { ModelProvider } from '../types';
|
|
3
2
|
import { LobeOpenAICompatibleFactory } from '../utils/openaiCompatibleFactory';
|
|
4
3
|
|
|
@@ -7,10 +6,5 @@ export const LobeOpenAI = LobeOpenAICompatibleFactory({
|
|
|
7
6
|
debug: {
|
|
8
7
|
chatCompletion: () => process.env.DEBUG_OPENAI_CHAT_COMPLETION === '1',
|
|
9
8
|
},
|
|
10
|
-
errorType: {
|
|
11
|
-
bizError: AgentRuntimeErrorType.OpenAIBizError,
|
|
12
|
-
invalidAPIKey: AgentRuntimeErrorType.NoOpenAIAPIKey,
|
|
13
|
-
},
|
|
14
|
-
|
|
15
9
|
provider: ModelProvider.OpenAI,
|
|
16
10
|
});
|
|
@@ -112,9 +112,9 @@ export const buildAnthropicMessages = (
|
|
|
112
112
|
|
|
113
113
|
export const buildAnthropicTools = (tools?: OpenAI.ChatCompletionTool[]) =>
|
|
114
114
|
tools?.map(
|
|
115
|
-
(tool): Anthropic.
|
|
115
|
+
(tool): Anthropic.Tool => ({
|
|
116
116
|
description: tool.function.description,
|
|
117
|
-
input_schema: tool.function.parameters as Anthropic.
|
|
117
|
+
input_schema: tool.function.parameters as Anthropic.Tool.InputSchema,
|
|
118
118
|
name: tool.function.name,
|
|
119
119
|
}),
|
|
120
120
|
);
|
|
@@ -99,27 +99,67 @@ describe('AnthropicStream', () => {
|
|
|
99
99
|
});
|
|
100
100
|
|
|
101
101
|
it('should handle tool use event and ReadableStream input', async () => {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
102
|
+
const streams = [
|
|
103
|
+
{
|
|
104
|
+
type: 'message_start',
|
|
105
|
+
message: {
|
|
106
|
+
id: 'msg_017aTuY86wNxth5TE544yqJq',
|
|
107
|
+
type: 'message',
|
|
108
|
+
role: 'assistant',
|
|
109
|
+
model: 'claude-3-sonnet-20240229',
|
|
110
|
+
content: [],
|
|
111
|
+
stop_reason: null,
|
|
112
|
+
stop_sequence: null,
|
|
113
|
+
usage: { input_tokens: 457, output_tokens: 1 },
|
|
110
114
|
},
|
|
111
115
|
},
|
|
112
|
-
|
|
116
|
+
{ type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } },
|
|
117
|
+
{ type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: '好' } },
|
|
118
|
+
{ type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: '的:' } },
|
|
119
|
+
|
|
120
|
+
{ type: 'content_block_stop', index: 0 },
|
|
121
|
+
{
|
|
122
|
+
type: 'content_block_start',
|
|
123
|
+
index: 1,
|
|
124
|
+
content_block: {
|
|
125
|
+
type: 'tool_use',
|
|
126
|
+
id: 'toolu_01WdYWxYFQ8iu5iZq1Dy9Saf',
|
|
127
|
+
name: 'realtime-weather____fetchCurrentWeather',
|
|
128
|
+
input: {},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
type: 'content_block_delta',
|
|
133
|
+
index: 1,
|
|
134
|
+
delta: { type: 'input_json_delta', partial_json: '' },
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
type: 'content_block_delta',
|
|
138
|
+
index: 1,
|
|
139
|
+
delta: { type: 'input_json_delta', partial_json: '{"city": "' },
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
type: 'content_block_delta',
|
|
143
|
+
index: 1,
|
|
144
|
+
delta: { type: 'input_json_delta', partial_json: '杭' },
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
type: 'content_block_delta',
|
|
148
|
+
index: 1,
|
|
149
|
+
delta: { type: 'input_json_delta', partial_json: '州"}' },
|
|
150
|
+
},
|
|
151
|
+
{ type: 'content_block_stop', index: 1 },
|
|
152
|
+
{
|
|
153
|
+
type: 'message_delta',
|
|
154
|
+
delta: { stop_reason: 'tool_use', stop_sequence: null },
|
|
155
|
+
usage: { output_tokens: 83 },
|
|
156
|
+
},
|
|
157
|
+
];
|
|
113
158
|
|
|
114
159
|
const mockReadableStream = new ReadableStream({
|
|
115
160
|
start(controller) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
message: { id: 'message_1', metadata: {} },
|
|
119
|
-
});
|
|
120
|
-
controller.enqueue(toolUseEvent);
|
|
121
|
-
controller.enqueue({
|
|
122
|
-
type: 'message_stop',
|
|
161
|
+
streams.forEach((chunk) => {
|
|
162
|
+
controller.enqueue(chunk);
|
|
123
163
|
});
|
|
124
164
|
controller.close();
|
|
125
165
|
},
|
|
@@ -139,19 +179,53 @@ describe('AnthropicStream', () => {
|
|
|
139
179
|
chunks.push(decoder.decode(chunk, { stream: true }));
|
|
140
180
|
}
|
|
141
181
|
|
|
142
|
-
expect(chunks).toEqual(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
182
|
+
expect(chunks).toEqual(
|
|
183
|
+
[
|
|
184
|
+
'id: msg_017aTuY86wNxth5TE544yqJq',
|
|
185
|
+
'event: data',
|
|
186
|
+
'data: {"id":"msg_017aTuY86wNxth5TE544yqJq","type":"message","role":"assistant","model":"claude-3-sonnet-20240229","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":457,"output_tokens":1}}\n',
|
|
187
|
+
'id: msg_017aTuY86wNxth5TE544yqJq',
|
|
188
|
+
'event: data',
|
|
189
|
+
'data: ""\n',
|
|
190
|
+
'id: msg_017aTuY86wNxth5TE544yqJq',
|
|
191
|
+
'event: text',
|
|
192
|
+
'data: "好"\n',
|
|
193
|
+
'id: msg_017aTuY86wNxth5TE544yqJq',
|
|
194
|
+
'event: text',
|
|
195
|
+
'data: "的:"\n',
|
|
196
|
+
'id: msg_017aTuY86wNxth5TE544yqJq',
|
|
197
|
+
'event: data',
|
|
198
|
+
'data: {"type":"content_block_stop","index":0}\n',
|
|
199
|
+
// Tool calls
|
|
200
|
+
'id: msg_017aTuY86wNxth5TE544yqJq',
|
|
201
|
+
'event: tool_calls',
|
|
202
|
+
`data: [{"function":{"arguments":"","name":"realtime-weather____fetchCurrentWeather"},"id":"toolu_01WdYWxYFQ8iu5iZq1Dy9Saf","index":0,"type":"function"}]\n`,
|
|
203
|
+
'id: msg_017aTuY86wNxth5TE544yqJq',
|
|
204
|
+
'event: tool_calls',
|
|
205
|
+
`data: [{"function":{"arguments":""},"index":0,"type":"function"}]\n`,
|
|
206
|
+
'id: msg_017aTuY86wNxth5TE544yqJq',
|
|
207
|
+
'event: tool_calls',
|
|
208
|
+
`data: [{"function":{"arguments":"{\\"city\\": \\""},"index":0,"type":"function"}]\n`,
|
|
209
|
+
'id: msg_017aTuY86wNxth5TE544yqJq',
|
|
210
|
+
'event: tool_calls',
|
|
211
|
+
|
|
212
|
+
`data: [{"function":{"arguments":"杭"},"index":0,"type":"function"}]\n`,
|
|
213
|
+
'id: msg_017aTuY86wNxth5TE544yqJq',
|
|
214
|
+
'event: tool_calls',
|
|
215
|
+
|
|
216
|
+
`data: [{"function":{"arguments":"州\\"}"},"index":0,"type":"function"}]\n`,
|
|
217
|
+
|
|
218
|
+
'id: msg_017aTuY86wNxth5TE544yqJq',
|
|
219
|
+
'event: data',
|
|
220
|
+
'data: {"type":"content_block_stop","index":1}\n',
|
|
221
|
+
|
|
222
|
+
'id: msg_017aTuY86wNxth5TE544yqJq',
|
|
223
|
+
'event: stop',
|
|
224
|
+
'data: "tool_use"\n',
|
|
225
|
+
].map((item) => `${item}\n`),
|
|
226
|
+
);
|
|
153
227
|
|
|
154
|
-
expect(onToolCallMock).toHaveBeenCalledTimes(
|
|
228
|
+
expect(onToolCallMock).toHaveBeenCalledTimes(5);
|
|
155
229
|
});
|
|
156
230
|
|
|
157
231
|
it('should handle ReadableStream input', async () => {
|
|
@@ -22,27 +22,39 @@ export const transformAnthropicStream = (
|
|
|
22
22
|
stack.id = chunk.message.id;
|
|
23
23
|
return { data: chunk.message, id: chunk.message.id, type: 'data' };
|
|
24
24
|
}
|
|
25
|
+
case 'content_block_start': {
|
|
26
|
+
if (chunk.content_block.type === 'tool_use') {
|
|
27
|
+
const toolChunk = chunk.content_block;
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
const toolCall: StreamToolCallChunkData = {
|
|
30
|
+
function: {
|
|
31
|
+
arguments: '',
|
|
32
|
+
name: toolChunk.name,
|
|
33
|
+
},
|
|
34
|
+
id: toolChunk.id,
|
|
35
|
+
index: 0,
|
|
36
|
+
type: 'function',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
stack.tool = { id: toolChunk.id, index: 0, name: toolChunk.name };
|
|
40
|
+
|
|
41
|
+
return { data: [toolCall], id: stack.id, type: 'tool_calls' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { data: chunk.content_block.text, id: stack.id, type: 'data' };
|
|
45
|
+
}
|
|
29
46
|
|
|
30
47
|
case 'content_block_delta': {
|
|
31
|
-
switch (chunk.delta.type
|
|
32
|
-
default:
|
|
48
|
+
switch (chunk.delta.type) {
|
|
33
49
|
case 'text_delta': {
|
|
34
50
|
return { data: chunk.delta.text, id: stack.id, type: 'text' };
|
|
35
51
|
}
|
|
36
52
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
// and maybe we need to update it when the feature is available
|
|
40
|
-
case 'tool_use': {
|
|
41
|
-
const delta = (chunk.delta as any).tool_use as Anthropic.Beta.Tools.ToolUseBlock;
|
|
53
|
+
case 'input_json_delta': {
|
|
54
|
+
const delta = chunk.delta.partial_json;
|
|
42
55
|
|
|
43
56
|
const toolCall: StreamToolCallChunkData = {
|
|
44
|
-
function: { arguments:
|
|
45
|
-
id: delta.id,
|
|
57
|
+
function: { arguments: delta },
|
|
46
58
|
index: 0,
|
|
47
59
|
type: 'function',
|
|
48
60
|
};
|
|
@@ -53,7 +65,12 @@ export const transformAnthropicStream = (
|
|
|
53
65
|
type: 'tool_calls',
|
|
54
66
|
} as StreamProtocolToolCallChunk;
|
|
55
67
|
}
|
|
68
|
+
|
|
69
|
+
default: {
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
56
72
|
}
|
|
73
|
+
return { data: chunk, id: stack.id, type: 'data' };
|
|
57
74
|
}
|
|
58
75
|
|
|
59
76
|
case 'message_delta': {
|
|
@@ -2,6 +2,11 @@ import { ChatStreamCallbacks } from '@/libs/agent-runtime';
|
|
|
2
2
|
|
|
3
3
|
export interface StreamStack {
|
|
4
4
|
id: string;
|
|
5
|
+
tool?: {
|
|
6
|
+
id: string;
|
|
7
|
+
index: number;
|
|
8
|
+
name: string;
|
|
9
|
+
};
|
|
5
10
|
}
|
|
6
11
|
|
|
7
12
|
export interface StreamProtocolChunk {
|
|
@@ -15,7 +20,7 @@ export interface StreamToolCallChunkData {
|
|
|
15
20
|
arguments?: string;
|
|
16
21
|
name?: string | null;
|
|
17
22
|
};
|
|
18
|
-
id
|
|
23
|
+
id?: string;
|
|
19
24
|
index: number;
|
|
20
25
|
type: 'function' | string;
|
|
21
26
|
}
|
|
@@ -44,6 +44,10 @@ export class ServerService implements IMessageService {
|
|
|
44
44
|
return lambdaClient.message.update.mutate({ id, value: { error } });
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
async updateMessagePluginError(id: string, error: ChatMessageError): Promise<any> {
|
|
48
|
+
return lambdaClient.message.update.mutate({ id, value: { pluginError: error } });
|
|
49
|
+
}
|
|
50
|
+
|
|
47
51
|
updateMessage(id: string, message: Partial<ChatMessage>): Promise<any> {
|
|
48
52
|
return lambdaClient.message.update.mutate({ id, value: message });
|
|
49
53
|
}
|
|
@@ -19,12 +19,14 @@ import { agentSelectors } from '@/store/agent/selectors';
|
|
|
19
19
|
import { chatHelpers } from '@/store/chat/helpers';
|
|
20
20
|
import { messageMapKey } from '@/store/chat/slices/message/utils';
|
|
21
21
|
import { ChatStore } from '@/store/chat/store';
|
|
22
|
-
import { ChatMessage, MessageToolCall } from '@/types/message';
|
|
22
|
+
import { ChatMessage, ChatMessageError, MessageToolCall } from '@/types/message';
|
|
23
23
|
import { TraceEventPayloads } from '@/types/trace';
|
|
24
24
|
import { setNamespace } from '@/utils/storeDebug';
|
|
25
25
|
import { nanoid } from '@/utils/uuid';
|
|
26
26
|
|
|
27
|
+
import type { ChatStoreState } from '../../initialState';
|
|
27
28
|
import { chatSelectors, topicSelectors } from '../../selectors';
|
|
29
|
+
import { preventLeavingFn, toggleBooleanList } from '../../utils';
|
|
28
30
|
import { MessageDispatch, messagesReducer } from './reducer';
|
|
29
31
|
|
|
30
32
|
const n = setNamespace('m');
|
|
@@ -84,6 +86,12 @@ export interface ChatMessageAction {
|
|
|
84
86
|
id?: string,
|
|
85
87
|
action?: string,
|
|
86
88
|
) => AbortController | undefined;
|
|
89
|
+
internal_toggleLoadingArrays: (
|
|
90
|
+
key: keyof ChatStoreState,
|
|
91
|
+
loading: boolean,
|
|
92
|
+
id?: string,
|
|
93
|
+
action?: string,
|
|
94
|
+
) => AbortController | undefined;
|
|
87
95
|
internal_toggleToolCallingStreaming: (id: string, streaming: boolean[] | undefined) => void;
|
|
88
96
|
internal_toggleMessageLoading: (loading: boolean, id: string) => void;
|
|
89
97
|
/**
|
|
@@ -123,6 +131,7 @@ export interface ChatMessageAction {
|
|
|
123
131
|
content: string,
|
|
124
132
|
toolCalls?: MessageToolCall[],
|
|
125
133
|
) => Promise<void>;
|
|
134
|
+
internal_updateMessageError: (id: string, error: ChatMessageError | null) => Promise<void>;
|
|
126
135
|
internal_createMessage: (
|
|
127
136
|
params: CreateMessageParams,
|
|
128
137
|
context?: { tempMessageId?: string; skipRefresh?: boolean },
|
|
@@ -136,24 +145,6 @@ export interface ChatMessageAction {
|
|
|
136
145
|
const getAgentConfig = () => agentSelectors.currentAgentConfig(useAgentStore.getState());
|
|
137
146
|
const getAgentChatConfig = () => agentSelectors.currentAgentChatConfig(useAgentStore.getState());
|
|
138
147
|
|
|
139
|
-
const preventLeavingFn = (e: BeforeUnloadEvent) => {
|
|
140
|
-
// set returnValue to trigger alert modal
|
|
141
|
-
// Note: No matter what value is set, the browser will display the standard text
|
|
142
|
-
e.returnValue = '你有正在生成中的请求,确定要离开吗?';
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
const toggleBooleanList = (ids: string[], id: string, loading: boolean) => {
|
|
146
|
-
return produce(ids, (draft) => {
|
|
147
|
-
if (loading) {
|
|
148
|
-
draft.push(id);
|
|
149
|
-
} else {
|
|
150
|
-
const index = draft.indexOf(id);
|
|
151
|
-
|
|
152
|
-
if (index >= 0) draft.splice(index, 1);
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
};
|
|
156
|
-
|
|
157
148
|
export const chatMessage: StateCreator<
|
|
158
149
|
ChatStore,
|
|
159
150
|
[['zustand/devtools', never]],
|
|
@@ -591,35 +582,7 @@ export const chatMessage: StateCreator<
|
|
|
591
582
|
};
|
|
592
583
|
},
|
|
593
584
|
internal_toggleChatLoading: (loading, id, action) => {
|
|
594
|
-
|
|
595
|
-
window.addEventListener('beforeunload', preventLeavingFn);
|
|
596
|
-
|
|
597
|
-
const abortController = new AbortController();
|
|
598
|
-
set(
|
|
599
|
-
{
|
|
600
|
-
abortController,
|
|
601
|
-
chatLoadingIds: toggleBooleanList(get().messageLoadingIds, id!, loading),
|
|
602
|
-
},
|
|
603
|
-
false,
|
|
604
|
-
action,
|
|
605
|
-
);
|
|
606
|
-
|
|
607
|
-
return abortController;
|
|
608
|
-
} else {
|
|
609
|
-
if (!id) {
|
|
610
|
-
set({ abortController: undefined, chatLoadingIds: [] }, false, action);
|
|
611
|
-
} else
|
|
612
|
-
set(
|
|
613
|
-
{
|
|
614
|
-
abortController: undefined,
|
|
615
|
-
chatLoadingIds: toggleBooleanList(get().messageLoadingIds, id, loading),
|
|
616
|
-
},
|
|
617
|
-
false,
|
|
618
|
-
action,
|
|
619
|
-
);
|
|
620
|
-
|
|
621
|
-
window.removeEventListener('beforeunload', preventLeavingFn);
|
|
622
|
-
}
|
|
585
|
+
return get().internal_toggleLoadingArrays('chatLoadingIds', loading, id, action);
|
|
623
586
|
},
|
|
624
587
|
internal_toggleMessageLoading: (loading, id) => {
|
|
625
588
|
set(
|
|
@@ -684,6 +647,11 @@ export const chatMessage: StateCreator<
|
|
|
684
647
|
await internal_coreProcessMessage(contextMessages, latestMsg.id, { traceId });
|
|
685
648
|
},
|
|
686
649
|
|
|
650
|
+
internal_updateMessageError: async (id, error) => {
|
|
651
|
+
get().internal_dispatchMessage({ id, type: 'updateMessages', value: { error } });
|
|
652
|
+
await messageService.updateMessage(id, { error });
|
|
653
|
+
await get().refreshMessages();
|
|
654
|
+
},
|
|
687
655
|
internal_updateMessageContent: async (id, content, toolCalls) => {
|
|
688
656
|
const { internal_dispatchMessage, refreshMessages, internal_transformToolCalls } = get();
|
|
689
657
|
|
|
@@ -762,4 +730,36 @@ export const chatMessage: StateCreator<
|
|
|
762
730
|
.catch();
|
|
763
731
|
}
|
|
764
732
|
},
|
|
733
|
+
|
|
734
|
+
internal_toggleLoadingArrays: (key, loading, id, action) => {
|
|
735
|
+
if (loading) {
|
|
736
|
+
window.addEventListener('beforeunload', preventLeavingFn);
|
|
737
|
+
|
|
738
|
+
const abortController = new AbortController();
|
|
739
|
+
set(
|
|
740
|
+
{
|
|
741
|
+
abortController,
|
|
742
|
+
[key]: toggleBooleanList(get()[key] as string[], id!, loading),
|
|
743
|
+
},
|
|
744
|
+
false,
|
|
745
|
+
action,
|
|
746
|
+
);
|
|
747
|
+
|
|
748
|
+
return abortController;
|
|
749
|
+
} else {
|
|
750
|
+
if (!id) {
|
|
751
|
+
set({ abortController: undefined, [key]: [] }, false, action);
|
|
752
|
+
} else
|
|
753
|
+
set(
|
|
754
|
+
{
|
|
755
|
+
abortController: undefined,
|
|
756
|
+
[key]: toggleBooleanList(get()[key] as string[], id, loading),
|
|
757
|
+
},
|
|
758
|
+
false,
|
|
759
|
+
action,
|
|
760
|
+
);
|
|
761
|
+
|
|
762
|
+
window.removeEventListener('beforeunload', preventLeavingFn);
|
|
763
|
+
}
|
|
764
|
+
},
|
|
765
765
|
});
|
|
@@ -26,6 +26,7 @@ export interface ChatMessageState {
|
|
|
26
26
|
*/
|
|
27
27
|
messagesInit: boolean;
|
|
28
28
|
messagesMap: Record<string, ChatMessage[]>;
|
|
29
|
+
pluginApiLoadingIds: string[];
|
|
29
30
|
/**
|
|
30
31
|
* the tool calling stream ids
|
|
31
32
|
*/
|
|
@@ -41,5 +42,6 @@ export const initialMessageState: ChatMessageState = {
|
|
|
41
42
|
messageLoadingIds: [],
|
|
42
43
|
messagesInit: false,
|
|
43
44
|
messagesMap: {},
|
|
45
|
+
pluginApiLoadingIds: [],
|
|
44
46
|
toolCallingStreamIds: {},
|
|
45
47
|
};
|
|
@@ -129,6 +129,8 @@ const isHasMessageLoading = (s: ChatStore) => s.messageLoadingIds.length > 0;
|
|
|
129
129
|
const isCreatingMessage = (s: ChatStore) => s.isCreatingMessage;
|
|
130
130
|
|
|
131
131
|
const isMessageGenerating = (id: string) => (s: ChatStore) => s.chatLoadingIds.includes(id);
|
|
132
|
+
const isPluginApiInvoking = (id: string) => (s: ChatStore) => s.pluginApiLoadingIds.includes(id);
|
|
133
|
+
|
|
132
134
|
const isToolCallStreaming = (id: string, index: number) => (s: ChatStore) => {
|
|
133
135
|
const isLoading = s.toolCallingStreamIds[id];
|
|
134
136
|
|
|
@@ -155,6 +157,7 @@ export const chatSelectors = {
|
|
|
155
157
|
isMessageEditing,
|
|
156
158
|
isMessageGenerating,
|
|
157
159
|
isMessageLoading,
|
|
160
|
+
isPluginApiInvoking,
|
|
158
161
|
isToolCallStreaming,
|
|
159
162
|
latestMessage,
|
|
160
163
|
showInboxWelcome,
|
|
@@ -112,7 +112,7 @@ describe('ChatPluginAction', () => {
|
|
|
112
112
|
|
|
113
113
|
vi.spyOn(storeState, 'refreshMessages');
|
|
114
114
|
vi.spyOn(storeState, 'triggerAIMessage').mockResolvedValue(undefined);
|
|
115
|
-
vi.spyOn(storeState, '
|
|
115
|
+
vi.spyOn(storeState, 'internal_togglePluginApiCalling').mockReturnValue(undefined);
|
|
116
116
|
|
|
117
117
|
const runSpy = vi.spyOn(chatService, 'runPluginApi').mockResolvedValue({
|
|
118
118
|
text: pluginApiResponse,
|
|
@@ -125,7 +125,7 @@ describe('ChatPluginAction', () => {
|
|
|
125
125
|
await result.current.invokeDefaultTypePlugin(messageId, pluginPayload);
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
-
expect(storeState.
|
|
128
|
+
expect(storeState.internal_togglePluginApiCalling).toHaveBeenCalledWith(
|
|
129
129
|
true,
|
|
130
130
|
messageId,
|
|
131
131
|
expect.any(String),
|
|
@@ -135,7 +135,7 @@ describe('ChatPluginAction', () => {
|
|
|
135
135
|
content: pluginApiResponse,
|
|
136
136
|
});
|
|
137
137
|
expect(storeState.refreshMessages).toHaveBeenCalled();
|
|
138
|
-
expect(storeState.
|
|
138
|
+
expect(storeState.internal_togglePluginApiCalling).toHaveBeenCalledWith(
|
|
139
139
|
false,
|
|
140
140
|
'message-id',
|
|
141
141
|
'plugin/fetchPlugin/end',
|
|
@@ -150,7 +150,7 @@ describe('ChatPluginAction', () => {
|
|
|
150
150
|
const storeState = useChatStore.getState();
|
|
151
151
|
vi.spyOn(storeState, 'refreshMessages');
|
|
152
152
|
vi.spyOn(storeState, 'triggerAIMessage').mockResolvedValue(undefined);
|
|
153
|
-
vi.spyOn(storeState, '
|
|
153
|
+
vi.spyOn(storeState, 'internal_togglePluginApiCalling').mockReturnValue(undefined);
|
|
154
154
|
|
|
155
155
|
vi.spyOn(chatService, 'runPluginApi').mockRejectedValue(error);
|
|
156
156
|
|
|
@@ -159,7 +159,7 @@ describe('ChatPluginAction', () => {
|
|
|
159
159
|
await result.current.invokeDefaultTypePlugin(messageId, pluginPayload);
|
|
160
160
|
});
|
|
161
161
|
|
|
162
|
-
expect(storeState.
|
|
162
|
+
expect(storeState.internal_togglePluginApiCalling).toHaveBeenCalledWith(
|
|
163
163
|
true,
|
|
164
164
|
messageId,
|
|
165
165
|
expect.any(String),
|
|
@@ -167,7 +167,7 @@ describe('ChatPluginAction', () => {
|
|
|
167
167
|
expect(chatService.runPluginApi).toHaveBeenCalledWith(pluginPayload, { trace: {} });
|
|
168
168
|
expect(messageService.updateMessageError).toHaveBeenCalledWith(messageId, error);
|
|
169
169
|
expect(storeState.refreshMessages).toHaveBeenCalled();
|
|
170
|
-
expect(storeState.
|
|
170
|
+
expect(storeState.internal_togglePluginApiCalling).toHaveBeenCalledWith(
|
|
171
171
|
false,
|
|
172
172
|
'message-id',
|
|
173
173
|
'plugin/fetchPlugin/end',
|
|
@@ -467,7 +467,7 @@ describe('ChatPluginAction', () => {
|
|
|
467
467
|
});
|
|
468
468
|
|
|
469
469
|
useChatStore.setState({
|
|
470
|
-
|
|
470
|
+
internal_togglePluginApiCalling: vi.fn(),
|
|
471
471
|
internal_updateMessageContent: vi.fn(),
|
|
472
472
|
text2image: vi.fn(),
|
|
473
473
|
});
|
|
@@ -491,12 +491,12 @@ describe('ChatPluginAction', () => {
|
|
|
491
491
|
);
|
|
492
492
|
|
|
493
493
|
// Verify that loading was toggled correctly
|
|
494
|
-
expect(result.current.
|
|
494
|
+
expect(result.current.internal_togglePluginApiCalling).toHaveBeenCalledWith(
|
|
495
495
|
true,
|
|
496
496
|
messageId,
|
|
497
497
|
expect.any(String),
|
|
498
498
|
);
|
|
499
|
-
expect(result.current.
|
|
499
|
+
expect(result.current.internal_togglePluginApiCalling).toHaveBeenCalledWith(false);
|
|
500
500
|
expect(useChatStore.getState().text2image).toHaveBeenCalled();
|
|
501
501
|
});
|
|
502
502
|
|
|
@@ -516,7 +516,7 @@ describe('ChatPluginAction', () => {
|
|
|
516
516
|
});
|
|
517
517
|
|
|
518
518
|
useChatStore.setState({
|
|
519
|
-
|
|
519
|
+
internal_togglePluginApiCalling: vi.fn(),
|
|
520
520
|
text2image: vi.fn(),
|
|
521
521
|
internal_updateMessageContent: vi.fn(),
|
|
522
522
|
});
|
|
@@ -540,12 +540,12 @@ describe('ChatPluginAction', () => {
|
|
|
540
540
|
);
|
|
541
541
|
|
|
542
542
|
// Verify that loading was toggled correctly
|
|
543
|
-
expect(result.current.
|
|
543
|
+
expect(result.current.internal_togglePluginApiCalling).toHaveBeenCalledWith(
|
|
544
544
|
true,
|
|
545
545
|
messageId,
|
|
546
546
|
expect.any(String),
|
|
547
547
|
);
|
|
548
|
-
expect(result.current.
|
|
548
|
+
expect(result.current.internal_togglePluginApiCalling).toHaveBeenCalledWith(false);
|
|
549
549
|
expect(useChatStore.getState().text2image).not.toHaveBeenCalled();
|
|
550
550
|
});
|
|
551
551
|
|
|
@@ -563,7 +563,7 @@ describe('ChatPluginAction', () => {
|
|
|
563
563
|
});
|
|
564
564
|
|
|
565
565
|
useChatStore.setState({
|
|
566
|
-
|
|
566
|
+
internal_togglePluginApiCalling: vi.fn(),
|
|
567
567
|
internal_updateMessageContent: vi.fn(),
|
|
568
568
|
text2image: vi.fn(),
|
|
569
569
|
refreshMessages: vi.fn(),
|
|
@@ -576,12 +576,12 @@ describe('ChatPluginAction', () => {
|
|
|
576
576
|
});
|
|
577
577
|
|
|
578
578
|
// Verify that loading was toggled correctly
|
|
579
|
-
expect(result.current.
|
|
579
|
+
expect(result.current.internal_togglePluginApiCalling).toHaveBeenCalledWith(
|
|
580
580
|
true,
|
|
581
581
|
messageId,
|
|
582
582
|
expect.any(String),
|
|
583
583
|
);
|
|
584
|
-
expect(result.current.
|
|
584
|
+
expect(result.current.internal_togglePluginApiCalling).toHaveBeenCalledWith(false);
|
|
585
585
|
|
|
586
586
|
// Verify that the message content was not updated
|
|
587
587
|
expect(result.current.internal_updateMessageContent).not.toHaveBeenCalled();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
|
|
1
2
|
import { PluginErrorType } from '@lobehub/chat-plugin-sdk';
|
|
2
3
|
import { t } from 'i18next';
|
|
3
4
|
import { Md5 } from 'ts-md5';
|
|
@@ -25,11 +26,6 @@ export interface ChatPluginAction {
|
|
|
25
26
|
triggerAiMessage?: boolean,
|
|
26
27
|
) => Promise<void>;
|
|
27
28
|
|
|
28
|
-
internal_callPluginApi: (id: string, payload: ChatToolPayload) => Promise<string | undefined>;
|
|
29
|
-
internal_invokeDifferentTypePlugin: (id: string, payload: ChatToolPayload) => Promise<any>;
|
|
30
|
-
internal_transformToolCalls: (toolCalls: MessageToolCall[]) => ChatToolPayload[];
|
|
31
|
-
internal_updatePluginError: (id: string, error: any) => Promise<void>;
|
|
32
|
-
|
|
33
29
|
invokeBuiltinTool: (id: string, payload: ChatToolPayload) => Promise<void>;
|
|
34
30
|
invokeDefaultTypePlugin: (id: string, payload: any) => Promise<string | undefined>;
|
|
35
31
|
invokeMarkdownTypePlugin: (id: string, payload: ChatToolPayload) => Promise<void>;
|
|
@@ -41,6 +37,16 @@ export interface ChatPluginAction {
|
|
|
41
37
|
|
|
42
38
|
triggerToolCalls: (id: string) => Promise<void>;
|
|
43
39
|
updatePluginState: (id: string, key: string, value: any) => Promise<void>;
|
|
40
|
+
|
|
41
|
+
internal_callPluginApi: (id: string, payload: ChatToolPayload) => Promise<string | undefined>;
|
|
42
|
+
internal_invokeDifferentTypePlugin: (id: string, payload: ChatToolPayload) => Promise<any>;
|
|
43
|
+
internal_togglePluginApiCalling: (
|
|
44
|
+
loading: boolean,
|
|
45
|
+
id?: string,
|
|
46
|
+
action?: string,
|
|
47
|
+
) => AbortController | undefined;
|
|
48
|
+
internal_transformToolCalls: (toolCalls: MessageToolCall[]) => ChatToolPayload[];
|
|
49
|
+
internal_updatePluginError: (id: string, error: any) => Promise<void>;
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
export const chatPlugin: StateCreator<
|
|
@@ -69,124 +75,17 @@ export const chatPlugin: StateCreator<
|
|
|
69
75
|
|
|
70
76
|
if (triggerAiMessage) await triggerAIMessage({ parentId: id });
|
|
71
77
|
},
|
|
72
|
-
internal_callPluginApi: async (id, payload) => {
|
|
73
|
-
const { internal_updateMessageContent, refreshMessages, internal_toggleChatLoading } = get();
|
|
74
|
-
let data: string;
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
const abortController = internal_toggleChatLoading(
|
|
78
|
-
true,
|
|
79
|
-
id,
|
|
80
|
-
n('fetchPlugin/start') as string,
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
const message = chatSelectors.getMessageById(id)(get());
|
|
84
|
-
|
|
85
|
-
const res = await chatService.runPluginApi(payload, {
|
|
86
|
-
signal: abortController?.signal,
|
|
87
|
-
trace: { observationId: message?.observationId, traceId: message?.traceId },
|
|
88
|
-
});
|
|
89
|
-
data = res.text;
|
|
90
|
-
|
|
91
|
-
// save traceId
|
|
92
|
-
if (res.traceId) {
|
|
93
|
-
await messageService.updateMessage(id, { traceId: res.traceId });
|
|
94
|
-
}
|
|
95
|
-
} catch (error) {
|
|
96
|
-
console.log(error);
|
|
97
|
-
const err = error as Error;
|
|
98
|
-
|
|
99
|
-
// ignore the aborted request error
|
|
100
|
-
if (!err.message.includes('The user aborted a request.')) {
|
|
101
|
-
await messageService.updateMessageError(id, error as any);
|
|
102
|
-
await refreshMessages();
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
data = '';
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
internal_toggleChatLoading(false, id, n('fetchPlugin/end') as string);
|
|
109
|
-
// 如果报错则结束了
|
|
110
|
-
if (!data) return;
|
|
111
|
-
|
|
112
|
-
await internal_updateMessageContent(id, data);
|
|
113
|
-
|
|
114
|
-
return data;
|
|
115
|
-
},
|
|
116
|
-
|
|
117
|
-
internal_invokeDifferentTypePlugin: async (id, payload) => {
|
|
118
|
-
switch (payload.type) {
|
|
119
|
-
case 'standalone': {
|
|
120
|
-
return await get().invokeStandaloneTypePlugin(id, payload);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
case 'markdown': {
|
|
124
|
-
return await get().invokeMarkdownTypePlugin(id, payload);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
case 'builtin': {
|
|
128
|
-
return await get().invokeBuiltinTool(id, payload);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
default: {
|
|
132
|
-
return await get().invokeDefaultTypePlugin(id, payload);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
},
|
|
136
|
-
|
|
137
|
-
internal_transformToolCalls: (toolCalls) => {
|
|
138
|
-
return toolCalls
|
|
139
|
-
.map((toolCall): ChatToolPayload | null => {
|
|
140
|
-
let payload: ChatToolPayload;
|
|
141
|
-
|
|
142
|
-
const [identifier, apiName, type] = toolCall.function.name.split(PLUGIN_SCHEMA_SEPARATOR);
|
|
143
|
-
|
|
144
|
-
if (!apiName) return null;
|
|
145
|
-
|
|
146
|
-
payload = {
|
|
147
|
-
apiName,
|
|
148
|
-
arguments: toolCall.function.arguments,
|
|
149
|
-
id: toolCall.id,
|
|
150
|
-
identifier,
|
|
151
|
-
type: (type ?? 'default') as any,
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
// if the apiName is md5, try to find the correct apiName in the plugins
|
|
155
|
-
if (apiName.startsWith(PLUGIN_SCHEMA_API_MD5_PREFIX)) {
|
|
156
|
-
const md5 = apiName.replace(PLUGIN_SCHEMA_API_MD5_PREFIX, '');
|
|
157
|
-
const manifest = pluginSelectors.getPluginManifestById(identifier)(
|
|
158
|
-
useToolStore.getState(),
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
const api = manifest?.api.find((api) => Md5.hashStr(api.name).toString() === md5);
|
|
162
|
-
if (api) {
|
|
163
|
-
payload.apiName = api.name;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return payload;
|
|
168
|
-
})
|
|
169
|
-
.filter(Boolean) as ChatToolPayload[];
|
|
170
|
-
},
|
|
171
|
-
|
|
172
|
-
internal_updatePluginError: async (id, error) => {
|
|
173
|
-
const { refreshMessages } = get();
|
|
174
|
-
|
|
175
|
-
await messageService.updateMessage(id, { pluginError: error });
|
|
176
|
-
await refreshMessages();
|
|
177
|
-
},
|
|
178
|
-
|
|
179
78
|
invokeBuiltinTool: async (id, payload) => {
|
|
180
|
-
const {
|
|
79
|
+
const { internal_togglePluginApiCalling, internal_updateMessageContent } = get();
|
|
181
80
|
const params = JSON.parse(payload.arguments);
|
|
182
|
-
|
|
81
|
+
internal_togglePluginApiCalling(true, id, n('invokeBuiltinTool') as string);
|
|
183
82
|
let data;
|
|
184
83
|
try {
|
|
185
84
|
data = await useToolStore.getState().invokeBuiltinTool(payload.apiName, params);
|
|
186
85
|
} catch (error) {
|
|
187
86
|
console.log(error);
|
|
188
87
|
}
|
|
189
|
-
|
|
88
|
+
internal_togglePluginApiCalling(false);
|
|
190
89
|
|
|
191
90
|
if (!data) return;
|
|
192
91
|
|
|
@@ -248,6 +147,8 @@ export const chatPlugin: StateCreator<
|
|
|
248
147
|
const message = chatSelectors.getMessageById(id)(get());
|
|
249
148
|
if (!message || message.role !== 'tool' || !message.plugin) return;
|
|
250
149
|
|
|
150
|
+
get().internal_updateMessageError(id, null);
|
|
151
|
+
|
|
251
152
|
const payload: ChatToolPayload = { ...message.plugin, id: message.tool_call_id! };
|
|
252
153
|
|
|
253
154
|
await get().internal_invokeDifferentTypePlugin(id, payload);
|
|
@@ -301,4 +202,116 @@ export const chatPlugin: StateCreator<
|
|
|
301
202
|
await messageService.updateMessagePluginState(id, { [key]: value });
|
|
302
203
|
await refreshMessages();
|
|
303
204
|
},
|
|
205
|
+
|
|
206
|
+
internal_callPluginApi: async (id, payload) => {
|
|
207
|
+
const { internal_updateMessageContent, refreshMessages, internal_togglePluginApiCalling } =
|
|
208
|
+
get();
|
|
209
|
+
let data: string;
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const abortController = internal_togglePluginApiCalling(
|
|
213
|
+
true,
|
|
214
|
+
id,
|
|
215
|
+
n('fetchPlugin/start') as string,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const message = chatSelectors.getMessageById(id)(get());
|
|
219
|
+
|
|
220
|
+
const res = await chatService.runPluginApi(payload, {
|
|
221
|
+
signal: abortController?.signal,
|
|
222
|
+
trace: { observationId: message?.observationId, traceId: message?.traceId },
|
|
223
|
+
});
|
|
224
|
+
data = res.text;
|
|
225
|
+
|
|
226
|
+
// save traceId
|
|
227
|
+
if (res.traceId) {
|
|
228
|
+
await messageService.updateMessage(id, { traceId: res.traceId });
|
|
229
|
+
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.log(error);
|
|
232
|
+
const err = error as Error;
|
|
233
|
+
|
|
234
|
+
// ignore the aborted request error
|
|
235
|
+
if (!err.message.includes('The user aborted a request.')) {
|
|
236
|
+
await messageService.updateMessageError(id, error as any);
|
|
237
|
+
await refreshMessages();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
data = '';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
internal_togglePluginApiCalling(false, id, n('fetchPlugin/end') as string);
|
|
244
|
+
// 如果报错则结束了
|
|
245
|
+
if (!data) return;
|
|
246
|
+
|
|
247
|
+
await internal_updateMessageContent(id, data);
|
|
248
|
+
|
|
249
|
+
return data;
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
internal_invokeDifferentTypePlugin: async (id, payload) => {
|
|
253
|
+
switch (payload.type) {
|
|
254
|
+
case 'standalone': {
|
|
255
|
+
return await get().invokeStandaloneTypePlugin(id, payload);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
case 'markdown': {
|
|
259
|
+
return await get().invokeMarkdownTypePlugin(id, payload);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
case 'builtin': {
|
|
263
|
+
return await get().invokeBuiltinTool(id, payload);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
default: {
|
|
267
|
+
return await get().invokeDefaultTypePlugin(id, payload);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
internal_togglePluginApiCalling: (loading, id, action) => {
|
|
273
|
+
return get().internal_toggleLoadingArrays('pluginApiLoadingIds', loading, id, action);
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
internal_transformToolCalls: (toolCalls) => {
|
|
277
|
+
return toolCalls
|
|
278
|
+
.map((toolCall): ChatToolPayload | null => {
|
|
279
|
+
let payload: ChatToolPayload;
|
|
280
|
+
|
|
281
|
+
const [identifier, apiName, type] = toolCall.function.name.split(PLUGIN_SCHEMA_SEPARATOR);
|
|
282
|
+
|
|
283
|
+
if (!apiName) return null;
|
|
284
|
+
|
|
285
|
+
payload = {
|
|
286
|
+
apiName,
|
|
287
|
+
arguments: toolCall.function.arguments,
|
|
288
|
+
id: toolCall.id,
|
|
289
|
+
identifier,
|
|
290
|
+
type: (type ?? 'default') as any,
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// if the apiName is md5, try to find the correct apiName in the plugins
|
|
294
|
+
if (apiName.startsWith(PLUGIN_SCHEMA_API_MD5_PREFIX)) {
|
|
295
|
+
const md5 = apiName.replace(PLUGIN_SCHEMA_API_MD5_PREFIX, '');
|
|
296
|
+
const manifest = pluginSelectors.getPluginManifestById(identifier)(
|
|
297
|
+
useToolStore.getState(),
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
const api = manifest?.api.find((api) => Md5.hashStr(api.name).toString() === md5);
|
|
301
|
+
if (api) {
|
|
302
|
+
payload.apiName = api.name;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return payload;
|
|
307
|
+
})
|
|
308
|
+
.filter(Boolean) as ChatToolPayload[];
|
|
309
|
+
},
|
|
310
|
+
internal_updatePluginError: async (id, error) => {
|
|
311
|
+
const { refreshMessages } = get();
|
|
312
|
+
|
|
313
|
+
get().internal_dispatchMessage({ id, type: 'updateMessages', value: { error } });
|
|
314
|
+
await messageService.updateMessage(id, { pluginError: error });
|
|
315
|
+
await refreshMessages();
|
|
316
|
+
},
|
|
304
317
|
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { produce } from 'immer';
|
|
2
|
+
|
|
3
|
+
export const preventLeavingFn = (e: BeforeUnloadEvent) => {
|
|
4
|
+
// set returnValue to trigger alert modal
|
|
5
|
+
// Note: No matter what value is set, the browser will display the standard text
|
|
6
|
+
e.returnValue = '你有正在生成中的请求,确定要离开吗?';
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const toggleBooleanList = (ids: string[], id: string, loading: boolean) => {
|
|
10
|
+
return produce(ids, (draft) => {
|
|
11
|
+
if (loading) {
|
|
12
|
+
draft.push(id);
|
|
13
|
+
} else {
|
|
14
|
+
const index = draft.indexOf(id);
|
|
15
|
+
|
|
16
|
+
if (index >= 0) draft.splice(index, 1);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
};
|