Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 54 additions & 4 deletions src/controllers/llmo/llmo-onboarding.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ async function publishToAdminHlx(filename, outputLocation, log) {
* @param {Function} say - Optional function to send messages (e.g., Slack say function)
* @returns {Promise<void>}
*/
export async function copyFilesToSharepoint(dataFolder, context, say = () => {}) {
export async function copyFilesToSharepoint(dataFolder, context, say = () => { }) {
const { log, env } = context;

const sharepointClient = await createSharePointClient(env);
Expand Down Expand Up @@ -209,7 +209,7 @@ export async function copyFilesToSharepoint(dataFolder, context, say = () => {})
* @param {Function} say - Optional function to send messages (e.g., Slack say function)
* @returns {Promise<void>}
*/
export async function updateIndexConfig(dataFolder, context, say = () => {}) {
export async function updateIndexConfig(dataFolder, context, say = () => { }) {
const { log, env } = context;

log.debug('Starting Git modification of helix query config');
Expand Down Expand Up @@ -260,7 +260,7 @@ export async function updateIndexConfig(dataFolder, context, say = () => {}) {
* @param {object} slackContext - Slack context (optional, for Slack operations)
* @returns {Promise<object>} The organization object
*/
export async function createOrFindOrganization(imsOrgId, context, say = () => {}) {
export async function createOrFindOrganization(imsOrgId, context, say = () => { }) {
const { dataAccess, log } = context;
const { Organization } = dataAccess;

Expand Down Expand Up @@ -320,7 +320,7 @@ export async function createOrFindSite(baseURL, organizationId, context) {
* @param {Function} say - Optional function to send messages (e.g., Slack say function)
* @returns {Promise<object>} The entitlement and enrollment objects
*/
export async function createEntitlementAndEnrollment(site, context, say = () => {}) {
export async function createEntitlementAndEnrollment(site, context, say = () => { }) {
const { log } = context;

try {
Expand All @@ -339,6 +339,35 @@ export async function createEntitlementAndEnrollment(site, context, say = () =>
}
}

export async function createEntitlementAndEnrollmentForOrg(organization, context, say = () => { }) {
const { log } = context;

try {
const tierClient = TierClient.createForOrg(context, organization, LLMO_PRODUCT_CODE);
const { entitlement: existingEntitlement } = await tierClient.checkValidEntitlement(LLMO_TIER);
const { entitlement } = await tierClient.createEntitlement(LLMO_TIER);

const wasNewlyCreated = !existingEntitlement
|| existingEntitlement.getId() !== entitlement.getId();

if (wasNewlyCreated) {
await say(`Successfully created LLMO entitlement ${entitlement.getId()} for organization ${organization.getId()}`);
} else {
await say(`Found existing LLMO entitlement ${entitlement.getId()} for organization ${organization.getId()}`);
}

log.info(`Successfully ensured LLMO access for organization ${organization.getId()} via entitlement ${entitlement.getId()}`);

return {
entitlement,
};
} catch (error) {
log.info(`Ensuring LLMO entitlement failed: ${error.message}`);
await say('❌ Ensuring LLMO entitlement failed');
throw error;
}
}

export async function enableAudits(site, context, audits = []) {
const { dataAccess } = context;
const { Configuration } = dataAccess;
Expand All @@ -350,6 +379,27 @@ export async function enableAudits(site, context, audits = []) {
await configuration.save();
}

export async function performLlmoOrgOnboarding(imsOrgId, context, say = () => { }) {
const { log } = context;

log.info(`Starting LLMO organization onboarding for IMS Org ID: ${imsOrgId}`);
await say(`:gear: Starting LLMO IMS org onboarding for *${imsOrgId}*...`);
const organization = await createOrFindOrganization(imsOrgId, context, say);

try {
const { entitlement } = await createEntitlementAndEnrollmentForOrg(organization, context, say);

return {
organization,
message: 'LLMO organization onboarding completed successfully',
entitlement,
};
} catch (error) {
log.error(`Error creating entitlement for organization: ${error.message}`);
throw new Error(`Failed to create LLMO entitlement for organization: ${error.message}`);
}
}

/**
* Complete LLMO onboarding process.
* @param {object} params - Onboarding parameters
Expand Down
1 change: 1 addition & 0 deletions src/controllers/slack.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export function initSlackBot(lambdaContext, App) {
app.view('onboard_site_modal', actions.onboardSiteModal(lambdaContext));
app.view('preflight_config_modal', actions.preflight_config_modal(lambdaContext));
app.view('onboard_llmo_modal', actions.onboardLLMOModal(lambdaContext));
app.view('onboard_llmo_org_modal', actions.onboardLLMOOrgModal(lambdaContext));
app.view('update_ims_org_modal', actions.updateIMSOrgModal(lambdaContext));

return app;
Expand Down
4 changes: 4 additions & 0 deletions src/support/slack/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
addEntitlementsAction,
updateOrgAction,
updateIMSOrgModal,
startLLMOOrgOnboarding,
onboardLLMOOrgModal,
} from './onboard-llmo-modal.js';
import { onboardSiteModal, startOnboarding } from './onboard-modal.js';
import { preflightConfigModal } from './preflight-config-modal.js';
Expand All @@ -34,9 +36,11 @@ const actions = {
rejectOrg,
onboardSiteModal,
onboardLLMOModal,
onboardLLMOOrgModal,
updateIMSOrgModal,
start_onboarding: startOnboarding,
start_llmo_onboarding: startLLMOOnboarding,
start_llmo_org_onboarding: startLLMOOrgOnboarding,
preflight_config_modal: preflightConfigModal,
open_preflight_config: openPreflightConfig,
add_entitlements_action: addEntitlementsAction,
Expand Down
150 changes: 150 additions & 0 deletions src/support/slack/actions/onboard-llmo-modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/

import { Config } from '@adobe/spacecat-shared-data-access/src/models/site/config.js';
import { Entitlement as EntitlementModel } from '@adobe/spacecat-shared-data-access/src/models/entitlement/index.js';
import {
postErrorMessage,
} from '../../../utils/slack/base.js';
Expand All @@ -19,8 +20,10 @@ import {
copyFilesToSharepoint,
updateIndexConfig,
enableAudits,
performLlmoOrgOnboarding,
} from '../../../controllers/llmo/llmo-onboarding.js';

const LLMO_TIER = EntitlementModel.TIERS.FREE_TRIAL;
const REFERRAL_TRAFFIC_AUDIT = 'llmo-referral-traffic';
const REFERRAL_TRAFFIC_IMPORT = 'traffic-analysis';
const AGENTIC_TRAFFIC_ANALYSIS_AUDIT = 'cdn-analysis';
Expand Down Expand Up @@ -791,3 +794,150 @@ export function updateIMSOrgModal(lambdaContext) {
}
};
}

/* Handles "Start Onboarding" button click for IMS org onboarding */
export function startLLMOOrgOnboarding(lambdaContext) {
const { log } = lambdaContext;

return async ({
ack, body, client, respond,
}) => {
try {
await ack();

const { user } = body;

await respond({
text: `:gear: ${user.name} started the IMS org onboarding process...`,
replace_original: true,
});

const originalChannel = body.channel?.id;
const originalThreadTs = body.message?.thread_ts || body.message?.ts;

await client.views.open({
trigger_id: body.trigger_id,
view: {
type: 'modal',
callback_id: 'onboard_llmo_org_modal',
private_metadata: JSON.stringify({
originalChannel,
originalThreadTs,
}),
title: {
type: 'plain_text',
text: 'Onboard IMS Org',
},
submit: {
type: 'plain_text',
text: 'Start Onboarding',
},
close: {
type: 'plain_text',
text: 'Cancel',
},
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: ':rocket: *LLMO IMS Org Onboarding*\n\nProvide the IMS Organization ID to onboard for LLMO.',
},
},
{
type: 'input',
block_id: 'ims_org_input',
element: {
type: 'plain_text_input',
action_id: 'ims_org_id',
placeholder: {
type: 'plain_text',
text: 'ABC123@AdobeOrg',
},
},
label: {
type: 'plain_text',
text: 'IMS Organization ID',
},
},
],
},
});

log.debug(`User ${user.id} started IMS org onboarding process.`);
} catch (error) {
log.error('Error starting IMS org onboarding:', error);
await postErrorMessage(respond, error);
}
};
}

/* Handles IMS org onboarding modal submission */
export function onboardLLMOOrgModal(lambdaContext) {
const { log } = lambdaContext;

return async ({ ack, body, client }) => {
try {
const { user, view } = body;
const { values } = view.state;

const imsOrgId = values.ims_org_input?.ims_org_id?.value?.trim();
const metadata = JSON.parse(view.private_metadata || '{}');
const { originalChannel, originalThreadTs } = metadata;

if (!imsOrgId) {
await ack({
response_action: 'errors',
errors: {
ims_org_input: 'IMS Organization ID is required',
},
});
return;
}

await ack();
const responseChannel = originalChannel || body.user.id;
const responseThreadTs = originalChannel ? originalThreadTs : undefined;

const slackContext = {
say: async (message) => {
await client.chat.postMessage({
channel: responseChannel,
text: message,
thread_ts: responseThreadTs,
});
},
client,
channelId: responseChannel,
threadTs: responseThreadTs,
};

try {
const result = await performLlmoOrgOnboarding(imsOrgId, lambdaContext, slackContext.say);
const { organization, message, entitlement } = result;
await slackContext.say(`:white_check_mark: *LLMO IMS org onboarding completed successfully!*

*Organization:* ${organization.getName()}
*IMS Org ID:* ${imsOrgId}
*Entitlement ID:* ${entitlement.getId()}
*Tier:* ${LLMO_TIER}
*Status:* ${message}

The organization has been onboarded for LLMO.`);

log.debug(`IMS org onboarding completed for ${imsOrgId} by user ${user.id}`);
} catch (error) {
log.error(`Error during IMS org onboarding: ${error.message}`);
await slackContext.say(`:x: ${error.message}`);
}
} catch (error) {
log.error('Error handling IMS org onboarding modal:', error);
await ack({
response_action: 'errors',
errors: {
ims_org_input: 'There was an error processing the onboarding request.',
},
});
}
};
}
37 changes: 35 additions & 2 deletions src/support/slack/commands/llmo-onboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ function LlmoOnboardCommand(context) {
const baseCommand = BaseCommand({
id: 'onboard-llmo',
name: 'Onboard LLMO',
description: 'Onboards a site for LLMO (Large Language Model Optimizer) through a modal interface.',
description: 'Onboards a site or IMS org for LLMO (Large Language Model Optimizer) through a modal interface.',
phrases: PHRASES,
usageText: `${PHRASES[0]} <site url>`,
usageText: `${PHRASES[0]} [site url]`,
});

const { log } = context;
Expand All @@ -51,6 +51,39 @@ function LlmoOnboardCommand(context) {

const [site] = args;

// If no site parameter provided, trigger IMS org onboarding flow
if (!site) {
const message = {
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: ':rocket: *LLMO IMS Org Onboarding*\n\nClick the button below to start the IMS organization onboarding process.',
},
},
{
type: 'actions',
elements: [
{
type: 'button',
text: {
type: 'plain_text',
text: 'Start Onboarding',
},
value: 'org_onboarding',
action_id: 'start_llmo_org_onboarding',
style: 'primary',
},
],
},
],
thread_ts: threadTs,
};
await say(message);
return;
}

const normalizedSite = extractURLFromSlackInput(site);

if (!normalizedSite) {
Expand Down
Loading