@lobehub/lobehub 2.0.0-next.85 → 2.0.0-next.87
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 +50 -0
- package/apps/desktop/src/main/modules/networkProxy/dispatcher.ts +16 -16
- package/apps/desktop/src/main/modules/networkProxy/tester.ts +11 -11
- package/apps/desktop/src/main/modules/networkProxy/urlBuilder.ts +3 -3
- package/apps/desktop/src/main/modules/networkProxy/validator.ts +10 -10
- package/changelog/v1.json +18 -0
- package/package.json +1 -1
- package/packages/agent-runtime/src/core/runtime.ts +36 -1
- package/packages/agent-runtime/src/types/event.ts +1 -0
- package/packages/agent-runtime/src/types/generalAgent.ts +16 -0
- package/packages/agent-runtime/src/types/instruction.ts +30 -0
- package/packages/agent-runtime/src/types/runtime.ts +7 -0
- package/packages/types/src/message/common/metadata.ts +3 -0
- package/packages/types/src/message/common/tools.ts +2 -2
- package/packages/types/src/tool/search/index.ts +8 -2
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/index.tsx +2 -2
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/useSend.ts +7 -2
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/useSend.ts +15 -14
- package/src/app/[variants]/(main)/chat/session/features/SessionListContent/List/Item/index.tsx +2 -2
- package/src/components/Analytics/MainInterfaceTracker.tsx +2 -2
- package/src/features/ChatInput/ActionBar/STT/browser.tsx +2 -2
- package/src/features/ChatInput/ActionBar/STT/openai.tsx +2 -2
- package/src/features/Conversation/MarkdownElements/LobeThinking/Render.tsx +3 -3
- package/src/features/Conversation/MarkdownElements/Thinking/Render.tsx +3 -3
- package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +1 -1
- package/src/features/Conversation/Messages/User/index.tsx +3 -3
- package/src/features/Conversation/Messages/index.tsx +3 -3
- package/src/features/Conversation/components/AutoScroll.tsx +2 -2
- package/src/features/PluginsUI/Render/StandaloneType/Iframe.tsx +3 -3
- package/src/features/Portal/Home/Body/Plugins/ArtifactList/index.tsx +3 -3
- package/src/features/ShareModal/ShareText/index.tsx +3 -3
- package/src/services/search.ts +2 -2
- package/src/store/chat/agents/GeneralChatAgent.ts +98 -0
- package/src/store/chat/agents/__tests__/GeneralChatAgent.test.ts +366 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/call-llm.test.ts +1217 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/call-tool.test.ts +1976 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/finish.test.ts +453 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/index.ts +4 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockInstructions.ts +126 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockMessages.ts +94 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockOperations.ts +96 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockStore.ts +138 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/assertions.ts +185 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/index.ts +3 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/operationTestUtils.ts +94 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/testExecutor.ts +139 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/request-human-approve.test.ts +545 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/resolve-aborted-tools.test.ts +686 -0
- package/src/store/chat/agents/createAgentExecutors.ts +313 -80
- package/src/store/chat/selectors.ts +1 -0
- package/src/store/chat/slices/aiChat/__tests__/ai-chat.integration.test.ts +667 -0
- package/src/store/chat/slices/aiChat/actions/__tests__/cancel-functionality.test.ts +137 -27
- package/src/store/chat/slices/aiChat/actions/__tests__/conversationControl.test.ts +163 -125
- package/src/store/chat/slices/aiChat/actions/__tests__/conversationLifecycle.test.ts +12 -2
- package/src/store/chat/slices/aiChat/actions/__tests__/fixtures.ts +0 -2
- package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +0 -2
- package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +286 -19
- package/src/store/chat/slices/aiChat/actions/__tests__/streamingStates.test.ts +0 -112
- package/src/store/chat/slices/aiChat/actions/conversationControl.ts +42 -99
- package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +90 -57
- package/src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts +5 -25
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +220 -98
- package/src/store/chat/slices/aiChat/actions/streamingStates.ts +0 -34
- package/src/store/chat/slices/aiChat/initialState.ts +0 -28
- package/src/store/chat/slices/aiChat/selectors.test.ts +280 -0
- package/src/store/chat/slices/aiChat/selectors.ts +31 -7
- package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +21 -30
- package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +29 -49
- package/src/store/chat/slices/builtinTool/actions/interpreter.ts +83 -48
- package/src/store/chat/slices/builtinTool/actions/localSystem.ts +78 -28
- package/src/store/chat/slices/builtinTool/actions/search.ts +146 -59
- package/src/store/chat/slices/builtinTool/selectors.test.ts +258 -0
- package/src/store/chat/slices/builtinTool/selectors.ts +25 -4
- package/src/store/chat/slices/message/action.test.ts +134 -16
- package/src/store/chat/slices/message/actions/internals.ts +33 -7
- package/src/store/chat/slices/message/actions/optimisticUpdate.ts +85 -52
- package/src/store/chat/slices/message/initialState.ts +0 -10
- package/src/store/chat/slices/message/selectors/messageState.ts +34 -12
- package/src/store/chat/slices/operation/__tests__/actions.test.ts +712 -16
- package/src/store/chat/slices/operation/__tests__/integration.test.ts +342 -0
- package/src/store/chat/slices/operation/__tests__/selectors.test.ts +257 -17
- package/src/store/chat/slices/operation/actions.ts +218 -11
- package/src/store/chat/slices/operation/selectors.ts +135 -6
- package/src/store/chat/slices/operation/types.ts +29 -3
- package/src/store/chat/slices/plugin/action.test.ts +30 -322
- package/src/store/chat/slices/plugin/actions/internals.ts +0 -14
- package/src/store/chat/slices/plugin/actions/optimisticUpdate.ts +21 -19
- package/src/store/chat/slices/plugin/actions/pluginTypes.ts +45 -27
- package/src/store/chat/slices/plugin/actions/publicApi.ts +3 -4
- package/src/store/chat/slices/plugin/actions/workflow.ts +0 -55
- package/src/store/chat/slices/thread/selectors/index.ts +4 -2
- package/src/store/chat/slices/topic/action.ts +3 -3
- package/src/store/chat/slices/translate/action.ts +54 -41
- package/src/tools/web-browsing/ExecutionRuntime/index.ts +5 -2
- package/src/tools/web-browsing/Portal/Search/Footer.tsx +11 -9
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import type { AgentEventDone } from '@lobechat/agent-runtime';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { createFinishInstruction } from './fixtures';
|
|
5
|
+
import { createMockStore } from './fixtures/mockStore';
|
|
6
|
+
import { createInitialState, createTestContext, executeWithMockContext } from './helpers';
|
|
7
|
+
|
|
8
|
+
describe('finish executor', () => {
|
|
9
|
+
describe('Basic Behavior', () => {
|
|
10
|
+
it('should complete execution successfully with reason', async () => {
|
|
11
|
+
// Given
|
|
12
|
+
const mockStore = createMockStore();
|
|
13
|
+
const context = createTestContext();
|
|
14
|
+
const instruction = createFinishInstruction('completed', 'All tasks finished');
|
|
15
|
+
const state = createInitialState({ sessionId: 'test-session', stepCount: 5 });
|
|
16
|
+
|
|
17
|
+
// When
|
|
18
|
+
const result = await executeWithMockContext({
|
|
19
|
+
executor: 'finish',
|
|
20
|
+
instruction,
|
|
21
|
+
state,
|
|
22
|
+
mockStore,
|
|
23
|
+
context,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Then
|
|
27
|
+
expect(result.newState.status).toBe('done');
|
|
28
|
+
expect(result.newState.lastModified).toBeDefined();
|
|
29
|
+
expect(new Date(result.newState.lastModified).getTime()).toBeGreaterThanOrEqual(
|
|
30
|
+
new Date(state.lastModified).getTime(),
|
|
31
|
+
);
|
|
32
|
+
expect(result.events).toHaveLength(1);
|
|
33
|
+
expect(result.events[0]).toMatchObject({
|
|
34
|
+
type: 'done',
|
|
35
|
+
reason: 'completed',
|
|
36
|
+
reasonDetail: 'All tasks finished',
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should preserve all state fields except status and lastModified', async () => {
|
|
41
|
+
// Given
|
|
42
|
+
const mockStore = createMockStore();
|
|
43
|
+
const context = createTestContext();
|
|
44
|
+
const instruction = createFinishInstruction('completed');
|
|
45
|
+
const state = createInitialState({
|
|
46
|
+
sessionId: 'test-session',
|
|
47
|
+
stepCount: 10,
|
|
48
|
+
messages: [{ role: 'user', content: 'test' } as any],
|
|
49
|
+
cost: {
|
|
50
|
+
total: 0.05,
|
|
51
|
+
calculatedAt: new Date().toISOString(),
|
|
52
|
+
currency: 'USD',
|
|
53
|
+
llm: { total: 0.04, currency: 'USD', byModel: [] },
|
|
54
|
+
tools: { total: 0.01, currency: 'USD', byTool: [] },
|
|
55
|
+
},
|
|
56
|
+
usage: {
|
|
57
|
+
humanInteraction: {
|
|
58
|
+
approvalRequests: 0,
|
|
59
|
+
promptRequests: 0,
|
|
60
|
+
selectRequests: 0,
|
|
61
|
+
totalWaitingTimeMs: 0,
|
|
62
|
+
},
|
|
63
|
+
llm: {
|
|
64
|
+
apiCalls: 2,
|
|
65
|
+
processingTimeMs: 100,
|
|
66
|
+
tokens: {
|
|
67
|
+
input: 100,
|
|
68
|
+
output: 200,
|
|
69
|
+
total: 300,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
tools: {
|
|
73
|
+
totalCalls: 2,
|
|
74
|
+
totalTimeMs: 500,
|
|
75
|
+
byTool: [],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// When
|
|
81
|
+
const result = await executeWithMockContext({
|
|
82
|
+
executor: 'finish',
|
|
83
|
+
instruction,
|
|
84
|
+
state,
|
|
85
|
+
mockStore,
|
|
86
|
+
context,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Then
|
|
90
|
+
expect(result.newState.sessionId).toBe(state.sessionId);
|
|
91
|
+
expect(result.newState.stepCount).toBe(state.stepCount);
|
|
92
|
+
expect(result.newState.messages).toEqual(state.messages);
|
|
93
|
+
expect(result.newState.usage).toEqual(state.usage);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should work without reasonDetail', async () => {
|
|
97
|
+
// Given
|
|
98
|
+
const mockStore = createMockStore();
|
|
99
|
+
const context = createTestContext();
|
|
100
|
+
const instruction = createFinishInstruction('max_turns_reached');
|
|
101
|
+
const state = createInitialState();
|
|
102
|
+
|
|
103
|
+
// When
|
|
104
|
+
const result = await executeWithMockContext({
|
|
105
|
+
executor: 'finish',
|
|
106
|
+
instruction,
|
|
107
|
+
state,
|
|
108
|
+
mockStore,
|
|
109
|
+
context,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Then
|
|
113
|
+
expect(result.newState.status).toBe('done');
|
|
114
|
+
const doneEvent = result.events[0] as AgentEventDone;
|
|
115
|
+
expect(doneEvent).toMatchObject({
|
|
116
|
+
type: 'done',
|
|
117
|
+
reason: 'max_turns_reached',
|
|
118
|
+
});
|
|
119
|
+
expect(doneEvent.reasonDetail).toBeUndefined();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('Different Finish Reasons', () => {
|
|
124
|
+
it('should handle "completed" reason', async () => {
|
|
125
|
+
// Given
|
|
126
|
+
const mockStore = createMockStore();
|
|
127
|
+
const context = createTestContext();
|
|
128
|
+
const instruction = createFinishInstruction('completed', 'Task completed successfully');
|
|
129
|
+
const state = createInitialState();
|
|
130
|
+
|
|
131
|
+
// When
|
|
132
|
+
const result = await executeWithMockContext({
|
|
133
|
+
executor: 'finish',
|
|
134
|
+
instruction,
|
|
135
|
+
state,
|
|
136
|
+
mockStore,
|
|
137
|
+
context,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Then
|
|
141
|
+
const doneEvent = result.events[0] as AgentEventDone;
|
|
142
|
+
expect(doneEvent.reason).toBe('completed');
|
|
143
|
+
expect(doneEvent.reasonDetail).toBe('Task completed successfully');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should handle "max_turns_reached" reason', async () => {
|
|
147
|
+
// Given
|
|
148
|
+
const mockStore = createMockStore();
|
|
149
|
+
const context = createTestContext();
|
|
150
|
+
const instruction = createFinishInstruction(
|
|
151
|
+
'max_turns_reached',
|
|
152
|
+
'Maximum conversation turns exceeded',
|
|
153
|
+
);
|
|
154
|
+
const state = createInitialState({ stepCount: 100 });
|
|
155
|
+
|
|
156
|
+
// When
|
|
157
|
+
const result = await executeWithMockContext({
|
|
158
|
+
executor: 'finish',
|
|
159
|
+
instruction,
|
|
160
|
+
state,
|
|
161
|
+
mockStore,
|
|
162
|
+
context,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Then
|
|
166
|
+
const doneEvent = result.events[0] as AgentEventDone;
|
|
167
|
+
expect(doneEvent.reason).toBe('max_turns_reached');
|
|
168
|
+
expect(result.newState.stepCount).toBe(100);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should handle "error" reason', async () => {
|
|
172
|
+
// Given
|
|
173
|
+
const mockStore = createMockStore();
|
|
174
|
+
const context = createTestContext();
|
|
175
|
+
const instruction = createFinishInstruction('error', 'Internal runtime error occurred');
|
|
176
|
+
const state = createInitialState();
|
|
177
|
+
|
|
178
|
+
// When
|
|
179
|
+
const result = await executeWithMockContext({
|
|
180
|
+
executor: 'finish',
|
|
181
|
+
instruction,
|
|
182
|
+
state,
|
|
183
|
+
mockStore,
|
|
184
|
+
context,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Then
|
|
188
|
+
const doneEvent = result.events[0] as AgentEventDone;
|
|
189
|
+
expect(doneEvent.reason).toBe('error');
|
|
190
|
+
expect(result.newState.status).toBe('done');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should handle "user_cancelled" reason', async () => {
|
|
194
|
+
// Given
|
|
195
|
+
const mockStore = createMockStore();
|
|
196
|
+
const context = createTestContext();
|
|
197
|
+
const instruction = createFinishInstruction('user_cancelled', 'User requested cancellation');
|
|
198
|
+
const state = createInitialState();
|
|
199
|
+
|
|
200
|
+
// When
|
|
201
|
+
const result = await executeWithMockContext({
|
|
202
|
+
executor: 'finish',
|
|
203
|
+
instruction,
|
|
204
|
+
state,
|
|
205
|
+
mockStore,
|
|
206
|
+
context,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Then
|
|
210
|
+
const doneEvent = result.events[0] as AgentEventDone;
|
|
211
|
+
expect(doneEvent.reason).toBe('user_cancelled');
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('Event Structure', () => {
|
|
216
|
+
it('should emit done event with finalState', async () => {
|
|
217
|
+
// Given
|
|
218
|
+
const mockStore = createMockStore();
|
|
219
|
+
const context = createTestContext();
|
|
220
|
+
const instruction = createFinishInstruction('completed');
|
|
221
|
+
const state = createInitialState();
|
|
222
|
+
|
|
223
|
+
// When
|
|
224
|
+
const result = await executeWithMockContext({
|
|
225
|
+
executor: 'finish',
|
|
226
|
+
instruction,
|
|
227
|
+
state,
|
|
228
|
+
mockStore,
|
|
229
|
+
context,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Then
|
|
233
|
+
expect(result.events).toHaveLength(1);
|
|
234
|
+
const doneEvent = result.events[0] as AgentEventDone;
|
|
235
|
+
expect(doneEvent).toHaveProperty('type', 'done');
|
|
236
|
+
expect(doneEvent).toHaveProperty('finalState');
|
|
237
|
+
expect(doneEvent).toHaveProperty('reason');
|
|
238
|
+
expect(doneEvent.finalState).toEqual(result.newState);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should include both reason and reasonDetail in event', async () => {
|
|
242
|
+
// Given
|
|
243
|
+
const mockStore = createMockStore();
|
|
244
|
+
const context = createTestContext();
|
|
245
|
+
const instruction = createFinishInstruction('completed', 'Detailed completion message');
|
|
246
|
+
const state = createInitialState();
|
|
247
|
+
|
|
248
|
+
// When
|
|
249
|
+
const result = await executeWithMockContext({
|
|
250
|
+
executor: 'finish',
|
|
251
|
+
instruction,
|
|
252
|
+
state,
|
|
253
|
+
mockStore,
|
|
254
|
+
context,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Then
|
|
258
|
+
const doneEvent = result.events[0] as AgentEventDone;
|
|
259
|
+
expect(doneEvent.reason).toBe('completed');
|
|
260
|
+
expect(doneEvent.reasonDetail).toBe('Detailed completion message');
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe('State Immutability', () => {
|
|
265
|
+
it('should not mutate original state', async () => {
|
|
266
|
+
// Given
|
|
267
|
+
const mockStore = createMockStore();
|
|
268
|
+
const context = createTestContext();
|
|
269
|
+
const instruction = createFinishInstruction('completed');
|
|
270
|
+
const state = createInitialState({ sessionId: 'test', stepCount: 5 });
|
|
271
|
+
const originalState = JSON.parse(JSON.stringify(state));
|
|
272
|
+
|
|
273
|
+
// When
|
|
274
|
+
const result = await executeWithMockContext({
|
|
275
|
+
executor: 'finish',
|
|
276
|
+
instruction,
|
|
277
|
+
state,
|
|
278
|
+
mockStore,
|
|
279
|
+
context,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// Then
|
|
283
|
+
expect(state).toEqual(originalState);
|
|
284
|
+
expect(result.newState).not.toBe(state);
|
|
285
|
+
expect(result.newState.status).toBe('done');
|
|
286
|
+
expect(state.status).toBe('running');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should create deep clone of state', async () => {
|
|
290
|
+
// Given
|
|
291
|
+
const mockStore = createMockStore();
|
|
292
|
+
const context = createTestContext();
|
|
293
|
+
const instruction = createFinishInstruction('completed');
|
|
294
|
+
const state = createInitialState({
|
|
295
|
+
messages: [{ role: 'user', content: 'test' } as any],
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// When
|
|
299
|
+
const result = await executeWithMockContext({
|
|
300
|
+
executor: 'finish',
|
|
301
|
+
instruction,
|
|
302
|
+
state,
|
|
303
|
+
mockStore,
|
|
304
|
+
context,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Then
|
|
308
|
+
expect(result.newState.messages).toEqual(state.messages);
|
|
309
|
+
expect(result.newState.messages).not.toBe(state.messages);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
describe('Timestamp Handling', () => {
|
|
314
|
+
it('should update lastModified timestamp', async () => {
|
|
315
|
+
// Given
|
|
316
|
+
const mockStore = createMockStore();
|
|
317
|
+
const context = createTestContext();
|
|
318
|
+
const instruction = createFinishInstruction('completed');
|
|
319
|
+
const oldTimestamp = new Date('2024-01-01').toISOString();
|
|
320
|
+
const state = createInitialState({ lastModified: oldTimestamp });
|
|
321
|
+
|
|
322
|
+
// When
|
|
323
|
+
const result = await executeWithMockContext({
|
|
324
|
+
executor: 'finish',
|
|
325
|
+
instruction,
|
|
326
|
+
state,
|
|
327
|
+
mockStore,
|
|
328
|
+
context,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Then
|
|
332
|
+
expect(result.newState.lastModified).not.toBe(oldTimestamp);
|
|
333
|
+
expect(new Date(result.newState.lastModified).getTime()).toBeGreaterThan(
|
|
334
|
+
new Date(oldTimestamp).getTime(),
|
|
335
|
+
);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should use ISO 8601 format for lastModified', async () => {
|
|
339
|
+
// Given
|
|
340
|
+
const mockStore = createMockStore();
|
|
341
|
+
const context = createTestContext();
|
|
342
|
+
const instruction = createFinishInstruction('completed');
|
|
343
|
+
const state = createInitialState();
|
|
344
|
+
|
|
345
|
+
// When
|
|
346
|
+
const result = await executeWithMockContext({
|
|
347
|
+
executor: 'finish',
|
|
348
|
+
instruction,
|
|
349
|
+
state,
|
|
350
|
+
mockStore,
|
|
351
|
+
context,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Then
|
|
355
|
+
const isoPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
356
|
+
expect(result.newState.lastModified).toMatch(isoPattern);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
describe('Store Interaction', () => {
|
|
361
|
+
it('should not call any store methods', async () => {
|
|
362
|
+
// Given
|
|
363
|
+
const mockStore = createMockStore();
|
|
364
|
+
const context = createTestContext();
|
|
365
|
+
const instruction = createFinishInstruction('completed');
|
|
366
|
+
const state = createInitialState();
|
|
367
|
+
|
|
368
|
+
// When
|
|
369
|
+
await executeWithMockContext({
|
|
370
|
+
executor: 'finish',
|
|
371
|
+
instruction,
|
|
372
|
+
state,
|
|
373
|
+
mockStore,
|
|
374
|
+
context,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Then - finish executor is pure and doesn't interact with store
|
|
378
|
+
expect(mockStore.optimisticCreateMessage).not.toHaveBeenCalled();
|
|
379
|
+
expect(mockStore.startOperation).not.toHaveBeenCalled();
|
|
380
|
+
expect(mockStore.completeOperation).not.toHaveBeenCalled();
|
|
381
|
+
expect(mockStore.failOperation).not.toHaveBeenCalled();
|
|
382
|
+
expect(mockStore.onOperationCancel).not.toHaveBeenCalled();
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
describe('Edge Cases', () => {
|
|
387
|
+
it('should handle empty reason string', async () => {
|
|
388
|
+
// Given
|
|
389
|
+
const mockStore = createMockStore();
|
|
390
|
+
const context = createTestContext();
|
|
391
|
+
const instruction = createFinishInstruction('');
|
|
392
|
+
const state = createInitialState();
|
|
393
|
+
|
|
394
|
+
// When
|
|
395
|
+
const result = await executeWithMockContext({
|
|
396
|
+
executor: 'finish',
|
|
397
|
+
instruction,
|
|
398
|
+
state,
|
|
399
|
+
mockStore,
|
|
400
|
+
context,
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Then
|
|
404
|
+
expect(result.newState.status).toBe('done');
|
|
405
|
+
const doneEvent = result.events[0] as AgentEventDone;
|
|
406
|
+
expect(doneEvent.reason).toBe('');
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('should handle very long reasonDetail', async () => {
|
|
410
|
+
// Given
|
|
411
|
+
const mockStore = createMockStore();
|
|
412
|
+
const context = createTestContext();
|
|
413
|
+
const longDetail = 'A'.repeat(10000);
|
|
414
|
+
const instruction = createFinishInstruction('completed', longDetail);
|
|
415
|
+
const state = createInitialState();
|
|
416
|
+
|
|
417
|
+
// When
|
|
418
|
+
const result = await executeWithMockContext({
|
|
419
|
+
executor: 'finish',
|
|
420
|
+
instruction,
|
|
421
|
+
state,
|
|
422
|
+
mockStore,
|
|
423
|
+
context,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// Then
|
|
427
|
+
const doneEvent = result.events[0] as AgentEventDone;
|
|
428
|
+
expect(doneEvent.reasonDetail).toBe(longDetail);
|
|
429
|
+
expect(doneEvent.reasonDetail?.length).toBe(10000);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('should handle state with empty messages array', async () => {
|
|
433
|
+
// Given
|
|
434
|
+
const mockStore = createMockStore();
|
|
435
|
+
const context = createTestContext();
|
|
436
|
+
const instruction = createFinishInstruction('completed');
|
|
437
|
+
const state = createInitialState({ messages: [] });
|
|
438
|
+
|
|
439
|
+
// When
|
|
440
|
+
const result = await executeWithMockContext({
|
|
441
|
+
executor: 'finish',
|
|
442
|
+
instruction,
|
|
443
|
+
state,
|
|
444
|
+
mockStore,
|
|
445
|
+
context,
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// Then
|
|
449
|
+
expect(result.newState.messages).toEqual([]);
|
|
450
|
+
expect(result.newState.status).toBe('done');
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentInstruction,
|
|
3
|
+
AgentInstructionCallLlm,
|
|
4
|
+
AgentInstructionCallTool,
|
|
5
|
+
GeneralAgentCallLLMInstructionPayload,
|
|
6
|
+
GeneralAgentCallingToolInstructionPayload,
|
|
7
|
+
} from '@lobechat/agent-runtime';
|
|
8
|
+
import type { ChatToolPayload } from '@lobechat/types';
|
|
9
|
+
import { nanoid } from '@lobechat/utils';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create a mock call_llm instruction
|
|
13
|
+
*/
|
|
14
|
+
export const createCallLLMInstruction = (
|
|
15
|
+
payload: Partial<GeneralAgentCallLLMInstructionPayload> = {},
|
|
16
|
+
): AgentInstructionCallLlm => {
|
|
17
|
+
return {
|
|
18
|
+
payload: {
|
|
19
|
+
messages: [],
|
|
20
|
+
model: 'gpt-4',
|
|
21
|
+
parentMessageId: `msg_${nanoid()}`,
|
|
22
|
+
provider: 'openai',
|
|
23
|
+
...payload,
|
|
24
|
+
} as GeneralAgentCallLLMInstructionPayload,
|
|
25
|
+
type: 'call_llm',
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a mock call_tool instruction
|
|
31
|
+
*/
|
|
32
|
+
export const createCallToolInstruction = (
|
|
33
|
+
toolCall: Partial<ChatToolPayload> = {},
|
|
34
|
+
options: {
|
|
35
|
+
parentMessageId?: string;
|
|
36
|
+
skipCreateToolMessage?: boolean;
|
|
37
|
+
} = {},
|
|
38
|
+
): AgentInstructionCallTool => {
|
|
39
|
+
const toolPayload: ChatToolPayload = {
|
|
40
|
+
apiName: 'search',
|
|
41
|
+
arguments: JSON.stringify({ query: 'test' }),
|
|
42
|
+
id: `tool_call_${nanoid()}`,
|
|
43
|
+
identifier: 'lobe-web-browsing',
|
|
44
|
+
type: 'default',
|
|
45
|
+
...toolCall,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
payload: {
|
|
50
|
+
parentMessageId: options.parentMessageId || `msg_${nanoid()}`,
|
|
51
|
+
skipCreateToolMessage: options.skipCreateToolMessage || false,
|
|
52
|
+
toolCalling: toolPayload,
|
|
53
|
+
} as GeneralAgentCallingToolInstructionPayload,
|
|
54
|
+
type: 'call_tool',
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create a mock request_human_approve instruction
|
|
60
|
+
*/
|
|
61
|
+
export const createRequestHumanApproveInstruction = (
|
|
62
|
+
pendingTools: ChatToolPayload[] = [],
|
|
63
|
+
options: {
|
|
64
|
+
reason?: string;
|
|
65
|
+
skipCreateToolMessage?: boolean;
|
|
66
|
+
} = {},
|
|
67
|
+
): AgentInstruction => {
|
|
68
|
+
const pendingToolsCalling = pendingTools.length
|
|
69
|
+
? pendingTools
|
|
70
|
+
: [
|
|
71
|
+
{
|
|
72
|
+
apiName: 'search',
|
|
73
|
+
arguments: JSON.stringify({ query: 'test' }),
|
|
74
|
+
id: `tool_call_${nanoid()}`,
|
|
75
|
+
identifier: 'lobe-web-browsing',
|
|
76
|
+
type: 'default',
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
pendingToolsCalling,
|
|
82
|
+
reason: options.reason,
|
|
83
|
+
skipCreateToolMessage: options.skipCreateToolMessage || false,
|
|
84
|
+
type: 'request_human_approve',
|
|
85
|
+
} as AgentInstruction;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Create a mock resolve_aborted_tools instruction
|
|
90
|
+
*/
|
|
91
|
+
export const createResolveAbortedToolsInstruction = (
|
|
92
|
+
toolsCalling: ChatToolPayload[] = [],
|
|
93
|
+
parentMessageId?: string,
|
|
94
|
+
): AgentInstruction => {
|
|
95
|
+
return {
|
|
96
|
+
payload: {
|
|
97
|
+
parentMessageId: parentMessageId || `msg_${nanoid()}`,
|
|
98
|
+
toolsCalling: toolsCalling.length
|
|
99
|
+
? toolsCalling
|
|
100
|
+
: [
|
|
101
|
+
{
|
|
102
|
+
apiName: 'search',
|
|
103
|
+
arguments: JSON.stringify({ query: 'test' }),
|
|
104
|
+
id: `tool_call_${nanoid()}`,
|
|
105
|
+
identifier: 'lobe-web-browsing',
|
|
106
|
+
type: 'default',
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
type: 'resolve_aborted_tools',
|
|
111
|
+
} as AgentInstruction;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Create a mock finish instruction
|
|
116
|
+
*/
|
|
117
|
+
export const createFinishInstruction = (
|
|
118
|
+
reason: string = 'completed',
|
|
119
|
+
reasonDetail?: string,
|
|
120
|
+
): AgentInstruction => {
|
|
121
|
+
return {
|
|
122
|
+
reason,
|
|
123
|
+
reasonDetail,
|
|
124
|
+
type: 'finish',
|
|
125
|
+
} as AgentInstruction;
|
|
126
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { UIChatMessage } from '@lobechat/types';
|
|
2
|
+
import { nanoid } from '@lobechat/utils';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create a mock assistant message
|
|
6
|
+
*/
|
|
7
|
+
export const createAssistantMessage = (overrides: Partial<UIChatMessage> = {}): UIChatMessage => {
|
|
8
|
+
return {
|
|
9
|
+
content: 'I am an AI assistant.',
|
|
10
|
+
createdAt: Date.now(),
|
|
11
|
+
id: `msg_${nanoid()}`,
|
|
12
|
+
meta: {},
|
|
13
|
+
model: 'gpt-4',
|
|
14
|
+
provider: 'openai',
|
|
15
|
+
role: 'assistant',
|
|
16
|
+
updatedAt: Date.now(),
|
|
17
|
+
...overrides,
|
|
18
|
+
} as UIChatMessage;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create a mock user message
|
|
23
|
+
*/
|
|
24
|
+
export const createUserMessage = (overrides: Partial<UIChatMessage> = {}): UIChatMessage => {
|
|
25
|
+
return {
|
|
26
|
+
content: 'Hello, AI!',
|
|
27
|
+
createdAt: Date.now(),
|
|
28
|
+
id: `msg_${nanoid()}`,
|
|
29
|
+
meta: {},
|
|
30
|
+
role: 'user',
|
|
31
|
+
updatedAt: Date.now(),
|
|
32
|
+
...overrides,
|
|
33
|
+
} as UIChatMessage;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create a mock tool message
|
|
38
|
+
*/
|
|
39
|
+
export const createToolMessage = (overrides: Partial<UIChatMessage> = {}): UIChatMessage => {
|
|
40
|
+
return {
|
|
41
|
+
content: '',
|
|
42
|
+
createdAt: Date.now(),
|
|
43
|
+
id: `msg_${nanoid()}`,
|
|
44
|
+
meta: {},
|
|
45
|
+
plugin: {
|
|
46
|
+
apiName: 'search',
|
|
47
|
+
arguments: JSON.stringify({ query: 'test' }),
|
|
48
|
+
identifier: 'lobe-web-browsing',
|
|
49
|
+
type: 'default',
|
|
50
|
+
},
|
|
51
|
+
role: 'tool',
|
|
52
|
+
tool_call_id: `tool_call_${nanoid()}`,
|
|
53
|
+
updatedAt: Date.now(),
|
|
54
|
+
...overrides,
|
|
55
|
+
} as UIChatMessage;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create a mock tool message with pending intervention
|
|
60
|
+
*/
|
|
61
|
+
export const createPendingToolMessage = (overrides: Partial<UIChatMessage> = {}): UIChatMessage => {
|
|
62
|
+
return createToolMessage({
|
|
63
|
+
pluginIntervention: { status: 'pending' },
|
|
64
|
+
...overrides,
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Create a mock tool message with aborted intervention
|
|
70
|
+
*/
|
|
71
|
+
export const createAbortedToolMessage = (overrides: Partial<UIChatMessage> = {}): UIChatMessage => {
|
|
72
|
+
return createToolMessage({
|
|
73
|
+
content: 'Tool execution was cancelled by user.',
|
|
74
|
+
pluginIntervention: { status: 'aborted' },
|
|
75
|
+
...overrides,
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Create a conversation history
|
|
81
|
+
*/
|
|
82
|
+
export const createConversationHistory = (messageCount: number = 3): UIChatMessage[] => {
|
|
83
|
+
const messages: UIChatMessage[] = [];
|
|
84
|
+
|
|
85
|
+
for (let i = 0; i < messageCount; i++) {
|
|
86
|
+
if (i % 2 === 0) {
|
|
87
|
+
messages.push(createUserMessage({ content: `User message ${i + 1}` }));
|
|
88
|
+
} else {
|
|
89
|
+
messages.push(createAssistantMessage({ content: `Assistant response ${i + 1}` }));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return messages;
|
|
94
|
+
};
|