Skip to content

Commit

Permalink
Merge branch 'main' into update-grammar
Browse files Browse the repository at this point in the history
  • Loading branch information
audrey-kho authored May 20, 2024
2 parents bf5d7fb + 3ea4ce6 commit 974f0ff
Show file tree
Hide file tree
Showing 82 changed files with 1,175 additions and 511 deletions.
18 changes: 18 additions & 0 deletions .jest/test-setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright 2023 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
window.matchMedia = window.matchMedia || function() {
return {
matches : false,
addListener : function() {},
removeListener: function() {}
};
};
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,19 @@ See: http://aem.live/docs/sidekick-generate-variations
- `aio console workspace select`
- Populate the `.env` file in the project root and fill it as shown [below](#env)

### Local Development

- `npm start` to start your local Dev server
- App will run on `localhost:9080` by default
- Actions will be deployed locally (requires Docker running)

### Testing

- Run `npm run lint && npm test` to run lint and unit tests for ui and actions
- Preview the Generate Variations app in the [QA workspace](https://experience-qa.adobe.com/?shell_source=local&devMode=true&shell_ims=prod#/aem/generate-variations/): `npm run preview`

### Debugging

By default, App Builder stores only failed activations. To enable the storage of all App Builder activations, set the `extraLogging` search query parameter to `true`, as shown in the following example:

```
https://experience.adobe.com/?extraLogging=true#/aem/generate-variations/
```

### Deployment

- `npm run deploy` to build and deploy all actions on Runtime and static files to CDN
Expand Down
2 changes: 1 addition & 1 deletion actions/AemClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class AemClient {

logger.debug(`Updating variation with ${updates.length} fields`);

await wretch(updateVariationUrl, true /* retryOnFailure */)
await wretch(updateVariationUrl, { shouldRetry: true } /* retryOnFailure */)
.headers({
'X-Adobe-Accept-Unsupported-API': '1',
'If-Match': eTag,
Expand Down
43 changes: 41 additions & 2 deletions actions/AuthAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,45 @@ const { checkForAdobeInternalUser } = require('./ActionUtils.js');

const logger = Core.Logger('AuthAction');

/**
* Extracts a user access token from either the Authorization header or the request payload
*
* @param {object} params action input parameters
* @returns {string|undefined} the token string, or undefined if not present
*/
function getAccessToken(params) {
// First, check if a bearer user access token is set
if (
params.__ow_headers
&& params.__ow_headers.authorization
&& params.__ow_headers.authorization.startsWith('Bearer ')
) {
return params.__ow_headers.authorization.substring('Bearer '.length);
}

// Second, check if a token has been passed through the payload
return params.accessToken;
}

/**
* Extracts an Adobe IMS organization ID from either the 'X-Org-Id' header or the request payload
*
* @param {object} params action input parameters
* @returns {string|undefined} the Adobe IMS organization ID string, or undefined if not present
*/
function getImsOrg(params) {
// First, check if an Adobe IMS organization ID has been passed through the 'X-Org-Id' header
if (
params.__ow_headers
&& params.__ow_headers['x-org-id']
) {
return params.__ow_headers['x-org-id'];
}

// Second, check if an Adobe IMS organization ID has been passed through the payload
return params.imsOrg;
}

async function isValidToken(endpoint, clientId, token) {
try {
const response = await wretch(`${endpoint}/ims/validate_token/v1`)
Expand Down Expand Up @@ -114,8 +153,8 @@ function asAuthAction(action) {
const earlyAccessToggle = params.FT_EARLY_ACCESS;
const ldSdkKey = params.LD_SDK_KEY;

// Extract the token from the params
const { imsOrg, accessToken } = params;
const accessToken = getAccessToken(params);
const imsOrg = getImsOrg(params);

// Validate the access token
if (!await isValidToken(imsEndpoint, clientId, accessToken)) {
Expand Down
8 changes: 6 additions & 2 deletions actions/FirefallClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const ERROR_CODES = {

function toFirefallError(error, defaultMessage) {
const errorMessage = ERROR_CODES[error.status] ?? defaultMessage;
return new InternalError(400, `IS-ERROR: ${errorMessage} (${error.status}).`);
return new InternalError(error.status ?? 500, `IS-ERROR: ${errorMessage} (${error.status}).`);
}

class FirefallClient {
Expand All @@ -39,8 +39,12 @@ class FirefallClient {
async completion(prompt, temperature = 0.0, model = 'gpt-4') {
const startTime = Date.now();

// must be aligned with the `aem-genai-assistant/generate` AppBuilder action timeout
// (subtracted 5 seconds to allow some buffer for AppBuilder)
const REQUEST_TIMEOUT = 295;

try {
const response = await wretch(`${this.endpoint}/v1/completions`)
const response = await wretch(`${this.endpoint}/v1/completions`, { requestTimeout: REQUEST_TIMEOUT })
.headers({
'x-gw-ims-org-id': this.org,
'x-api-key': this.apiKey,
Expand Down
12 changes: 11 additions & 1 deletion actions/GenericAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,18 @@ function createResponse(status, body) {
};
}

function isObject(obj) {
return typeof obj === 'object' && !Array.isArray(obj) && obj !== null;
}

function createSuccessResponse(body) {
return createResponse(200, body);
if (!isObject(body)) {
return createResponse(200, body);
}

// If there is a status code in the body, use it; otherwise, default to 200.
const { statusCode, ...response } = body;
return createResponse(statusCode ?? 200, response);
}

function createErrorResponse(status, message) {
Expand Down
17 changes: 12 additions & 5 deletions actions/Network.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ const { Core } = require('@adobe/aio-sdk');

const logger = Core.Logger('FirefallAction');

const REQUEST_TIMEOUT = 55 * 1000;
const REQUEST_TIMEOUT = 55; // in seconds
const SHOULD_RETRY = false;
const DEFAULT_OPTIONS = {
shouldRetry: SHOULD_RETRY,
requestTimeout: REQUEST_TIMEOUT, // in seconds
};

function createWretchError(status, message) {
const error = new WretchError();
Expand All @@ -28,16 +33,18 @@ function createWretchError(status, message) {
return error;
}

function wretchWithOptions(url, shouldRetry = false) {
function wretchWithOptions(url, options = DEFAULT_OPTIONS) {
// overwrite default options
const finalOptions = { ...DEFAULT_OPTIONS, ...options };
return wretch(url)
.middlewares(shouldRetry ? [retry()] : [])
.middlewares(finalOptions.shouldRetry ? [retry()] : [])
.addon(AbortAddon())
.resolve((resolver) => resolver.setTimeout(REQUEST_TIMEOUT))
.resolve((resolver) => resolver.setTimeout(finalOptions.requestTimeout * 1000))
.resolve((resolver) => {
return resolver.fetchError((error) => {
if (error.name === 'AbortError') {
logger.error('Request aborted', error);
throw createWretchError(408, 'Request timed out');
throw createWretchError(504, 'Gateway Timeout');
}
logger.error('Network error', error);
throw createWretchError(500, 'Network error');
Expand Down
49 changes: 43 additions & 6 deletions actions/complete/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,52 @@
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

const openwhisk = require('openwhisk');
const { asGenericAction } = require('../GenericAction.js');
const { asAuthAction } = require('../AuthAction.js');
const { asFirefallAction } = require('../FirefallAction.js');

const STATUS_RUNNING = 'running';
const STATUS_COMPLETED = 'completed';

async function main(params) {
const {
prompt, temperature, model, firefallClient,
} = params;
return firefallClient.completion(prompt ?? 'Who are you?', temperature ?? 0.0, model ?? 'gpt-4');
const { jobId } = params;

const ow = openwhisk();

if (!jobId) {
const activation = await ow.actions.invoke({
name: 'aem-genai-assistant/generate',
blocking: false,
result: false,
params,
});

return {
statusCode: 202,
jobId: activation.activationId,
};
} else {
try {
const activation = await ow.activations.get(jobId);

return {
jobId,
status: STATUS_COMPLETED,
result: activation.response.result,
};
} catch (error) {
if (error instanceof Error && error.constructor.name === 'OpenWhiskError' && error.statusCode === 404) {
// For valid activation IDs that haven't finished yet, App Builder (OpenWhisk) returns a 404 error
// instead of any data or status. Therefore, we will return a status of 'running' to inform the client to wait.
return {
jobId,
status: STATUS_RUNNING,
};
}
throw error;
}
}
}

exports.main = asGenericAction(asAuthAction(asFirefallAction(main)));
exports.main = asGenericAction(asAuthAction(main));
22 changes: 22 additions & 0 deletions actions/generate/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright 2024 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

const { asFirefallAction } = require('../FirefallAction.js');

function main(params) {
const {
prompt, temperature, model, firefallClient,
} = params;
return firefallClient.completion(prompt ?? 'Who are you?', temperature ?? 0.0, model ?? 'gpt-4');
}

exports.main = asFirefallAction(main);
11 changes: 9 additions & 2 deletions app.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ application:
web: true
runtime: nodejs:18
inputs:
FIREFALL_ENDPOINT: $FIREFALL_ENDPOINT
FIREFALL_API_KEY: $FIREFALL_API_KEY
IMS_ENDPOINT: $IMS_ENDPOINT
IMS_CLIENT_ID: $IMS_CLIENT_ID
IMS_SERVICE_CLIENT_ID: $IMS_SERVICE_CLIENT_ID
Expand All @@ -20,6 +18,15 @@ application:
IMS_PRODUCT_CONTEXT: $IMS_PRODUCT_CONTEXT
FT_EARLY_ACCESS: $FT_EARLY_ACCESS
LD_SDK_KEY: $LD_SDK_KEY
generate:
function: actions/generate/index.js
web: false
runtime: nodejs:18
limits:
timeout: 300000 # in ms (300 sec)
inputs:
FIREFALL_ENDPOINT: $FIREFALL_ENDPOINT
FIREFALL_API_KEY: $FIREFALL_API_KEY
feedback:
function: actions/feedback/index.js
web: true
Expand Down
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ module.exports = {
'\\.(css|less)$':
'<rootDir>/.jest/styleMock.js',
},
setupFiles: ["<rootDir>/.jest/test-setup.js"]
};
Loading

0 comments on commit 974f0ff

Please sign in to comment.