@automattic/jetpack-ai-client 0.1.0 → 0.1.1

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.
@@ -0,0 +1,24 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { SVG, Path } from '@wordpress/components';
5
+ import React from 'react';
6
+
7
+ const speakTone = (
8
+ <SVG width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
9
+ <Path
10
+ fillRule="evenodd"
11
+ clipRule="evenodd"
12
+ d="M12.5 10C12.5 11.3807 11.3807 12.5 10 12.5C8.61929 12.5 7.5 11.3807 7.5 10C7.5 8.61929 8.61929 7.5 10 7.5C11.3807 7.5 12.5 8.61929 12.5 10ZM14 10C14 12.2091 12.2091 14 10 14C7.79086 14 6 12.2091 6 10C6 7.79086 7.79086 6 10 6C12.2091 6 14 7.79086 14 10ZM16.75 21V19C16.75 17.4812 15.5188 16.25 14 16.25L6 16.25C4.48122 16.25 3.25 17.4812 3.25 19V21H4.75L4.75 19C4.75 18.3096 5.30964 17.75 6 17.75L14 17.75C14.6904 17.75 15.25 18.3096 15.25 19V21H16.75Z"
13
+ fill="currentColor"
14
+ />
15
+ <Path
16
+ fillRule="evenodd"
17
+ clipRule="evenodd"
18
+ d="M19.976 16.3599C21.2507 14.5642 22.0001 12.3695 22.0001 9.99969C22.0001 7.63128 21.2515 5.43769 19.9782 3.64258L18.754 4.50967C19.8537 6.05996 20.5001 7.95434 20.5001 9.99969C20.5001 12.0464 19.8528 13.9419 18.7519 15.4928L19.976 16.3599ZM17.3357 14.4897C18.2357 13.222 18.7648 11.6727 18.7648 9.99969C18.7648 8.32808 18.2365 6.77984 17.3379 5.51279L16.1137 6.37988C16.8387 7.4021 17.2648 8.65114 17.2648 9.99969C17.2648 11.3496 16.8378 12.5998 16.1116 13.6226L17.3357 14.4897Z"
19
+ fill="currentColor"
20
+ />
21
+ </SVG>
22
+ );
23
+
24
+ export default speakTone;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { Icon } from '@wordpress/components';
5
+ import React from 'react';
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import * as allIcons from '../index';
10
+ import styles from './style.module.scss';
11
+ /**
12
+ * Types
13
+ */
14
+ import type { Meta } from '@storybook/react';
15
+
16
+ export default {
17
+ title: 'JS Packages/AI Client/Icons',
18
+ component: allIcons,
19
+ parameters: {},
20
+ } as Meta< typeof allIcons >;
21
+
22
+ /**
23
+ * Icons story components.
24
+ *
25
+ * @returns {object} - story component
26
+ */
27
+ function IconsStory() {
28
+ return (
29
+ <div className={ styles[ 'icons-container' ] }>
30
+ { Object.entries( allIcons ).map( ( [ name, icon ] ) => {
31
+ return (
32
+ <div key={ name } className={ styles[ 'icon-container' ] }>
33
+ <Icon icon={ icon } size={ 32 } />
34
+ <div className={ styles[ 'icon-name' ] }>{ name }</div>
35
+ </div>
36
+ );
37
+ } ) }
38
+ </div>
39
+ );
40
+ }
41
+
42
+ const Template = args => <IconsStory { ...args } />;
43
+
44
+ const DefaultArgs = {};
45
+ export const Default = Template.bind( {} );
46
+ Default.args = DefaultArgs;
@@ -0,0 +1,21 @@
1
+ .icons-container {
2
+ display: flex;
3
+ flex-wrap: wrap;
4
+ margin-bottom: 20px;
5
+ flex-direction: column;
6
+ }
7
+
8
+ .icon-container {
9
+ padding: 10px;
10
+ box-shadow: 0 0 1px inset rgba(0, 0, 0 , 0.5 );
11
+ background-color: white;
12
+ border-radius: 5px;
13
+ margin: 2px;
14
+ display: flex;
15
+ gap: 8px;
16
+ align-items: center;
17
+ }
18
+
19
+ .icon-name {
20
+ font-size: 14px;
21
+ }
@@ -6,19 +6,32 @@ import debugFactory from 'debug';
6
6
  /*
7
7
  * Types & constants
8
8
  */
9
- import type { PromptItemProps } from '../types';
9
+ import {
10
+ ERROR_MODERATION,
11
+ ERROR_NETWORK,
12
+ ERROR_QUOTA_EXCEEDED,
13
+ ERROR_SERVICE_UNAVAILABLE,
14
+ ERROR_UNCLEAR_PROMPT,
15
+ } from '../types';
16
+ import type { PromptMessagesProp, PromptProp } from '../types';
10
17
 
11
18
  type SuggestionsEventSourceConstructorArgs = {
12
19
  url?: string;
13
- question: string | PromptItemProps[];
20
+ question: PromptProp;
14
21
  token: string;
15
22
  options?: {
16
23
  postId?: number;
17
24
  feature?: 'ai-assistant-experimental' | string | undefined;
18
25
  fromCache?: boolean;
26
+ functions?: Array< object >;
19
27
  };
20
28
  };
21
29
 
30
+ type FunctionCallProps = {
31
+ name?: string;
32
+ arguments?: string;
33
+ };
34
+
22
35
  const debug = debugFactory( 'jetpack-ai-client:suggestions-event-source' );
23
36
 
24
37
  /**
@@ -41,12 +54,17 @@ const debug = debugFactory( 'jetpack-ai-client:suggestions-event-source' );
41
54
  */
42
55
  export default class SuggestionsEventSource extends EventTarget {
43
56
  fullMessage: string;
57
+ fullFunctionCall: FunctionCallProps;
44
58
  isPromptClear: boolean;
45
59
  controller: AbortController;
46
60
 
47
61
  constructor( data: SuggestionsEventSourceConstructorArgs ) {
48
62
  super();
49
63
  this.fullMessage = '';
64
+ this.fullFunctionCall = {
65
+ name: '',
66
+ arguments: '',
67
+ };
50
68
  this.isPromptClear = false;
51
69
 
52
70
  // The AbortController is used to close the fetchEventSource connection
@@ -61,7 +79,18 @@ export default class SuggestionsEventSource extends EventTarget {
61
79
  token,
62
80
  options = {},
63
81
  }: SuggestionsEventSourceConstructorArgs ) {
64
- const bodyData = { post_id: options?.postId, messages: [], question: '', feature: '' };
82
+ const bodyData: {
83
+ post_id?: number;
84
+ messages?: PromptMessagesProp;
85
+ question?: PromptProp;
86
+ feature?: string;
87
+ functions?: Array< object >;
88
+ } = {};
89
+
90
+ // Populate body data with post id
91
+ if ( options?.postId ) {
92
+ bodyData.post_id = options.postId;
93
+ }
65
94
 
66
95
  // If the url is not provided, we use the default one
67
96
  if ( ! url ) {
@@ -76,7 +105,7 @@ export default class SuggestionsEventSource extends EventTarget {
76
105
  debug( 'URL not provided, using default: %o', url );
77
106
  }
78
107
 
79
- // question can be a string or an array of PromptItemProps
108
+ // question can be a string or an array of PromptMessagesProp
80
109
  if ( Array.isArray( question ) ) {
81
110
  bodyData.messages = question;
82
111
  } else {
@@ -89,6 +118,12 @@ export default class SuggestionsEventSource extends EventTarget {
89
118
  bodyData.feature = options.feature;
90
119
  }
91
120
 
121
+ // Propagate the functions option
122
+ if ( options?.functions?.length ) {
123
+ debug( 'Functions: %o', options.functions );
124
+ bodyData.functions = options.functions;
125
+ }
126
+
92
127
  await fetchEventSource( url, {
93
128
  openWhenHidden: true,
94
129
  method: 'POST',
@@ -128,7 +163,7 @@ export default class SuggestionsEventSource extends EventTarget {
128
163
  * service unavailable
129
164
  */
130
165
  if ( response.status === 503 ) {
131
- this.dispatchEvent( new CustomEvent( 'error_service_unavailable' ) );
166
+ this.dispatchEvent( new CustomEvent( ERROR_SERVICE_UNAVAILABLE ) );
132
167
  }
133
168
 
134
169
  /*
@@ -136,7 +171,7 @@ export default class SuggestionsEventSource extends EventTarget {
136
171
  * you exceeded your current quota please check your plan and billing details
137
172
  */
138
173
  if ( response.status === 429 ) {
139
- this.dispatchEvent( new CustomEvent( 'error_quota_exceeded' ) );
174
+ this.dispatchEvent( new CustomEvent( ERROR_QUOTA_EXCEEDED ) );
140
175
  }
141
176
 
142
177
  /*
@@ -144,7 +179,7 @@ export default class SuggestionsEventSource extends EventTarget {
144
179
  * request flagged by moderation system
145
180
  */
146
181
  if ( response.status === 422 ) {
147
- this.dispatchEvent( new CustomEvent( 'error_moderation' ) );
182
+ this.dispatchEvent( new CustomEvent( ERROR_MODERATION ) );
148
183
  }
149
184
 
150
185
  throw new Error();
@@ -168,7 +203,7 @@ export default class SuggestionsEventSource extends EventTarget {
168
203
  const replacedMessage = this.fullMessage.replace( /__|(\*\*)/g, '' );
169
204
  if ( replacedMessage.startsWith( 'JETPACK_AI_ERROR' ) ) {
170
205
  // The unclear prompt marker was found, so we dispatch an error event
171
- this.dispatchEvent( new CustomEvent( 'error_unclear_prompt' ) );
206
+ this.dispatchEvent( new CustomEvent( ERROR_UNCLEAR_PROMPT ) );
172
207
  } else if ( 'JETPACK_AI_ERROR'.startsWith( replacedMessage ) ) {
173
208
  // Partial unclear prompt marker was found, so we wait for more data and print a debug message without dispatching an event
174
209
  debug( this.fullMessage );
@@ -184,14 +219,31 @@ export default class SuggestionsEventSource extends EventTarget {
184
219
 
185
220
  processEvent( e: EventSourceMessage ) {
186
221
  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 );
222
+ if ( this.fullMessage.length ) {
223
+ // Dispatch an event with the full content
224
+ this.dispatchEvent( new CustomEvent( 'done', { detail: this.fullMessage } ) );
225
+ debug( 'Done: %o', this.fullMessage );
226
+ return;
227
+ }
228
+
229
+ if ( this.fullFunctionCall.name.length ) {
230
+ this.dispatchEvent( new CustomEvent( 'function_done', { detail: this.fullFunctionCall } ) );
231
+ debug( 'Done: %o', this.fullFunctionCall );
232
+ return;
233
+ }
234
+ }
235
+
236
+ let data;
237
+ try {
238
+ data = JSON.parse( e.data );
239
+ } catch ( err ) {
240
+ debug( 'Error parsing JSON', e, err );
190
241
  return;
191
242
  }
243
+ const { delta } = data?.choices?.[ 0 ] ?? { delta: { content: null, function_call: null } };
244
+ const chunk = delta.content;
245
+ const functionCallChunk = delta.function_call;
192
246
 
193
- const data = JSON.parse( e.data );
194
- const chunk = data.choices[ 0 ].delta.content;
195
247
  if ( chunk ) {
196
248
  this.fullMessage += chunk;
197
249
  this.checkForUnclearPrompt();
@@ -200,20 +252,36 @@ export default class SuggestionsEventSource extends EventTarget {
200
252
  // Dispatch an event with the chunk
201
253
  this.dispatchEvent( new CustomEvent( 'chunk', { detail: chunk } ) );
202
254
  // Dispatch an event with the full message
255
+ debug( 'suggestion: %o', this.fullMessage );
203
256
  this.dispatchEvent( new CustomEvent( 'suggestion', { detail: this.fullMessage } ) );
204
257
  }
205
258
  }
259
+
260
+ if ( functionCallChunk ) {
261
+ if ( functionCallChunk.name != null ) {
262
+ this.fullFunctionCall.name += functionCallChunk.name;
263
+ }
264
+
265
+ if ( functionCallChunk.arguments != null ) {
266
+ this.fullFunctionCall.arguments += functionCallChunk.arguments;
267
+ }
268
+
269
+ // Dispatch an event with the function call
270
+ this.dispatchEvent(
271
+ new CustomEvent( 'functionCallChunk', { detail: this.fullFunctionCall } )
272
+ );
273
+ }
206
274
  }
207
275
 
208
276
  processConnectionError( response ) {
209
277
  debug( 'Connection error: %o', response );
210
- this.dispatchEvent( new CustomEvent( 'error_network', { detail: response } ) );
278
+ this.dispatchEvent( new CustomEvent( ERROR_NETWORK, { detail: response } ) );
211
279
  }
212
280
 
213
281
  processErrorEvent( e ) {
214
282
  debug( 'onerror: %o', e );
215
283
 
216
284
  // Dispatch a generic network error event
217
- this.dispatchEvent( new CustomEvent( 'error_network', { detail: e } ) );
285
+ this.dispatchEvent( new CustomEvent( ERROR_NETWORK, { detail: e } ) );
218
286
  }
219
287
  }
package/src/types.ts CHANGED
@@ -1,4 +1,24 @@
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
+
7
+ export type SuggestionErrorCode =
8
+ | typeof ERROR_SERVICE_UNAVAILABLE
9
+ | typeof ERROR_QUOTA_EXCEEDED
10
+ | typeof ERROR_MODERATION
11
+ | typeof ERROR_NETWORK
12
+ | typeof ERROR_UNCLEAR_PROMPT;
13
+
14
+ /*
15
+ * Prompt types
16
+ */
1
17
  export type PromptItemProps = {
2
18
  role: 'system' | 'user' | 'assistant';
3
19
  content: string;
4
20
  };
21
+
22
+ export type PromptMessagesProp = Array< PromptItemProps >;
23
+
24
+ export type PromptProp = PromptMessagesProp | string;
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
- }