@worktango/ai-assistant 0.0.26 → 0.0.28
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/package.json +3 -7
- package/types.d.ts +18 -2
- package/src/backend/express.test.ts +0 -208
- package/src/backend/express.ts +0 -105
- package/src/frontend/AiAssistant.test.tsx +0 -19
- package/src/frontend/AiAssistant.tsx +0 -26
- package/src/frontend/components/AiAssistantComponent.scss +0 -24
- package/src/frontend/components/AiAssistantComponent.test.tsx +0 -270
- package/src/frontend/components/AiAssistantComponent.tsx +0 -246
- package/src/frontend/components/messageRenderers/AssistantMessage.test.tsx +0 -154
- package/src/frontend/components/messageRenderers/AssistantMessage.tsx +0 -104
- package/src/frontend/components/messageRenderers/UserMessage.test.tsx +0 -103
- package/src/frontend/components/messageRenderers/UserMessage.tsx +0 -43
- package/src/frontend/config.ts +0 -54
- package/src/frontend/reactComponent.ts +0 -6
- package/src/frontend/tools/generatedSdks.ts +0 -44
- package/src/frontend/tools/llm-tools/getCurrentUser.test.ts +0 -100
- package/src/frontend/tools/llm-tools/getCurrentUser.ts +0 -158
- package/src/frontend/tools/llm-tools/index.ts +0 -6
- package/src/frontend/tools/llm-tools/kazooPlatform/getRewardsAndRecognitionCurrentUser.test.ts +0 -76
- package/src/frontend/tools/llm-tools/kazooPlatform/getRewardsAndRecognitionCurrentUser.ts +0 -36
- package/src/frontend/tools/llm-tools/pulsePlatform/.gitkeep +0 -0
- package/src/frontend/tools/llm-tools/shared/.gitkeep +0 -0
- package/src/frontend/tools/useAiAssistantSession.test.ts +0 -768
- package/src/frontend/tools/useAiAssistantSession.ts +0 -374
- package/src/frontend/useStyles.ts +0 -36
|
@@ -1,768 +0,0 @@
|
|
|
1
|
-
import { ChatData, SupportedTextModel } from "@kazoohr/llm/types";
|
|
2
|
-
import { act, renderHook } from "@kazoohr/test";
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
ChatItemWithTimestamp,
|
|
6
|
-
useAiAssistantSession,
|
|
7
|
-
} from "./useAiAssistantSession";
|
|
8
|
-
|
|
9
|
-
const mockFetch = jest.fn();
|
|
10
|
-
global.fetch = mockFetch;
|
|
11
|
-
|
|
12
|
-
const mockDecode = jest.fn();
|
|
13
|
-
class MockTextDecoder {
|
|
14
|
-
decode(input: Uint8Array): string {
|
|
15
|
-
return mockDecode(input);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
global.TextDecoder = MockTextDecoder as any;
|
|
19
|
-
|
|
20
|
-
describe("useAiAssistantSession", () => {
|
|
21
|
-
let mockReader: { read: jest.Mock };
|
|
22
|
-
|
|
23
|
-
beforeEach(() => {
|
|
24
|
-
jest.clearAllMocks();
|
|
25
|
-
mockDecode.mockClear();
|
|
26
|
-
|
|
27
|
-
mockReader = {
|
|
28
|
-
read: jest.fn(),
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
mockFetch.mockResolvedValue({
|
|
32
|
-
ok: true,
|
|
33
|
-
body: {
|
|
34
|
-
getReader: () => mockReader,
|
|
35
|
-
},
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("initializes with default values", () => {
|
|
40
|
-
const { result } = renderHook(() => useAiAssistantSession());
|
|
41
|
-
|
|
42
|
-
expect(result.current.isLoading).toBe(false);
|
|
43
|
-
expect(result.current.messages).toEqual([]);
|
|
44
|
-
expect(result.current.debugInfo).toEqual([]);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("initializes with provided messages", () => {
|
|
48
|
-
const initialMessages: ChatItemWithTimestamp[] = [
|
|
49
|
-
{
|
|
50
|
-
role: "user",
|
|
51
|
-
parts: [{ type: "text", text: "Hello" }],
|
|
52
|
-
timestamp: new Date(),
|
|
53
|
-
},
|
|
54
|
-
];
|
|
55
|
-
|
|
56
|
-
const { result } = renderHook(() =>
|
|
57
|
-
useAiAssistantSession({ initialMessages })
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
expect(result.current.messages).toEqual(initialMessages);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("adds a message correctly", () => {
|
|
64
|
-
const { result } = renderHook(() => useAiAssistantSession());
|
|
65
|
-
|
|
66
|
-
const newMessage: ChatItemWithTimestamp = {
|
|
67
|
-
role: "user",
|
|
68
|
-
parts: [{ type: "text", text: "Test message" }],
|
|
69
|
-
timestamp: new Date(),
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
act(() => {
|
|
73
|
-
result.current.addMessage(newMessage);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
expect(result.current.messages).toEqual([newMessage]);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("sends a user message and processes the response stream", async () => {
|
|
80
|
-
const onBeforeLlmCall = jest.fn();
|
|
81
|
-
const onAfterLlmCall = jest.fn();
|
|
82
|
-
const onChunkAdded = jest.fn();
|
|
83
|
-
|
|
84
|
-
const streamedChunks: ChatData[] = [
|
|
85
|
-
{ type: "text", text: "Hello" },
|
|
86
|
-
{ type: "text", text: " world" },
|
|
87
|
-
{ type: "toolCall", toolName: "testTool", args: { test: true } },
|
|
88
|
-
];
|
|
89
|
-
|
|
90
|
-
let chunkIndex = 0;
|
|
91
|
-
mockReader.read.mockImplementation(() => {
|
|
92
|
-
if (chunkIndex < streamedChunks.length) {
|
|
93
|
-
mockDecode.mockReturnValueOnce(
|
|
94
|
-
JSON.stringify(streamedChunks[chunkIndex])
|
|
95
|
-
);
|
|
96
|
-
chunkIndex++;
|
|
97
|
-
return Promise.resolve({ done: false, value: new Uint8Array() });
|
|
98
|
-
}
|
|
99
|
-
return Promise.resolve({ done: true });
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
const { result } = renderHook(() =>
|
|
103
|
-
useAiAssistantSession({
|
|
104
|
-
onBeforeLlmCall,
|
|
105
|
-
onAfterLlmCall,
|
|
106
|
-
onChunkAdded,
|
|
107
|
-
})
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
await act(async () => {
|
|
111
|
-
await result.current.sendUserMessage("Test message");
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
expect(mockFetch).toHaveBeenCalledWith("/api/api-ai-assistant/v1/chat", {
|
|
115
|
-
method: "POST",
|
|
116
|
-
body: expect.any(String),
|
|
117
|
-
credentials: "include",
|
|
118
|
-
headers: {
|
|
119
|
-
"Content-Type": "application/json",
|
|
120
|
-
Accept: "text/event-stream",
|
|
121
|
-
},
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
expect(onBeforeLlmCall).toHaveBeenCalled();
|
|
125
|
-
expect(onAfterLlmCall).toHaveBeenCalled();
|
|
126
|
-
expect(onChunkAdded).toHaveBeenCalledTimes(3); // Once for each chunk
|
|
127
|
-
|
|
128
|
-
expect(result.current.messages).toHaveLength(3);
|
|
129
|
-
expect(result.current.messages[0]).toEqual({
|
|
130
|
-
role: "user",
|
|
131
|
-
parts: [{ type: "text", text: "Test message" }],
|
|
132
|
-
timestamp: expect.any(Date),
|
|
133
|
-
});
|
|
134
|
-
expect(result.current.messages[1]).toEqual({
|
|
135
|
-
role: "model",
|
|
136
|
-
parts: [
|
|
137
|
-
{ type: "toolCall", toolName: "testTool", args: { test: true } },
|
|
138
|
-
{ type: "text", text: "Hello world" },
|
|
139
|
-
],
|
|
140
|
-
timestamp: expect.any(Date),
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("handles fetch errors gracefully", async () => {
|
|
145
|
-
const onError = jest.fn();
|
|
146
|
-
mockFetch.mockRejectedValue(new Error("Network error"));
|
|
147
|
-
|
|
148
|
-
const { result } = renderHook(() =>
|
|
149
|
-
useAiAssistantSession({
|
|
150
|
-
onError,
|
|
151
|
-
})
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
await act(async () => {
|
|
155
|
-
await result.current.sendUserMessage("Test message");
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
expect(onError).toHaveBeenCalledWith(expect.any(Error));
|
|
159
|
-
expect(result.current.messages).toHaveLength(0);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it("handles stream processing errors", async () => {
|
|
163
|
-
const onError = jest.fn();
|
|
164
|
-
mockReader.read.mockRejectedValue(new Error("Stream error"));
|
|
165
|
-
|
|
166
|
-
const { result } = renderHook(() =>
|
|
167
|
-
useAiAssistantSession({
|
|
168
|
-
onError,
|
|
169
|
-
})
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
await act(async () => {
|
|
173
|
-
await result.current.sendUserMessage("Test message");
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
expect(onError).toHaveBeenCalledWith(expect.any(Error));
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it("respects model and temperature settings", async () => {
|
|
180
|
-
const { result } = renderHook(() =>
|
|
181
|
-
useAiAssistantSession({
|
|
182
|
-
model: "large" as SupportedTextModel,
|
|
183
|
-
temperature: 0.7,
|
|
184
|
-
})
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
await act(async () => {
|
|
188
|
-
await result.current.sendUserMessage("Test message");
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
const requestBody = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
192
|
-
expect(requestBody.model).toBe("large");
|
|
193
|
-
expect(requestBody.temperature).toBe(0.7);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it("handles non-ok fetch response", async () => {
|
|
197
|
-
mockFetch.mockResolvedValue({
|
|
198
|
-
ok: false,
|
|
199
|
-
status: 500,
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
const { result } = renderHook(() => useAiAssistantSession());
|
|
203
|
-
|
|
204
|
-
await act(async () => {
|
|
205
|
-
await result.current.sendUserMessage("Test message");
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
// Should only contain the user message
|
|
209
|
-
expect(result.current.messages).toHaveLength(0);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it("handles onBeforeLlmCall error", async () => {
|
|
213
|
-
const onError = jest.fn();
|
|
214
|
-
const onBeforeLlmCall = jest.fn().mockImplementation(() => {
|
|
215
|
-
throw new Error("Before LLM call error");
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
const { result } = renderHook(() =>
|
|
219
|
-
useAiAssistantSession({
|
|
220
|
-
onBeforeLlmCall,
|
|
221
|
-
onError,
|
|
222
|
-
})
|
|
223
|
-
);
|
|
224
|
-
|
|
225
|
-
await act(async () => {
|
|
226
|
-
await result.current.sendUserMessage("Test message");
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
expect(onBeforeLlmCall).toHaveBeenCalled();
|
|
230
|
-
expect(result.current.debugInfo).toContainEqual(
|
|
231
|
-
expect.objectContaining({
|
|
232
|
-
level: "error",
|
|
233
|
-
message: "Error calling onBeforeLlmCall",
|
|
234
|
-
})
|
|
235
|
-
);
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it("handles onAfterLlmCall error", async () => {
|
|
239
|
-
const onError = jest.fn();
|
|
240
|
-
const onAfterLlmCall = jest.fn().mockImplementation(() => {
|
|
241
|
-
throw new Error("After LLM call error");
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
mockReader.read.mockResolvedValue({ done: true });
|
|
245
|
-
|
|
246
|
-
const { result } = renderHook(() =>
|
|
247
|
-
useAiAssistantSession({
|
|
248
|
-
onAfterLlmCall,
|
|
249
|
-
onError,
|
|
250
|
-
})
|
|
251
|
-
);
|
|
252
|
-
|
|
253
|
-
await act(async () => {
|
|
254
|
-
await result.current.sendUserMessage("Test message");
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
expect(onAfterLlmCall).toHaveBeenCalled();
|
|
258
|
-
expect(result.current.debugInfo).toContainEqual(
|
|
259
|
-
expect.objectContaining({
|
|
260
|
-
level: "error",
|
|
261
|
-
message: "Error calling onAfterLlmCall",
|
|
262
|
-
})
|
|
263
|
-
);
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
it("handles setMessages directly", () => {
|
|
267
|
-
const { result } = renderHook(() => useAiAssistantSession());
|
|
268
|
-
|
|
269
|
-
const newMessages: ChatItemWithTimestamp[] = [
|
|
270
|
-
{
|
|
271
|
-
role: "user",
|
|
272
|
-
parts: [{ type: "text", text: "New message" }],
|
|
273
|
-
timestamp: new Date(),
|
|
274
|
-
},
|
|
275
|
-
];
|
|
276
|
-
|
|
277
|
-
act(() => {
|
|
278
|
-
result.current.setMessages(newMessages);
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
expect(result.current.messages).toEqual(newMessages);
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it("handles multiple tool calls in response", async () => {
|
|
285
|
-
const streamedChunks: ChatData[] = [
|
|
286
|
-
{ type: "toolCall", toolName: "tool1", args: { test: 1 } },
|
|
287
|
-
{ type: "toolCall", toolName: "tool2", args: { test: 2 } },
|
|
288
|
-
];
|
|
289
|
-
|
|
290
|
-
let chunkIndex = 0;
|
|
291
|
-
mockReader.read.mockImplementation(() => {
|
|
292
|
-
if (chunkIndex < streamedChunks.length) {
|
|
293
|
-
mockDecode.mockReturnValueOnce(
|
|
294
|
-
JSON.stringify(streamedChunks[chunkIndex])
|
|
295
|
-
);
|
|
296
|
-
chunkIndex++;
|
|
297
|
-
return Promise.resolve({ done: false, value: new Uint8Array() });
|
|
298
|
-
}
|
|
299
|
-
return Promise.resolve({ done: true });
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
const { result } = renderHook(() => useAiAssistantSession());
|
|
303
|
-
|
|
304
|
-
await act(async () => {
|
|
305
|
-
await result.current.sendUserMessage("Test message");
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
// Verify that all tool calls are in the message
|
|
309
|
-
expect(result.current.messages[1].parts).toEqual(streamedChunks);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it("handles mixed text and tool calls in response", async () => {
|
|
313
|
-
const streamedChunks: ChatData[] = [
|
|
314
|
-
{ type: "text", text: "Hello" },
|
|
315
|
-
{ type: "toolCall", toolName: "tool1", args: { test: 1 } },
|
|
316
|
-
{ type: "text", text: " world" },
|
|
317
|
-
];
|
|
318
|
-
|
|
319
|
-
let chunkIndex = 0;
|
|
320
|
-
mockReader.read.mockImplementation(() => {
|
|
321
|
-
if (chunkIndex < streamedChunks.length) {
|
|
322
|
-
mockDecode.mockReturnValueOnce(
|
|
323
|
-
JSON.stringify(streamedChunks[chunkIndex])
|
|
324
|
-
);
|
|
325
|
-
chunkIndex++;
|
|
326
|
-
return Promise.resolve({ done: false, value: new Uint8Array() });
|
|
327
|
-
}
|
|
328
|
-
return Promise.resolve({ done: true });
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
const { result } = renderHook(() => useAiAssistantSession());
|
|
332
|
-
|
|
333
|
-
await act(async () => {
|
|
334
|
-
await result.current.sendUserMessage("Test message");
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
// Verify that tool calls and concatenated text are in the message
|
|
338
|
-
expect(result.current.messages[1].parts).toEqual([
|
|
339
|
-
{ type: "toolCall", toolName: "tool1", args: { test: 1 } },
|
|
340
|
-
{ type: "text", text: "Hello world" },
|
|
341
|
-
]);
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
it("handles response with no reader available", async () => {
|
|
345
|
-
const onError = jest.fn();
|
|
346
|
-
mockFetch.mockResolvedValue({
|
|
347
|
-
ok: true,
|
|
348
|
-
body: {
|
|
349
|
-
getReader: () => undefined,
|
|
350
|
-
},
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
const { result } = renderHook(() =>
|
|
354
|
-
useAiAssistantSession({
|
|
355
|
-
onError,
|
|
356
|
-
})
|
|
357
|
-
);
|
|
358
|
-
|
|
359
|
-
await act(async () => {
|
|
360
|
-
await result.current.sendUserMessage("Test message");
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
expect(onError).toHaveBeenCalledWith(
|
|
364
|
-
new Error("Unexpected client error. Response body reader not available.")
|
|
365
|
-
);
|
|
366
|
-
expect(result.current.messages).toHaveLength(0);
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
it("handles response with null body", async () => {
|
|
370
|
-
const onError = jest.fn();
|
|
371
|
-
mockFetch.mockResolvedValue({
|
|
372
|
-
ok: true,
|
|
373
|
-
body: null,
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
const { result } = renderHook(() =>
|
|
377
|
-
useAiAssistantSession({
|
|
378
|
-
onError,
|
|
379
|
-
})
|
|
380
|
-
);
|
|
381
|
-
|
|
382
|
-
await act(async () => {
|
|
383
|
-
await result.current.sendUserMessage("Test message");
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
expect(onError).toHaveBeenCalledWith(
|
|
387
|
-
new Error("Unexpected client error. Response body reader not available.")
|
|
388
|
-
);
|
|
389
|
-
expect(result.current.messages).toHaveLength(0);
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
it("handles unknown chunk type in response", async () => {
|
|
393
|
-
const streamedChunks = [
|
|
394
|
-
{ type: "text", text: "Hello" } as ChatData,
|
|
395
|
-
{
|
|
396
|
-
type: "toolResult",
|
|
397
|
-
toolName: "unknownTool",
|
|
398
|
-
result: { test: true },
|
|
399
|
-
} as ChatData,
|
|
400
|
-
{ type: "text", text: " world" } as ChatData,
|
|
401
|
-
];
|
|
402
|
-
|
|
403
|
-
let chunkIndex = 0;
|
|
404
|
-
mockReader.read.mockImplementation(() => {
|
|
405
|
-
if (chunkIndex < streamedChunks.length) {
|
|
406
|
-
mockDecode.mockReturnValueOnce(
|
|
407
|
-
JSON.stringify(streamedChunks[chunkIndex])
|
|
408
|
-
);
|
|
409
|
-
chunkIndex++;
|
|
410
|
-
return Promise.resolve({ done: false, value: new Uint8Array() });
|
|
411
|
-
}
|
|
412
|
-
return Promise.resolve({ done: true });
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
const { result } = renderHook(() => useAiAssistantSession());
|
|
416
|
-
|
|
417
|
-
await act(async () => {
|
|
418
|
-
await result.current.sendUserMessage("Test message");
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
// Unknown chunk type should be ignored, only text chunks should be concatenated
|
|
422
|
-
expect(result.current.messages[1].parts).toEqual([
|
|
423
|
-
{ type: "text", text: "Hello world" },
|
|
424
|
-
]);
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
it("handles tool calls and executes them", async () => {
|
|
428
|
-
const mockTool = {
|
|
429
|
-
name: "testTool",
|
|
430
|
-
description: "A test tool",
|
|
431
|
-
fulfill: jest.fn().mockResolvedValue({ success: true }),
|
|
432
|
-
};
|
|
433
|
-
|
|
434
|
-
const streamedChunks = [
|
|
435
|
-
{
|
|
436
|
-
type: "toolCall",
|
|
437
|
-
toolName: "testTool",
|
|
438
|
-
args: { test: true },
|
|
439
|
-
} as ChatData,
|
|
440
|
-
];
|
|
441
|
-
|
|
442
|
-
let chunkIndex = 0;
|
|
443
|
-
mockReader.read.mockImplementation(() => {
|
|
444
|
-
if (chunkIndex < streamedChunks.length) {
|
|
445
|
-
mockDecode.mockReturnValueOnce(
|
|
446
|
-
JSON.stringify(streamedChunks[chunkIndex])
|
|
447
|
-
);
|
|
448
|
-
chunkIndex++;
|
|
449
|
-
return Promise.resolve({ done: false, value: new Uint8Array() });
|
|
450
|
-
}
|
|
451
|
-
return Promise.resolve({ done: true });
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
const { result } = renderHook(() =>
|
|
455
|
-
useAiAssistantSession({
|
|
456
|
-
tools: [mockTool],
|
|
457
|
-
})
|
|
458
|
-
);
|
|
459
|
-
|
|
460
|
-
await act(async () => {
|
|
461
|
-
await result.current.sendUserMessage("Test message");
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
expect(mockTool.fulfill).toHaveBeenCalledWith({ test: true });
|
|
465
|
-
expect(result.current.messages[2]).toEqual({
|
|
466
|
-
role: "tool",
|
|
467
|
-
parts: [
|
|
468
|
-
{
|
|
469
|
-
type: "toolResult",
|
|
470
|
-
toolName: "testTool",
|
|
471
|
-
result: { success: true },
|
|
472
|
-
},
|
|
473
|
-
],
|
|
474
|
-
});
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
it("handles tool not found scenario", async () => {
|
|
478
|
-
const onError = jest.fn();
|
|
479
|
-
const streamedChunks = [
|
|
480
|
-
{
|
|
481
|
-
type: "toolCall",
|
|
482
|
-
toolName: "nonExistentTool",
|
|
483
|
-
args: { test: true },
|
|
484
|
-
} as ChatData,
|
|
485
|
-
];
|
|
486
|
-
|
|
487
|
-
let chunkIndex = 0;
|
|
488
|
-
mockReader.read.mockImplementation(() => {
|
|
489
|
-
if (chunkIndex < streamedChunks.length) {
|
|
490
|
-
mockDecode.mockReturnValueOnce(
|
|
491
|
-
JSON.stringify(streamedChunks[chunkIndex])
|
|
492
|
-
);
|
|
493
|
-
chunkIndex++;
|
|
494
|
-
return Promise.resolve({ done: false, value: new Uint8Array() });
|
|
495
|
-
}
|
|
496
|
-
return Promise.resolve({ done: true });
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
const { result } = renderHook(() =>
|
|
500
|
-
useAiAssistantSession({
|
|
501
|
-
onError,
|
|
502
|
-
tools: [],
|
|
503
|
-
})
|
|
504
|
-
);
|
|
505
|
-
|
|
506
|
-
await act(async () => {
|
|
507
|
-
await result.current.sendUserMessage("Test message");
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
expect(onError).toHaveBeenCalledWith(
|
|
511
|
-
new Error("Tool nonExistentTool not found")
|
|
512
|
-
);
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
it("protects against infinite loops in stream reading", async () => {
|
|
516
|
-
const streamedChunks = [{ type: "text", text: "test" } as ChatData];
|
|
517
|
-
|
|
518
|
-
// Mock an implementation that never returns done: true
|
|
519
|
-
mockReader.read.mockImplementation(() => {
|
|
520
|
-
mockDecode.mockReturnValueOnce(JSON.stringify(streamedChunks[0]));
|
|
521
|
-
return Promise.resolve({ done: false, value: new Uint8Array() });
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
const { result } = renderHook(() => useAiAssistantSession());
|
|
525
|
-
|
|
526
|
-
await act(async () => {
|
|
527
|
-
await result.current.sendUserMessage("Test message");
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
// The loop should have broken after 1,000,000 iterations
|
|
531
|
-
expect(mockReader.read).toHaveBeenCalledTimes(1_000_000);
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
it("initializes messages with timestamps when not provided", () => {
|
|
535
|
-
const initialMessages = [
|
|
536
|
-
{
|
|
537
|
-
role: "user" as const,
|
|
538
|
-
parts: [{ type: "text" as const, text: "Hello" }],
|
|
539
|
-
},
|
|
540
|
-
];
|
|
541
|
-
|
|
542
|
-
const { result } = renderHook(() =>
|
|
543
|
-
useAiAssistantSession({ initialMessages })
|
|
544
|
-
);
|
|
545
|
-
|
|
546
|
-
expect(result.current.messages[0]).toEqual({
|
|
547
|
-
...initialMessages[0],
|
|
548
|
-
timestamp: expect.any(Date),
|
|
549
|
-
});
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
it("handles tool execution and result processing", async () => {
|
|
553
|
-
const mockTool = {
|
|
554
|
-
name: "testTool",
|
|
555
|
-
description: "A test tool",
|
|
556
|
-
fulfill: jest.fn().mockResolvedValue({ success: true }),
|
|
557
|
-
};
|
|
558
|
-
|
|
559
|
-
const streamedChunks = [
|
|
560
|
-
{
|
|
561
|
-
type: "toolCall",
|
|
562
|
-
toolName: "testTool",
|
|
563
|
-
args: { test: true },
|
|
564
|
-
} as ChatData,
|
|
565
|
-
{ type: "text", text: "Some text" } as ChatData,
|
|
566
|
-
];
|
|
567
|
-
|
|
568
|
-
let chunkIndex = 0;
|
|
569
|
-
mockReader.read.mockImplementation(() => {
|
|
570
|
-
if (chunkIndex < streamedChunks.length) {
|
|
571
|
-
mockDecode.mockReturnValueOnce(
|
|
572
|
-
JSON.stringify(streamedChunks[chunkIndex])
|
|
573
|
-
);
|
|
574
|
-
chunkIndex++;
|
|
575
|
-
return Promise.resolve({ done: false, value: new Uint8Array() });
|
|
576
|
-
}
|
|
577
|
-
return Promise.resolve({ done: true });
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
const { result } = renderHook(() =>
|
|
581
|
-
useAiAssistantSession({
|
|
582
|
-
tools: [mockTool],
|
|
583
|
-
})
|
|
584
|
-
);
|
|
585
|
-
|
|
586
|
-
await act(async () => {
|
|
587
|
-
await result.current.sendUserMessage("Test message");
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
// Verify tool execution
|
|
591
|
-
expect(mockTool.fulfill).toHaveBeenCalledWith({ test: true });
|
|
592
|
-
|
|
593
|
-
// Verify messages are updated correctly
|
|
594
|
-
expect(result.current.messages).toHaveLength(3); // User message, model response, and tool result
|
|
595
|
-
expect(result.current.messages[2]).toEqual({
|
|
596
|
-
role: "tool",
|
|
597
|
-
parts: [
|
|
598
|
-
{
|
|
599
|
-
type: "toolResult",
|
|
600
|
-
toolName: "testTool",
|
|
601
|
-
result: { success: true },
|
|
602
|
-
},
|
|
603
|
-
],
|
|
604
|
-
});
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
it("handles multiple tools execution in sequence", async () => {
|
|
608
|
-
const mockTool1 = {
|
|
609
|
-
name: "tool1",
|
|
610
|
-
description: "First test tool",
|
|
611
|
-
fulfill: jest.fn().mockResolvedValue({ result: 1 }),
|
|
612
|
-
};
|
|
613
|
-
|
|
614
|
-
const mockTool2 = {
|
|
615
|
-
name: "tool2",
|
|
616
|
-
description: "Second test tool",
|
|
617
|
-
fulfill: jest.fn().mockResolvedValue({ result: 2 }),
|
|
618
|
-
};
|
|
619
|
-
|
|
620
|
-
const streamedChunks = [
|
|
621
|
-
{ type: "toolCall", toolName: "tool1", args: { test: 1 } } as ChatData,
|
|
622
|
-
{ type: "toolCall", toolName: "tool2", args: { test: 2 } } as ChatData,
|
|
623
|
-
];
|
|
624
|
-
|
|
625
|
-
let chunkIndex = 0;
|
|
626
|
-
mockReader.read.mockImplementation(() => {
|
|
627
|
-
if (chunkIndex < streamedChunks.length) {
|
|
628
|
-
mockDecode.mockReturnValueOnce(
|
|
629
|
-
JSON.stringify(streamedChunks[chunkIndex])
|
|
630
|
-
);
|
|
631
|
-
chunkIndex++;
|
|
632
|
-
return Promise.resolve({ done: false, value: new Uint8Array() });
|
|
633
|
-
}
|
|
634
|
-
return Promise.resolve({ done: true });
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
const { result } = renderHook(() =>
|
|
638
|
-
useAiAssistantSession({
|
|
639
|
-
tools: [mockTool1, mockTool2],
|
|
640
|
-
})
|
|
641
|
-
);
|
|
642
|
-
|
|
643
|
-
await act(async () => {
|
|
644
|
-
await result.current.sendUserMessage("Test message");
|
|
645
|
-
});
|
|
646
|
-
|
|
647
|
-
// Verify both tools were executed
|
|
648
|
-
expect(mockTool1.fulfill).toHaveBeenCalledWith({ test: 1 });
|
|
649
|
-
expect(mockTool2.fulfill).toHaveBeenCalledWith({ test: 2 });
|
|
650
|
-
|
|
651
|
-
// Verify messages contain both tool results
|
|
652
|
-
expect(result.current.messages[2]).toEqual({
|
|
653
|
-
role: "tool",
|
|
654
|
-
parts: [
|
|
655
|
-
{
|
|
656
|
-
type: "toolResult",
|
|
657
|
-
toolName: "tool1",
|
|
658
|
-
result: { result: 1 },
|
|
659
|
-
},
|
|
660
|
-
{
|
|
661
|
-
type: "toolResult",
|
|
662
|
-
toolName: "tool2",
|
|
663
|
-
result: { result: 2 },
|
|
664
|
-
},
|
|
665
|
-
],
|
|
666
|
-
});
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
it("handles tool execution with empty tool results", async () => {
|
|
670
|
-
const mockTool = {
|
|
671
|
-
name: "testTool",
|
|
672
|
-
description: "A test tool",
|
|
673
|
-
fulfill: jest.fn().mockResolvedValue(undefined),
|
|
674
|
-
};
|
|
675
|
-
|
|
676
|
-
const streamedChunks = [
|
|
677
|
-
{
|
|
678
|
-
type: "toolCall",
|
|
679
|
-
toolName: "testTool",
|
|
680
|
-
args: { test: true },
|
|
681
|
-
} as ChatData,
|
|
682
|
-
];
|
|
683
|
-
|
|
684
|
-
let chunkIndex = 0;
|
|
685
|
-
mockReader.read.mockImplementation(() => {
|
|
686
|
-
if (chunkIndex < streamedChunks.length) {
|
|
687
|
-
mockDecode.mockReturnValueOnce(
|
|
688
|
-
JSON.stringify(streamedChunks[chunkIndex])
|
|
689
|
-
);
|
|
690
|
-
chunkIndex++;
|
|
691
|
-
return Promise.resolve({ done: false, value: new Uint8Array() });
|
|
692
|
-
}
|
|
693
|
-
return Promise.resolve({ done: true });
|
|
694
|
-
});
|
|
695
|
-
|
|
696
|
-
const { result } = renderHook(() =>
|
|
697
|
-
useAiAssistantSession({
|
|
698
|
-
tools: [mockTool],
|
|
699
|
-
})
|
|
700
|
-
);
|
|
701
|
-
|
|
702
|
-
await act(async () => {
|
|
703
|
-
await result.current.sendUserMessage("Test message");
|
|
704
|
-
});
|
|
705
|
-
|
|
706
|
-
// Verify tool execution
|
|
707
|
-
expect(mockTool.fulfill).toHaveBeenCalledWith({ test: true });
|
|
708
|
-
|
|
709
|
-
// Verify messages are updated correctly
|
|
710
|
-
expect(result.current.messages[2]).toEqual({
|
|
711
|
-
role: "tool",
|
|
712
|
-
parts: [
|
|
713
|
-
{
|
|
714
|
-
type: "toolResult",
|
|
715
|
-
toolName: "testTool",
|
|
716
|
-
result: undefined,
|
|
717
|
-
},
|
|
718
|
-
],
|
|
719
|
-
});
|
|
720
|
-
});
|
|
721
|
-
|
|
722
|
-
it("handles empty tool calls", async () => {
|
|
723
|
-
const mockTool = {
|
|
724
|
-
name: "testTool",
|
|
725
|
-
description: "A test tool",
|
|
726
|
-
fulfill: jest.fn(),
|
|
727
|
-
};
|
|
728
|
-
|
|
729
|
-
const streamedChunks = [
|
|
730
|
-
{
|
|
731
|
-
type: "toolCall",
|
|
732
|
-
toolName: "testTool",
|
|
733
|
-
args: { test: true },
|
|
734
|
-
} as ChatData,
|
|
735
|
-
];
|
|
736
|
-
|
|
737
|
-
let chunkIndex = 0;
|
|
738
|
-
mockReader.read.mockImplementation(() => {
|
|
739
|
-
if (chunkIndex < streamedChunks.length) {
|
|
740
|
-
mockDecode.mockReturnValueOnce(
|
|
741
|
-
JSON.stringify(streamedChunks[chunkIndex])
|
|
742
|
-
);
|
|
743
|
-
chunkIndex++;
|
|
744
|
-
return Promise.resolve({ done: false, value: new Uint8Array() });
|
|
745
|
-
}
|
|
746
|
-
return Promise.resolve({ done: true });
|
|
747
|
-
});
|
|
748
|
-
|
|
749
|
-
const { result } = renderHook(() =>
|
|
750
|
-
useAiAssistantSession({
|
|
751
|
-
tools: [mockTool],
|
|
752
|
-
})
|
|
753
|
-
);
|
|
754
|
-
|
|
755
|
-
// Send a message that will trigger tool calls
|
|
756
|
-
await act(async () => {
|
|
757
|
-
await result.current.sendUserMessage("Test message");
|
|
758
|
-
});
|
|
759
|
-
|
|
760
|
-
// Send another message to verify tool calls are cleared
|
|
761
|
-
mockReader.read.mockImplementation(() => Promise.resolve({ done: true }));
|
|
762
|
-
await act(async () => {
|
|
763
|
-
await result.current.sendUserMessage("Another message");
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
expect(mockTool.fulfill).toHaveBeenCalledTimes(1);
|
|
767
|
-
});
|
|
768
|
-
});
|