Skip to content

Commit

Permalink
refactor: plugin manifest utils (#10959)
Browse files Browse the repository at this point in the history
* refactor: util

* refactor: more

refactor: more

refactor: fix

refactor: fix

refactor: template

test: fix

test: ut 1

test: 2

test: more

test: test more

test: test 4

test: more

test: more

refactor: string

* refactor: string
  • Loading branch information
yuqizhou77 authored Mar 6, 2024
1 parent 9f3f330 commit 155e086
Show file tree
Hide file tree
Showing 17 changed files with 770 additions and 60 deletions.
8 changes: 5 additions & 3 deletions packages/fx-core/resource/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@
"error.NoSubscriptionFound": "Unable to find a subscription.",
"error.TrustCertificateCancelError": "User canceled. For Teams to trust the self-signed SSL certificate used by the toolkit, add the certificate to your certificate store.",
"error.VideoFilterAppNotRemoteSupported": "Teams Toolkit doesn't support video filter app in remote. Check the README.md file in project root folder.",
"error.appstudio.teamsAppRequiredPropertyMissing": "Missing required property \"%s\" in \"%s\"",
"error.appstudio.teamsAppCreateFailed": "Unable to create Teams app in Teams Developer Portal due to %s",
"error.appstudio.teamsAppUpdateFailed": "Unable to update Teams app with ID %s in Teams Developer Portal due to %s",
"error.appstudio.apiFailed": "Unable to make API call to Developer Portal. Check [Output panel](command:fx-extension.showOutputChannel) for details.",
Expand Down Expand Up @@ -215,6 +216,7 @@
"error.generator.FetchSampleInfoError": "Unable to fetch sample info",
"error.generator.DownloadSampleApiLimitError": "Unable to download sample due to throttling. Retry later after rate limit reset (This may take up to 1 hour). Alternatively, you can go to %s to git clone the repo manually",
"error.generator.DownloadSampleNetworkError": "Unable to download sample due to network error. Check your network connection and retry. Alternatively, you can go to %s to git clone the repo manually",
"error.copilotPlugin.apiSpecNotUsedInPlugin": "\"%s\" is not used in the plugin.",
"error.copilotPlugin.openAiPluginManifest.CannotGetManifest": "Unable to get OpenAI plugin manifest from '%s'.",
"error.copilotPlugin.noExtraAPICanBeAdded": "No API can be added. Only GET and POST methods with at most one required parameter and no auth are supported. Methods defined in manifest.json are not listed.",
"error.m365.NotExtendedToM365Error": "Unable to extend Teams app to Microsoft 365. Use 'teamsApp/extendToM365' action to extend your Teams app to Microsoft 365.",
Expand Down Expand Up @@ -287,9 +289,9 @@
"core.createProjectQuestion.projectType.outlookAddin.title": "App Features Using an Outlook Add-in",
"core.createProjectQuestion.projectType.tab.detail": "Embed your own web content in Teams, Outlook, and the Microsoft 365 app",
"core.createProjectQuestion.projectType.tab.title": "App Features Using a Tab",
"core.createProjectQuestion.projectType.copilotPlugin.detail": "Create a plugin to extend Microsoft 365 Copilot using your APIs",
"core.createProjectQuestion.projectType.copilotPlugin.label": "Plugin",
"core.createProjectQuestion.projectType.copilotPlugin.title": "Plugin for Copilot",
"core.createProjectQuestion.projectType.copilotPlugin.detail": "Create a plugin to extend Microsoft Copilot for Microsoft 365 using your APIs",
"core.createProjectQuestion.projectType.copilotPlugin.label": "Copilot Plugin",
"core.createProjectQuestion.projectType.copilotPlugin.title": "Copilot Plugin",
"core.createProjectQuestion.projectType.copilotPlugin.placeholder": "Select an option",
"core.createProjectQuestion.projectType.customCopilot.detail": "Build intelligent chatbot in Microsoft Teams easily using Teams AI Library",
"core.createProjectQuestion.projectType.customCopilot.label": "Custom Copilot",
Expand Down
22 changes: 17 additions & 5 deletions packages/fx-core/src/component/coordinator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,11 +292,23 @@ class Coordinator {
if (res.isErr()) {
return err(res.error);
}
} else if (
meArchitecture === MeArchitectureOptions.apiSpec().id ||
capability === CapabilityOptions.copilotPluginApiSpec().id
) {
const res = await CopilotPluginGenerator.generateFromApiSpec(context, inputs, projectPath);
} else if (capability === CapabilityOptions.copilotPluginApiSpec().id) {
const res = await CopilotPluginGenerator.generatePluginFromApiSpec(
context,
inputs,
projectPath
);
if (res.isErr()) {
return err(res.error);
} else {
warnings = res.value.warnings;
}
} else if (meArchitecture === MeArchitectureOptions.apiSpec().id) {
const res = await CopilotPluginGenerator.generateMeFromApiSpec(
context,
inputs,
projectPath
);
if (res.isErr()) {
return err(res.error);
} else {
Expand Down
8 changes: 8 additions & 0 deletions packages/fx-core/src/component/driver/teamsApp/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,12 @@ export class AppStudioError {
getLocalizedString("error.appstudio.teamsAppPublishConflict", teamsAppId),
],
};

public static readonly TeamsAppRequiredPropertyMissingError = {
name: "TeamsAppMissingRequiredCapability",
message: (property: string, path: string): [string, string] => [
getDefaultString("error.appstudio.teamsAppRequiredPropertyMissing", property, path),
getLocalizedString("error.appstudio.teamsAppRequiredPropertyMissing", property, path),
],
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,29 @@ export class ManifestUtils {
return ids;
}

public async getPluginFilePath(
manifest: TeamsAppManifest,
manifestPath: string
): Promise<Result<string, FxError>> {
const pluginFile = manifest.apiPlugins?.[0]?.pluginFile;
if (pluginFile) {
const plugin = path.resolve(path.dirname(manifestPath), pluginFile);
const doesFileExist = await fs.pathExists(plugin);
if (doesFileExist) {
return ok(plugin);
} else {
return err(new FileNotFoundError("ManifestUtils", pluginFile));
}
} else {
return err(
AppStudioResultFactory.UserError(
AppStudioError.TeamsAppRequiredPropertyMissingError.name,
AppStudioError.TeamsAppRequiredPropertyMissingError.message("plugins", manifestPath)
)
);
}
}

async getManifestV3(
manifestTemplatePath: string,
context?: WrapDriverContext,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import {
FxError,
PluginManifestSchema,
Result,
TeamsAppManifest,
err,
ok,
} from "@microsoft/teamsfx-api";
import fs from "fs-extra";
import { FileNotFoundError, JSONSyntaxError } from "../../../../error/common";
import stripBom from "strip-bom";
import path from "path";
import { manifestUtils } from "./ManifestUtils";

export class PluginManifestUtils {
public async readPluginManifestFile(
path: string
): Promise<Result<PluginManifestSchema, FxError>> {
if (!(await fs.pathExists(path))) {
return err(new FileNotFoundError("PluginManifestUtils", path));
}
// Be compatible with UTF8-BOM encoding
// Avoid Unexpected token error at JSON.parse()
let content = await fs.readFile(path, { encoding: "utf-8" });
content = stripBom(content);

try {
const manifest = JSON.parse(content) as PluginManifestSchema;
return ok(manifest);
} catch (e) {
return err(new JSONSyntaxError(path, e, "PluginManifestUtils"));
}
}

public async getApiSpecFilePathFromTeamsManifest(
manifest: TeamsAppManifest,
manifestPath: string
): Promise<Result<string[], FxError>> {
const pluginFilePathRes = await manifestUtils.getPluginFilePath(manifest, manifestPath);
if (pluginFilePathRes.isErr()) {
return err(pluginFilePathRes.error);
}
const pluginFilePath = pluginFilePathRes.value;
const pluginContentRes = await this.readPluginManifestFile(pluginFilePath);
if (pluginContentRes.isErr()) {
return err(pluginContentRes.error);
}
const apiSpecFiles = await this.getApiSpecFilePathFromPlugin(
pluginContentRes.value,
pluginFilePath
);
return ok(apiSpecFiles);
}

async getApiSpecFilePathFromPlugin(
plugin: PluginManifestSchema,
pluginPath: string
): Promise<string[]> {
const runtimes = plugin.runtimes;
const files: string[] = [];
if (!runtimes) {
return files;
}
for (const runtime of runtimes) {
if (runtime.type === "OpenApi" && runtime.spec?.url) {
const specFile = path.resolve(path.dirname(pluginPath), runtime.spec.url);
if (await fs.pathExists(specFile)) {
files.push(specFile);
}
}
}

return files;
}
}

export const pluginManifestUtils = new PluginManifestUtils();
51 changes: 41 additions & 10 deletions packages/fx-core/src/component/generator/copilotPlugin/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
specParserGenerateResultTelemetryEvent,
specParserGenerateResultWarningsTelemetryProperty,
isYamlSpecFile,
invalidApiSpecErrorName,
} from "./helper";
import { getLocalizedString } from "../../../common/localizeUtils";
import { manifestUtils } from "../../driver/teamsApp/utils/ManifestUtils";
Expand All @@ -54,6 +55,7 @@ import { isApiKeyEnabled, isMultipleParametersEnabled } from "../../../common/fe
import { merge } from "lodash";

const fromApiSpecComponentName = "copilot-plugin-existing-api";
const pluginFromApiSpecComponentName = "api-copilot-plugin-existing-api";
const fromApiSpecTemplateName = "copilot-plugin-existing-api";
const fromApiSpecWithApiKeyTemplateName = "copilot-plugin-existing-api-api-key";
const fromOpenAIPlugincomponentName = "copilot-plugin-from-oai-plugin";
Expand All @@ -62,7 +64,6 @@ const apiSpecFolderName = "apiSpecificationFile";
const apiSpecYamlFileName = "openapi.yaml";
const apiSpecJsonFileName = "openapi.json";

const invalidApiSpecErrorName = "invalid-api-spec";
const copilotPluginExistingApiSpecUrlTelemetryEvent = "copilot-plugin-existing-api-spec-url";

const apiPluginFromApiSpecTemplateName = "api-plugin-existing-api";
Expand Down Expand Up @@ -95,7 +96,7 @@ export class CopilotPluginGenerator {
errorSource: fromApiSpecComponentName,
}),
])
public static async generateFromApiSpec(
public static async generateMeFromApiSpec(
context: Context,
inputs: Inputs,
destinationPath: string,
Expand All @@ -105,13 +106,43 @@ export class CopilotPluginGenerator {
const authApi = (inputs.supportedApisFromApiSpec as ApiOperation[]).find(
(api) => !!api.data.authName && apiOperations.includes(api.id)
);
const isApiPlugin =
inputs[QuestionNames.Capabilities] === CapabilityOptions.copilotPluginApiSpec().id;
const templateName = isApiPlugin
? apiPluginFromApiSpecTemplateName
: authApi
? fromApiSpecWithApiKeyTemplateName
: fromApiSpecTemplateName;

const templateName = authApi ? fromApiSpecWithApiKeyTemplateName : fromApiSpecTemplateName;
const componentName = fromApiSpecComponentName;

merge(actionContext?.telemetryProps, { [telemetryProperties.templateName]: templateName });

return await this.generate(
context,
inputs,
destinationPath,
templateName,
componentName,
false,
authApi?.data
);
}

@hooks([
ActionExecutionMW({
enableTelemetry: true,
telemetryComponentName: pluginFromApiSpecComponentName,
telemetryEventName: TelemetryEvents.Generate,
errorSource: pluginFromApiSpecComponentName,
}),
])
public static async generatePluginFromApiSpec(
context: Context,
inputs: Inputs,
destinationPath: string,
actionContext?: ActionContext
): Promise<Result<CopilotPluginGeneratorResult, FxError>> {
const apiOperations = inputs[QuestionNames.ApiOperation] as string[];
const authApi = (inputs.supportedApisFromApiSpec as ApiOperation[]).find(
(api) => !!api.data.authName && apiOperations.includes(api.id)
);

const templateName = apiPluginFromApiSpecTemplateName;
const componentName = fromApiSpecComponentName;

merge(actionContext?.telemetryProps, { [telemetryProperties.templateName]: templateName });
Expand All @@ -122,7 +153,7 @@ export class CopilotPluginGenerator {
destinationPath,
templateName,
componentName,
isApiPlugin,
true,
authApi?.data
);
}
Expand Down
Loading

0 comments on commit 155e086

Please sign in to comment.