@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.
Files changed (26) hide show
  1. package/package.json +3 -7
  2. package/types.d.ts +18 -2
  3. package/src/backend/express.test.ts +0 -208
  4. package/src/backend/express.ts +0 -105
  5. package/src/frontend/AiAssistant.test.tsx +0 -19
  6. package/src/frontend/AiAssistant.tsx +0 -26
  7. package/src/frontend/components/AiAssistantComponent.scss +0 -24
  8. package/src/frontend/components/AiAssistantComponent.test.tsx +0 -270
  9. package/src/frontend/components/AiAssistantComponent.tsx +0 -246
  10. package/src/frontend/components/messageRenderers/AssistantMessage.test.tsx +0 -154
  11. package/src/frontend/components/messageRenderers/AssistantMessage.tsx +0 -104
  12. package/src/frontend/components/messageRenderers/UserMessage.test.tsx +0 -103
  13. package/src/frontend/components/messageRenderers/UserMessage.tsx +0 -43
  14. package/src/frontend/config.ts +0 -54
  15. package/src/frontend/reactComponent.ts +0 -6
  16. package/src/frontend/tools/generatedSdks.ts +0 -44
  17. package/src/frontend/tools/llm-tools/getCurrentUser.test.ts +0 -100
  18. package/src/frontend/tools/llm-tools/getCurrentUser.ts +0 -158
  19. package/src/frontend/tools/llm-tools/index.ts +0 -6
  20. package/src/frontend/tools/llm-tools/kazooPlatform/getRewardsAndRecognitionCurrentUser.test.ts +0 -76
  21. package/src/frontend/tools/llm-tools/kazooPlatform/getRewardsAndRecognitionCurrentUser.ts +0 -36
  22. package/src/frontend/tools/llm-tools/pulsePlatform/.gitkeep +0 -0
  23. package/src/frontend/tools/llm-tools/shared/.gitkeep +0 -0
  24. package/src/frontend/tools/useAiAssistantSession.test.ts +0 -768
  25. package/src/frontend/tools/useAiAssistantSession.ts +0 -374
  26. package/src/frontend/useStyles.ts +0 -36
@@ -1,374 +0,0 @@
1
- import React from "react";
2
-
3
- import { coalesce } from "@kazoohr/helpers";
4
- import {
5
- type ToolDeclaration,
6
- type ChatItem,
7
- type SupportedTextModel,
8
- ChatData,
9
- ChatDataText,
10
- ChatDataToolResult,
11
- ChatDataToolCall,
12
- } from "@kazoohr/llm/types";
13
-
14
- import { getLlmCallUrl } from "../config";
15
-
16
- export type ChatItemWithTimestamp = ChatItem & { timestamp?: Date };
17
-
18
- /**
19
- * This hook is used to manage the state of the AI assistant session.
20
- * It handles as much as possible of the LLM session, so that the dev
21
- * can focus on efficiently rendering the UI.
22
- */
23
- // eslint-disable-next-line max-lines-per-function, complexity
24
- export function useAiAssistantSession({
25
- model,
26
- initialMessages,
27
- temperature,
28
- onAfterLlmCall,
29
- onBeforeLlmCall,
30
- onError,
31
- tools,
32
- onChunkAdded,
33
- }: {
34
- model?: SupportedTextModel;
35
- initialMessages?: ChatItemWithTimestamp[];
36
- temperature?: number;
37
- onAfterLlmCall?: (messages: ChatItem[]) => void;
38
- onBeforeLlmCall?: () => void;
39
- onError?: (error: Error) => void;
40
- tools?: ToolDeclaration[];
41
- onChunkAdded?: () => void;
42
- } = {}) {
43
- const [isLoading, setIsLoading] = React.useState(false);
44
- const [messages, setMessages] = React.useState<ChatItemWithTimestamp[]>(
45
- () =>
46
- initialMessages?.map((msg) => ({
47
- ...msg,
48
- timestamp: coalesce(msg.timestamp, new Date()), // Adds timestamp to each message, even if not provided
49
- })) ?? []
50
- );
51
-
52
- /**
53
- * We use a ref to store the debug info. Debug info shouldn't trigger a re-
54
- * render.
55
- */
56
- const debugInfo = React.useRef<DebugInfo>([]);
57
-
58
- /**
59
- * Sometimes, props change, but we don't want to recreate the callback functions
60
- * so we use a ref to store them, so that our callbacks always grab the latest
61
- * value.
62
- */
63
- const propRefs = React.useRef<PropRefs>({
64
- onAfterLlmCall,
65
- onBeforeLlmCall,
66
- onError,
67
- tools,
68
- });
69
-
70
- const sendMessages = React.useCallback(
71
- // eslint-disable-next-line max-lines-per-function, complexity
72
- async (messages: ChatItem[]) => {
73
- try {
74
- if (propRefs.current.onBeforeLlmCall) {
75
- debugInfo.current.push({
76
- time: new Date(),
77
- message: "Calling onBeforeLlmCall",
78
- level: "info",
79
- });
80
- propRefs.current.onBeforeLlmCall();
81
- }
82
- } catch (error) {
83
- debugInfo.current.push({
84
- time: new Date(),
85
- message: "Error calling onBeforeLlmCall",
86
- data: error,
87
- level: "error",
88
- });
89
- }
90
-
91
- const requestBody = {
92
- model,
93
- temperature,
94
- stream: true,
95
- messages,
96
- tools,
97
- };
98
-
99
- setIsLoading(true);
100
-
101
- debugInfo.current.push({
102
- time: new Date(),
103
- message: "Calling LLM API",
104
- data: requestBody,
105
- level: "info",
106
- });
107
-
108
- const response = await fetch(getLlmCallUrl(), {
109
- method: "POST",
110
- body: JSON.stringify(requestBody),
111
- credentials: "include",
112
- headers: {
113
- "Content-Type": "application/json",
114
- Accept: "text/event-stream",
115
- },
116
- }).catch((error) => {
117
- debugInfo.current.push({
118
- time: new Date(),
119
- message: "Error calling LLM API",
120
- data: error,
121
- level: "error",
122
- });
123
- propRefs.current.onError?.(
124
- new Error("Unexpected error calling LLM API. Please try again.")
125
- );
126
- });
127
-
128
- setIsLoading(false);
129
-
130
- if (!response || !response.ok) {
131
- propRefs.current.onError?.(
132
- new Error("Unable to connect to LLM API. Please try again.")
133
- );
134
- return false;
135
- }
136
-
137
- try {
138
- const reader = response.body?.getReader();
139
- if (!reader) {
140
- propRefs.current.onError?.(
141
- new Error(
142
- "Unexpected client error. Response body reader not available."
143
- )
144
- );
145
- debugInfo.current.push({
146
- time: new Date(),
147
- message: "No reader available",
148
- level: "error",
149
- });
150
- return false;
151
- }
152
-
153
- const decoder = new TextDecoder();
154
- const chatDataText: ChatDataText = {
155
- type: "text",
156
- text: "",
157
- };
158
- const toolCalls: ChatDataToolCall[] = [];
159
-
160
- // Add the user's message immediately
161
- const newMessages: ChatItem[] = [...messages];
162
- setMessages(newMessages);
163
-
164
- let i = 1_000_000; // Bounding our loop, just in case
165
- while (i--) {
166
- const { done, value } = await reader.read();
167
- if (done) {
168
- break;
169
- }
170
-
171
- const chunk = decoder.decode(value);
172
- debugInfo.current.push({
173
- time: new Date(),
174
- message: "Received chunk",
175
- data: chunk,
176
- level: "info",
177
- });
178
- const parsedChunk: ChatData = JSON.parse(chunk);
179
-
180
- if (parsedChunk.type === "text") {
181
- chatDataText.text += parsedChunk.text;
182
- } else if (parsedChunk.type === "toolCall") {
183
- toolCalls.push(parsedChunk);
184
- }
185
-
186
- // Update the last message's text content
187
- setMessages((currentMessages) => {
188
- const updatedMessages = [...currentMessages];
189
-
190
- if (updatedMessages[updatedMessages.length - 1]?.role !== "model") {
191
- updatedMessages.push({
192
- role: "model",
193
- parts: [],
194
- timestamp: new Date(),
195
- });
196
- }
197
-
198
- updatedMessages[updatedMessages.length - 1].parts = [
199
- ...toolCalls,
200
- ...(chatDataText.text ? [chatDataText] : []),
201
- ];
202
-
203
- if (onChunkAdded) {
204
- onChunkAdded();
205
- }
206
-
207
- return updatedMessages;
208
- });
209
- }
210
-
211
- debugInfo.current.push({
212
- time: new Date(),
213
- message: "Completed streaming response",
214
- level: "info",
215
- data: {
216
- chatDataText,
217
- toolCalls,
218
- },
219
- });
220
-
221
- try {
222
- if (propRefs.current.onAfterLlmCall) {
223
- debugInfo.current.push({
224
- time: new Date(),
225
- message: "Calling onAfterLlmCall",
226
- level: "info",
227
- });
228
- propRefs.current.onAfterLlmCall(newMessages);
229
- }
230
- } catch (error) {
231
- debugInfo.current.push({
232
- time: new Date(),
233
- message: "Error calling onAfterLlmCall",
234
- data: error,
235
- level: "error",
236
- });
237
- } finally {
238
- // This branch re-runs this task if the last message from the LLM was
239
- // a tool call.
240
- handlePendingToolCalls({
241
- toolCalls,
242
- setMessages,
243
- sendMessages,
244
- propRefs,
245
- debugInfo,
246
- });
247
- }
248
- return true;
249
- } catch (streamError) {
250
- debugInfo.current.push({
251
- time: new Date(),
252
- message: "Error processing stream",
253
- data: {
254
- message: streamError.message,
255
- stack: streamError.stack,
256
- },
257
- level: "error",
258
- });
259
- propRefs.current.onError?.(
260
- new Error("Unexpected error serving the request. Please try again.")
261
- );
262
- return false;
263
- }
264
- },
265
- [model, temperature]
266
- );
267
-
268
- const sendUserMessage = React.useCallback(
269
- async (message: string) => {
270
- const originalMessages = [...messages];
271
- const newMessage: ChatItemWithTimestamp = {
272
- role: "user",
273
- parts: [{ type: "text", text: message }],
274
- timestamp: new Date(),
275
- };
276
- addMessage(newMessage);
277
- const success = await sendMessages([...originalMessages, newMessage]);
278
- if (!success) {
279
- debugInfo.current.push({
280
- time: new Date(),
281
- message: "Error sending message. Reverting to original messages",
282
- data: originalMessages,
283
- level: "error",
284
- });
285
- setMessages(originalMessages);
286
- }
287
- },
288
- [messages]
289
- );
290
-
291
- const addMessage = React.useCallback(
292
- (message: ChatItem) => {
293
- setMessages([...messages, message]);
294
- },
295
- [messages]
296
- );
297
-
298
- return {
299
- isLoading,
300
- messages,
301
- setMessages,
302
- debugInfo: debugInfo.current,
303
- addMessage,
304
- sendUserMessage,
305
- };
306
- }
307
-
308
- type PropRefs = {
309
- onAfterLlmCall?: (messages: ChatItem[]) => void;
310
- onBeforeLlmCall?: () => void;
311
- onError?: (error: Error) => void;
312
- tools?: ToolDeclaration[];
313
- };
314
-
315
- type DebugInfo = {
316
- time: Date;
317
- message: string;
318
- data?: any;
319
- level: "info" | "error" | "warn";
320
- }[];
321
-
322
- async function handlePendingToolCalls({
323
- toolCalls,
324
- sendMessages,
325
- setMessages,
326
- propRefs,
327
- debugInfo,
328
- }: {
329
- toolCalls: ChatDataToolCall[];
330
- setMessages: React.Dispatch<React.SetStateAction<ChatItem[]>>;
331
- sendMessages: (messages: ChatItem[]) => Promise<boolean>;
332
- propRefs: React.MutableRefObject<PropRefs>;
333
- debugInfo: React.MutableRefObject<DebugInfo>;
334
- }): Promise<void> {
335
- if (toolCalls.length === 0) {
336
- return;
337
- }
338
-
339
- const toolResults: ChatDataToolResult[] = [];
340
-
341
- for (const toolCall of toolCalls) {
342
- const tool = propRefs.current.tools?.find(
343
- (tool) => tool.name === toolCall.toolName
344
- );
345
- if (!tool) {
346
- propRefs.current.onError?.(
347
- new Error(`Tool ${toolCall.toolName} not found`)
348
- );
349
- debugInfo.current.push({
350
- time: new Date(),
351
- message: `Tool ${toolCall.toolName} not found`,
352
- data: {
353
- toolCall,
354
- tools: propRefs.current.tools,
355
- },
356
- level: "error",
357
- });
358
- continue;
359
- }
360
-
361
- const result = await tool.fulfill(toolCall.args);
362
- toolResults.push({
363
- type: "toolResult" as const,
364
- toolName: toolCall.toolName,
365
- result,
366
- });
367
- }
368
- const newMessage = { role: "tool" as const, parts: toolResults };
369
- setMessages((currentMessages) => {
370
- const newMessages = [...currentMessages, newMessage];
371
- sendMessages(newMessages);
372
- return newMessages;
373
- });
374
- }
@@ -1,36 +0,0 @@
1
- import React from "react";
2
-
3
- import { setStateValue } from "@kazoohr/helpers";
4
-
5
- const CONFETTI_CSS = "$$CONFETTI_CSS_REPLACEME$$";
6
-
7
- /**
8
- * This hook injects the Confetti CSS into the page and sets a flag in the
9
- * window object to indicate that the CSS has been injected. We do this so
10
- * that we can avoid a flash of unstyled content (FOUC); and to make the
11
- * process of shipping this component far simpler.
12
- */
13
- export function useStyles() {
14
- const [isLoaded, setIsLoaded] = React.useState(false);
15
- React.useEffect(() => {
16
- // I could avoid this by updating our root `types`; but that would leave
17
- // evidence of this ugly hack elsewhere in a codebase that it's not even
18
- // needed for. So we'll just live with a lint rule ignore.
19
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
20
- const windowAsAny = window as any;
21
- // istanbul ignore if - noop if the styles have already been injected
22
- if (windowAsAny._confettiStylesHaveBeenInjected) {
23
- setTimeout(setStateValue(setIsLoaded, true), 50);
24
- return;
25
- }
26
- const styles = document.createElement("style");
27
- styles.innerHTML = CONFETTI_CSS;
28
- document.head.appendChild(styles);
29
- windowAsAny._confettiStylesHaveBeenInjected = true;
30
- setTimeout(setStateValue(setIsLoaded, true), 50);
31
- }, []);
32
-
33
- return {
34
- isLoaded,
35
- };
36
- }