Skip to content

Commit 0454cb1

Browse files
committed
feat(slack/onboard-llmo): add IMS org onboarding option
1 parent 6d2012b commit 0454cb1

File tree

8 files changed

+510
-11
lines changed

8 files changed

+510
-11
lines changed

src/controllers/llmo/llmo-onboarding.js

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ async function publishToAdminHlx(filename, outputLocation, log) {
170170
* @param {Function} say - Optional function to send messages (e.g., Slack say function)
171171
* @returns {Promise<void>}
172172
*/
173-
export async function copyFilesToSharepoint(dataFolder, context, say = () => {}) {
173+
export async function copyFilesToSharepoint(dataFolder, context, say = () => { }) {
174174
const { log, env } = context;
175175

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

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

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

326326
try {
@@ -339,6 +339,35 @@ export async function createEntitlementAndEnrollment(site, context, say = () =>
339339
}
340340
}
341341

342+
export async function createEntitlementAndEnrollmentForOrg(organization, context, say = () => { }) {
343+
const { log } = context;
344+
345+
try {
346+
const tierClient = TierClient.createForOrg(context, organization, LLMO_PRODUCT_CODE);
347+
const { entitlement: existingEntitlement } = await tierClient.checkValidEntitlement(LLMO_TIER);
348+
const { entitlement } = await tierClient.createEntitlement(LLMO_TIER);
349+
350+
const wasNewlyCreated = !existingEntitlement
351+
|| existingEntitlement.getId() !== entitlement.getId();
352+
353+
if (wasNewlyCreated) {
354+
await say(`Successfully created LLMO entitlement ${entitlement.getId()} for organization ${organization.getId()}`);
355+
} else {
356+
await say(`Found existing LLMO entitlement ${entitlement.getId()} for organization ${organization.getId()}`);
357+
}
358+
359+
log.info(`Successfully ensured LLMO access for organization ${organization.getId()} via entitlement ${entitlement.getId()}`);
360+
361+
return {
362+
entitlement,
363+
};
364+
} catch (error) {
365+
log.info(`Ensuring LLMO entitlement failed: ${error.message}`);
366+
await say('❌ Ensuring LLMO entitlement failed');
367+
throw error;
368+
}
369+
}
370+
342371
export async function enableAudits(site, context, audits = []) {
343372
const { dataAccess } = context;
344373
const { Configuration } = dataAccess;
@@ -350,6 +379,27 @@ export async function enableAudits(site, context, audits = []) {
350379
await configuration.save();
351380
}
352381

382+
export async function performLlmoOrgOnboarding(imsOrgId, context, say = () => { }) {
383+
const { log } = context;
384+
385+
log.info(`Starting LLMO organization onboarding for IMS Org ID: ${imsOrgId}`);
386+
await say(`:gear: Starting LLMO IMS org onboarding for *${imsOrgId}*...`);
387+
const organization = await createOrFindOrganization(imsOrgId, context, say);
388+
389+
try {
390+
const { entitlement } = await createEntitlementAndEnrollmentForOrg(organization, context, say);
391+
392+
return {
393+
organization,
394+
message: 'LLMO organization onboarding completed successfully',
395+
entitlement,
396+
};
397+
} catch (error) {
398+
log.error(`Error creating entitlement for organization: ${error.message}`);
399+
throw new Error(`Failed to create LLMO entitlement for organization: ${error.message}`);
400+
}
401+
}
402+
353403
/**
354404
* Complete LLMO onboarding process.
355405
* @param {object} params - Onboarding parameters

src/controllers/slack.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export function initSlackBot(lambdaContext, App) {
8080
app.view('onboard_site_modal', actions.onboardSiteModal(lambdaContext));
8181
app.view('preflight_config_modal', actions.preflight_config_modal(lambdaContext));
8282
app.view('onboard_llmo_modal', actions.onboardLLMOModal(lambdaContext));
83+
app.view('onboard_llmo_org_modal', actions.onboardLLMOOrgModal(lambdaContext));
8384
app.view('update_ims_org_modal', actions.updateIMSOrgModal(lambdaContext));
8485

8586
return app;

src/support/slack/actions/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {
2121
addEntitlementsAction,
2222
updateOrgAction,
2323
updateIMSOrgModal,
24+
startLLMOOrgOnboarding,
25+
onboardLLMOOrgModal,
2426
} from './onboard-llmo-modal.js';
2527
import { onboardSiteModal, startOnboarding } from './onboard-modal.js';
2628
import { preflightConfigModal } from './preflight-config-modal.js';
@@ -34,9 +36,11 @@ const actions = {
3436
rejectOrg,
3537
onboardSiteModal,
3638
onboardLLMOModal,
39+
onboardLLMOOrgModal,
3740
updateIMSOrgModal,
3841
start_onboarding: startOnboarding,
3942
start_llmo_onboarding: startLLMOOnboarding,
43+
start_llmo_org_onboarding: startLLMOOrgOnboarding,
4044
preflight_config_modal: preflightConfigModal,
4145
open_preflight_config: openPreflightConfig,
4246
add_entitlements_action: addEntitlementsAction,

src/support/slack/actions/onboard-llmo-modal.js

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
*/
1212

1313
import { Config } from '@adobe/spacecat-shared-data-access/src/models/site/config.js';
14+
import { Entitlement as EntitlementModel } from '@adobe/spacecat-shared-data-access/src/models/entitlement/index.js';
1415
import {
1516
postErrorMessage,
1617
} from '../../../utils/slack/base.js';
@@ -19,8 +20,10 @@ import {
1920
copyFilesToSharepoint,
2021
updateIndexConfig,
2122
enableAudits,
23+
performLlmoOrgOnboarding,
2224
} from '../../../controllers/llmo/llmo-onboarding.js';
2325

26+
const LLMO_TIER = EntitlementModel.TIERS.FREE_TRIAL;
2427
const REFERRAL_TRAFFIC_AUDIT = 'llmo-referral-traffic';
2528
const REFERRAL_TRAFFIC_IMPORT = 'traffic-analysis';
2629
const AGENTIC_TRAFFIC_ANALYSIS_AUDIT = 'cdn-analysis';
@@ -791,3 +794,150 @@ export function updateIMSOrgModal(lambdaContext) {
791794
}
792795
};
793796
}
797+
798+
/* Handles "Start Onboarding" button click for IMS org onboarding */
799+
export function startLLMOOrgOnboarding(lambdaContext) {
800+
const { log } = lambdaContext;
801+
802+
return async ({
803+
ack, body, client, respond,
804+
}) => {
805+
try {
806+
await ack();
807+
808+
const { user } = body;
809+
810+
await respond({
811+
text: `:gear: ${user.name} started the IMS org onboarding process...`,
812+
replace_original: true,
813+
});
814+
815+
const originalChannel = body.channel?.id;
816+
const originalThreadTs = body.message?.thread_ts || body.message?.ts;
817+
818+
await client.views.open({
819+
trigger_id: body.trigger_id,
820+
view: {
821+
type: 'modal',
822+
callback_id: 'onboard_llmo_org_modal',
823+
private_metadata: JSON.stringify({
824+
originalChannel,
825+
originalThreadTs,
826+
}),
827+
title: {
828+
type: 'plain_text',
829+
text: 'Onboard IMS Org',
830+
},
831+
submit: {
832+
type: 'plain_text',
833+
text: 'Start Onboarding',
834+
},
835+
close: {
836+
type: 'plain_text',
837+
text: 'Cancel',
838+
},
839+
blocks: [
840+
{
841+
type: 'section',
842+
text: {
843+
type: 'mrkdwn',
844+
text: ':rocket: *LLMO IMS Org Onboarding*\n\nProvide the IMS Organization ID to onboard for LLMO.',
845+
},
846+
},
847+
{
848+
type: 'input',
849+
block_id: 'ims_org_input',
850+
element: {
851+
type: 'plain_text_input',
852+
action_id: 'ims_org_id',
853+
placeholder: {
854+
type: 'plain_text',
855+
text: 'ABC123@AdobeOrg',
856+
},
857+
},
858+
label: {
859+
type: 'plain_text',
860+
text: 'IMS Organization ID',
861+
},
862+
},
863+
],
864+
},
865+
});
866+
867+
log.debug(`User ${user.id} started IMS org onboarding process.`);
868+
} catch (error) {
869+
log.error('Error starting IMS org onboarding:', error);
870+
await postErrorMessage(respond, error);
871+
}
872+
};
873+
}
874+
875+
/* Handles IMS org onboarding modal submission */
876+
export function onboardLLMOOrgModal(lambdaContext) {
877+
const { log } = lambdaContext;
878+
879+
return async ({ ack, body, client }) => {
880+
try {
881+
const { user, view } = body;
882+
const { values } = view.state;
883+
884+
const imsOrgId = values.ims_org_input?.ims_org_id?.value?.trim();
885+
const metadata = JSON.parse(view.private_metadata || '{}');
886+
const { originalChannel, originalThreadTs } = metadata;
887+
888+
if (!imsOrgId) {
889+
await ack({
890+
response_action: 'errors',
891+
errors: {
892+
ims_org_input: 'IMS Organization ID is required',
893+
},
894+
});
895+
return;
896+
}
897+
898+
await ack();
899+
const responseChannel = originalChannel || body.user.id;
900+
const responseThreadTs = originalChannel ? originalThreadTs : undefined;
901+
902+
const slackContext = {
903+
say: async (message) => {
904+
await client.chat.postMessage({
905+
channel: responseChannel,
906+
text: message,
907+
thread_ts: responseThreadTs,
908+
});
909+
},
910+
client,
911+
channelId: responseChannel,
912+
threadTs: responseThreadTs,
913+
};
914+
915+
try {
916+
const result = await performLlmoOrgOnboarding(imsOrgId, lambdaContext, slackContext.say);
917+
const { organization, message, entitlement } = result;
918+
await slackContext.say(`:white_check_mark: *LLMO IMS org onboarding completed successfully!*
919+
920+
*Organization:* ${organization.getName()}
921+
*IMS Org ID:* ${imsOrgId}
922+
*Entitlement ID:* ${entitlement.getId()}
923+
*Tier:* ${LLMO_TIER}
924+
*Status:* ${message}
925+
926+
The organization has been onboarded for LLMO.`);
927+
928+
log.debug(`IMS org onboarding completed for ${imsOrgId} by user ${user.id}`);
929+
} catch (error) {
930+
log.error(`Error during IMS org onboarding: ${error.message}`);
931+
await slackContext.say(`:x: ${error.message}`);
932+
}
933+
} catch (error) {
934+
log.error('Error handling IMS org onboarding modal:', error);
935+
await ack({
936+
response_action: 'errors',
937+
errors: {
938+
ims_org_input: 'There was an error processing the onboarding request.',
939+
},
940+
});
941+
}
942+
};
943+
}

src/support/slack/commands/llmo-onboard.js

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ function LlmoOnboardCommand(context) {
3030
const baseCommand = BaseCommand({
3131
id: 'onboard-llmo',
3232
name: 'Onboard LLMO',
33-
description: 'Onboards a site for LLMO (Large Language Model Optimizer) through a modal interface.',
33+
description: 'Onboards a site or IMS org for LLMO (Large Language Model Optimizer) through a modal interface.',
3434
phrases: PHRASES,
35-
usageText: `${PHRASES[0]} <site url>`,
35+
usageText: `${PHRASES[0]} [site url]`,
3636
});
3737

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

5252
const [site] = args;
5353

54+
// If no site parameter provided, trigger IMS org onboarding flow
55+
if (!site) {
56+
const message = {
57+
blocks: [
58+
{
59+
type: 'section',
60+
text: {
61+
type: 'mrkdwn',
62+
text: ':rocket: *LLMO IMS Org Onboarding*\n\nClick the button below to start the IMS organization onboarding process.',
63+
},
64+
},
65+
{
66+
type: 'actions',
67+
elements: [
68+
{
69+
type: 'button',
70+
text: {
71+
type: 'plain_text',
72+
text: 'Start Onboarding',
73+
},
74+
value: 'org_onboarding',
75+
action_id: 'start_llmo_org_onboarding',
76+
style: 'primary',
77+
},
78+
],
79+
},
80+
],
81+
thread_ts: threadTs,
82+
};
83+
await say(message);
84+
return;
85+
}
86+
5487
const normalizedSite = extractURLFromSlackInput(site);
5588

5689
if (!normalizedSite) {

0 commit comments

Comments
 (0)