@adobe/spacecat-shared-gpt-client 1.2.22 → 1.3.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # [@adobe/spacecat-shared-gpt-client-v1.3.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-gpt-client-v1.3.0...@adobe/spacecat-shared-gpt-client-v1.3.1) (2024-11-15)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * SITES-26948 [Import Assistant] Allow calls to Firefall to use specified ims org id ([#443](https://github.com/adobe/spacecat-shared/issues/443)) ([a63016c](https://github.com/adobe/spacecat-shared/commit/a63016c426ae8a4c0f7f66653e49f1098d466ee8))
7
+
8
+ # [@adobe/spacecat-shared-gpt-client-v1.3.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-gpt-client-v1.2.22...@adobe/spacecat-shared-gpt-client-v1.3.0) (2024-11-12)
9
+
10
+
11
+ ### Features
12
+
13
+ * SITES-26591 [Import Assistant] Expand GPT client to user chat endpoint ([#436](https://github.com/adobe/spacecat-shared/issues/436)) ([4aaca2e](https://github.com/adobe/spacecat-shared/commit/4aaca2ede0fb9b019c8c88c2f7afd396065088b1))
14
+
1
15
  # [@adobe/spacecat-shared-gpt-client-v1.2.22](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-gpt-client-v1.2.21...@adobe/spacecat-shared-gpt-client-v1.2.22) (2024-11-11)
2
16
 
3
17
 
package/README.md CHANGED
@@ -10,6 +10,11 @@ To use the `FirefallClient`, you need to configure it with the following paramet
10
10
  - `FIREFALL_API_KEY`: Your API key for accessing the Firefall API.
11
11
  - `FIREFALL_API_CAPABILITY_NAME`: The capability name for the Firefall API.
12
12
 
13
+ Optionally, you can specify the IMS ORG ID to use when calling the Firefall APIs. If this value is not specified, the IMS_CLIENT_ID (see below) will
14
+ be used for the header's value:
15
+
16
+ - `FIREFALL_IMS_ORG_ID`: The IMS ORG ID to use when calling the Firefall APIs and tracking the request.
17
+
13
18
  These parameters can be set through environment variables or passed directly to the `FirefallClient.createFrom` method.
14
19
 
15
20
  Additionally, the configuration for the `@adobe/spacecat-shared-ims-client` library is required to fetch the service access token from the IMS API:
@@ -42,7 +47,12 @@ try {
42
47
 
43
48
  ### Fetching Insights
44
49
 
50
+ #### Via Capability Execution endpoint
51
+
45
52
  ```javascript
53
+ /**
54
+ * Fetch insights using the Firefall's capability execution endpoint.
55
+ */
46
56
  async function fetchInsights(prompt) {
47
57
  try {
48
58
  const client = FirefallClient.createFrom({
@@ -58,7 +68,7 @@ async function fetchInsights(prompt) {
58
68
  log: console,
59
69
  });
60
70
 
61
- const insights = await client.fetch(prompt);
71
+ const insights = await client.fetchCapabilityExecution(prompt);
62
72
  console.log('Insights:', insights);
63
73
  } catch (error) {
64
74
  console.error('Failed to fetch insights:', error.message);
@@ -68,6 +78,41 @@ async function fetchInsights(prompt) {
68
78
  fetchInsights('How can we improve customer satisfaction?');
69
79
  ```
70
80
 
81
+ #### Via Chat Completions endpoint
82
+
83
+ ```javascript
84
+ /**
85
+ * Fetch completions using the Firefall's chat completions endpoint.
86
+ */
87
+ async function fetchCompletions(prompt) {
88
+ try {
89
+ const client = FirefallClient.createFrom({
90
+ env: {
91
+ FIREFALL_API_ENDPOINT: 'https://api.firefall.example.com',
92
+ FIREFALL_API_KEY: 'yourApiKey',
93
+ IMS_HOST: 'ims.example.com',
94
+ IMS_CLIENT_ID: 'yourClientId',
95
+ IMS_CLIENT_CODE: 'yourClientCode',
96
+ IMS_CLIENT_SECRET: 'yourClientSecret',
97
+ },
98
+ log: console,
99
+ });
100
+ const options = {
101
+ imageUrls: ['data:image/png;base64,iVBORw0KGgoAAAA...='],
102
+ model:'gpt-4-vision',
103
+ responseFormat: undefined,
104
+ };
105
+
106
+ const response = await client.fetchChatCompletion(prompt, { options });
107
+ console.log('Response:', JSON.stringify(response));
108
+ } catch (error) {
109
+ console.error('Failed to fetch chat completion:', error.message);
110
+ }
111
+ }
112
+
113
+ fetchCompletions('Identify all food items in this image', { imageUrls: ['data:image/png;base64,iVBORw0KGgoAAAA...='] });
114
+ ```
115
+
71
116
  Ensure that you replace `'path/to/firefall-client'` with the actual path to the `FirefallClient` class in your project and adjust the configuration parameters according to your Firefall API credentials.
72
117
 
73
118
  ## Testing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-gpt-client",
3
- "version": "1.2.22",
3
+ "version": "1.3.1",
4
4
  "description": "Shared modules of the Spacecat Services - GPT Client",
5
5
  "type": "module",
6
6
  "engines": {
@@ -16,7 +16,14 @@ import { hasText, isObject, isValidUrl } from '@adobe/spacecat-shared-utils';
16
16
 
17
17
  import { fetch as httpFetch } from '../utils.js';
18
18
 
19
- function validateFirefallResponse(response) {
19
+ const USER_ROLE_IMAGE_URL_TYPE = 'image_url';
20
+ const USER_ROLE_TEXT_TYPE = 'text';
21
+ const SYSTEM_ROLE = 'system';
22
+ const USER_ROLE = 'user';
23
+ const AZURE_CHAT_OPENAI_LLM_TYPE = 'azure_chat_openai';
24
+ const JSON_OBJECT_RESPONSE_FORMAT = 'json_object';
25
+
26
+ function validateCapabilityExecutionResponse(response) {
20
27
  return !(!isObject(response)
21
28
  || !Array.isArray(response.generations)
22
29
  || response.generations.length === 0
@@ -25,6 +32,17 @@ function validateFirefallResponse(response) {
25
32
  || !hasText(response.generations[0][0].text));
26
33
  }
27
34
 
35
+ function validateChatCompletionResponse(response) {
36
+ return isObject(response)
37
+ && Array.isArray(response?.choices)
38
+ && response.choices.length > 0
39
+ && response.choices[0]?.message;
40
+ }
41
+
42
+ function isBase64UrlImage(base64String) {
43
+ return base64String.startsWith('data:image') && base64String.endsWith('=') && base64String.includes('base64');
44
+ }
45
+
28
46
  export default class FirefallClient {
29
47
  static createFrom(context) {
30
48
  const { log = console } = context;
@@ -32,7 +50,8 @@ export default class FirefallClient {
32
50
 
33
51
  const {
34
52
  FIREFALL_API_ENDPOINT: apiEndpoint,
35
- IMS_CLIENT_ID: imsOrg,
53
+ IMS_CLIENT_ID: imsClientId,
54
+ FIREFALL_IMS_ORG_ID: firefallImsOrgId,
36
55
  FIREFALL_API_KEY: apiKey,
37
56
  FIREFALL_API_POLL_INTERVAL: pollInterval = 2000,
38
57
  FIREFALL_API_CAPABILITY_NAME: capabilityName = 'gpt4_32k_completions_capability',
@@ -51,7 +70,7 @@ export default class FirefallClient {
51
70
  apiKey,
52
71
  capabilityName,
53
72
  imsClient,
54
- imsOrg,
73
+ imsOrg: firefallImsOrgId || imsClientId,
55
74
  pollInterval,
56
75
  }, log);
57
76
  }
@@ -89,15 +108,16 @@ export default class FirefallClient {
89
108
  this.log.debug(`${message}: took ${duration}ms`);
90
109
  }
91
110
 
92
- async #submitJob(prompt) {
111
+ /**
112
+ * Submit a prompt to the Firefall API.
113
+ * @param body The body of the request.
114
+ * @param path The Firefall API path.
115
+ * @returns {Promise<unknown>}
116
+ */
117
+ async #submitPrompt(body, path) {
93
118
  const apiAuth = await this.#getApiAuth();
94
119
 
95
- const body = JSON.stringify({
96
- input: prompt,
97
- capability_name: this.config.capabilityName,
98
- });
99
-
100
- const url = createUrl(`${this.config.apiEndpoint}/v2/capability_execution/job`);
120
+ const url = createUrl(`${this.config.apiEndpoint}${path}`);
101
121
  const headers = {
102
122
  'Content-Type': 'application/json',
103
123
  Authorization: `Bearer ${apiAuth}`,
@@ -121,7 +141,7 @@ export default class FirefallClient {
121
141
  }
122
142
 
123
143
  /* eslint-disable no-await-in-loop */
124
- async #pollJobStatus(jobId) {
144
+ async #pollJobStatus(jobId, path) {
125
145
  const apiAuth = await this.#getApiAuth();
126
146
 
127
147
  let jobStatusResponse;
@@ -130,7 +150,7 @@ export default class FirefallClient {
130
150
  (resolve) => { setTimeout(resolve, this.config.pollInterval); },
131
151
  ); // Wait for 2 seconds before polling
132
152
 
133
- const url = `${this.config.apiEndpoint}/v2/capability_execution/job/${jobId}`;
153
+ const url = `${this.config.apiEndpoint}${path}/${jobId}`;
134
154
  const headers = {
135
155
  Authorization: `Bearer ${apiAuth}`,
136
156
  'x-api-key': this.config.apiKey,
@@ -161,23 +181,131 @@ export default class FirefallClient {
161
181
  return jobStatusResponse;
162
182
  }
163
183
 
164
- async fetch(prompt) {
184
+ /**
185
+ * Fetches data from Firefall Chat Completion API.
186
+ * @param prompt The text prompt to provide to Firefall
187
+ * @param options The options for the call, with optional properties:
188
+ * - imageUrls: An array of URLs of the images to provide to Firefall
189
+ * - model: LLM Model to use (default: gpt-4-turbo). Use 'gpt-4-vision' with images.
190
+ * - responseFormat: The response format to request from Firefall (accepts: json_object)
191
+ * @returns {Object} - AI response
192
+ */
193
+ async fetchChatCompletion(prompt, options = {}) {
194
+ const {
195
+ imageUrls,
196
+ responseFormat,
197
+ model: llmModel = 'gpt-4-turbo',
198
+ } = options || {};
199
+ const hasImageUrls = imageUrls && imageUrls.length > 0;
200
+
201
+ const getBody = () => {
202
+ const userRole = {
203
+ role: USER_ROLE,
204
+ content: [
205
+ {
206
+ type: USER_ROLE_TEXT_TYPE,
207
+ text: prompt,
208
+ },
209
+ ],
210
+ };
211
+
212
+ if (hasImageUrls) {
213
+ imageUrls
214
+ .filter((iu) => isValidUrl(iu) || isBase64UrlImage(iu))
215
+ .forEach((imageUrl) => {
216
+ userRole.content.push({
217
+ type: USER_ROLE_IMAGE_URL_TYPE,
218
+ image_url: {
219
+ url: imageUrl,
220
+ },
221
+ });
222
+ });
223
+ }
224
+
225
+ const body = {
226
+ llm_metadata: {
227
+ model_name: llmModel,
228
+ llm_type: AZURE_CHAT_OPENAI_LLM_TYPE,
229
+ },
230
+ messages: [
231
+ userRole,
232
+ ],
233
+ };
234
+ if (responseFormat === JSON_OBJECT_RESPONSE_FORMAT) {
235
+ body.response_format = {
236
+ type: JSON_OBJECT_RESPONSE_FORMAT,
237
+ };
238
+ body.messages.push({
239
+ role: SYSTEM_ROLE,
240
+ content: 'You are a helpful assistant designed to output JSON.',
241
+ });
242
+ }
243
+
244
+ return body;
245
+ };
246
+
247
+ // Validate inputs
165
248
  if (!hasText(prompt)) {
166
249
  throw new Error('Invalid prompt received');
167
250
  }
251
+ if (hasImageUrls && !Array.isArray(imageUrls)) {
252
+ throw new Error('imageUrls must be an array.');
253
+ }
168
254
 
255
+ let chatSubmissionResponse;
169
256
  try {
170
257
  const startTime = process.hrtime.bigint();
171
- const jobSubmissionResponse = await this.#submitJob(prompt);
172
- const jobStatusResponse = await this.#pollJobStatus(jobSubmissionResponse.job_id);
173
- this.#logDuration('Firefall API call', startTime);
258
+ const body = getBody();
259
+
260
+ chatSubmissionResponse = await this.#submitPrompt(JSON.stringify(body), '/v2/chat/completions');
261
+ this.#logDuration('Firefall API Chat Completion call', startTime);
262
+ } catch (error) {
263
+ this.log.error('Error while fetching data from Firefall chat API: ', error.message);
264
+ throw error;
265
+ }
266
+
267
+ if (!validateChatCompletionResponse(chatSubmissionResponse)) {
268
+ this.log.error(
269
+ 'Could not obtain data from Firefall: Invalid response format.',
270
+ );
271
+ throw new Error('Invalid response format.');
272
+ }
273
+ if (!chatSubmissionResponse.choices.some((ch) => hasText(ch?.message?.content))) {
274
+ throw new Error('Prompt completed but no output was found.');
275
+ }
276
+
277
+ return chatSubmissionResponse;
278
+ }
279
+
280
+ /**
281
+ * Fetches data from Firefall API.
282
+ * @param prompt The text prompt to provide to Firefall
283
+ * @returns {string} - AI response
284
+ */
285
+ async fetchCapabilityExecution(prompt) {
286
+ if (!hasText(prompt)) {
287
+ throw new Error('Invalid prompt received');
288
+ }
289
+
290
+ try {
291
+ const startTime = process.hrtime.bigint();
292
+
293
+ const body = JSON.stringify({
294
+ input: prompt,
295
+ capability_name: this.config.capabilityName,
296
+ });
297
+ const path = '/v2/capability_execution/job';
298
+
299
+ const jobSubmissionResponse = await this.#submitPrompt(body, path);
300
+ const jobStatusResponse = await this.#pollJobStatus(jobSubmissionResponse.job_id, path);
301
+ this.#logDuration('Firefall API Capability Execution call', startTime);
174
302
 
175
303
  const { output } = jobStatusResponse;
176
304
  if (!output || !output.capability_response) {
177
305
  throw new Error('Job completed but no output was found');
178
306
  }
179
307
 
180
- if (!validateFirefallResponse(output.capability_response)) {
308
+ if (!validateCapabilityExecutionResponse(output.capability_response)) {
181
309
  this.log.error('Could not obtain data from Firefall: Invalid response format.');
182
310
  throw new Error('Invalid response format.');
183
311
  }
@@ -189,8 +317,15 @@ export default class FirefallClient {
189
317
 
190
318
  return result.text;
191
319
  } catch (error) {
192
- this.log.error('Error while fetching data from Firefall API: ', error.message);
320
+ this.log.error('Error while fetching data from Firefall Capability Execution API: ', error.message);
193
321
  throw error;
194
322
  }
195
323
  }
324
+
325
+ /**
326
+ * @deprecated since version 1.2.19. Use fetchCapabilityExecution instead.
327
+ */
328
+ async fetch(prompt) {
329
+ return this.fetchCapabilityExecution(prompt);
330
+ }
196
331
  }