Skip to content

Commit

Permalink
perf(spec-parser): support generate adaptive card for type B schema (#…
Browse files Browse the repository at this point in the history
…11351)

Co-authored-by: rentu <rentu@microsoft.com>
  • Loading branch information
SLdragon and SLdragon authored Apr 11, 2024
1 parent 5ddf860 commit 4e708f0
Show file tree
Hide file tree
Showing 8 changed files with 479 additions and 34 deletions.
118 changes: 85 additions & 33 deletions packages/spec-parser/src/adaptiveCardWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
// Licensed under the MIT license.
"use strict";

import { ResponseSemanticsObject } from "@microsoft/teams-manifest";
import { ConstantString } from "./constants";
import {
AdaptiveCard,
ArrayElement,
ImageElement,
InferredProperties,
PreviewCardTemplate,
TextBlockElement,
WrappedAdaptiveCard,
Expand All @@ -26,6 +28,36 @@ export function wrapAdaptiveCard(card: AdaptiveCard, jsonPath: string): WrappedA
return result;
}

export function wrapResponseSemantics(
card: AdaptiveCard,
jsonPath: string
): ResponseSemanticsObject {
const props = inferProperties(card);
const dataPath = jsonPath === "$" ? "$" : "$." + jsonPath;
const result: ResponseSemanticsObject = {
data_path: dataPath,
};

if (props.title || props.subtitle || props.imageUrl) {
result.properties = {};
if (props.title) {
result.properties.title = "$." + props.title;
}

if (props.subtitle) {
result.properties.subtitle = "$." + props.subtitle;
}

if (props.imageUrl) {
result.properties.url = "$." + props.imageUrl;
}
}

result.static_template = card as any;

return result;
}

/**
* Infers the preview card template from an Adaptive Card and a JSON path.
* The preview card template includes a title and an optional subtitle and image.
Expand All @@ -39,9 +71,32 @@ export function wrapAdaptiveCard(card: AdaptiveCard, jsonPath: string): WrappedA
*/
export function inferPreviewCardTemplate(card: AdaptiveCard): PreviewCardTemplate {
const result: PreviewCardTemplate = {
title: "",
title: "result",
};
const textBlockElements = new Set<TextBlockElement>();
const inferredProperties = inferProperties(card);
if (inferredProperties.title) {
result.title = `\${if(${inferredProperties.title}, ${inferredProperties.title}, 'N/A')}`;
}

if (inferredProperties.subtitle) {
result.subtitle = `\${if(${inferredProperties.subtitle}, ${inferredProperties.subtitle}, 'N/A')}`;
}

if (inferredProperties.imageUrl) {
result.image = {
url: `\${${inferredProperties.imageUrl}}`,
alt: `\${if(${inferredProperties.imageUrl}, ${inferredProperties.imageUrl}, 'N/A')}`,
$when: `\${${inferredProperties.imageUrl} != null}`,
};
}

return result;
}

function inferProperties(card: AdaptiveCard): InferredProperties {
const result: InferredProperties = {};

const nameSet = new Set<string>();

let rootObject: (TextBlockElement | ArrayElement | ImageElement)[];
if (card.body[0]?.type === ConstantString.ContainerType) {
Expand All @@ -55,45 +110,46 @@ export function inferPreviewCardTemplate(card: AdaptiveCard): PreviewCardTemplat
const textElement = element as TextBlockElement;
const index = textElement.text.indexOf("${if(");
if (index > 0) {
textElement.text = textElement.text.substring(index);
textBlockElements.add(textElement);
const text = textElement.text.substring(index);
const match = text.match(/\${if\(([^,]+),/);
const property = match ? match[1] : "";
if (property) {
nameSet.add(property);
}
}
} else if (element.type === ConstantString.ImageType) {
const imageElement = element as ImageElement;
const match = imageElement.url.match(/\${([^,]+)}/);
const property = match ? match[1] : "";
if (property) {
nameSet.add(property);
}
}
}

for (const element of textBlockElements) {
const text = element.text;
if (!result.title && Utils.isWellKnownName(text, ConstantString.WellknownTitleName)) {
result.title = text;
textBlockElements.delete(element);
for (const name of nameSet) {
if (!result.title && Utils.isWellKnownName(name, ConstantString.WellknownTitleName)) {
result.title = name;
nameSet.delete(name);
} else if (
!result.subtitle &&
Utils.isWellKnownName(text, ConstantString.WellknownSubtitleName)
Utils.isWellKnownName(name, ConstantString.WellknownSubtitleName)
) {
result.subtitle = text;
textBlockElements.delete(element);
} else if (!result.image && Utils.isWellKnownName(text, ConstantString.WellknownImageName)) {
const match = text.match(/\${if\(([^,]+),/);
const property = match ? match[1] : "";
if (property) {
result.image = {
url: `\${${property}}`,
alt: text,
$when: `\${${property} != null}`,
};
}
textBlockElements.delete(element);
result.subtitle = name;
nameSet.delete(name);
} else if (!result.imageUrl && Utils.isWellKnownName(name, ConstantString.WellknownImageName)) {
result.imageUrl = name;
nameSet.delete(name);
}
}

for (const element of textBlockElements) {
const text = element.text;
for (const name of nameSet) {
if (!result.title) {
result.title = text;
textBlockElements.delete(element);
result.title = name;
nameSet.delete(name);
} else if (!result.subtitle) {
result.subtitle = text;
textBlockElements.delete(element);
result.subtitle = name;
nameSet.delete(name);
}
}

Expand All @@ -102,9 +158,5 @@ export function inferPreviewCardTemplate(card: AdaptiveCard): PreviewCardTemplat
delete result.subtitle;
}

if (!result.title) {
result.title = "result";
}

return result;
}
1 change: 1 addition & 0 deletions packages/spec-parser/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export class ConstantString {
static readonly AdaptiveCardSchema = "http://adaptivecards.io/schemas/adaptive-card.json";
static readonly AdaptiveCardType = "AdaptiveCard";
static readonly TextBlockType = "TextBlock";
static readonly ImageType = "Image";
static readonly ContainerType = "Container";
static readonly RegistrationIdPostfix = "REGISTRATION_ID";
static readonly OAuthRegistrationIdPostFix = "OAUTH_REGISTRATION_ID";
Expand Down
11 changes: 11 additions & 0 deletions packages/spec-parser/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,11 @@ export interface ParseOptions {
*/
allowConversationStarters?: boolean;

/**
* If true, the parser will allow response semantics in plugin file. Only take effect in Copilot project
*/
allowResponseSemantics?: boolean;

/**
* The type of project that the parser is being used for.
* Project can be SME/Copilot/TeamsAi
Expand Down Expand Up @@ -299,3 +304,9 @@ export interface InvalidAPIInfo {
api: string;
reason: ErrorType[];
}

export interface InferredProperties {
title?: string;
subtitle?: string;
imageUrl?: string;
}
10 changes: 10 additions & 0 deletions packages/spec-parser/src/manifestUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {
FunctionParameters,
FunctionParameter,
} from "@microsoft/teams-manifest";
import { AdaptiveCardGenerator } from "./adaptiveCardGenerator";
import { wrapResponseSemantics } from "./adaptiveCardWrapper";

export class ManifestUpdater {
static async updateManifestWithAiPlugin(
Expand Down Expand Up @@ -183,6 +185,14 @@ export class ManifestUpdater {
parameters: parameters,
};

if (options.allowResponseSemantics) {
const [card, jsonPath] = AdaptiveCardGenerator.generateAdaptiveCard(operationItem);
const responseSemantic = wrapResponseSemantics(card, jsonPath);
funcObj.capabilities = {
response_semantics: responseSemantic,
};
}

functions.push(funcObj);
functionNames.push(operationId);
if (description) {
Expand Down
1 change: 1 addition & 0 deletions packages/spec-parser/src/specParser.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export class SpecParser {
allowOauth2: false,
allowMethods: ["get", "post"],
allowConversationStarters: false,
allowResponseSemantics: false,
projectType: ProjectType.SME,
};

Expand Down
1 change: 1 addition & 0 deletions packages/spec-parser/src/specParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export class SpecParser {
allowOauth2: false,
allowMethods: ["get", "post"],
allowConversationStarters: false,
allowResponseSemantics: false,
projectType: ProjectType.SME,
};

Expand Down
74 changes: 73 additions & 1 deletion packages/spec-parser/test/adaptiveCardWrapper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,87 @@
import { expect } from "chai";
import "mocha";
import sinon from "sinon";
import { inferPreviewCardTemplate, wrapAdaptiveCard } from "../src/adaptiveCardWrapper";
import {
inferPreviewCardTemplate,
wrapAdaptiveCard,
wrapResponseSemantics,
} from "../src/adaptiveCardWrapper";
import { AdaptiveCard } from "../src/interfaces";
import { ConstantString } from "../src/constants";
import exp from "constants";

describe("adaptiveCardWrapper", () => {
afterEach(() => {
sinon.restore();
});

describe("wrapResponseSemantics", () => {
it("should infer response semanitcs card template correctly", () => {
const card: AdaptiveCard = {
type: "AdaptiveCard",
version: "1.5",
body: [
{
type: "TextBlock",
text: "id: ${if(id, id, 'N/A')}",
wrap: true,
},
{
type: "TextBlock",
text: "petId: ${if(petId, petId, 'N/A')}",
wrap: true,
},
{
$when: "${imageUrl != null}",
type: "Image",
url: "${imageUrl}",
},
],
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
};

const result = wrapResponseSemantics(card, "$");

expect(result.data_path).to.equal("$");
expect(result.properties!.title).to.equal("$.petId");
expect(result.properties!.subtitle).to.equal("$.id");
expect(result.properties!.url).to.equal("$.imageUrl");
});

it("should infer response semanitcs card with json path correctly", () => {
const card: AdaptiveCard = {
type: "AdaptiveCard",
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
version: "1.5",
body: [
{
type: "Container",
$data: "${$root}",
items: [
{
type: "TextBlock",
text: "name: ${if(name, name, 'N/A')}",
wrap: true,
},
{
type: "TextBlock",
text: "age: ${if(age, age, 'N/A')}",
wrap: true,
},
],
},
],
};

const result = wrapResponseSemantics(card, "items");

expect(result.data_path).to.equal("$.items");
expect(result.properties!.title).to.equal("$.name");
expect(result.properties!.subtitle).to.equal("$.age");
expect(result.properties!.url).to.be.undefined;
});
});

describe("inferPreviewCardTemplate", () => {
it("should infer preview card template correctly", () => {
const card: AdaptiveCard = {
Expand Down
Loading

0 comments on commit 4e708f0

Please sign in to comment.