@lobehub/lobehub 2.0.0-next.85 → 2.0.0-next.86

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.
Files changed (88) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/apps/desktop/src/main/modules/networkProxy/dispatcher.ts +16 -16
  3. package/apps/desktop/src/main/modules/networkProxy/tester.ts +11 -11
  4. package/apps/desktop/src/main/modules/networkProxy/urlBuilder.ts +3 -3
  5. package/apps/desktop/src/main/modules/networkProxy/validator.ts +10 -10
  6. package/changelog/v1.json +9 -0
  7. package/package.json +1 -1
  8. package/packages/agent-runtime/src/core/runtime.ts +36 -1
  9. package/packages/agent-runtime/src/types/event.ts +1 -0
  10. package/packages/agent-runtime/src/types/generalAgent.ts +16 -0
  11. package/packages/agent-runtime/src/types/instruction.ts +30 -0
  12. package/packages/agent-runtime/src/types/runtime.ts +7 -0
  13. package/packages/types/src/message/common/metadata.ts +3 -0
  14. package/packages/types/src/message/common/tools.ts +2 -2
  15. package/packages/types/src/tool/search/index.ts +8 -2
  16. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/index.tsx +2 -2
  17. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/useSend.ts +7 -2
  18. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/useSend.ts +15 -14
  19. package/src/app/[variants]/(main)/chat/session/features/SessionListContent/List/Item/index.tsx +2 -2
  20. package/src/features/ChatInput/ActionBar/STT/browser.tsx +2 -2
  21. package/src/features/ChatInput/ActionBar/STT/openai.tsx +2 -2
  22. package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +1 -1
  23. package/src/features/Conversation/Messages/User/index.tsx +3 -3
  24. package/src/features/Conversation/Messages/index.tsx +3 -3
  25. package/src/features/Conversation/components/AutoScroll.tsx +2 -2
  26. package/src/services/search.ts +2 -2
  27. package/src/store/chat/agents/GeneralChatAgent.ts +98 -0
  28. package/src/store/chat/agents/__tests__/GeneralChatAgent.test.ts +366 -0
  29. package/src/store/chat/agents/__tests__/createAgentExecutors/call-llm.test.ts +1217 -0
  30. package/src/store/chat/agents/__tests__/createAgentExecutors/call-tool.test.ts +1976 -0
  31. package/src/store/chat/agents/__tests__/createAgentExecutors/finish.test.ts +453 -0
  32. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/index.ts +4 -0
  33. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockInstructions.ts +126 -0
  34. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockMessages.ts +94 -0
  35. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockOperations.ts +96 -0
  36. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockStore.ts +138 -0
  37. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/assertions.ts +185 -0
  38. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/index.ts +3 -0
  39. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/operationTestUtils.ts +94 -0
  40. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/testExecutor.ts +139 -0
  41. package/src/store/chat/agents/__tests__/createAgentExecutors/request-human-approve.test.ts +545 -0
  42. package/src/store/chat/agents/__tests__/createAgentExecutors/resolve-aborted-tools.test.ts +686 -0
  43. package/src/store/chat/agents/createAgentExecutors.ts +313 -80
  44. package/src/store/chat/selectors.ts +1 -0
  45. package/src/store/chat/slices/aiChat/__tests__/ai-chat.integration.test.ts +667 -0
  46. package/src/store/chat/slices/aiChat/actions/__tests__/cancel-functionality.test.ts +137 -27
  47. package/src/store/chat/slices/aiChat/actions/__tests__/conversationControl.test.ts +163 -125
  48. package/src/store/chat/slices/aiChat/actions/__tests__/conversationLifecycle.test.ts +12 -2
  49. package/src/store/chat/slices/aiChat/actions/__tests__/fixtures.ts +0 -2
  50. package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +0 -2
  51. package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +286 -19
  52. package/src/store/chat/slices/aiChat/actions/__tests__/streamingStates.test.ts +0 -112
  53. package/src/store/chat/slices/aiChat/actions/conversationControl.ts +42 -99
  54. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +90 -57
  55. package/src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts +5 -25
  56. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +220 -98
  57. package/src/store/chat/slices/aiChat/actions/streamingStates.ts +0 -34
  58. package/src/store/chat/slices/aiChat/initialState.ts +0 -28
  59. package/src/store/chat/slices/aiChat/selectors.test.ts +280 -0
  60. package/src/store/chat/slices/aiChat/selectors.ts +31 -7
  61. package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +21 -30
  62. package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +29 -49
  63. package/src/store/chat/slices/builtinTool/actions/interpreter.ts +83 -48
  64. package/src/store/chat/slices/builtinTool/actions/localSystem.ts +78 -28
  65. package/src/store/chat/slices/builtinTool/actions/search.ts +146 -59
  66. package/src/store/chat/slices/builtinTool/selectors.test.ts +258 -0
  67. package/src/store/chat/slices/builtinTool/selectors.ts +25 -4
  68. package/src/store/chat/slices/message/action.test.ts +134 -16
  69. package/src/store/chat/slices/message/actions/internals.ts +33 -7
  70. package/src/store/chat/slices/message/actions/optimisticUpdate.ts +85 -52
  71. package/src/store/chat/slices/message/initialState.ts +0 -10
  72. package/src/store/chat/slices/message/selectors/messageState.ts +34 -12
  73. package/src/store/chat/slices/operation/__tests__/actions.test.ts +712 -16
  74. package/src/store/chat/slices/operation/__tests__/integration.test.ts +342 -0
  75. package/src/store/chat/slices/operation/__tests__/selectors.test.ts +257 -17
  76. package/src/store/chat/slices/operation/actions.ts +218 -11
  77. package/src/store/chat/slices/operation/selectors.ts +135 -6
  78. package/src/store/chat/slices/operation/types.ts +29 -3
  79. package/src/store/chat/slices/plugin/action.test.ts +30 -322
  80. package/src/store/chat/slices/plugin/actions/internals.ts +0 -14
  81. package/src/store/chat/slices/plugin/actions/optimisticUpdate.ts +21 -19
  82. package/src/store/chat/slices/plugin/actions/pluginTypes.ts +45 -27
  83. package/src/store/chat/slices/plugin/actions/publicApi.ts +3 -4
  84. package/src/store/chat/slices/plugin/actions/workflow.ts +0 -55
  85. package/src/store/chat/slices/thread/selectors/index.ts +4 -2
  86. package/src/store/chat/slices/translate/action.ts +54 -41
  87. package/src/tools/web-browsing/ExecutionRuntime/index.ts +5 -2
  88. 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,4 @@
1
+ export * from './mockInstructions';
2
+ export * from './mockMessages';
3
+ export * from './mockOperations';
4
+ export * from './mockStore';
@@ -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
+ };