diff --git a/packages/manifest/src/index.ts b/packages/manifest/src/index.ts index 466d23d581..a522b4e6fa 100644 --- a/packages/manifest/src/index.ts +++ b/packages/manifest/src/index.ts @@ -235,4 +235,9 @@ export class ManifestUtil { return telemetryProperties; } + + static async useCopilotExtensionsInSchema(manifest: TeamsAppManifest): Promise { + const schema = await this.fetchSchema(manifest); + return !!schema.properties.copilotExtensions; + } } diff --git a/packages/manifest/test/index.test.ts b/packages/manifest/test/index.test.ts index c523301707..506e9ed04d 100644 --- a/packages/manifest/test/index.test.ts +++ b/packages/manifest/test/index.test.ts @@ -99,6 +99,40 @@ describe("Manifest manipulation", async () => { chai.expect(result[0]).to.contain("/manifestVersion"); }); }); + describe("useCopilotExtensionsInSchema", async () => { + let fetchSchemaStub: sinon.SinonStub; + + beforeEach(() => { + fetchSchemaStub = sinon.stub(ManifestUtil, "fetchSchema"); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should return true when copilotExtensions exist in schema definitions", async () => { + const mockSchema = { + properties: { + copilotExtensions: {}, + }, + }; + + fetchSchemaStub.resolves(mockSchema); + + const result = await ManifestUtil.useCopilotExtensionsInSchema({} as any); + chai.assert.isTrue(result); + }); + + it("should return false when copilotExtensions do not exist in schema definitions", async () => { + const mockSchema = { + properties: {}, + }; + fetchSchemaStub.resolves(mockSchema); + + const result = await ManifestUtil.useCopilotExtensionsInSchema({} as any); + chai.assert.isFalse(result); + }); + }); }); async function loadSchema(): Promise { diff --git a/packages/spec-parser/src/manifestUpdater.ts b/packages/spec-parser/src/manifestUpdater.ts index 018884f9f4..c695d74d42 100644 --- a/packages/spec-parser/src/manifestUpdater.ts +++ b/packages/spec-parser/src/manifestUpdater.ts @@ -24,6 +24,7 @@ import { PluginManifestSchema, FunctionObject, AuthObject, + ManifestUtil, } from "@microsoft/teams-manifest"; import { AdaptiveCardGenerator } from "./adaptiveCardGenerator"; import { wrapResponseSemantics } from "./adaptiveCardWrapper"; @@ -41,7 +42,9 @@ export class ManifestUpdater { const manifest: TeamsAppManifest = await fs.readJSON(manifestPath); const apiPluginRelativePath = ManifestUpdater.getRelativePath(manifestPath, apiPluginFilePath); - if (manifest.copilotExtensions) { + const useCopilotExtensionsInSchema = await ManifestUtil.useCopilotExtensionsInSchema(manifest); + if (manifest.copilotExtensions || useCopilotExtensionsInSchema) { + manifest.copilotExtensions = manifest.copilotExtensions || {}; if (!options.isGptPlugin) { manifest.copilotExtensions.plugins = [ { @@ -52,8 +55,7 @@ export class ManifestUpdater { ManifestUpdater.updateManifestDescription(manifest, spec); } } else { - (manifest as any).copilotAgents = (manifest as any).copilotAgents || {}; - + manifest.copilotAgents = manifest.copilotAgents || {}; if (!options.isGptPlugin) { (manifest as any).copilotAgents.plugins = [ { diff --git a/packages/spec-parser/test/manifestUpdater.test.ts b/packages/spec-parser/test/manifestUpdater.test.ts index 70f444f042..d8c83a982f 100644 --- a/packages/spec-parser/test/manifestUpdater.test.ts +++ b/packages/spec-parser/test/manifestUpdater.test.ts @@ -12,8 +12,11 @@ import { AuthInfo, ErrorType, ParseOptions, ProjectType, WarningType } from "../ import { ConstantString } from "../src/constants"; import { Utils } from "../src/utils"; import { PluginManifestSchema } from "@microsoft/teams-manifest"; - +import { ManifestUtil } from "@microsoft/teams-manifest"; describe("updateManifestWithAiPlugin", () => { + beforeEach(() => { + sinon.stub(ManifestUtil, "useCopilotExtensionsInSchema").resolves(false); + }); afterEach(() => { sinon.restore(); }); @@ -122,7 +125,7 @@ describe("updateManifestWithAiPlugin", () => { expect(warnings).to.deep.equal([]); }); - it("should generate response semantics based on the response", async () => { + it("should generate response semantics based on the response - 1", async () => { const spec: any = { openapi: "3.0.2", info: { @@ -287,6 +290,173 @@ describe("updateManifestWithAiPlugin", () => { expect(warnings).to.deep.equal([]); }); + it("should generate response semantics based on the response - 2", async () => { + sinon.restore(); + sinon.stub(ManifestUtil, "useCopilotExtensionsInSchema").resolves(true); + const spec: any = { + openapi: "3.0.2", + info: { + title: "My API", + description: "My API description", + }, + servers: [ + { + url: "/v3", + }, + ], + paths: { + "/pets": { + get: { + operationId: "getPets", + summary: "Get all pets", + description: "Returns all pets from the system that the user has access to", + parameters: [ + { + name: "limit", + description: "Maximum number of pets to return", + required: true, + schema: { + type: "integer", + }, + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + description: { + type: "string", + }, + imageUrl: { + type: "string", + }, + id: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + const manifestPath = "/path/to/your/manifest.json"; + const outputSpecPath = "/path/to/your/spec/outputSpec.yaml"; + const pluginFilePath = "/path/to/your/ai-plugin.json"; + + const originalManifest = { + name: { short: "Original Name", full: "Original Full Name" }, + description: { short: "Original Short Description", full: "Original Full Description" }, + }; + const expectedManifest = { + name: { short: "Original Name", full: "Original Full Name" }, + description: { short: "My API", full: "My API description" }, + copilotExtensions: { + plugins: [ + { + file: "ai-plugin.json", + id: "plugin_1", + }, + ], + }, + }; + + const expectedPlugins: PluginManifestSchema = { + $schema: ConstantString.PluginManifestSchema, + schema_version: "v2.1", + name_for_human: "Original Name", + namespace: "originalname", + description_for_human: "My API description", + functions: [ + { + name: "getPets", + description: "Returns all pets from the system that the user has access to", + capabilities: { + response_semantics: { + data_path: "$", + properties: { + subtitle: "$.description", + title: "$.name", + url: "$.imageUrl", + }, + static_template: { + $schema: "http://adaptivecards.io/schemas/adaptive-card.json", + body: [ + { + text: "name: ${if(name, name, 'N/A')}", + type: "TextBlock", + wrap: true, + }, + { + text: "description: ${if(description, description, 'N/A')}", + type: "TextBlock", + wrap: true, + }, + { + $when: "${imageUrl != null && imageUrl != ''}", + type: "Image", + url: "${imageUrl}", + }, + { + text: "id: ${if(id, id, 'N/A')}", + type: "TextBlock", + wrap: true, + }, + ], + type: "AdaptiveCard", + version: "1.5", + }, + }, + }, + }, + ], + runtimes: [ + { + type: "OpenApi", + auth: { + type: "None", + }, + spec: { + url: "spec/outputSpec.yaml", + }, + run_for_functions: ["getPets"], + }, + ], + }; + sinon.stub(fs, "readJSON").resolves(originalManifest); + sinon + .stub(fs, "pathExists") + .withArgs(manifestPath) + .resolves(true) + .withArgs(pluginFilePath) + .resolves(false); + + const options: ParseOptions = { + allowMethods: ["get", "post"], + allowResponseSemantics: true, + }; + const [manifest, apiPlugin, warnings] = await ManifestUpdater.updateManifestWithAiPlugin( + manifestPath, + outputSpecPath, + pluginFilePath, + spec, + options + ); + + expect(manifest).to.deep.equal(expectedManifest); + expect(apiPlugin).to.deep.equal(expectedPlugins); + expect(warnings).to.deep.equal([]); + }); + it("should not generate response semantics and return warnings if api response schema contains anyof", async () => { const spec: any = { openapi: "3.0.2",