diff --git a/.eslintrc.js b/.eslintrc.js index 4534a69bbd8..df59c52629a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,11 +5,13 @@ const dictionary = [ 'activation', 'activations', 'adaptive', + 'ai', 'app', 'apply', 'approve', 'assets', 'bin', + 'builder', 'catalog', 'checklist', 'client', @@ -42,6 +44,7 @@ const dictionary = [ 'management', 'member', 'messaging', + 'model', 'news', 'oauth2', 'office365', diff --git a/docs/docs/cmd/pp/aibuildermodel/aibuildermodel-list.md b/docs/docs/cmd/pp/aibuildermodel/aibuildermodel-list.md new file mode 100644 index 00000000000..aaed2a7f610 --- /dev/null +++ b/docs/docs/cmd/pp/aibuildermodel/aibuildermodel-list.md @@ -0,0 +1,95 @@ +# pp aibuildermodel list + +List available AI builder models in the specified Power Platform environment + +## Usage + +```sh +pp aibuildermodel list [options] +``` + +## Options + +`-e, --environment ` +: The name of the environment + +`--asAdmin` +: Run the command as admin for environments you do not have explicitly assigned permissions to + +--8<-- "docs/cmd/_global.md" + +## Examples + +List all AI Builder models in a specific environment + +```sh +m365 pp aibuildermodel list --environment "Default-d87a7535-dd31-4437-bfe1-95340acd55c5" +``` + +List all AI Builder models in a specific environment as admin + +```sh +m365 pp aibuildermodel list --environment "Default-d87a7535-dd31-4437-bfe1-95340acd55c5" --asAdmin +``` + +## Response + +=== "JSON" + + ```json + [ + { + "statecode": 0, + "_msdyn_templateid_value": "10707e4e-1d56-e911-8194-000d3a6cd5a5", + "msdyn_modelcreationcontext": "{}", + "createdon": "2022-11-29T11:58:45Z", + "_ownerid_value": "5fa787c1-1c4d-ed11-bba1-000d3a2caf7f", + "modifiedon": "2022-11-29T11:58:45Z", + "msdyn_sharewithorganizationoncreate": false, + "msdyn_aimodelidunique": "b0328b67-47e2-4202-8189-e617ec9a88bd", + "solutionid": "fd140aae-4df4-11dd-bd17-0019b9312238", + "ismanaged": false, + "versionnumber": 1458121, + "msdyn_name": "Document Processing 11/29/2022, 12:58:43 PM", + "introducedversion": "1.0", + "statuscode": 0, + "_modifiedby_value": "5fa787c1-1c4d-ed11-bba1-000d3a2caf7f", + "overwritetime": "1900-01-01T00:00:00Z", + "componentstate": 0, + "_createdby_value": "5fa787c1-1c4d-ed11-bba1-000d3a2caf7f", + "_owningbusinessunit_value": "6da087c1-1c4d-ed11-bba1-000d3a2caf7f", + "_owninguser_value": "5fa787c1-1c4d-ed11-bba1-000d3a2caf7f", + "msdyn_aimodelid": "08ffffbe-ec1c-4e64-b64b-dd1db926c613", + "_msdyn_activerunconfigurationid_value": null, + "overriddencreatedon": null, + "_msdyn_retrainworkflowid_value": null, + "importsequencenumber": null, + "_msdyn_scheduleinferenceworkflowid_value": null, + "_modifiedonbehalfby_value": null, + "utcconversiontimezonecode": null, + "_createdonbehalfby_value": null, + "_owningteam_value": null, + "timezoneruleversionnumber": null, + "iscustomizable": { + "Value": true, + "CanBeChanged": true, + "ManagedPropertyLogicalName": "iscustomizableanddeletable" + } + } + ] + ``` + +=== "Text" + + ```text + createdon modifiedon msdyn_aimodelid msdyn_name + -------------------- -------------------- ------------------------------------ ------------------------------------------- + 2022-10-25T14:44:48Z 2022-10-25T14:44:48Z 08ffffbe-ec1c-4e64-b64b-dd1db926c613 Document Processing 11/29/2022, 12:58:43 PM + ``` + +=== "CSV" + + ```csv + msdyn_name,msdyn_aimodelid,createdon,modifiedon + "Document Processing 11/29/2022, 12:58:43 PM",08ffffbe-ec1c-4e64-b64b-dd1db926c613,2022-11-29T11:58:45Z,2022-11-29T11:58:45Z + ``` diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 455713d4764..f574d150ecc 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -263,6 +263,8 @@ nav: - run list: cmd/flow/run/run-list.md - run resubmit: cmd/flow/run/run-resubmit.md - Power Platform (pp): + - aibuildermodel: + - aibuildermodel list: cmd/pp/aibuildermodel/aibuildermodel-list.md - card: - card clone: cmd/pp/card/card-clone.md - card get: cmd/pp/card/card-get.md diff --git a/src/m365/pp/commands.ts b/src/m365/pp/commands.ts index 7d929bbd237..921ee45b4b1 100644 --- a/src/m365/pp/commands.ts +++ b/src/m365/pp/commands.ts @@ -1,6 +1,7 @@ const prefix: string = 'pp'; export default { + AIBUILDERMODEL_LIST: `${prefix} aibuildermodel list`, CARD_CLONE: `${prefix} card clone`, CARD_GET: `${prefix} card get`, CARD_LIST: `${prefix} card list`, diff --git a/src/m365/pp/commands/aibuildermodel/aibuildermodel-list.spec.ts b/src/m365/pp/commands/aibuildermodel/aibuildermodel-list.spec.ts new file mode 100644 index 00000000000..dcfb4d35c0f --- /dev/null +++ b/src/m365/pp/commands/aibuildermodel/aibuildermodel-list.spec.ts @@ -0,0 +1,161 @@ +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { telemetry } from '../../../../telemetry'; +import auth from '../../../../Auth'; +import { Logger } from '../../../../cli/Logger'; +import Command, { CommandError } from '../../../../Command'; +import request from '../../../../request'; +import { pid } from '../../../../utils/pid'; +import { sinonUtil } from '../../../../utils/sinonUtil'; +import commands from '../../commands'; +import { powerPlatform } from '../../../../utils/powerPlatform'; +const command: Command = require('./aibuildermodel-list'); + +describe(commands.AIBUILDERMODEL_LIST, () => { + //#region Mocked Responses + const envUrl = "https://contoso-dev.api.crm4.dynamics.com"; + const validEnvironment = "4be50206-9576-4237-8b17-38d8aadfaa36"; + const modelsResponse: any = { + "value": [ + { + "@odata.etag": "W/\"1458121\"", + "statecode": 0, + "_msdyn_templateid_value": "10707e4e-1d56-e911-8194-000d3a6cd5a5", + "msdyn_modelcreationcontext": "{}", + "createdon": "2022-11-29T11:58:45Z", + "_ownerid_value": "5fa787c1-1c4d-ed11-bba1-000d3a2caf7f", + "modifiedon": "2022-11-29T11:58:45Z", + "msdyn_sharewithorganizationoncreate": false, + "msdyn_aimodelidunique": "b0328b67-47e2-4202-8189-e617ec9a88bd", + "solutionid": "fd140aae-4df4-11dd-bd17-0019b9312238", + "ismanaged": false, + "versionnumber": 1458121, + "msdyn_name": "Document Processing 11/29/2022, 12:58:43 PM", + "introducedversion": "1.0", + "statuscode": 0, + "_modifiedby_value": "5fa787c1-1c4d-ed11-bba1-000d3a2caf7f", + "overwritetime": "1900-01-01T00:00:00Z", + "componentstate": 0, + "_createdby_value": "5fa787c1-1c4d-ed11-bba1-000d3a2caf7f", + "_owningbusinessunit_value": "6da087c1-1c4d-ed11-bba1-000d3a2caf7f", + "_owninguser_value": "5fa787c1-1c4d-ed11-bba1-000d3a2caf7f", + "msdyn_aimodelid": "08ffffbe-ec1c-4e64-b64b-dd1db926c613", + "_msdyn_activerunconfigurationid_value": null, + "overriddencreatedon": null, + "_msdyn_retrainworkflowid_value": null, + "importsequencenumber": null, + "_msdyn_scheduleinferenceworkflowid_value": null, + "_modifiedonbehalfby_value": null, + "utcconversiontimezonecode": null, + "_createdonbehalfby_value": null, + "_owningteam_value": null, + "timezoneruleversionnumber": null, + "iscustomizable": { + "Value": true, + "CanBeChanged": true, + "ManagedPropertyLogicalName": "iscustomizableanddeletable" + } + } + ] + }; + //#endregion + + let log: string[]; + let logger: Logger; + let loggerLogSpy: sinon.SinonSpy; + + before(() => { + sinon.stub(auth, 'restoreAuth').callsFake(() => Promise.resolve()); + sinon.stub(telemetry, 'trackEvent').callsFake(() => { }); + sinon.stub(pid, 'getProcessName').callsFake(() => ''); + auth.service.connected = true; + }); + + beforeEach(() => { + log = []; + logger = { + log: (msg: string) => { + log.push(msg); + }, + logRaw: (msg: string) => { + log.push(msg); + }, + logToStderr: (msg: string) => { + log.push(msg); + } + }; + loggerLogSpy = sinon.spy(logger, 'log'); + }); + + afterEach(() => { + sinonUtil.restore([ + request.get, + powerPlatform.getDynamicsInstanceApiUrl + ]); + }); + + after(() => { + sinonUtil.restore([ + auth.restoreAuth, + telemetry.trackEvent, + pid.getProcessName + ]); + auth.service.connected = false; + }); + + it('has correct name', () => { + assert.strictEqual(command.name, commands.AIBUILDERMODEL_LIST); + }); + + it('has a description', () => { + assert.notStrictEqual(command.description, null); + }); + + it('defines correct properties for the default output', () => { + assert.deepStrictEqual(command.defaultProperties(), ['msdyn_name', 'msdyn_aimodelid', 'createdon', 'modifiedon']); + }); + + it('retrieves AI Builder models', async () => { + sinon.stub(powerPlatform, 'getDynamicsInstanceApiUrl').callsFake(async () => envUrl); + + sinon.stub(request, 'get').callsFake(async opts => { + if ((opts.url === `https://contoso-dev.api.crm4.dynamics.com/api/data/v9.0/msdyn_aimodels?$filter=iscustomizable/Value eq true`)) { + if (opts.headers && + opts.headers.accept && + (opts.headers.accept as string).indexOf('application/json') === 0) { + return modelsResponse; + } + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { verbose: true, environment: validEnvironment } }); + assert(loggerLogSpy.calledWith(modelsResponse.value)); + + }); + + it('correctly handles API OData error', async () => { + sinon.stub(powerPlatform, 'getDynamicsInstanceApiUrl').callsFake(async () => envUrl); + + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://contoso-dev.api.crm4.dynamics.com/api/data/v9.0/msdyn_aimodels?$filter=iscustomizable/Value eq true`) { + if ((opts.headers?.accept as string)?.indexOf('application/json') === 0) { + throw { + error: { + 'odata.error': { + code: '-1, InvalidOperationException', + message: { + value: `Resource '' does not exist or one of its queried reference-property objects are not present` + } + } + } + }; + } + } + }); + + await assert.rejects(command.action(logger, { options: { environment: validEnvironment } } as any), + new CommandError(`Resource '' does not exist or one of its queried reference-property objects are not present`)); + }); +}); diff --git a/src/m365/pp/commands/aibuildermodel/aibuildermodel-list.ts b/src/m365/pp/commands/aibuildermodel/aibuildermodel-list.ts new file mode 100644 index 00000000000..76f38fa39b6 --- /dev/null +++ b/src/m365/pp/commands/aibuildermodel/aibuildermodel-list.ts @@ -0,0 +1,73 @@ +import { Logger } from '../../../../cli/Logger'; +import GlobalOptions from '../../../../GlobalOptions'; +import { odata } from '../../../../utils/odata'; +import { powerPlatform } from '../../../../utils/powerPlatform'; +import PowerPlatformCommand from '../../../base/PowerPlatformCommand'; +import commands from '../../commands'; + +interface CommandArgs { + options: Options; +} + +interface Options extends GlobalOptions { + environment: string; + asAdmin?: boolean; +} + +class PpAiBuilderModelListCommand extends PowerPlatformCommand { + public get name(): string { + return commands.AIBUILDERMODEL_LIST; + } + + public get description(): string { + return 'List available AI builder models in the specified Power Platform environment.'; + } + + public defaultProperties(): string[] | undefined { + return ['msdyn_name', 'msdyn_aimodelid', 'createdon', 'modifiedon']; + } + + constructor() { + super(); + + this.#initTelemetry(); + this.#initOptions(); + } + + #initTelemetry(): void { + this.telemetry.push((args: CommandArgs) => { + Object.assign(this.telemetryProperties, { + asAdmin: !!args.options.asAdmin + }); + }); + } + + #initOptions(): void { + this.options.unshift( + { + option: '-e, --environment ' + }, + { + option: '--asAdmin' + } + ); + } + + public async commandAction(logger: Logger, args: CommandArgs): Promise { + if (this.verbose) { + logger.logToStderr(`Retrieving available AI Builder models`); + } + + try { + const dynamicsApiUrl = await powerPlatform.getDynamicsInstanceApiUrl(args.options.environment, args.options.asAdmin); + + const aimodels = await odata.getAllItems(`${dynamicsApiUrl}/api/data/v9.0/msdyn_aimodels?$filter=iscustomizable/Value eq true`); + logger.log(aimodels); + } + catch (err: any) { + this.handleRejectedODataJsonPromise(err); + } + } +} + +module.exports = new PpAiBuilderModelListCommand(); \ No newline at end of file