@lobehub/lobehub 2.1.23 → 2.1.25

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 (53) hide show
  1. package/.github/workflows/claude-auto-e2e-testing.yml +131 -0
  2. package/CHANGELOG.md +42 -0
  3. package/changelog/v2.json +14 -0
  4. package/docs/development/database-schema.dbml +43 -14
  5. package/package.json +1 -1
  6. package/packages/database/migrations/0078_added_id_nanoid_for_replacing_id.sql +7 -0
  7. package/packages/database/migrations/0079_update_id_nanoid_from_casted_id.sql +7 -0
  8. package/packages/database/migrations/0080_add_constraint_unique_not_null_to_id_nanoid.sql +27 -0
  9. package/packages/database/migrations/0081_switch_forgien_key_to_id_nanoid.sql +37 -0
  10. package/packages/database/migrations/0082_set_id_nanoid_as_primary.sql +20 -0
  11. package/packages/database/migrations/0083_remove_id_seq_identity_column.sql +7 -0
  12. package/packages/database/migrations/0084_rename_id_nanoid_to_id.sql +53 -0
  13. package/packages/database/migrations/0085_remove_id_unique_constraint.sql +7 -0
  14. package/packages/database/migrations/meta/0078_snapshot.json +11515 -0
  15. package/packages/database/migrations/meta/0079_snapshot.json +11515 -0
  16. package/packages/database/migrations/meta/0080_snapshot.json +11554 -0
  17. package/packages/database/migrations/meta/0081_snapshot.json +11554 -0
  18. package/packages/database/migrations/meta/0082_snapshot.json +11554 -0
  19. package/packages/database/migrations/meta/0083_snapshot.json +11435 -0
  20. package/packages/database/migrations/meta/0084_snapshot.json +11435 -0
  21. package/packages/database/migrations/meta/0085_snapshot.json +11396 -0
  22. package/packages/database/migrations/meta/_journal.json +56 -0
  23. package/packages/database/src/models/__tests__/apiKey.test.ts +18 -6
  24. package/packages/database/src/models/apiKey.ts +5 -5
  25. package/packages/database/src/schemas/apiKey.ts +6 -2
  26. package/packages/database/src/schemas/ragEvals.ts +27 -20
  27. package/packages/database/src/schemas/rbac.ts +15 -15
  28. package/packages/database/src/server/models/ragEval/dataset.ts +3 -3
  29. package/packages/database/src/server/models/ragEval/datasetRecord.ts +5 -5
  30. package/packages/database/src/server/models/ragEval/evaluation.ts +3 -3
  31. package/packages/database/src/server/models/ragEval/evaluationRecord.ts +6 -6
  32. package/packages/memory-user-memory/src/prompts/layers/activity.ts +19 -18
  33. package/packages/memory-user-memory/src/prompts/layers/context.ts +39 -38
  34. package/packages/memory-user-memory/src/prompts/layers/experience.ts +40 -39
  35. package/packages/memory-user-memory/src/prompts/layers/identity.ts +55 -48
  36. package/packages/memory-user-memory/src/prompts/layers/preference.ts +42 -41
  37. package/packages/types/src/apiKey.ts +1 -1
  38. package/packages/types/src/eval/dataset.ts +2 -2
  39. package/packages/types/src/eval/evaluation.ts +3 -3
  40. package/src/app/[variants]/(main)/settings/apikey/features/ApiKey.tsx +2 -2
  41. package/src/server/routers/async/ragEval.ts +1 -1
  42. package/src/server/routers/lambda/apiKey.ts +3 -3
  43. package/src/server/routers/lambda/ragEval.ts +10 -10
  44. package/src/server/routers/tools/_helpers/scheduleToolCallReport.test.ts +589 -0
  45. package/src/services/ragEval.ts +10 -10
  46. package/src/store/chat/slices/aiChat/actions/StreamingHandler.ts +6 -5
  47. package/src/store/chat/slices/aiChat/actions/__tests__/StreamingHandler.test.ts +302 -0
  48. package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +168 -0
  49. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +11 -2
  50. package/src/store/chat/slices/aiChat/actions/types/streaming.ts +12 -8
  51. package/src/store/chat/slices/message/actions/optimisticUpdate.ts +1 -1
  52. package/src/store/library/slices/ragEval/actions/dataset.ts +3 -3
  53. package/src/store/library/slices/ragEval/actions/evaluation.ts +3 -3
@@ -0,0 +1,302 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { StreamingHandler } from '../StreamingHandler';
4
+ import type { StreamChunk, StreamingCallbacks, StreamingContext } from '../types/streaming';
5
+
6
+ // Helper to create a mock streaming context
7
+ const createContext = (overrides: Partial<StreamingContext> = {}): StreamingContext => ({
8
+ agentId: 'test-agent',
9
+ messageId: 'test-message',
10
+ operationId: 'test-op',
11
+ ...overrides,
12
+ });
13
+
14
+ // Helper to create mock callbacks
15
+ const createCallbacks = (overrides: Partial<StreamingCallbacks> = {}): StreamingCallbacks => ({
16
+ onContentUpdate: vi.fn(),
17
+ onGroundingUpdate: vi.fn(),
18
+ onImagesUpdate: vi.fn(),
19
+ onReasoningComplete: vi.fn(),
20
+ onReasoningStart: vi.fn().mockReturnValue('reasoning-op-id'),
21
+ onReasoningUpdate: vi.fn(),
22
+ onToolCallsUpdate: vi.fn(),
23
+ toggleToolCallingStreaming: vi.fn(),
24
+ transformToolCalls: vi.fn().mockReturnValue([]),
25
+ uploadBase64Image: vi
26
+ .fn()
27
+ .mockResolvedValue({ id: 'img-id', url: 'https://uploaded.url/img.png' }),
28
+ ...overrides,
29
+ });
30
+
31
+ describe('StreamingHandler', () => {
32
+ describe('content_part image handling', () => {
33
+ it('should pass contentMetadata with isMultimodal when content_part image chunks are received', () => {
34
+ const callbacks = createCallbacks();
35
+ const handler = new StreamingHandler(createContext(), callbacks);
36
+
37
+ // Send a text content_part
38
+ handler.handleChunk({
39
+ type: 'content_part',
40
+ partType: 'text',
41
+ content: 'Here is an image: ',
42
+ });
43
+
44
+ // Send an image content_part
45
+ handler.handleChunk({
46
+ type: 'content_part',
47
+ partType: 'image',
48
+ content: 'base64imagedata',
49
+ mimeType: 'image/png',
50
+ });
51
+
52
+ // The last onContentUpdate call should include contentMetadata
53
+ const lastCall = (callbacks.onContentUpdate as ReturnType<typeof vi.fn>).mock.calls.at(-1);
54
+ expect(lastCall).toBeDefined();
55
+
56
+ const [content, reasoning, contentMetadata] = lastCall!;
57
+ expect(content).toBe('Here is an image: ');
58
+ expect(contentMetadata).toBeDefined();
59
+ expect(contentMetadata.isMultimodal).toBe(true);
60
+ expect(contentMetadata.tempDisplayContent).toBeDefined();
61
+ // tempDisplayContent should be a serialized JSON string containing the parts
62
+ const parsed = JSON.parse(contentMetadata.tempDisplayContent);
63
+ expect(parsed).toHaveLength(2);
64
+ expect(parsed[0]).toEqual({ type: 'text', text: 'Here is an image: ' });
65
+ expect(parsed[1]).toEqual(
66
+ expect.objectContaining({
67
+ type: 'image',
68
+ image: expect.stringContaining('data:image/png;base64,'),
69
+ }),
70
+ );
71
+ });
72
+
73
+ it('should NOT pass contentMetadata when only text content_part chunks are received', () => {
74
+ const callbacks = createCallbacks();
75
+ const handler = new StreamingHandler(createContext(), callbacks);
76
+
77
+ handler.handleChunk({
78
+ type: 'content_part',
79
+ partType: 'text',
80
+ content: 'Hello ',
81
+ });
82
+ handler.handleChunk({
83
+ type: 'content_part',
84
+ partType: 'text',
85
+ content: 'world',
86
+ });
87
+
88
+ const lastCall = (callbacks.onContentUpdate as ReturnType<typeof vi.fn>).mock.calls.at(-1);
89
+ expect(lastCall).toBeDefined();
90
+
91
+ const [content, _reasoning, contentMetadata] = lastCall!;
92
+ expect(content).toBe('Hello world');
93
+ // No contentMetadata when there are no images
94
+ expect(contentMetadata).toBeUndefined();
95
+ });
96
+
97
+ it('should include isMultimodal in final result metadata when content has images', async () => {
98
+ const callbacks = createCallbacks();
99
+ const handler = new StreamingHandler(createContext(), callbacks);
100
+
101
+ // Send mixed content
102
+ handler.handleChunk({
103
+ type: 'content_part',
104
+ partType: 'text',
105
+ content: 'A cat: ',
106
+ });
107
+ handler.handleChunk({
108
+ type: 'content_part',
109
+ partType: 'image',
110
+ content: 'base64catimage',
111
+ mimeType: 'image/jpeg',
112
+ });
113
+
114
+ const result = await handler.handleFinish({});
115
+
116
+ expect(result.metadata.isMultimodal).toBe(true);
117
+ // Content should be serialized JSON containing text + image parts
118
+ const parsed = JSON.parse(result.content);
119
+ expect(parsed).toHaveLength(2);
120
+ expect(parsed[0].type).toBe('text');
121
+ expect(parsed[1].type).toBe('image');
122
+ });
123
+
124
+ it('should NOT include isMultimodal in final result when only text content', async () => {
125
+ const callbacks = createCallbacks();
126
+ const handler = new StreamingHandler(createContext(), callbacks);
127
+
128
+ handler.handleChunk({ type: 'text', text: 'Hello world' });
129
+
130
+ const result = await handler.handleFinish({});
131
+
132
+ expect(result.metadata.isMultimodal).toBeUndefined();
133
+ expect(result.content).toBe('Hello world');
134
+ });
135
+ });
136
+
137
+ describe('text chunk handling', () => {
138
+ it('should accumulate text chunks and notify via onContentUpdate', () => {
139
+ const callbacks = createCallbacks();
140
+ const handler = new StreamingHandler(createContext(), callbacks);
141
+
142
+ handler.handleChunk({ type: 'text', text: 'Hello' });
143
+ handler.handleChunk({ type: 'text', text: ' World' });
144
+
145
+ expect(callbacks.onContentUpdate).toHaveBeenCalledTimes(2);
146
+ expect(handler.getOutput()).toBe('Hello World');
147
+ });
148
+ });
149
+
150
+ describe('reasoning chunk handling', () => {
151
+ it('should track reasoning content and start/end timing', () => {
152
+ const callbacks = createCallbacks();
153
+ const handler = new StreamingHandler(createContext(), callbacks);
154
+
155
+ // Reasoning starts
156
+ handler.handleChunk({ type: 'reasoning', text: 'Let me think...' });
157
+ expect(callbacks.onReasoningStart).toHaveBeenCalledTimes(1);
158
+ expect(callbacks.onReasoningUpdate).toHaveBeenCalledWith({ content: 'Let me think...' });
159
+
160
+ // Text ends reasoning
161
+ handler.handleChunk({ type: 'text', text: 'Answer' });
162
+ expect(handler.getThinkingDuration()).toBeDefined();
163
+ expect(handler.getThinkingDuration()).toBeGreaterThanOrEqual(0);
164
+ });
165
+
166
+ it('should include reasoning in final result with signature', async () => {
167
+ const callbacks = createCallbacks();
168
+ const handler = new StreamingHandler(createContext(), callbacks);
169
+
170
+ handler.handleChunk({ type: 'reasoning', text: 'Thinking' });
171
+ handler.handleChunk({ type: 'text', text: 'Answer' });
172
+
173
+ const result = await handler.handleFinish({
174
+ reasoning: { content: 'Thinking', signature: 'test-sig' },
175
+ });
176
+
177
+ expect(result.metadata.reasoning).toBeDefined();
178
+ expect(result.metadata.reasoning?.content).toBe('Thinking');
179
+ expect(result.metadata.reasoning?.signature).toBe('test-sig');
180
+ // Duration may be 0 in fast tests (which becomes undefined due to `0 && ...` check)
181
+ // So we just verify it's a number or undefined
182
+ expect(
183
+ result.metadata.reasoning?.duration === undefined ||
184
+ typeof result.metadata.reasoning?.duration === 'number',
185
+ ).toBe(true);
186
+ });
187
+ });
188
+
189
+ describe('reasoning_part with images', () => {
190
+ it('should handle reasoning_part image chunks and report isMultimodal', () => {
191
+ const callbacks = createCallbacks();
192
+ const handler = new StreamingHandler(createContext(), callbacks);
193
+
194
+ handler.handleChunk({
195
+ type: 'reasoning_part',
196
+ partType: 'text',
197
+ content: 'Thinking about image: ',
198
+ });
199
+ handler.handleChunk({
200
+ type: 'reasoning_part',
201
+ partType: 'image',
202
+ content: 'base64data',
203
+ mimeType: 'image/png',
204
+ });
205
+
206
+ const lastCall = (callbacks.onReasoningUpdate as ReturnType<typeof vi.fn>).mock.calls.at(-1);
207
+ expect(lastCall).toBeDefined();
208
+ expect(lastCall![0].isMultimodal).toBe(true);
209
+ expect(lastCall![0].tempDisplayContent).toBeDefined();
210
+ });
211
+ });
212
+
213
+ describe('tool_calls handling', () => {
214
+ it('should mark as function call when tool_calls chunk is received', () => {
215
+ const callbacks = createCallbacks();
216
+ const handler = new StreamingHandler(createContext(), callbacks);
217
+
218
+ handler.handleChunk({
219
+ type: 'tool_calls',
220
+ tool_calls: [
221
+ { id: 'tool-1', type: 'function', function: { name: 'test', arguments: '{}' } },
222
+ ],
223
+ isAnimationActives: [true],
224
+ });
225
+
226
+ expect(handler.getIsFunctionCall()).toBe(true);
227
+ expect(callbacks.toggleToolCallingStreaming).toHaveBeenCalledWith('test-message', [true]);
228
+ });
229
+ });
230
+
231
+ describe('handleFinish with tool calls', () => {
232
+ it('should process final tool calls and set isFunctionCall', async () => {
233
+ const callbacks = createCallbacks({
234
+ transformToolCalls: vi.fn().mockReturnValue([{ identifier: 'test', arguments: '{}' }]),
235
+ });
236
+ const handler = new StreamingHandler(createContext(), callbacks);
237
+
238
+ const result = await handler.handleFinish({
239
+ toolCalls: [{ id: 'tool-1', type: 'function', function: { name: 'test', arguments: '' } }],
240
+ });
241
+
242
+ expect(result.isFunctionCall).toBe(true);
243
+ expect(result.tools).toBeDefined();
244
+ expect(result.tools).toHaveLength(1);
245
+ });
246
+ });
247
+
248
+ describe('base64_image handling', () => {
249
+ it('should dispatch images immediately and upload async', async () => {
250
+ const callbacks = createCallbacks();
251
+ const handler = new StreamingHandler(createContext(), callbacks);
252
+
253
+ handler.handleChunk({
254
+ type: 'base64_image',
255
+ image: { id: 'img-1', data: 'base64data' },
256
+ images: [{ id: 'img-1', data: 'base64data' }],
257
+ });
258
+
259
+ expect(callbacks.onImagesUpdate).toHaveBeenCalledWith([
260
+ { alt: 'img-1', id: 'img-1', url: 'base64data' },
261
+ ]);
262
+
263
+ // After finish, uploaded images should be in the result
264
+ const result = await handler.handleFinish({});
265
+ expect(result.metadata.imageList).toBeDefined();
266
+ expect(result.metadata.imageList).toHaveLength(1);
267
+ expect(result.metadata.imageList![0].url).toBe('https://uploaded.url/img.png');
268
+ });
269
+ });
270
+
271
+ describe('grounding handling', () => {
272
+ it('should forward citations to onGroundingUpdate', () => {
273
+ const callbacks = createCallbacks();
274
+ const handler = new StreamingHandler(createContext(), callbacks);
275
+
276
+ handler.handleChunk({
277
+ type: 'grounding',
278
+ grounding: {
279
+ citations: [{ url: 'https://example.com', title: 'Example' }],
280
+ searchQueries: ['query'],
281
+ },
282
+ } as any);
283
+
284
+ expect(callbacks.onGroundingUpdate).toHaveBeenCalledWith({
285
+ citations: [{ url: 'https://example.com', title: 'Example' }],
286
+ searchQueries: ['query'],
287
+ });
288
+ });
289
+
290
+ it('should skip grounding when no citations', () => {
291
+ const callbacks = createCallbacks();
292
+ const handler = new StreamingHandler(createContext(), callbacks);
293
+
294
+ handler.handleChunk({
295
+ type: 'grounding',
296
+ grounding: { citations: [], searchQueries: [] },
297
+ } as any);
298
+
299
+ expect(callbacks.onGroundingUpdate).not.toHaveBeenCalled();
300
+ });
301
+ });
302
+ });
@@ -1791,6 +1791,174 @@ describe('StreamingExecutor actions', () => {
1791
1791
  });
1792
1792
  });
1793
1793
 
1794
+ describe('content_part multimodal streaming', () => {
1795
+ it('should dispatch isMultimodal metadata when content_part image chunks are received', async () => {
1796
+ // Mock file store to prevent upload from hanging
1797
+ const fileStoreMod = await import('@/store/file/store');
1798
+ vi.spyOn(fileStoreMod, 'getFileStoreState').mockReturnValue({
1799
+ uploadBase64FileWithProgress: vi
1800
+ .fn()
1801
+ .mockResolvedValue({ id: 'img-id', url: 'https://cdn.example.com/img.png' }),
1802
+ } as any);
1803
+
1804
+ const { result } = renderHook(() => useChatStore());
1805
+ const messages = [createMockMessage({ role: 'user' })];
1806
+ const dispatchSpy = vi.spyOn(result.current, 'internal_dispatchMessage');
1807
+
1808
+ // Create operation for this test
1809
+ const { operationId } = result.current.startOperation({
1810
+ type: 'execAgentRuntime',
1811
+ context: {
1812
+ agentId: TEST_IDS.SESSION_ID,
1813
+ topicId: null,
1814
+ messageId: TEST_IDS.ASSISTANT_MESSAGE_ID,
1815
+ },
1816
+ label: 'Test AI Generation',
1817
+ });
1818
+
1819
+ const streamSpy = vi
1820
+ .spyOn(chatService, 'createAssistantMessageStream')
1821
+ .mockImplementation(async ({ onMessageHandle, onFinish }) => {
1822
+ // Send text content_part
1823
+ await onMessageHandle?.({
1824
+ type: 'content_part',
1825
+ partType: 'text',
1826
+ content: 'Here is a cat: ',
1827
+ } as any);
1828
+ // Send image content_part
1829
+ await onMessageHandle?.({
1830
+ type: 'content_part',
1831
+ partType: 'image',
1832
+ content: 'base64catimage',
1833
+ mimeType: 'image/jpeg',
1834
+ } as any);
1835
+ await onFinish?.('Here is a cat: ', {} as any);
1836
+ });
1837
+
1838
+ await act(async () => {
1839
+ await result.current.internal_fetchAIChatMessage({
1840
+ messages,
1841
+ messageId: TEST_IDS.ASSISTANT_MESSAGE_ID,
1842
+ model: 'gemini-3-pro-image-preview',
1843
+ provider: 'google',
1844
+ operationId,
1845
+ agentConfig: createMockResolvedAgentConfig(),
1846
+ });
1847
+ });
1848
+
1849
+ // Find dispatch calls with metadata.isMultimodal
1850
+ const multimodalDispatches = dispatchSpy.mock.calls.filter((call) => {
1851
+ const dispatch = call[0];
1852
+ return (
1853
+ dispatch?.type === 'updateMessage' &&
1854
+ 'value' in dispatch &&
1855
+ dispatch.value?.metadata?.isMultimodal === true
1856
+ );
1857
+ });
1858
+
1859
+ // Should have dispatched at least once with isMultimodal metadata during streaming
1860
+ expect(multimodalDispatches.length).toBeGreaterThanOrEqual(1);
1861
+
1862
+ // Verify the dispatch includes tempDisplayContent
1863
+ const firstMultimodalDispatch = multimodalDispatches[0][0] as any;
1864
+ expect(firstMultimodalDispatch.value.metadata.tempDisplayContent).toBeDefined();
1865
+
1866
+ streamSpy.mockRestore();
1867
+ });
1868
+
1869
+ it('should call optimisticUpdateMessageContent with isMultimodal metadata for multimodal content', async () => {
1870
+ // Mock file store to prevent upload from hanging
1871
+ const fileStoreMod = await import('@/store/file/store');
1872
+ vi.spyOn(fileStoreMod, 'getFileStoreState').mockReturnValue({
1873
+ uploadBase64FileWithProgress: vi
1874
+ .fn()
1875
+ .mockResolvedValue({ id: 'img-id', url: 'https://cdn.example.com/img.png' }),
1876
+ } as any);
1877
+
1878
+ const { result } = renderHook(() => useChatStore());
1879
+ const messages = [createMockMessage({ role: 'user' })];
1880
+ const updateContentSpy = vi.spyOn(result.current, 'optimisticUpdateMessageContent');
1881
+
1882
+ const streamSpy = vi
1883
+ .spyOn(chatService, 'createAssistantMessageStream')
1884
+ .mockImplementation(async ({ onMessageHandle, onFinish }) => {
1885
+ await onMessageHandle?.({
1886
+ type: 'content_part',
1887
+ partType: 'text',
1888
+ content: 'Generated image: ',
1889
+ } as any);
1890
+ await onMessageHandle?.({
1891
+ type: 'content_part',
1892
+ partType: 'image',
1893
+ content: 'base64imagedata',
1894
+ mimeType: 'image/png',
1895
+ } as any);
1896
+ await onFinish?.('Generated image: ', {} as any);
1897
+ });
1898
+
1899
+ await act(async () => {
1900
+ await result.current.internal_fetchAIChatMessage({
1901
+ messages,
1902
+ messageId: TEST_IDS.ASSISTANT_MESSAGE_ID,
1903
+ model: 'gemini-3-pro-image-preview',
1904
+ provider: 'google',
1905
+ agentConfig: createMockResolvedAgentConfig(),
1906
+ });
1907
+ });
1908
+
1909
+ // optimisticUpdateMessageContent should be called with metadata containing isMultimodal
1910
+ expect(updateContentSpy).toHaveBeenCalledWith(
1911
+ TEST_IDS.ASSISTANT_MESSAGE_ID,
1912
+ expect.any(String), // serialized JSON content
1913
+ expect.objectContaining({
1914
+ metadata: expect.objectContaining({
1915
+ isMultimodal: true,
1916
+ }),
1917
+ }),
1918
+ expect.any(Object),
1919
+ );
1920
+
1921
+ streamSpy.mockRestore();
1922
+ });
1923
+
1924
+ it('should NOT dispatch isMultimodal metadata for plain text streaming', async () => {
1925
+ const { result } = renderHook(() => useChatStore());
1926
+ const messages = [createMockMessage({ role: 'user' })];
1927
+ const dispatchSpy = vi.spyOn(result.current, 'internal_dispatchMessage');
1928
+
1929
+ const streamSpy = vi
1930
+ .spyOn(chatService, 'createAssistantMessageStream')
1931
+ .mockImplementation(async ({ onMessageHandle, onFinish }) => {
1932
+ await onMessageHandle?.({ type: 'text', text: 'Hello World' } as any);
1933
+ await onFinish?.('Hello World', {} as any);
1934
+ });
1935
+
1936
+ await act(async () => {
1937
+ await result.current.internal_fetchAIChatMessage({
1938
+ messages,
1939
+ messageId: TEST_IDS.ASSISTANT_MESSAGE_ID,
1940
+ model: 'gpt-4o-mini',
1941
+ provider: 'openai',
1942
+ agentConfig: createMockResolvedAgentConfig(),
1943
+ });
1944
+ });
1945
+
1946
+ // No dispatch should contain isMultimodal metadata
1947
+ const multimodalDispatches = dispatchSpy.mock.calls.filter((call) => {
1948
+ const dispatch = call[0];
1949
+ return (
1950
+ dispatch?.type === 'updateMessage' &&
1951
+ 'value' in dispatch &&
1952
+ dispatch.value?.metadata?.isMultimodal === true
1953
+ );
1954
+ });
1955
+
1956
+ expect(multimodalDispatches).toHaveLength(0);
1957
+
1958
+ streamSpy.mockRestore();
1959
+ });
1960
+ });
1961
+
1794
1962
  describe('isSubTask filtering', () => {
1795
1963
  it('should filter out lobe-gtd tools when isSubTask is true', async () => {
1796
1964
  const { result } = renderHook(() => useChatStore());
@@ -420,12 +420,21 @@ export const streamingExecutor: StateCreator<
420
420
  const handler = new StreamingHandler(
421
421
  { messageId, operationId, agentId, groupId, topicId },
422
422
  {
423
- onContentUpdate: (content, reasoning) => {
423
+ onContentUpdate: (content, reasoning, contentMetadata) => {
424
424
  internal_dispatchMessage(
425
425
  {
426
426
  id: messageId,
427
427
  type: 'updateMessage',
428
- value: { content, reasoning },
428
+ value: {
429
+ content,
430
+ reasoning,
431
+ ...(contentMetadata && {
432
+ metadata: {
433
+ isMultimodal: contentMetadata.isMultimodal,
434
+ tempDisplayContent: contentMetadata.tempDisplayContent,
435
+ },
436
+ }),
437
+ },
429
438
  },
430
439
  { operationId },
431
440
  );
@@ -40,7 +40,11 @@ export type GroundingData = GroundingSearch;
40
40
  */
41
41
  export interface StreamingCallbacks {
42
42
  /** Content update */
43
- onContentUpdate: (content: string, reasoning?: ReasoningState) => void;
43
+ onContentUpdate: (
44
+ content: string,
45
+ reasoning?: ReasoningState,
46
+ contentMetadata?: { isMultimodal: boolean; tempDisplayContent: string },
47
+ ) => void;
44
48
  /** Search grounding update */
45
49
  onGroundingUpdate: (grounding: GroundingData) => void;
46
50
  /** Image list update */
@@ -101,19 +105,19 @@ export interface StreamingResult {
101
105
  * Stream chunk types
102
106
  */
103
107
  export type StreamChunk =
104
- | { text: string, type: 'text'; }
105
- | { text: string, type: 'reasoning'; }
106
- | { content: string; mimeType?: string, partType: 'text' | 'image'; type: 'reasoning_part'; }
107
- | { content: string; mimeType?: string, partType: 'text' | 'image'; type: 'content_part'; }
108
+ | { text: string; type: 'text' }
109
+ | { text: string; type: 'reasoning' }
110
+ | { content: string; mimeType?: string; partType: 'text' | 'image'; type: 'reasoning_part' }
111
+ | { content: string; mimeType?: string; partType: 'text' | 'image'; type: 'content_part' }
108
112
  | {
109
113
  isAnimationActives?: boolean[];
110
114
  tool_calls: MessageToolCall[];
111
115
  type: 'tool_calls';
112
116
  }
113
- | { grounding?: GroundingData, type: 'grounding'; }
117
+ | { grounding?: GroundingData; type: 'grounding' }
114
118
  | {
115
- image: { data: string, id: string; };
116
- images: { data: string, id: string; }[];
119
+ image: { data: string; id: string };
120
+ images: { data: string; id: string }[];
117
121
  type: 'base64_image';
118
122
  }
119
123
  | { type: 'stop' };
@@ -223,7 +223,7 @@ export const messageOptimisticUpdate: StateCreator<
223
223
  {
224
224
  id,
225
225
  type: 'updateMessage',
226
- value: { content },
226
+ value: { content, metadata: extra?.metadata },
227
227
  },
228
228
  context,
229
229
  );
@@ -19,10 +19,10 @@ const FETCH_DATASET_RECORD_KEY = 'FETCH_DATASET_RECORD_KEY';
19
19
  export interface RAGEvalDatasetAction {
20
20
  createNewDataset: (params: CreateNewEvalDatasets) => Promise<void>;
21
21
 
22
- importDataset: (file: File, datasetId: number) => Promise<void>;
22
+ importDataset: (file: File, datasetId: string) => Promise<void>;
23
23
  refreshDatasetList: () => Promise<void>;
24
- removeDataset: (id: number) => Promise<void>;
25
- useFetchDatasetRecords: (datasetId: number | null) => SWRResponse<EvalDatasetRecord[]>;
24
+ removeDataset: (id: string) => Promise<void>;
25
+ useFetchDatasetRecords: (datasetId: string | null) => SWRResponse<EvalDatasetRecord[]>;
26
26
  useFetchDatasets: (knowledgeBaseId: string) => SWRResponse<RAGEvalDataSetItem[]>;
27
27
  }
28
28
 
@@ -9,13 +9,13 @@ import { type KnowledgeBaseStore } from '@/store/library/store';
9
9
  const FETCH_EVALUATION_LIST_KEY = 'FETCH_EVALUATION_LIST_KEY';
10
10
 
11
11
  export interface RAGEvalEvaluationAction {
12
- checkEvaluationStatus: (id: number) => Promise<void>;
12
+ checkEvaluationStatus: (id: string) => Promise<void>;
13
13
 
14
14
  createNewEvaluation: (params: CreateNewEvalEvaluation) => Promise<void>;
15
15
  refreshEvaluationList: () => Promise<void>;
16
16
 
17
- removeEvaluation: (id: number) => Promise<void>;
18
- runEvaluation: (id: number) => Promise<void>;
17
+ removeEvaluation: (id: string) => Promise<void>;
18
+ runEvaluation: (id: string) => Promise<void>;
19
19
 
20
20
  useFetchEvaluationList: (knowledgeBaseId: string) => SWRResponse<RAGEvalDataSetItem[]>;
21
21
  }