@automattic/jetpack-ai-client 0.1.0 → 0.1.2

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 (32) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +18 -96
  3. package/index.ts +28 -1
  4. package/package.json +17 -4
  5. package/src/ask-question/Readme.md +1 -1
  6. package/src/ask-question/index.ts +21 -7
  7. package/src/components/ai-control/Readme.md +29 -0
  8. package/src/components/ai-control/index.tsx +165 -0
  9. package/src/components/ai-control/stories/index.stories.tsx +68 -0
  10. package/src/components/ai-control/style.scss +130 -0
  11. package/src/components/ai-status-indicator/Readme.md +11 -0
  12. package/src/components/ai-status-indicator/index.tsx +43 -0
  13. package/src/components/ai-status-indicator/stories/index.mdx +25 -0
  14. package/src/components/ai-status-indicator/stories/index.stories.tsx +84 -0
  15. package/src/components/ai-status-indicator/style.scss +50 -0
  16. package/src/data-flow/Readme.md +115 -0
  17. package/src/data-flow/context.tsx +76 -0
  18. package/src/data-flow/index.ts +3 -0
  19. package/src/data-flow/use-ai-context.ts +88 -0
  20. package/src/data-flow/with-ai-assistant-data.tsx +52 -0
  21. package/src/hooks/use-ai-suggestions/Readme.md +127 -0
  22. package/src/hooks/use-ai-suggestions/index.ts +358 -0
  23. package/src/icons/Readme.md +22 -0
  24. package/src/icons/ai-assistant.tsx +31 -0
  25. package/src/icons/index.ts +3 -0
  26. package/src/icons/origami-plane.tsx +20 -0
  27. package/src/icons/speak-tone.tsx +24 -0
  28. package/src/icons/stories/index.stories.tsx +46 -0
  29. package/src/icons/stories/style.module.scss +21 -0
  30. package/src/suggestions-event-source/index.ts +113 -15
  31. package/src/types.ts +48 -0
  32. package/src/index.js +0 -26
@@ -6,19 +6,34 @@ import debugFactory from 'debug';
6
6
  /*
7
7
  * Types & constants
8
8
  */
9
- import type { PromptItemProps } from '../types';
9
+ import { getErrorData } from '../hooks/use-ai-suggestions';
10
+ import {
11
+ ERROR_MODERATION,
12
+ ERROR_NETWORK,
13
+ ERROR_QUOTA_EXCEEDED,
14
+ ERROR_RESPONSE,
15
+ ERROR_SERVICE_UNAVAILABLE,
16
+ ERROR_UNCLEAR_PROMPT,
17
+ } from '../types';
18
+ import type { PromptMessagesProp, PromptProp, SuggestionErrorCode } from '../types';
10
19
 
11
20
  type SuggestionsEventSourceConstructorArgs = {
12
21
  url?: string;
13
- question: string | PromptItemProps[];
22
+ question: PromptProp;
14
23
  token: string;
15
24
  options?: {
16
25
  postId?: number;
17
26
  feature?: 'ai-assistant-experimental' | string | undefined;
18
27
  fromCache?: boolean;
28
+ functions?: Array< object >;
19
29
  };
20
30
  };
21
31
 
32
+ type FunctionCallProps = {
33
+ name?: string;
34
+ arguments?: string;
35
+ };
36
+
22
37
  const debug = debugFactory( 'jetpack-ai-client:suggestions-event-source' );
23
38
 
24
39
  /**
@@ -41,12 +56,17 @@ const debug = debugFactory( 'jetpack-ai-client:suggestions-event-source' );
41
56
  */
42
57
  export default class SuggestionsEventSource extends EventTarget {
43
58
  fullMessage: string;
59
+ fullFunctionCall: FunctionCallProps;
44
60
  isPromptClear: boolean;
45
61
  controller: AbortController;
46
62
 
47
63
  constructor( data: SuggestionsEventSourceConstructorArgs ) {
48
64
  super();
49
65
  this.fullMessage = '';
66
+ this.fullFunctionCall = {
67
+ name: '',
68
+ arguments: '',
69
+ };
50
70
  this.isPromptClear = false;
51
71
 
52
72
  // The AbortController is used to close the fetchEventSource connection
@@ -61,7 +81,18 @@ export default class SuggestionsEventSource extends EventTarget {
61
81
  token,
62
82
  options = {},
63
83
  }: SuggestionsEventSourceConstructorArgs ) {
64
- const bodyData = { post_id: options?.postId, messages: [], question: '', feature: '' };
84
+ const bodyData: {
85
+ post_id?: number;
86
+ messages?: PromptMessagesProp;
87
+ question?: PromptProp;
88
+ feature?: string;
89
+ functions?: Array< object >;
90
+ } = {};
91
+
92
+ // Populate body data with post id
93
+ if ( options?.postId ) {
94
+ bodyData.post_id = options.postId;
95
+ }
65
96
 
66
97
  // If the url is not provided, we use the default one
67
98
  if ( ! url ) {
@@ -76,7 +107,7 @@ export default class SuggestionsEventSource extends EventTarget {
76
107
  debug( 'URL not provided, using default: %o', url );
77
108
  }
78
109
 
79
- // question can be a string or an array of PromptItemProps
110
+ // question can be a string or an array of PromptMessagesProp
80
111
  if ( Array.isArray( question ) ) {
81
112
  bodyData.messages = question;
82
113
  } else {
@@ -89,6 +120,12 @@ export default class SuggestionsEventSource extends EventTarget {
89
120
  bodyData.feature = options.feature;
90
121
  }
91
122
 
123
+ // Propagate the functions option
124
+ if ( options?.functions?.length ) {
125
+ debug( 'Functions: %o', options.functions );
126
+ bodyData.functions = options.functions;
127
+ }
128
+
92
129
  await fetchEventSource( url, {
93
130
  openWhenHidden: true,
94
131
  method: 'POST',
@@ -115,6 +152,9 @@ export default class SuggestionsEventSource extends EventTarget {
115
152
  if ( response.ok ) {
116
153
  return;
117
154
  }
155
+
156
+ let errorCode: SuggestionErrorCode;
157
+
118
158
  if (
119
159
  response.status >= 400 &&
120
160
  response.status <= 500 &&
@@ -128,7 +168,8 @@ export default class SuggestionsEventSource extends EventTarget {
128
168
  * service unavailable
129
169
  */
130
170
  if ( response.status === 503 ) {
131
- this.dispatchEvent( new CustomEvent( 'error_service_unavailable' ) );
171
+ errorCode = ERROR_SERVICE_UNAVAILABLE;
172
+ this.dispatchEvent( new CustomEvent( ERROR_SERVICE_UNAVAILABLE ) );
132
173
  }
133
174
 
134
175
  /*
@@ -136,7 +177,8 @@ export default class SuggestionsEventSource extends EventTarget {
136
177
  * you exceeded your current quota please check your plan and billing details
137
178
  */
138
179
  if ( response.status === 429 ) {
139
- this.dispatchEvent( new CustomEvent( 'error_quota_exceeded' ) );
180
+ errorCode = ERROR_QUOTA_EXCEEDED;
181
+ this.dispatchEvent( new CustomEvent( ERROR_QUOTA_EXCEEDED ) );
140
182
  }
141
183
 
142
184
  /*
@@ -144,9 +186,17 @@ export default class SuggestionsEventSource extends EventTarget {
144
186
  * request flagged by moderation system
145
187
  */
146
188
  if ( response.status === 422 ) {
147
- this.dispatchEvent( new CustomEvent( 'error_moderation' ) );
189
+ errorCode = ERROR_MODERATION;
190
+ this.dispatchEvent( new CustomEvent( ERROR_MODERATION ) );
148
191
  }
149
192
 
193
+ // Always dispatch a global ERROR_RESPONSE event
194
+ this.dispatchEvent(
195
+ new CustomEvent( ERROR_RESPONSE, {
196
+ detail: getErrorData( errorCode ),
197
+ } )
198
+ );
199
+
150
200
  throw new Error();
151
201
  },
152
202
 
@@ -168,7 +218,12 @@ export default class SuggestionsEventSource extends EventTarget {
168
218
  const replacedMessage = this.fullMessage.replace( /__|(\*\*)/g, '' );
169
219
  if ( replacedMessage.startsWith( 'JETPACK_AI_ERROR' ) ) {
170
220
  // The unclear prompt marker was found, so we dispatch an error event
171
- this.dispatchEvent( new CustomEvent( 'error_unclear_prompt' ) );
221
+ this.dispatchEvent( new CustomEvent( ERROR_UNCLEAR_PROMPT ) );
222
+ this.dispatchEvent(
223
+ new CustomEvent( ERROR_RESPONSE, {
224
+ detail: getErrorData( ERROR_UNCLEAR_PROMPT ),
225
+ } )
226
+ );
172
227
  } else if ( 'JETPACK_AI_ERROR'.startsWith( replacedMessage ) ) {
173
228
  // Partial unclear prompt marker was found, so we wait for more data and print a debug message without dispatching an event
174
229
  debug( this.fullMessage );
@@ -184,14 +239,31 @@ export default class SuggestionsEventSource extends EventTarget {
184
239
 
185
240
  processEvent( e: EventSourceMessage ) {
186
241
  if ( e.data === '[DONE]' ) {
187
- // Dispatch an event with the full content
188
- this.dispatchEvent( new CustomEvent( 'done', { detail: this.fullMessage } ) );
189
- debug( 'Done: %o', this.fullMessage );
242
+ if ( this.fullMessage.length ) {
243
+ // Dispatch an event with the full content
244
+ this.dispatchEvent( new CustomEvent( 'done', { detail: this.fullMessage } ) );
245
+ debug( 'Done: %o', this.fullMessage );
246
+ return;
247
+ }
248
+
249
+ if ( this.fullFunctionCall.name.length ) {
250
+ this.dispatchEvent( new CustomEvent( 'function_done', { detail: this.fullFunctionCall } ) );
251
+ debug( 'Done: %o', this.fullFunctionCall );
252
+ return;
253
+ }
254
+ }
255
+
256
+ let data;
257
+ try {
258
+ data = JSON.parse( e.data );
259
+ } catch ( err ) {
260
+ debug( 'Error parsing JSON', e, err );
190
261
  return;
191
262
  }
263
+ const { delta } = data?.choices?.[ 0 ] ?? { delta: { content: null, function_call: null } };
264
+ const chunk = delta.content;
265
+ const functionCallChunk = delta.function_call;
192
266
 
193
- const data = JSON.parse( e.data );
194
- const chunk = data.choices[ 0 ].delta.content;
195
267
  if ( chunk ) {
196
268
  this.fullMessage += chunk;
197
269
  this.checkForUnclearPrompt();
@@ -200,20 +272,46 @@ export default class SuggestionsEventSource extends EventTarget {
200
272
  // Dispatch an event with the chunk
201
273
  this.dispatchEvent( new CustomEvent( 'chunk', { detail: chunk } ) );
202
274
  // Dispatch an event with the full message
275
+ debug( 'suggestion: %o', this.fullMessage );
203
276
  this.dispatchEvent( new CustomEvent( 'suggestion', { detail: this.fullMessage } ) );
204
277
  }
205
278
  }
279
+
280
+ if ( functionCallChunk ) {
281
+ if ( functionCallChunk.name != null ) {
282
+ this.fullFunctionCall.name += functionCallChunk.name;
283
+ }
284
+
285
+ if ( functionCallChunk.arguments != null ) {
286
+ this.fullFunctionCall.arguments += functionCallChunk.arguments;
287
+ }
288
+
289
+ // Dispatch an event with the function call
290
+ this.dispatchEvent(
291
+ new CustomEvent( 'functionCallChunk', { detail: this.fullFunctionCall } )
292
+ );
293
+ }
206
294
  }
207
295
 
208
296
  processConnectionError( response ) {
209
297
  debug( 'Connection error: %o', response );
210
- this.dispatchEvent( new CustomEvent( 'error_network', { detail: response } ) );
298
+ this.dispatchEvent( new CustomEvent( ERROR_NETWORK, { detail: response } ) );
299
+ this.dispatchEvent(
300
+ new CustomEvent( ERROR_RESPONSE, {
301
+ detail: getErrorData( ERROR_NETWORK ),
302
+ } )
303
+ );
211
304
  }
212
305
 
213
306
  processErrorEvent( e ) {
214
307
  debug( 'onerror: %o', e );
215
308
 
216
309
  // Dispatch a generic network error event
217
- this.dispatchEvent( new CustomEvent( 'error_network', { detail: e } ) );
310
+ this.dispatchEvent( new CustomEvent( ERROR_NETWORK, { detail: e } ) );
311
+ this.dispatchEvent(
312
+ new CustomEvent( ERROR_RESPONSE, {
313
+ detail: getErrorData( ERROR_NETWORK ),
314
+ } )
315
+ );
218
316
  }
219
317
  }
package/src/types.ts CHANGED
@@ -1,4 +1,52 @@
1
+ export const ERROR_SERVICE_UNAVAILABLE = 'error_service_unavailable' as const;
2
+ export const ERROR_QUOTA_EXCEEDED = 'error_quota_exceeded' as const;
3
+ export const ERROR_MODERATION = 'error_moderation' as const;
4
+ export const ERROR_NETWORK = 'error_network' as const;
5
+ export const ERROR_UNCLEAR_PROMPT = 'error_unclear_prompt' as const;
6
+ export const ERROR_RESPONSE = 'error_response' as const;
7
+
8
+ export type SuggestionErrorCode =
9
+ | typeof ERROR_SERVICE_UNAVAILABLE
10
+ | typeof ERROR_QUOTA_EXCEEDED
11
+ | typeof ERROR_MODERATION
12
+ | typeof ERROR_NETWORK
13
+ | typeof ERROR_UNCLEAR_PROMPT
14
+ | typeof ERROR_RESPONSE;
15
+
16
+ /*
17
+ * Prompt types
18
+ */
1
19
  export type PromptItemProps = {
2
20
  role: 'system' | 'user' | 'assistant';
3
21
  content: string;
4
22
  };
23
+
24
+ export type PromptMessagesProp = Array< PromptItemProps >;
25
+
26
+ export type PromptProp = PromptMessagesProp | string;
27
+
28
+ /*
29
+ * Data Flow types
30
+ */
31
+ export type { UseAiContextOptions } from './data-flow/use-ai-context';
32
+ export type { RequestingErrorProps } from './hooks/use-ai-suggestions';
33
+
34
+ /*
35
+ * Requests types
36
+ */
37
+
38
+ const REQUESTING_STATE_INIT = 'init' as const;
39
+ const REQUESTING_STATE_REQUESTING = 'requesting' as const;
40
+ const REQUESTING_STATE_SUGGESTING = 'suggesting' as const;
41
+ const REQUESTING_STATE_DONE = 'done' as const;
42
+ const REQUESTING_STATE_ERROR = 'error' as const;
43
+
44
+ export const REQUESTING_STATES = [
45
+ REQUESTING_STATE_INIT,
46
+ REQUESTING_STATE_REQUESTING,
47
+ REQUESTING_STATE_SUGGESTING,
48
+ REQUESTING_STATE_DONE,
49
+ REQUESTING_STATE_ERROR,
50
+ ] as const;
51
+
52
+ export type RequestingStateProp = ( typeof REQUESTING_STATES )[ number ];
package/src/index.js DELETED
@@ -1,26 +0,0 @@
1
- import apiFetch from '@wordpress/api-fetch';
2
-
3
- /**
4
- * Fetches images from Jetpack AI
5
- *
6
- * It's up to the consumer to catch errors
7
- *
8
- * @param {*} prompt - The prompt to send to Jetpack AI
9
- * @param {*} postId - The post ID where the completion is being requested
10
- * @returns {Promise<Array<string>>} The images
11
- */
12
- export function requestImages( prompt, postId ) {
13
- return apiFetch( {
14
- path: '/wpcom/v2/jetpack-ai/images/generations',
15
- method: 'POST',
16
- data: {
17
- prompt,
18
- post_id: postId,
19
- },
20
- } ).then( res => {
21
- const images = res.data.map( image => {
22
- return 'data:image/png;base64,' + image.b64_json;
23
- } );
24
- return images;
25
- } );
26
- }