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

fix: Improve reliability of Orchestrator downloader #6672

Merged
merged 15 commits into from
Apr 6, 2021
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
/** @jsx jsx */
import { css, jsx } from '@emotion/core';
import { SharedColors } from '@uifabric/fluent-theme';
import { FontSizes } from '@uifabric/styling';
import formatMessage from 'format-message';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import { ProgressIndicator } from 'office-ui-fabric-react/lib/ProgressIndicator';
Expand All @@ -22,15 +23,30 @@ const cardDetail = css`
flex-grow: 1;
`;

const cardDescription = css`
text-size-adjust: none;
font-size: 12px;
line-height: 16px;
margin-right: 16px;
word-break: break-word;
`;

const infoType = css`
margin-top: 4px;
color: ${SharedColors.cyanBlue10};
`;

const errorIcon = css`
color: ${SharedColors.red10},
marginRight: 8,
paddingLeft: 12,
fontSize: ${FontSizes.mediumPlus},
`;

export const orchestratorDownloadNotificationProps = (): CardProps => {
return {
title: '',
description: formatMessage('Downloading Language Model'),
description: formatMessage('Orchestrator: Downloading language model'),
type: 'pending',
onRenderCardContent: (props) => (
<div css={cardContent}>
Expand All @@ -42,3 +58,19 @@ export const orchestratorDownloadNotificationProps = (): CardProps => {
),
};
};

export const orchestratorDownloadErrorProps = (err: string): CardProps => {
return {
title: '',
description: err,
type: 'error',
onRenderCardContent: (props) => (
<div css={cardContent}>
<Icon css={errorIcon} iconName="ErrorBadge" />
<div css={cardDetail}>
<div css={cardDescription}>{props.description}</div>
</div>
</div>
),
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -31,31 +31,33 @@ describe('Orchestrator model picking logic', () => {
{ id: 'test.en-sg.dialog', content: { $kind: SDKKinds.OrchestratorRecognizer } },
];
expect(availableLanguageModels(recognizerFiles)).toHaveLength(1);
expect(availableLanguageModels(recognizerFiles)).toEqual(['en']);
expect(availableLanguageModels(recognizerFiles)).toEqual([{ kind: 'en_intent', name: 'default' }]);
});

it('return en model under correct circumstances', () => {
const enModel = { kind: 'en_intent', name: 'default' };
expect(
availableLanguageModels([{ id: 'test.en-us.dialog', content: { $kind: SDKKinds.OrchestratorRecognizer } }])
).toEqual(['en']);
).toEqual([enModel]);
expect(
availableLanguageModels([{ id: 'test.EN-us.dialog', content: { $kind: SDKKinds.OrchestratorRecognizer } }])
).toEqual(['en']);
).toEqual([enModel]);
expect(
availableLanguageModels([{ id: 'test.en-US.dialog', content: { $kind: SDKKinds.OrchestratorRecognizer } }])
).toEqual(['en']);
).toEqual([enModel]);
expect(
availableLanguageModels([{ id: 'test.en.dialog', content: { $kind: SDKKinds.OrchestratorRecognizer } }])
).toEqual(['en']);
).toEqual([enModel]);
expect(
availableLanguageModels([{ id: 'test.en-anything.dialog', content: { $kind: SDKKinds.OrchestratorRecognizer } }])
).toEqual(['en']);
).toEqual([enModel]);
});

it('return multilang model under correct circumstances', () => {
const multilingualModel = { kind: 'multilingual_intent', name: 'default' };
expect(
availableLanguageModels([{ id: 'test.it.dialog', content: { $kind: SDKKinds.OrchestratorRecognizer } }])
).toEqual(['multilang']);
).toEqual([multilingualModel]);
expect(
availableLanguageModels([{ id: 'test.jp.dialog', content: { $kind: SDKKinds.OrchestratorRecognizer } }])
).toHaveLength(0);
Expand All @@ -65,19 +67,19 @@ describe('Orchestrator model picking logic', () => {

expect(
availableLanguageModels([{ id: 'test.zh-cn.dialog', content: { $kind: SDKKinds.OrchestratorRecognizer } }])
).toEqual(['multilang']);
).toEqual([multilingualModel]);
expect(
availableLanguageModels([{ id: 'test.zh-CN.dialog', content: { $kind: SDKKinds.OrchestratorRecognizer } }])
).toEqual(['multilang']);
).toEqual([multilingualModel]);
expect(
availableLanguageModels([{ id: 'test.rwk-tz.dialog', content: { $kind: SDKKinds.OrchestratorRecognizer } }])
).toEqual(['multilang']);
).toEqual([multilingualModel]);
expect(availableLanguageModels([{ id: 'test.pap', content: { $kind: SDKKinds.OrchestratorRecognizer } }])).toEqual([
'multilang',
multilingualModel,
]);
expect(
availableLanguageModels([{ id: 'test.tr-cy', content: { $kind: SDKKinds.OrchestratorRecognizer } }])
).toEqual(['multilang']);
).toEqual([multilingualModel]);
expect(
availableLanguageModels([{ id: 'test.nope', content: { $kind: SDKKinds.OrchestratorRecognizer } }])
).toHaveLength(0);
Expand All @@ -92,6 +94,9 @@ describe('Orchestrator model picking logic', () => {
{ id: 'test.zh.dialog', content: { $kind: SDKKinds.OrchestratorRecognizer } },
];
expect(availableLanguageModels(recognizerFiles)).toHaveLength(2);
expect(availableLanguageModels(recognizerFiles)).toEqual(['en', 'multilang']);
expect(availableLanguageModels(recognizerFiles)).toEqual([
{ kind: 'en_intent', name: 'default' },
{ kind: 'multilingual_intent', name: 'default' },
]);
});
});
120 changes: 70 additions & 50 deletions Composer/packages/client/src/recoilModel/dispatchers/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,69 @@
// Licensed under the MIT License.
/* eslint-disable react-hooks/rules-of-hooks */

import { RecognizerFile, SDKKinds } from '@bfc/shared';
import { DialogSetting, RecognizerFile, SDKKinds, OrchestratorModelRequest, DownloadState } from '@bfc/shared';
import { CallbackInterface, useRecoilCallback } from 'recoil';
import partition from 'lodash/partition';

import { orchestratorDownloadNotificationProps } from '../../components/Orchestrator/DownloadNotification';
import {
orchestratorDownloadErrorProps,
orchestratorDownloadNotificationProps,
} from '../../components/Orchestrator/DownloadNotification';
import httpClient from '../../utils/httpUtil';
import { dispatcherState } from '../../../src/recoilModel';
import { recognizersSelectorFamily } from '../selectors/recognizers';
import { Locales } from '../../locales';
import { settingsState } from '../atoms';

import { createNotification } from './notification';

export const downloadModel = (addr: string, model: 'en' | 'multilang', notificationStartCallback: () => void) => {
return new Promise<boolean>((resolve, reject) => {
httpClient.post(addr, { language: model }).then((resp) => {
if (resp.status === 201) {
resolve(true);
return;
}

if (resp.status !== 200) {
reject(false);
return;
}

notificationStartCallback();

const statusUri = resp.data;
import { createNotification, updateNotificationInternal } from './notification';

const pollUntilDone = (predicate: () => Promise<boolean>, pollingIntervalMs: number) =>
hatpick marked this conversation as resolved.
Show resolved Hide resolved
new Promise((resolve, reject) => {
try {
const timer = setInterval(async () => {
const resp = await httpClient.get(statusUri);

if (resp.status === 200 && resp.data < 2) {
if (await predicate()) {
clearInterval(timer);
resolve(true);
}
}, 1000);
});
}, pollingIntervalMs);
} catch (err) {
reject(err);
}
});

export const downloadModel = async (
addr: string,
modelRequest: OrchestratorModelRequest,
notificationStartCallback: () => void
) => {
const resp = await httpClient.post(addr, { modelData: modelRequest });

// Model has been downloaded before
if (resp.status === 201) {
return true;
}

notificationStartCallback();

return await pollUntilDone(async () => (await httpClient.get(resp.data)).data !== DownloadState.DOWNLOADING, 5000);
};

export const availableLanguageModels = (recognizerFiles: RecognizerFile[]) => {
export const availableLanguageModels = (recognizerFiles: RecognizerFile[], botSettings?: DialogSetting) => {
const dialogsUsingOrchestrator = recognizerFiles.filter(
({ id, content }) => content.$kind === SDKKinds.OrchestratorRecognizer
({ content }) => content.$kind === SDKKinds.OrchestratorRecognizer
);
const languageModels: ('en' | 'multilang')[] = [];
const languageModels: OrchestratorModelRequest[] = [];
if (dialogsUsingOrchestrator.length) {
// pull out languages that Orchestrator has to support
const [enLuFiles, multiLangLuFiles] = partition(dialogsUsingOrchestrator, (f) =>
f.id.split('.')?.[1]?.toLowerCase()?.startsWith('en')
);

if (enLuFiles.length) {
languageModels.push('en');
languageModels.push({
kind: 'en_intent',
name: botSettings?.orchestrator?.model?.en_intent ?? 'default',
});
}

if (
Expand All @@ -64,36 +73,47 @@ export const availableLanguageModels = (recognizerFiles: RecognizerFile[]) => {
.filter((id) => id !== undefined)
.some((lang) => Locales.map((l) => l.locale).includes(lang?.toLowerCase()))
) {
languageModels.push('multilang');
languageModels.push({
kind: 'multilingual_intent',
name: botSettings?.orchestrator?.model?.multilingual_intent ?? 'default',
});
}
}
return languageModels;
};

export const orchestratorDispatcher = () => {
const downloadLanguageModels = useRecoilCallback(({ snapshot }: CallbackInterface) => async (projectId: string) => {
const recognizers = await snapshot.getPromise(recognizersSelectorFamily(projectId));

const isUsingOrchestrator = recognizers.some(
({ id, content }) => content.$kind === SDKKinds.OrchestratorRecognizer
);

if (isUsingOrchestrator) {
// Download Model Notification
const { addNotification, deleteNotification } = await snapshot.getPromise(dispatcherState);
const notification = createNotification(orchestratorDownloadNotificationProps());

try {
for (const languageModel of availableLanguageModels(recognizers)) {
await downloadModel('/orchestrator/download', languageModel, () => {
addNotification(notification);
});
const downloadLanguageModels = useRecoilCallback(
(callbackHelpers: CallbackInterface) => async (projectId: string) => {
const { snapshot } = callbackHelpers;

const recognizers = await snapshot.getPromise(recognizersSelectorFamily(projectId));

const isUsingOrchestrator = recognizers.some(({ content }) => content.$kind === SDKKinds.OrchestratorRecognizer);

if (isUsingOrchestrator) {
// Download Model Notification
const { addNotification, deleteNotification } = await snapshot.getPromise(dispatcherState);
const downloadNotification = createNotification(orchestratorDownloadNotificationProps());
const botSettings = await snapshot.getPromise(settingsState(projectId));

try {
for (const languageModel of availableLanguageModels(recognizers, botSettings)) {
await downloadModel('/orchestrator/download', languageModel, () => {
taicchoumsft marked this conversation as resolved.
Show resolved Hide resolved
updateNotificationInternal(callbackHelpers, downloadNotification.id, downloadNotification);
});
}
} catch (err) {
const errorNotification = createNotification(
orchestratorDownloadErrorProps(err?.response?.data?.message || err.message)
);
addNotification(errorNotification);
} finally {
deleteNotification(downloadNotification.id);
}
} finally {
deleteNotification(notification.id);
}
}
});
);

return {
downloadLanguageModels,
Expand Down
Loading