Skip to content

Commit

Permalink
linter fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
Ben Brown committed Aug 24, 2018
1 parent 9fe8dcc commit 3adba9e
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 81 deletions.
125 changes: 63 additions & 62 deletions libraries/botbuilder-dialogs/src/choices/findValues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,18 @@
* @module botbuilder-dialogs
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import { Token, TokenizerFunction, defaultTokenizer } from './tokenizer';
import { ModelResult } from './modelResult';
import { defaultTokenizer, Token, TokenizerFunction } from './tokenizer';

/**
* Basic search options used to control how choices are recognized in a users utterance.
* Basic search options used to control how choices are recognized in a users utterance.
*/
export interface FindValuesOptions {
/**
* (Optional) if true, then only some of the tokens in a value need to exist to be considered
/**
* (Optional) if true, then only some of the tokens in a value need to exist to be considered
* a match. The default value is "false".
*/
allowPartialMatches?: boolean;
Expand All @@ -24,16 +23,16 @@ export interface FindValuesOptions {
*/
locale?: string;

/**
/**
* (Optional) maximum tokens allowed between two matched tokens in the utterance. So with
* a max distance of 2 the value "second last" would match the utterance "second from the last"
* but it wouldn't match "Wait a second. That's not the last one is it?".
* The default value is "2".
* but it wouldn't match "Wait a second. That's not the last one is it?".
* The default value is "2".
*/
maxTokenDistance?: number;

/**
* (Optional) tokenizer to use when parsing the utterance and values being recognized.
* (Optional) tokenizer to use when parsing the utterance and values being recognized.
*/
tokenizer?: TokenizerFunction;
}
Expand All @@ -42,74 +41,75 @@ export interface FindValuesOptions {
* INTERNAL: Raw search result returned by `findValues()`.
*/
export interface FoundValue {
/**
* The value that was matched.
/**
* The value that was matched.
*/
value: string;

/**
* The index of the value that was matched.
*/
index: number;
/**

/**
* The accuracy with which the value matched the specified portion of the utterance. A
* value of 1.0 would indicate a perfect match.
*/
score: number;
}

/**
* INTERNAL: A value that can be sorted and still refer to its original position within a source
/**
* INTERNAL: A value that can be sorted and still refer to its original position within a source
* array. The `findChoices()` function expands the passed in choices to individual `SortedValue`
* instances and passes them to `findValues()`. Each individual `Choice` can result in multiple
* synonyms that should be searched for so this data structure lets us pass each synonym as a value
* to search while maintaining the index of the choice that value came from.
* to search while maintaining the index of the choice that value came from.
*/
export interface SortedValue {
/** The value that will be sorted. */
// The value that will be sorted.
value: string;

/** The values original position within its unsorted array. */
// The values original position within its unsorted array.
index: number;
}

/**
* INTERNAL: Low-level function that searches for a set of values within an utterance. Higher level
* functions like `findChoices()` and `recognizeChoices()` are layered above this function. In most
* cases its easier to just call one of the higher level functions instead but this function contains
/**
* INTERNAL: Low-level function that searches for a set of values within an utterance. Higher level
* functions like `findChoices()` and `recognizeChoices()` are layered above this function. In most
* cases its easier to just call one of the higher level functions instead but this function contains
* the fuzzy search algorithm that drives choice recognition.
* @param utterance The text or user utterance to search over.
* @param values List of values to search over.
* @param options (Optional) options used to tweak the search that's performed.
* @param options (Optional) options used to tweak the search that's performed.
*/
export function findValues(utterance: string, values: SortedValue[], options?: FindValuesOptions): ModelResult<FoundValue>[] {
function indexOfToken(token: Token, startPos: number): number {
for (let i = startPos; i < tokens.length; i++) {
for (let i: number = startPos; i < tokens.length; i++) {
if (tokens[i].normalized === token.normalized) {
return i;
}
}

return -1;
}

function matchValue(index: number, value: string, vTokens: Token[], startPos: number): ModelResult<FoundValue>|undefined {
// Match value to utterance and calculate total deviation.
// - The tokens are matched in order so "second last" will match in
// - The tokens are matched in order so "second last" will match in
// "the second from last one" but not in "the last from the second one".
// - The total deviation is a count of the number of tokens skipped in the
// - The total deviation is a count of the number of tokens skipped in the
// match so for the example above the number of tokens matched would be
// 2 and the total deviation would be 1.
let matched = 0;
let totalDeviation = 0;
let start = -1;
let end = -1;
vTokens.forEach((token) => {
// 2 and the total deviation would be 1.
let matched: number = 0;
let totalDeviation: number = 0;
let start: number = -1;
let end: number = -1;
vTokens.forEach((token: Token) => {
// Find the position of the token in the utterance.
const pos = indexOfToken(token, startPos);
const pos: number = indexOfToken(token, startPos);
if (pos >= 0) {
// Calculate the distance between the current tokens position and the previous tokens distance.
const distance = matched > 0 ? pos - startPos : 0;
const distance: number = matched > 0 ? pos - startPos : 0;
if (distance <= maxDistance) {
// Update count of tokens matched and move start pointer to search for next token after
// the current token.
Expand All @@ -118,7 +118,7 @@ export function findValues(utterance: string, values: SortedValue[], options?: F
startPos = pos + 1;

// Update start & end position that will track the span of the utterance that's matched.
if (start < 0) { start = pos }
if (start < 0) { start = pos; }
end = pos;
}
}
Expand All @@ -127,20 +127,20 @@ export function findValues(utterance: string, values: SortedValue[], options?: F
// Calculate score and format result
// - The start & end positions and the results text field will be corrected by the caller.
let result: ModelResult<FoundValue>|undefined;
if (matched > 0 && (matched == vTokens.length || opt.allowPartialMatches)) {
// Percentage of tokens matched. If matching "second last" in
if (matched > 0 && (matched === vTokens.length || opt.allowPartialMatches)) {
// Percentage of tokens matched. If matching "second last" in
// "the second from last one" the completeness would be 1.0 since
// all tokens were found.
const completeness = matched / vTokens.length;
const completeness: number = matched / vTokens.length;

// Accuracy of the match. The accuracy is reduced by additional tokens
// occurring in the value that weren't in the utterance. So an utterance
// of "second last" matched against a value of "second from last" would
// result in an accuracy of 0.5.
const accuracy = (matched / (matched + totalDeviation))
// result in an accuracy of 0.5.
const accuracy: number = (matched / (matched + totalDeviation));

// The final score is simply the completeness multiplied by the accuracy.
const score = completeness * accuracy;
const score: number = completeness * accuracy;

// Format result
result = {
Expand All @@ -151,30 +151,31 @@ export function findValues(utterance: string, values: SortedValue[], options?: F
value: value,
index: index,
score: score
}
}
} as ModelResult<FoundValue>;
}

return result;
}

// Sort values in descending order by length so that the longest value is searched over first.
const list = values.sort((a, b) => b.value.length - a.value.length);
const list: SortedValue[] = values.sort((a: SortedValue, b: SortedValue) => b.value.length - a.value.length);

// Search for each value within the utterance.
let matches: ModelResult<FoundValue>[] = [];
const opt = options || {};
const tokenizer = (opt.tokenizer || defaultTokenizer);
const tokens = tokenizer(utterance, opt.locale);
const maxDistance = opt.maxTokenDistance !== undefined ? opt.maxTokenDistance : 2;
list.forEach((entry, index) => {
const opt: FindValuesOptions = options || {};
const tokenizer: TokenizerFunction = (opt.tokenizer || defaultTokenizer);
const tokens: Token[] = tokenizer(utterance, opt.locale);
const maxDistance: number = opt.maxTokenDistance !== undefined ? opt.maxTokenDistance : 2;
list.forEach((entry: SortedValue) => {
// Find all matches for a value
// - To match "last one" in "the last time I chose the last one" we need
// - To match "last one" in "the last time I chose the last one" we need
// to re-search the string starting from the end of the previous match.
// - The start & end position returned for the match are token positions.
let startPos = 0;
const vTokens = tokenizer(entry.value.trim(), opt.locale);
let startPos: number = 0;
const vTokens: Token[] = tokenizer(entry.value.trim(), opt.locale);
while (startPos < tokens.length) {
const match = matchValue(entry.index, entry.value, vTokens, startPos);
const match: ModelResult<FoundValue> = matchValue(entry.index, entry.value, vTokens, startPos);
if (match) {
startPos = match.end + 1;
matches.push(match);
Expand All @@ -185,19 +186,19 @@ export function findValues(utterance: string, values: SortedValue[], options?: F
});

// Sort matches by score descending
matches = matches.sort((a,b) => b.resolution.score - a.resolution.score);
matches = matches.sort((a: ModelResult<FoundValue>, b: ModelResult<FoundValue>) => b.resolution.score - a.resolution.score);

// Filter out duplicate matching indexes and overlapping characters.
// - The start & end positions are token positions and need to be translated to
// - The start & end positions are token positions and need to be translated to
// character positions before returning. We also need to populate the "text"
// field as well.
// field as well.
const results: ModelResult<FoundValue>[] = [];
const foundIndexes: { [index: number]: boolean } = {};
const usedTokens: { [index: number]: boolean } = {};
matches.forEach((match) => {
matches.forEach((match: ModelResult<FoundValue>) => {
// Apply filters
let add = !foundIndexes.hasOwnProperty(match.resolution.index);
for (let i = match.start; i <= match.end; i++) {
let add: boolean = !foundIndexes.hasOwnProperty(match.resolution.index);
for (let i: number = match.start; i <= match.end; i++) {
if (usedTokens[i]) {
add = false;
break;
Expand All @@ -208,7 +209,7 @@ export function findValues(utterance: string, values: SortedValue[], options?: F
if (add) {
// Update filter info
foundIndexes[match.resolution.index] = true;
for (let i = match.start; i <= match.end; i++) { usedTokens[i] = true }
for (let i: number = match.start; i <= match.end; i++) { usedTokens[i] = true; }

// Translate start & end and populate text field
match.start = tokens[match.start].start;
Expand All @@ -219,5 +220,5 @@ export function findValues(utterance: string, values: SortedValue[], options?: F
});

// Return the results sorted by position in the utterance
return results.sort((a,b) => a.start - b.start);
return results.sort((a: ModelResult<FoundValue>, b: ModelResult<FoundValue>) => a.start - b.start);
}
2 changes: 1 addition & 1 deletion libraries/botbuilder-dialogs/src/choices/modelResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ export interface ModelResult<T extends Object = {}> {

// The recognized entity.
resolution: T;
}
}
42 changes: 24 additions & 18 deletions libraries/botbuilder-dialogs/src/prompts/datetimePrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,50 +5,51 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { TurnContext, InputHints } from 'botbuilder-core';
import { Prompt, PromptOptions, PromptValidator, PromptRecognizerResult } from './prompt';
import * as Recognizers from '@microsoft/recognizers-text-date-time';
import { Activity, InputHints, TurnContext } from 'botbuilder-core';
import { Prompt, PromptOptions, PromptRecognizerResult, PromptValidator } from './prompt';

export interface DateTimeResolution {
/**
* TIMEX expression representing ambiguity of the recognized time.
/**
* TIMEX expression representing ambiguity of the recognized time.
*/
timex: string;

/**
* Type of time recognized. Possible values are 'date', 'time', 'datetime', 'daterange',
/**
* Type of time recognized. Possible values are 'date', 'time', 'datetime', 'daterange',
* 'timerange', 'datetimerange', 'duration', or 'set'.
*/
type: string;

/**
/**
* Value of the specified [type](#type) that's a reasonable approximation given the ambiguity
* of the [timex](#timex).
*/
value: string;
}

/**
* Prompts a user to enter a datetime expression.
*
* Prompts a user to enter a datetime expression.
*
* @remarks
* By default the prompt will return to the calling dialog a `FoundDatetime[]` but this can be
* By default the prompt will return to the calling dialog a `FoundDatetime[]` but this can be
* overridden using a custom `PromptValidator`.
*/
export class DateTimePrompt extends Prompt<DateTimeResolution[]> {

public defaultLocale: string|undefined;

/**
* Creates a new `DatetimePrompt` instance.
* @param dialogId Unique ID of the dialog within its parent `DialogSet`.
* @param validator (Optional) validator that will be called each time the user responds to the prompt. If the validator replies with a message no additional retry prompt will be sent.
* @param validator (Optional) validator that will be called each time the user responds to the prompt. If the validator replies with a message no additional retry prompt will be sent.
* @param defaultLocale (Optional) locale to use if `dc.context.activity.locale` not specified. Defaults to a value of `en-us`.
*/
constructor(dialogId: string, validator?: PromptValidator<DateTimeResolution[]>, defaultLocale?: string) {
super(dialogId, validator);
this.defaultLocale = defaultLocale;
}

public defaultLocale: string|undefined;

protected async onPrompt(context: TurnContext, state: any, options: PromptOptions, isRetry: boolean): Promise<void> {
if (isRetry && options.retryPrompt) {
await context.sendActivity(options.retryPrompt, undefined, InputHints.ExpectingInput);
Expand All @@ -57,16 +58,21 @@ export class DateTimePrompt extends Prompt<DateTimeResolution[]> {
}
}

protected async onRecognize(context: TurnContext, state: any, options: PromptOptions): Promise<PromptRecognizerResult<DateTimeResolution[]>> {
protected async onRecognize(
context: TurnContext,
state: any,
options: PromptOptions
): Promise<PromptRecognizerResult<DateTimeResolution[]>> {
const result: PromptRecognizerResult<DateTimeResolution[]> = { succeeded: false };
const activity = context.activity;
const utterance = activity.text;
const locale = activity.locale || this.defaultLocale || 'en-us';
const results = Recognizers.recognizeDateTime(utterance, locale);
const activity: Activity = context.activity;
const utterance: string = activity.text;
const locale: string = activity.locale || this.defaultLocale || 'en-us';
const results: any[] = Recognizers.recognizeDateTime(utterance, locale);
if (results.length > 0 && results[0].resolution) {
result.succeeded = true;
result.value = results[0].resolution.values;
}

return result;
}
}

0 comments on commit 3adba9e

Please sign in to comment.