Skip to content

Commit

Permalink
Merge pull request #38664 from code-dot-org/model-manager
Browse files Browse the repository at this point in the history
[AiLab] Model Manager Dialog and autogenerated design elements and code
  • Loading branch information
Erin007 committed Feb 15, 2021
2 parents 2b5bbdf + 66b4898 commit 448cfe4
Show file tree
Hide file tree
Showing 14 changed files with 254 additions and 13 deletions.
10 changes: 6 additions & 4 deletions apps/src/MLTrainers.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const KNN = require('ml-knn');
import KNN from 'ml-knn';

const KNNTrainers = ['knnClassify', 'knnRegress'];

Expand Down Expand Up @@ -31,18 +31,20 @@ modelData = {
}
value in testData is the converted algorithm-ready number, not the string
TODO: convert string data in testValues using the featureNumberKey
*/
export function predict(modelData) {
// Determine which algorithm to use.
if (KNNTrainers.includes(modelData.selectedTrainer)) {
// Re-instantiate the trained model.
const model = KNN.load(modelData.trainedModel);
// Prepare test data.
const testValues = modelData.selectedFeatures.map(
feature => modelData.testData[feature]
const testValues = modelData.selectedFeatures.map(feature =>
parseInt(modelData.testData[feature])
);
// Make a prediction.
const rawPrediction = model.predict(testValues)[0];
const rawPrediction = model.predict(testValues);
// Convert prediction to human readable (if needed)
const prediction = Object.keys(modelData.featureNumberKey).includes(
modelData.labelColumn
Expand Down
2 changes: 2 additions & 0 deletions apps/src/applab/AppLabView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import ExternalRedirectDialog from '@cdo/apps/applab/ExternalRedirectDialog';
class AppLabView extends React.Component {
static propTypes = {
handleVersionHistory: PropTypes.func.isRequired,
autogenerateML: PropTypes.func.isRequired,
hasDataMode: PropTypes.bool.isRequired,
hasDesignMode: PropTypes.bool.isRequired,
interfaceMode: PropTypes.oneOf([
Expand Down Expand Up @@ -57,6 +58,7 @@ class AppLabView extends React.Component {
<CodeWorkspace
withSettingsCog
style={{display: codeWorkspaceVisible ? 'block' : 'none'}}
autogenerateML={this.props.autogenerateML}
/>
{this.props.hasDesignMode && <ProtectedDesignWorkspace />}
{this.props.hasDataMode && (
Expand Down
74 changes: 74 additions & 0 deletions apps/src/applab/ai.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import $ from 'jquery';
import designMode from './designMode';

function generateCodeDesignElements(modelId, modelData) {
var x = 20;
var y = 20;
var SPACER_PIXELS = 20;
designMode.onInsertEvent(`var testValues = {};`);
modelData.selectedFeatures.forEach(feature => {
y = y + SPACER_PIXELS;
var label = designMode.createElement('LABEL', x, y);
label.textContent = feature + ':';
label.id = 'design_' + feature + '_label';
label.style.width = '300px';
y = y + SPACER_PIXELS;
if (Object.keys(modelData.featureNumberKey).includes(feature)) {
var selectId = feature + '_dropdown';
var select = designMode.createElement('DROPDOWN', x, y);
select.id = 'design_' + selectId;
// App Lab automatically addss "option 1" and "option 2", remove them.
select.options.remove(0);
select.options.remove(0);
Object.keys(modelData.featureNumberKey[feature]).forEach(option => {
var optionElement = document.createElement('option');
optionElement.text = option;
select.options.add(optionElement);
});
} else {
var input = designMode.createElement('TEXT_INPUT');
input.id = 'design_' + feature + '_input';
}
var addFeature = `testValues.${feature} = getText("${selectId}");`;
designMode.onInsertEvent(addFeature);
});
y = y + 2 * SPACER_PIXELS;
var label = designMode.createElement('LABEL', x, y);
label.textContent = modelData.labelColumn;
// TODO: this could be problematic if the name isn't formatted appropriately
label.id = 'design_' + modelData.name + '_label';
label.style.width = '300px';
y = y + SPACER_PIXELS;
var predictionId = modelData.name + '_prediction';
var prediction = designMode.createElement('TEXT_INPUT', x, y);
prediction.id = 'design_' + predictionId;
y = y + 2 * SPACER_PIXELS;
var predictButton = designMode.createElement('BUTTON', x, y);
predictButton.textContent = 'Predict';
var predictButtonId = modelData.name + '_predict';
designMode.updateProperty(predictButton, 'id', predictButtonId);
var predictOnClick = `onEvent("${predictButtonId}", "click", function() {
getPrediction("${
modelData.name
}", "${modelId}", testValues, function(value) {
setText("${predictionId}", value);
});
});`;
designMode.onInsertEvent(predictOnClick);
}

export default function autogenerateML(modelId) {
return new Promise(function(resolve, reject) {
$.ajax({
url: `/api/v1/ml_models/${modelId}`,
method: 'GET'
})
.then(modelData => {
generateCodeDesignElements(modelId, modelData);
return resolve();
})
.fail((jqXhr, status) => {
return alert({message: 'An error occurred'});
});
});
}
9 changes: 9 additions & 0 deletions apps/src/applab/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -534,3 +534,12 @@ export function drawChartFromRecords(
callback: callback
});
}

export function getPrediction(modelName, modelId, testValues, callback) {
return Applab.executeCmd(null, 'getPrediction', {
modelName,
modelId,
testValues,
callback
});
}
4 changes: 3 additions & 1 deletion apps/src/applab/applab.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ import {setExportGeneratedProperties} from '../code-studio/components/exportDial
import {userAlreadyReportedAbuse} from '@cdo/apps/reportAbuse';
import {workspace_running_background, white} from '@cdo/apps/util/color';
import {MB_API} from '../lib/kits/maker/boards/microBit/MicroBitConstants';
import autogenerateML from '@cdo/apps/applab/ai';

/**
* Create a namespace for the application.
Expand Down Expand Up @@ -973,7 +974,8 @@ Applab.render = function() {
isEditingProject: project.isEditing(),
screenIds: designMode.getAllScreenIds(),
onScreenCreate: designMode.createScreen,
handleVersionHistory: Applab.handleVersionHistory
handleVersionHistory: Applab.handleVersionHistory,
autogenerateML: autogenerateML
});
ReactDOM.render(
<Provider store={getStore()}>
Expand Down
2 changes: 2 additions & 0 deletions apps/src/applab/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from '../lib/util/javascriptMode';
import {commands as audioCommands} from '@cdo/apps/lib/util/audioApi';
import {commands as timeoutCommands} from '@cdo/apps/lib/util/timeoutApi';
import {commands as mlCommands} from '@cdo/apps/lib/util/mlApi';
import * as makerCommands from '@cdo/apps/lib/kits/maker/commands';
import {getAppOptions} from '@cdo/apps/code-studio/initApp/loadApp';
import {AllowedWebRequestHeaders} from '@cdo/apps/util/sharedConstants';
Expand Down Expand Up @@ -2291,4 +2292,5 @@ function stopLoadingSpinnerFor(elementId) {
// Include playSound, stopSound, etc.
Object.assign(applabCommands, audioCommands);
Object.assign(applabCommands, timeoutCommands);
Object.assign(applabCommands, mlCommands);
Object.assign(applabCommands, makerCommands);
11 changes: 11 additions & 0 deletions apps/src/applab/dropletConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from './setPropertyDropdown';
import {getStore} from '../redux';
import * as applabConstants from './constants';
import experiments from '../util/experiments';

var DEFAULT_WIDTH = applabConstants.APP_WIDTH.toString();
var DEFAULT_HEIGHT = (
Expand Down Expand Up @@ -1131,6 +1132,16 @@ export var blocks = [
}
];

if (experiments.isEnabled(experiments.APPLAB_ML)) {
blocks.push({
func: 'getPrediction',
parent: api,
category: 'Data',
paletteParams: ['model_name', 'model_id', 'testValues', 'callback'],
params: ['"myModel"', '"modelId"', 'testValues', 'function (value) {\n \n}']
});
}

export const categories = {
'UI controls': {
id: 'uicontrols',
Expand Down
77 changes: 77 additions & 0 deletions apps/src/code-studio/components/ModelManagerDialog.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import PropTypes from 'prop-types';
import React from 'react';
import $ from 'jquery';
import BaseDialog from '@cdo/apps/templates/BaseDialog';
import Button from '@cdo/apps/templates/Button';

export default class ModelManagerDialog extends React.Component {
static propTypes = {
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
autogenerateML: PropTypes.func
};

state = {
models: [],
isImportPending: false
};

componentDidMount() {
this.getModelList();
}

closeModelManager = () => {
this.props.onClose();
};

getModelList = () => {
$.ajax({
url: '/api/v1/ml_models/names',
method: 'GET'
}).then(models => {
this.setState({models});
});
};

importMLModel = async () => {
this.setState({isImportPending: true});
const modelId = this.root.value;
await this.props.autogenerateML(modelId);
this.setState({isImportPending: false});
this.closeModelManager();
};

render() {
const {isOpen} = this.props;
const noModels = this.state.models.length === 0;

return (
<div>
<BaseDialog
isOpen={isOpen}
handleClose={this.closeModelManager}
useUpdatedStyles
>
<h2>Machine Learning Models</h2>
<select name="model" ref={element => (this.root = element)}>
{this.state.models.map(model => (
<option key={model.id} value={model.id}>
{model.name}
</option>
))}
</select>
{noModels && <div>You have not trained any AI models yet.</div>}
<Button
text={'Import'}
color={Button.ButtonColor.orange}
onClick={this.importMLModel}
disabled={noModels}
isPending={this.state.isImportPending}
pendingText={'Importing...'}
/>
<h3>Model card details will go here.</h3>
</BaseDialog>
</div>
);
}
}
28 changes: 26 additions & 2 deletions apps/src/lib/ui/SettingsCog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import PopUpMenu from './PopUpMenu';
import ConfirmEnableMakerDialog from './ConfirmEnableMakerDialog';
import LibraryManagerDialog from '@cdo/apps/code-studio/components/libraries/LibraryManagerDialog';
import {getStore} from '../../redux';
import experiments from '@cdo/apps/util/experiments';
import ModelManagerDialog from '@cdo/apps/code-studio/components/ModelManagerDialog';

const style = {
iconContainer: {
Expand Down Expand Up @@ -43,7 +45,8 @@ class SettingsCog extends Component {
static propTypes = {
isRunning: PropTypes.bool,
runModeIndicators: PropTypes.bool,
showMakerToggle: PropTypes.bool
showMakerToggle: PropTypes.bool,
autogenerateML: PropTypes.func
};

// This ugly two-flag state is a workaround for an event-handling bug in
Expand All @@ -55,7 +58,8 @@ class SettingsCog extends Component {
open: false,
canOpen: true,
confirmingEnableMaker: false,
managingLibraries: false
managingLibraries: false,
managingModels: false
};

open = () => this.setState({open: true, canOpen: false});
Expand All @@ -77,6 +81,11 @@ class SettingsCog extends Component {
this.setState({managingLibraries: true});
};

manageModels = () => {
this.close();
this.setState({managingModels: true});
};

toggleMakerToolkit = () => {
this.close();
if (!makerToolkitRedux.isEnabled(getStore().getState())) {
Expand All @@ -97,6 +106,7 @@ class SettingsCog extends Component {
showConfirmation = () => this.setState({confirmingEnableMaker: true});
hideConfirmation = () => this.setState({confirmingEnableMaker: false});
closeLibraryManager = () => this.setState({managingLibraries: false});
closeModelManager = () => this.setState({managingModels: false});

setTargetPoint(icon) {
if (!icon) {
Expand Down Expand Up @@ -145,10 +155,20 @@ class SettingsCog extends Component {
{this.areLibrariesEnabled() && (
<ManageLibraries onClick={this.manageLibraries} />
)}
{experiments.isEnabled(experiments.APPLAB_ML) && (
<ManageModels onClick={this.manageModels} />
)}
{this.props.showMakerToggle && (
<ToggleMaker onClick={this.toggleMakerToolkit} />
)}
</PopUpMenu>
{experiments.isEnabled(experiments.APPLAB_ML) && (
<ModelManagerDialog
isOpen={this.state.managingModels}
onClose={this.closeModelManager}
autogenerateML={this.props.autogenerateML}
/>
)}
<ConfirmEnableMakerDialog
isOpen={this.state.confirmingEnableMaker}
handleConfirm={this.confirmEnableMaker}
Expand All @@ -173,6 +193,10 @@ ManageAssets.propTypes = {
last: PropTypes.bool
};

export function ManageModels(props) {
return <PopUpMenu.Item {...props}>{'Manage Models'}</PopUpMenu.Item>;
}

export function ManageLibraries(props) {
return <PopUpMenu.Item {...props}>{msg.manageLibraries()}</PopUpMenu.Item>;
}
Expand Down
28 changes: 28 additions & 0 deletions apps/src/lib/util/mlApi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* global Promise */

import $ from 'jquery';
import {predict} from '@cdo/apps/MLTrainers';

export const commands = {
async getPrediction(opts) {
return new Promise((resolve, reject) => {
$.ajax({
url: '/api/v1/ml_models/' + opts.modelId,
method: 'GET'
})
.then(modelData => {
const predictParams = {
...modelData,
testData: opts.testValues
};
const result = predict(predictParams);
opts.callback(result);
return resolve();
})
.fail((jqXhr, status) => {
opts.callback('Error: prediction failed');
return reject({message: 'An error occurred'});
});
});
}
};

0 comments on commit 448cfe4

Please sign in to comment.