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

Enable Orchestrator entity recognition when entity model is provided #3441

Merged
merged 24 commits into from
Mar 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d4b2446
Removed orchestratorRecognizer
tsuwandy Mar 23, 2021
57ade54
check entity model folder and load orchestrator with entity model
tsuwandy Mar 23, 2021
67110fc
Add telemetry updates
daveta Mar 24, 2021
665fc64
Merge branch 'tiens-orchestrator-recognizer' of https://github.com/mi…
daveta Mar 24, 2021
16d9700
Merge branch 'main' into tiens-orchestrator-recognizer
tsuwandy Mar 25, 2021
413aa89
Update orchestratorAdaptiveRecognizer.ts
tsuwandy Mar 25, 2021
0564524
added entity test
tsuwandy Mar 25, 2021
26cb176
Updates for CR feedback
daveta Mar 25, 2021
44fef57
addressed review comments, fixed some issues from yarn lint
tsuwandy Mar 25, 2021
e2cbae9
Merge branch 'tiens-orchestrator-recognizer' of https://github.com/mi…
tsuwandy Mar 25, 2021
a44aee7
fixed tests
tsuwandy Mar 25, 2021
038dfe3
yarn lint fixes
tsuwandy Mar 25, 2021
bd50f33
addressed more PR comments
tsuwandy Mar 25, 2021
c1f1297
Updates
daveta Mar 25, 2021
1c74d5a
Remove lodash/omit
daveta Mar 25, 2021
17382f7
Lint issues
daveta Mar 25, 2021
c4f1516
Add comment for omit
daveta Mar 25, 2021
5567d31
Fix test:orchestator command
daveta Mar 25, 2021
925732d
Update package to latest R13. We'll update to rc and full R13 build …
daveta Mar 25, 2021
ec65bd1
Merge branch 'main' into tiens-orchestrator-recognizer
tsuwandy Mar 25, 2021
7b2da8e
updated per PR comments, added tests
tsuwandy Mar 25, 2021
395066c
Merge branch 'main' into tiens-orchestrator-recognizer
tsuwandy Mar 26, 2021
2a78304
Merge branch 'main' into tiens-orchestrator-recognizer
joshgummersall Mar 26, 2021
678f192
updated schema
tsuwandy Mar 26, 2021
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 libraries/botbuilder-ai-orchestrator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"botbuilder-dialogs": "4.1.6",
"botbuilder-dialogs-adaptive": "4.1.6",
"botbuilder-dialogs-declarative": "4.1.6",
"orchestrator-core": "4.12.0-preview",
"orchestrator-core": "next",
"uuid": "^8.3.2"
},
"scripts": {
Expand Down
3 changes: 1 addition & 2 deletions libraries/botbuilder-ai-orchestrator/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,5 @@
* Licensed under the MIT License.
*/

export { OrchestratorAdaptiveRecognizer } from './orchestratorAdaptiveRecognizer';
export { OrchestratorAdaptiveRecognizer, LabelType } from './orchestratorAdaptiveRecognizer';
export { OrchestratorComponentRegistration } from './orchestratorComponentRegistration';
export { OrchestratorRecognizer } from './orchestratorRecognizer';
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
StringExpression,
StringExpressionConverter,
} from 'adaptive-expressions';

import { AdaptiveRecognizer } from 'botbuilder-dialogs-adaptive';
import { Activity, RecognizerResult } from 'botbuilder-core';
import { Converter, ConverterFactory, DialogContext, Recognizer, RecognizerConfiguration } from 'botbuilder-dialogs';
Expand All @@ -34,29 +35,40 @@ export interface OrchestratorAdaptiveRecognizerConfiguration extends RecognizerC
externalEntityRecognizer?: Recognizer;
}

type LabelResolver = {
score(
text: string
): {
score: number;
closest_text: string;
label: {
name: string;
};
}[];
};
export enum LabelType {
Intent = 1,
Entity = 2,
}

type Orchestrator = {
interface Label {
name: string;
span: {
offset: number;
length: number;
};
}

interface Result {
score: number;
closest_text: string;
label: Label;
}

interface LabelResolver {
score(text: string, labelType?: number): Result[];
}

interface Orchestrator {
createLabelResolver(snapshot: Uint8Array): LabelResolver;
};
}

/**
* Class that represents an adaptive Orchestrator recognizer.
*/
export class OrchestratorAdaptiveRecognizer
extends AdaptiveRecognizer
implements OrchestratorAdaptiveRecognizerConfiguration {
public static $kind = 'Microsoft.OrchestratorRecognizer';
public static $kind = 'Microsoft.OrchestratorAdaptiveRecognizer';

/**
* Path to Orchestrator base model folder.
Expand Down Expand Up @@ -84,15 +96,25 @@ export class OrchestratorAdaptiveRecognizer
*/
public externalEntityRecognizer: Recognizer;

/**
* Enable entity detection if entity model exists inside modelFolder. Defaults to false.
*/
public scoreEntities = false;

/**
* Intent name if ambiguous intents are detected.
*/
public readonly chooseIntent: string = 'ChooseIntent';
public readonly chooseIntent = 'ChooseIntent';

/**
* Full intent recognition results are available under this property
*/
public readonly resultProperty = 'result';

/**
* Full recognition results are available under this property
* Full entity recognition results are available under this property
*/
public readonly resultProperty: string = 'result';
public readonly entityProperty = 'entityResult';

public getConverter(property: keyof OrchestratorAdaptiveRecognizerConfiguration): Converter | ConverterFactory {
switch (property) {
Expand Down Expand Up @@ -192,17 +214,17 @@ export class OrchestratorAdaptiveRecognizer
recognizerResult[this.resultProperty] = results;

if (results.length) {
const topScoringIntent = results[0].label.name;
const topScore = results[0].score;

// if top scoring intent is less than threshold, return None
if (topScore < this.unknownIntentFilterScore) {
recognizerResult.intents.None = { score: 1.0 };
} else {
// add top score
recognizerResult.intents[`${topScoringIntent}`] ??= {
score: topScore,
};
// add all scores
recognizerResult.intents = results.reduce(function (intents, result) {
intents[result.label.name] = { score: result.score };
return intents;
}, {});

// disambiguate
if (detectAmbiguity) {
Expand Down Expand Up @@ -237,6 +259,8 @@ export class OrchestratorAdaptiveRecognizer
recognizerResult.intents.None = { score: 1.0 };
}

await this.tryScoreEntities(text, recognizerResult);

await dc.context.sendTraceActivity(
'OrchestratorAdaptiveRecognizer',
recognizerResult,
Expand All @@ -253,6 +277,68 @@ export class OrchestratorAdaptiveRecognizer
return recognizerResult;
}

private getTopTwoIntents(result: RecognizerResult): { name: string; score: number }[] {
if (!result || !result.intents) {
throw new Error('result is empty');
}
const intents = Object.entries(result.intents)
.map((intent) => {
return { name: intent[0], score: +intent[1].score };
})
.sort((a, b) => b.score - a.score);
intents.length = 2;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we mutating array length directly? Should perhaps use .slice(0, 2) instead?


return intents;
}

/**
* Uses the RecognizerResult to create a list of properties to be included when tracking the result in telemetry.
*
* @param {RecognizerResult} recognizerResult Recognizer Result.
* @param {Record<string, string>} telemetryProperties A list of properties to append or override the properties created using the RecognizerResult.
* @param {DialogContext} dialogContext Dialog Context.
* @returns {Record<string, string>} A collection of properties that can be included when calling the TrackEvent method on the TelemetryClient.
*/
protected fillRecognizerResultTelemetryProperties(
recognizerResult: RecognizerResult,
telemetryProperties: Record<string, string>,
dialogContext?: DialogContext
): Record<string, string> {
const topTwo = this.getTopTwoIntents(recognizerResult);
Copy link
Contributor

Choose a reason for hiding this comment

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

const [topOne, topTwo] = this.getTopTwoIntents(...) ?? []; extracts those array elements directly, then you can just do topOne.name, topOne.score, etc.

const intent = Object.entries(recognizerResult.intents);
// customRecognizerProps = recognizerResult with following properties omitted:
// text, alteredText, intents, entities
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { text, alteredText, intents, entities, ...customRecognizerProps } = recognizerResult;
const properties: Record<string, string> = {
TopIntent: intent.length > 0 ? topTwo[0].name : undefined,
TopIntentScore: intent.length > 0 ? topTwo[0].score.toString() : undefined,
NextIntent: intent.length > 1 ? topTwo[1].name : undefined,
NextIntentScore: intent.length > 1 ? topTwo[1].score.toString() : undefined,
Intents: intent.length > 0 ? JSON.stringify(recognizerResult.intents) : undefined,
Entities: recognizerResult.entities ? JSON.stringify(recognizerResult.entities) : undefined,
AdditionalProperties: JSON.stringify(customRecognizerProps),
};

let logPersonalInformation =
this.logPersonalInformation instanceof BoolExpression
? this.logPersonalInformation.getValue(dialogContext.state)
: this.logPersonalInformation;
if (logPersonalInformation == undefined) logPersonalInformation = false;

if (logPersonalInformation) {
properties['Text'] = recognizerResult.text;
properties['AlteredText'] = recognizerResult.alteredText;
}

// Additional Properties can override "stock" properties.
if (telemetryProperties) {
return Object.assign({}, properties, telemetryProperties);
}

return properties;
}

private _initializeModel() {
if (!this._modelFolder) {
throw new Error(`Missing "ModelFolder" information.`);
Expand All @@ -268,8 +354,14 @@ export class OrchestratorAdaptiveRecognizer
if (!existsSync(fullModelFolder)) {
throw new Error(`Model folder does not exist at ${fullModelFolder}.`);
}

const entityModelFolder = resolve(this._modelFolder, 'entity');
this.scoreEntities = existsSync(entityModelFolder);

const orchestrator = new oc.Orchestrator();
if (!orchestrator.load(fullModelFolder)) {
if (this.scoreEntities && !orchestrator.load(fullModelFolder, entityModelFolder)) {
throw new Error(`Model load failed.`);
} else if (!orchestrator.load(fullModelFolder)) {
throw new Error(`Model load failed.`);
}
OrchestratorAdaptiveRecognizer.orchestrator = orchestrator;
Expand All @@ -287,4 +379,54 @@ export class OrchestratorAdaptiveRecognizer
this._resolver = OrchestratorAdaptiveRecognizer.orchestrator.createLabelResolver(snapshot);
}
}

private async tryScoreEntities(text: string, recognizerResult: RecognizerResult) {
if (!this.scoreEntities) {
return;
}

const results = await this._resolver.score(text, LabelType.Entity);
if (!results) {
throw new Error(`Failed scoring entities for: ${text}`);
}

// Add full entity recognition result as a 'entityResult' property
recognizerResult[this.entityProperty] = results;
if (results.length) {
recognizerResult.entities ??= {};

results.forEach((result: Result) => {
const entityType = result.label.name;

// add value
const values = recognizerResult.entities[entityType] ?? [];
recognizerResult.entities[entityType] = values;

const span = result.label.span;
const entityText = text.substr(span.offset, span.length);
values.push({
type: entityType,
score: result.score,
text: entityText,
start: span.offset,
end: span.offset + span.length,
});

// get/create $instance
recognizerResult.entities['$instance'] ??= {};
const instanceRoot = recognizerResult.entities['$instance'];

// add instanceData
instanceRoot[entityType] ??= [];
const instanceData = instanceRoot[entityType];
instanceData.push({
startIndex: span.offset,
endIndex: span.offset + span.length,
score: result.score,
text: entityText,
type: entityType,
});
});
}
}
}

This file was deleted.

15 changes: 12 additions & 3 deletions libraries/botbuilder-ai-orchestrator/tests/mockResolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,22 @@

/* eslint-disable @typescript-eslint/explicit-module-boundary-types */

const { LabelType } = require('../lib');
class MockResolver {
constructor(score) {
constructor(score, entityScore) {
this._score = score;
this._entityScore = entityScore;
}

score(_text) {
return this._score;
score(_text, labelType = LabelType.Intent) {
switch (labelType) {
case LabelType.Intent:
return this._score;
case LabelType.Entity:
return this._entityScore;
default:
throw new Error('Label type not supported!');
}
}
}

Expand Down
Loading