Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf(spec-parser): update to support type b ai-plugin #10972

Merged
merged 7 commits into from
Mar 7, 2024
Merged
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
2 changes: 1 addition & 1 deletion packages/spec-parser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"types": "dist/src/index.d.ts",
"scripts": {
"build": "rollup -c",
"test:unit:node": "nyc --no-clean -- mocha \"test/*.test.ts\" -r config/mocha.env.ts --config config/.mocharc.json",
"test:unit:node": "nyc --no-clean -- mocha -r config/mocha.env.ts --config config/.mocharc.json",
"test:unit:browser": "karma start karma.conf.cjs --single-run --unit",
"test:unit": "npm run test:unit:node && npm run test:unit:browser ",
"lint:staged": "lint-staged",
Expand Down
2 changes: 2 additions & 0 deletions packages/spec-parser/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export class ConstantString {
static readonly MultipleAPIKeyNotSupported =
"Multiple API keys are not supported. Please make sure that all selected APIs use the same API key.";

static readonly UnsupportedSchema = "Unsupported schema in %s %s: %s";

static readonly WrappedCardVersion = "devPreview";
static readonly WrappedCardSchema =
"https://developer.microsoft.com/json-schemas/teams/vDevPreview/MicrosoftTeams.ResponseRenderingTemplate.schema.json";
Expand Down
1 change: 1 addition & 0 deletions packages/spec-parser/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ export interface ParseOptions {
allowAPIKeyAuth?: boolean;
allowMultipleParameters?: boolean;
allowOauth2?: boolean;
isCopilot?: boolean;
}

export interface APIInfo {
Expand Down
186 changes: 178 additions & 8 deletions packages/spec-parser/src/manifestUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,185 @@ import {
IComposeExtension,
IMessagingExtensionCommand,
TeamsAppManifest,
PluginManifestSchema,
FunctionObject,
FunctionParameters,
FunctionParameter,
} from "@microsoft/teams-manifest";

export class ManifestUpdater {
static async updateManifestWithAiPlugin(
manifestPath: string,
outputSpecPath: string,
apiPluginFilePath: string,
spec: OpenAPIV3.Document
): Promise<[TeamsAppManifest, PluginManifestSchema]> {
const manifest: TeamsAppManifest = await fs.readJSON(manifestPath);
const apiPluginRelativePath = ManifestUpdater.getRelativePath(manifestPath, apiPluginFilePath);
manifest.apiPlugins = [
{
pluginFile: apiPluginRelativePath,
},
];

ManifestUpdater.updateManifestDescription(manifest, spec);

const specRelativePath = ManifestUpdater.getRelativePath(manifestPath, outputSpecPath);
const apiPlugin = ManifestUpdater.generatePluginManifestSchema(spec, specRelativePath);

return [manifest, apiPlugin];
}

static updateManifestDescription(manifest: TeamsAppManifest, spec: OpenAPIV3.Document): void {
manifest.description = {
short: spec.info.title.slice(0, ConstantString.ShortDescriptionMaxLens),
full: (spec.info.description ?? manifest.description.full)?.slice(
0,
ConstantString.FullDescriptionMaxLens
),
};
}

static mapOpenAPISchemaToFuncParam(
schema: OpenAPIV3.SchemaObject,
method: string,
pathUrl: string
): FunctionParameter {
let parameter: FunctionParameter;
if (
schema.type === "string" ||
schema.type === "boolean" ||
schema.type === "integer" ||
schema.type === "number" ||
schema.type === "array"
) {
parameter = schema as any;
} else {
throw new SpecParserError(
Utils.format(ConstantString.UnsupportedSchema, method, pathUrl, JSON.stringify(schema)),
ErrorType.UpdateManifestFailed
);
}

return parameter;
}

static generatePluginManifestSchema(
spec: OpenAPIV3.Document,
specRelativePath: string
): PluginManifestSchema {
const functions: FunctionObject[] = [];
const functionNames: string[] = [];

const paths = spec.paths;

for (const pathUrl in paths) {
const pathItem = paths[pathUrl];
if (pathItem) {
const operations = pathItem;
for (const method in operations) {
if (ConstantString.AllOperationMethods.includes(method)) {
const operationItem = (operations as any)[method] as OpenAPIV3.OperationObject;
if (operationItem) {
const operationId = operationItem.operationId!;
const description = operationItem.description ?? "";
const paramObject = operationItem.parameters as OpenAPIV3.ParameterObject[];
const requestBody = operationItem.requestBody as OpenAPIV3.ParameterObject;

const parameters: FunctionParameters = {
type: "object",
properties: {},
required: [],
};

if (paramObject) {
for (let i = 0; i < paramObject.length; i++) {
const param = paramObject[i];

const schema = param.schema as OpenAPIV3.SchemaObject;

parameters.properties![param.name] = ManifestUpdater.mapOpenAPISchemaToFuncParam(
schema,
method,
pathUrl
);

if (param.required) {
parameters.required!.push(param.name);
}

if (!parameters.properties![param.name].description) {
parameters.properties![param.name].description = param.description ?? "";
}
}
}

if (requestBody) {
const requestJsonBody = requestBody.content!["application/json"];
const requestBodySchema = requestJsonBody.schema as OpenAPIV3.SchemaObject;

if (requestBodySchema.type === "object") {
if (requestBodySchema.required) {
parameters.required!.push(...requestBodySchema.required);
}

for (const property in requestBodySchema.properties) {
const schema = requestBodySchema.properties[property] as OpenAPIV3.SchemaObject;
parameters.properties![property] = ManifestUpdater.mapOpenAPISchemaToFuncParam(
schema,
method,
pathUrl
);
}
} else {
throw new SpecParserError(
Utils.format(
ConstantString.UnsupportedSchema,
method,
pathUrl,
JSON.stringify(requestBodySchema)
),
ErrorType.UpdateManifestFailed
);
}
}

const funcObj: FunctionObject = {
name: operationId,
description: description,
parameters: parameters,
};

functions.push(funcObj);
functionNames.push(operationId);
}
}
}
}
}

const apiPlugin: PluginManifestSchema = {
schema_version: "v2",
name_for_human: spec.info.title,
description_for_human: spec.info.description ?? "<Please add description of the plugin>",
functions: functions,
runtimes: [
{
type: "OpenApi",
auth: {
type: "none", // TODO, support auth in the future
},
spec: {
url: specRelativePath,
},
run_for_functions: functionNames,
},
],
};

return apiPlugin;
}

static async updateManifest(
manifestPath: string,
outputSpecPath: string,
Expand Down Expand Up @@ -67,14 +243,8 @@ export class ManifestUpdater {
}
}

updatedPart.description = {
short: spec.info.title.slice(0, ConstantString.ShortDescriptionMaxLens),
full: (spec.info.description ?? originalManifest.description.full)?.slice(
0,
ConstantString.FullDescriptionMaxLens
),
};

updatedPart.description = originalManifest.description;
ManifestUpdater.updateManifestDescription(updatedPart, spec);
updatedPart.composeExtensions = isMe === undefined || isMe === true ? [composeExtension] : [];

const updatedManifest = { ...originalManifest, ...updatedPart };
Expand Down
6 changes: 4 additions & 2 deletions packages/spec-parser/src/specFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
allowMissingId: boolean,
allowAPIKeyAuth: boolean,
allowMultipleParameters: boolean,
allowOauth2: boolean
allowOauth2: boolean,
isCopilot: boolean
): OpenAPIV3.Document {
try {
const newSpec = { ...unResolveSpec };
Expand All @@ -33,7 +34,8 @@
allowMissingId,
allowAPIKeyAuth,
allowMultipleParameters,
allowOauth2
allowOauth2,
isCopilot
)
) {
continue;
Expand All @@ -42,11 +44,11 @@
if (!newPaths[path]) {
newPaths[path] = { ...unResolveSpec.paths[path] };
for (const m of ConstantString.AllOperationMethods) {
delete (newPaths[path] as any)[m];

Check warning

Code scanning / CodeQL

Prototype-polluting assignment Medium

This assignment may alter Object.prototype if a malicious '__proto__' string is injected from
library input
.
This assignment may alter Object.prototype if a malicious '__proto__' string is injected from
library input
.
This assignment may alter Object.prototype if a malicious '__proto__' string is injected from
library input
.
This assignment may alter Object.prototype if a malicious '__proto__' string is injected from
library input
.
This assignment may alter Object.prototype if a malicious '__proto__' string is injected from
library input
.
This assignment may alter Object.prototype if a malicious '__proto__' string is injected from
library input
.
}
}

(newPaths[path] as any)[methodName] = (unResolveSpec.paths[path] as any)[methodName];

Check warning

Code scanning / CodeQL

Prototype-polluting assignment Medium

This assignment may alter Object.prototype if a malicious '__proto__' string is injected from
library input
.
This assignment may alter Object.prototype if a malicious '__proto__' string is injected from
library input
.
This assignment may alter Object.prototype if a malicious '__proto__' string is injected from
library input
.
This assignment may alter Object.prototype if a malicious '__proto__' string is injected from
library input
.
This assignment may alter Object.prototype if a malicious '__proto__' string is injected from
library input
.
This assignment may alter Object.prototype if a malicious '__proto__' string is injected from
library input
.

// Add the operationId if missing
if (!(newPaths[path] as any)[methodName].operationId) {
Expand Down
24 changes: 22 additions & 2 deletions packages/spec-parser/src/specParser.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export class SpecParser {
allowAPIKeyAuth: false,
allowMultipleParameters: false,
allowOauth2: false,
isCopilot: false,
};

/**
Expand Down Expand Up @@ -92,7 +93,8 @@ export class SpecParser {
this.options.allowMissingId,
this.options.allowAPIKeyAuth,
this.options.allowMultipleParameters,
this.options.allowOauth2
this.options.allowOauth2,
this.options.isCopilot
);
} catch (err) {
throw new SpecParserError((err as Error).toString(), ErrorType.ValidateFailed);
Expand Down Expand Up @@ -163,6 +165,23 @@ export class SpecParser {
throw new Error("Method not implemented.");
}

/**
* Generates and update artifacts from the OpenAPI specification file. Generate Adaptive Cards, update Teams app manifest, and generate a new OpenAPI specification file.
* @param manifestPath A file path of the Teams app manifest file to update.
* @param filter An array of strings that represent the filters to apply when generating the artifacts. If filter is empty, it would process nothing.
* @param outputSpecPath File path of the new OpenAPI specification file to generate. If not specified or empty, no spec file will be generated.
* @param pluginFilePath File path of the api plugin file to generate.
*/
// eslint-disable-next-line @typescript-eslint/require-await
async generateForCopilot(
manifestPath: string,
filter: string[],
outputSpecPath: string,
pluginFilePath: string,
signal?: AbortSignal
): Promise<GenerateResult> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we also make ai-plugin.json as an input parameter? I think user may customize the file location when they want to use the codelens to add a new API?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

throw new Error("Method not implemented.");
}
/**
* Generates and update artifacts from the OpenAPI specification file. Generate Adaptive Cards, update Teams app manifest, and generate a new OpenAPI specification file.
* @param manifestPath A file path of the Teams app manifest file to update.
Expand Down Expand Up @@ -206,7 +225,8 @@ export class SpecParser {
this.options.allowMissingId,
this.options.allowAPIKeyAuth,
this.options.allowMultipleParameters,
this.options.allowOauth2
this.options.allowOauth2,
this.options.isCopilot
);
this.apiMap = result;
return result;
Expand Down
Loading
Loading