Skip to content

Commit

Permalink
Merge pull request #12482 from OfficeDev/anchenyi/copilot_hot_fix_spe…
Browse files Browse the repository at this point in the history
…c_parser

fix: copilot spec parser
  • Loading branch information
MSFT-yiz committed Sep 30, 2024
2 parents 467e728 + cd563f2 commit e1b1d2b
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 5 deletions.
5 changes: 5 additions & 0 deletions packages/manifest/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,4 +235,9 @@ export class ManifestUtil {

return telemetryProperties;
}

static async useCopilotExtensionsInSchema(manifest: TeamsAppManifest): Promise<boolean> {
const schema = await this.fetchSchema(manifest);
return !!schema.properties.copilotExtensions;
}
}
34 changes: 34 additions & 0 deletions packages/manifest/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TeamsAppManifestJSONSchema> {
Expand Down
8 changes: 5 additions & 3 deletions packages/spec-parser/src/manifestUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
PluginManifestSchema,
FunctionObject,
AuthObject,
ManifestUtil,
} from "@microsoft/teams-manifest";
import { AdaptiveCardGenerator } from "./adaptiveCardGenerator";
import { wrapResponseSemantics } from "./adaptiveCardWrapper";
Expand All @@ -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 = [
{
Expand All @@ -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 = [
{
Expand Down
174 changes: 172 additions & 2 deletions packages/spec-parser/test/manifestUpdater.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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",
Expand Down

0 comments on commit e1b1d2b

Please sign in to comment.