botbuilder-dialogs-adaptive
Version:
Rule system for the Microsoft BotBuilder dialog system.
190 lines (165 loc) • 7.05 kB
text/typescript
/**
* @module botbuilder-dialogs-adaptive
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Culture } from '@microsoft/recognizers-text-suite';
import { Activity, Entity, RecognizerResult } from 'botbuilder';
import { Converter, ConverterFactory, DialogContext } from 'botbuilder-dialogs';
import { IntentPattern } from './intentPattern';
import { EntityRecognizer, TextEntity, EntityRecognizerSet } from './entityRecognizers';
import { RecognizerSetConfiguration } from './recognizerSet';
import { AdaptiveRecognizer } from './adaptiveRecognizer';
import { TelemetryLoggerConstants } from '../telemetryLoggerConstants';
type IntentPatternInput = {
intent: string;
pattern: string;
};
class IntentPatternsConverter implements Converter<IntentPatternInput[], IntentPattern[]> {
convert(items: IntentPatternInput[] | IntentPattern[]): IntentPattern[] {
const results: IntentPattern[] = [];
items.forEach((item) => {
results.push(item instanceof IntentPattern ? item : new IntentPattern(item.intent, item.pattern));
});
return results;
}
}
export interface RegexRecognizerConfiguration extends RecognizerSetConfiguration {
intents?: IntentPatternInput[] | IntentPattern[];
}
/**
* Recognizer implementation which uses regex expressions to identify intents.
*/
export class RegexRecognizer extends AdaptiveRecognizer implements RegexRecognizerConfiguration {
static $kind = 'Microsoft.RegexRecognizer';
/**
* Array of patterns -> intent names.
*/
intents: IntentPattern[] = [];
/**
* The entity recognizers.
*/
entities: EntityRecognizer[] = [];
/**
* @param property The key of the conditional selector configuration.
* @returns The converter for the selector configuration.
*/
getConverter(property: keyof RegexRecognizerConfiguration): Converter | ConverterFactory {
switch (property) {
case 'intents':
return new IntentPatternsConverter();
default:
return super.getConverter(property);
}
}
/**
* Runs current DialogContext.TurnContext.Activity through a recognizer and returns a [RecognizerResult](xref:botbuilder-core.RecognizerResult).
*
* @param dialogContext The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation.
* @param activity [Activity](xref:botframework-schema.Activity) to recognize.
* @param telemetryProperties Optional, additional properties to be logged to telemetry with the LuisResult event.
* @param telemetryMetrics Optional, additional metrics to be logged to telemetry with the LuisResult event.
* @returns {Promise<RecognizerResult>} Analysis of utterance.
*/
async recognize(
dialogContext: DialogContext,
activity: Activity,
telemetryProperties?: { [key: string]: string },
telemetryMetrics?: { [key: string]: number },
): Promise<RecognizerResult> {
const text = activity.text ?? '';
const locale = activity.locale ?? Culture.English;
const recognizerResult: RecognizerResult = {
text: text,
intents: {},
entities: {},
};
if (!text) {
// nothing to recognize, return empty result
return recognizerResult;
}
const textEntity: Entity = new TextEntity(text);
textEntity.start = 0;
textEntity.end = text.length;
textEntity.score = 1.0;
const entityPool: Entity[] = [textEntity];
for (const intentPattern of this.intents) {
const matches: RegExpExecArray[] = [];
let matched: RegExpExecArray;
const regexp = intentPattern.regex;
while ((matched = regexp.exec(text))) {
matches.push(matched);
if (regexp.lastIndex == text.length || !regexp.lastIndex) {
break; // to avoid infinite loop
}
}
if (matches.length === 0) {
continue;
}
// TODO length weighted match and multiple intents
const intentKey = intentPattern.intent.split(' ').join('_');
recognizerResult.intents[`${intentKey}`] ??= {
score: 1.0,
pattern: intentPattern.pattern,
};
matches.forEach((match: RegExpExecArray) => {
if (match.groups) {
Object.entries(match.groups)
.filter(([_name, text]) => text !== undefined)
.forEach(([name, text]) => {
const entity: Entity = {
type: name,
text,
start: match.index,
end: match.index + text.length,
};
entityPool.push(entity);
});
}
});
}
if (this.entities) {
const entitySet = new EntityRecognizerSet(...this.entities);
const newEntities = await entitySet.recognizeEntities(dialogContext, text, locale, entityPool);
entityPool.push(...newEntities);
}
entityPool
.filter((e) => e !== textEntity)
.forEach((entityResult) => {
const { type: entityType, text: entityText } = entityResult;
recognizerResult.entities[`${entityType}`] ??= [];
recognizerResult.entities[`${entityType}`].push(entityText);
recognizerResult.entities['$instance'] ??= {};
const instanceRoot = recognizerResult.entities['$instance'];
instanceRoot[`${entityType}`] ??= [];
const instanceData = instanceRoot[`${entityType}`];
const instance = {
startIndex: entityResult.start,
endIndex: entityResult.end,
score: 1.0,
text: entityText,
type: entityType,
resolution: entityResult.resolution,
};
instanceData.push(instance);
});
if (Object.entries(recognizerResult.intents).length == 0) {
recognizerResult.intents.None = { score: 1.0 };
}
await dialogContext.context.sendTraceActivity(
'RegexRecognizer',
recognizerResult,
'RecognizerResult',
'Regex RecognizerResult',
);
this.trackRecognizerResult(
dialogContext,
TelemetryLoggerConstants.RegexRecognizerResultEvent,
this.fillRecognizerResultTelemetryProperties(recognizerResult, telemetryProperties, dialogContext),
telemetryMetrics,
);
return recognizerResult;
}
}