veyron-browser: Add autocomplete implementation

With the paper-autocomplete web component, it is time to gather
and suggest values for each input.

There are 3 current inputs
* name; suggest based on the glob at that level, triggered by '/'
* glob; unclear
* methodInput: suggest based on previous entries to this input

methodInput will be modified to have suggestions based on a learner.
For now, we require an exact signature, methodName, and argName match.
The perceptron-rule will update each suggestion's rank.
When predicting, the best suggestions will be returned.

In the future, it may be nice to generalize even when the signature,
methodName, and argName don't match exactly.

Change-Id: Ie293958e45b955c6ac2c0b10fed0baf034a772b3
diff --git a/src/components/browse/index.js b/src/components/browse/index.js
index fcd7046..4aebc3a 100644
--- a/src/components/browse/index.js
+++ b/src/components/browse/index.js
@@ -29,6 +29,11 @@
     'learner-autorpc',
     smartService.constants.LEARNER_AUTORPC
   );
+  smartService.loadOrRegister(
+    'learner-method-input',
+    smartService.constants.LEARNER_METHOD_INPUT,
+    { minThreshold: 0.2, maxValues: 5 }
+  );
 
   var selectedItemDetails = itemDetailsComponent();
 
diff --git a/src/components/browse/item-details/render-detail.js b/src/components/browse/item-details/format-detail.js
similarity index 72%
rename from src/components/browse/item-details/render-detail.js
rename to src/components/browse/item-details/format-detail.js
index 9912724..f256fe0 100644
--- a/src/components/browse/item-details/render-detail.js
+++ b/src/components/browse/item-details/format-detail.js
@@ -1,6 +1,6 @@
 /*
  * The plugins listed here are listed in order with highest priority first.
- * Plugins must export functions shouldRender(input) and render(input).
+ * Plugins must export functions shouldFormat(input) and format(input).
  * The default plugin should always be last.
  */
 var plugins = [
@@ -8,17 +8,17 @@
   require('./plugins/default.js')
 ];
 
-module.exports = renderDetail;
+module.exports = formatDetail;
 
 /*
  * Transforms the input into the desired detail output.
  * Various plugins are tested until the correct one is found.
  * With the default plugin, this should always return something.
  */
-function renderDetail(input) {
+function formatDetail(input) {
   for (var i = 0; i < plugins.length; i++) {
-    if (plugins[i].shouldRender(input)) {
-      return plugins[i].render(input);
+    if (plugins[i].shouldFormat(input)) {
+      return plugins[i].format(input);
     }
   }
   console.error('No plugins rendered the detail', input);
diff --git a/src/components/browse/item-details/index.js b/src/components/browse/item-details/index.js
index c70b57c..c484f34 100644
--- a/src/components/browse/item-details/index.js
+++ b/src/components/browse/item-details/index.js
@@ -10,6 +10,7 @@
 var h = mercury.h;
 var css = require('./index.css');
 var debug = require('debug')('components:browse:item-details');
+var purgeMercuryArray = require('../../../lib/mercury/purgeMercuryArray');
 
 module.exports = create;
 module.exports.render = render;
@@ -64,7 +65,13 @@
      * List of selected RPC method inputs
      * @type {Array<string>}
      */
-    methodInputArguments: mercury.array([])
+    methodInputArguments: mercury.array([]),
+
+    /*
+     * List of selected RPC method input suggestions for each input.
+     * @type {Array<Array<string>>}
+     */
+    methodInputArgumentSuggestions: mercury.array([])
   });
 
   var events = mercury.input([
@@ -217,7 +224,7 @@
     return h('pre', {
       'ev-click': mercury.event(events.methodSelected, {
         methodName: name,
-        numArgs: param.inArgs.length
+        signature: sig
       })
     }, text);
   }
@@ -227,20 +234,23 @@
  * Renders a input field form for the selected method.
  */
 function renderMethodInput(state, events) {
-  if (state.selectedMethod === '') {
+  var method = state.selectedMethod;
+  if (method === '') {
     return h('div.method-input', 'No method selected');
   }
   // Display the selected method name
-  var methodNameHeader = h('pre', state.selectedMethod);
+  var methodNameHeader = h('pre', method);
 
   // Form for filling up the arguments
-  var param = state.signature[state.selectedMethod];
+  var param = state.signature[method];
   var argForm = []; // contains form elements
   var args = state.methodInputArguments; // contains form values
   for (var i = 0; i < param.inArgs.length; i++) {
     // Fill argForm with the relevant form element.
+    var argName = param.inArgs[i];
+    var suggestions = state.methodInputArgumentSuggestions[i];
     argForm.push(
-      renderMethodInputArgument(state.selectedMethod, param.inArgs[i], args, i)
+      renderMethodInputArgument(method, argName, suggestions, args, i)
     );
   }
 
@@ -273,27 +283,22 @@
  * Renders an input element whose change events modify the given args array at
  * the specified index. The placeholder is generally an argument name.
  */
-function renderMethodInputArgument(methodName, placeholder, args, index) {
-  // TODO(alexfandrianto): Replace these children with the autocomplete
-  // suggestions relevant to this input, using paper-item components.
-  var children = [
-    h('paper-item', { 'label': new AttributeHook('churae') }),
-    h('paper-item', { 'label': new AttributeHook('donut') }),
-    h('paper-item', { 'label': new AttributeHook('churof') }),
-    h('paper-item', { 'label': new AttributeHook('donute') }),
-    h('paper-item', { 'label': new AttributeHook('churoo') }),
-    h('paper-item', { 'label': new AttributeHook('donua') }),
-    h('paper-item', { 'label': new AttributeHook('macaroon') })
-  ];
+function renderMethodInputArgument(
+  methodName, placeholder, suggestions, args, index) {
+
+  // The children are the suggestions for this paper-autocomplete input.
+  var children = suggestions.map(function(suggestion) {
+    return h('paper-item', { 'label': new AttributeHook(suggestion) });
+  });
 
   var changeEvent = new PaperInputValueEvent(function(data) {
-    debug('change', data);
+    debug(methodName, placeholder, 'value changed.', data);
     args[index] = data;
   });
   // TODO(alexfandrianto): Remove the inputEvent. It is only here for debug
   // while we are getting used to the paper-autocomplete element.
   var inputEvent = new PaperInputValueEvent(function(data) {
-    debug('input', data);
+    debug(methodName, placeholder, 'value inputted.', data);
   });
 
   // TODO(alexfandrianto): Note that Mercury and Polymer create a bug together.
@@ -445,11 +450,25 @@
   });
   events.methodSelected(function(data) {
     state.selectedMethod.set(data.methodName);
-    state.methodInputArguments.splice(0,
-      state.methodInputArguments.getLength());
-    for (var i = 0; i < data.numArgs; i++) {
+
+    // Clear the current method input arguments and suggestions.
+    purgeMercuryArray(state.methodInputArguments);
+    purgeMercuryArray(state.methodInputArgumentSuggestions);
+
+    // Then prepare to initial arguments and their suggestions.
+    var argNames = data.signature[data.methodName].inArgs;
+    var input = {
+      methodName: data.methodName,
+      signature: data.signature
+    };
+    argNames.forEach(function(arg) {
       state.methodInputArguments.push(undefined);
-    }
+
+      input.argName = arg;
+      state.methodInputArgumentSuggestions.push(
+        smartService.predict('learner-method-input', input)
+      );
+    });
   });
   events.methodCalled(makeRPC.bind(null, state));
   events.methodRemoved(function(data) {
diff --git a/src/components/browse/item-details/make-rpc.js b/src/components/browse/item-details/make-rpc.js
index 10153c1..5cc5c51 100644
--- a/src/components/browse/item-details/make-rpc.js
+++ b/src/components/browse/item-details/make-rpc.js
@@ -1,58 +1,40 @@
 var browseService = require('../../../services/browse-service');
 var smartService = require('../../../services/smart-service');
 var debug = require('debug')('components:browse:item-details:make-rpc');
-var renderDetail = require('./render-detail');
+var formatDetail = require('./format-detail');
 
 module.exports = makeRPC;
 
 /*
  * Use the browseService to perform an RPC request.
  * Put the results in the state and record this request in the smartService.
- * Note that the recorded results are rendered according to renderDetail.
+ * Note that the recorded results are rendered according to formatDetail.
  * data needs to have (name, methodName, args, hasParams, signature)
  */
 function makeRPC(state, data) {
   browseService.makeRPC(data.name, data.methodName, data.args).then(
     function(result) {
-      debug('Received:', result);
+      // Since the RPC was successful, we can assume the inputs were good.
+      if (data.signature[data.methodName].inArgs.length > 0) {
+        learnMethodInput(state, data);
+      }
 
-      var expectedOutArgs = state.signature()[data.methodName].numOutArgs;
       // Do not process results we expect to be empty.
       // TODO(alexfandrianto): Streaming results are ignored with this logic.
+      var expectedOutArgs = state.signature()[data.methodName].numOutArgs;
       if (expectedOutArgs === 1) { // Error is the only possible out argument.
         return;
       }
 
-      // Use renderDetail to process the raw result into a renderable format.
-      var renderedResult = renderDetail(result);
-      state.methodOutputs.push(renderedResult);
+      // Draw the results.
+      formatResult(state, data, result);
 
-      // If we received a result for a 0-parameter RPC, add to the details page.
+      // Learn which parameterless RPCs are good to recommend.
       if (!data.hasParams) {
-        // Store the data we received in our state for later rendering.
-        var detail = state.details.get(data.name);
-        if (detail === undefined) {
-          detail = {};
-        }
-        detail[data.methodName] = renderedResult;
-        state.details.put(data.name, detail);
-
-        // Log the successful RPC to the smart service.
-        var input = {
-          methodName: data.methodName,
-          signature: data.signature,
-          name: data.name,
-          reward: 1
-        };
-        smartService.record('learner-autorpc', input);
-
-        // For debug, display what our prediction would be.
-        debug('PredictA:', smartService.predict('learner-autorpc', input));
-
-        // Save after making a successful parameterless RPC.
-        smartService.save('learner-autorpc');
+        learnAutoRPC(state, data);
       }
-    },
+    }
+  ).catch(
     function(err) {
       debug('Error during RPC',
         data.name,
@@ -61,4 +43,68 @@
       );
     }
   );
+}
+
+/*
+ * The result will be rendered. The rendering is stored in the state.
+ */
+function formatResult(state, data, result) {
+  // Use formatDetail to process the raw result into a renderable format.
+  var formattedResult = formatDetail(result);
+  state.methodOutputs.push(formattedResult);
+
+  // If we received a result for a 0-parameter RPC, add to the details page.
+  if (!data.hasParams) {
+    var detail = state.details.get(data.name);
+    if (detail === undefined) {
+      detail = {};
+    }
+    detail[data.methodName] = formattedResult;
+    state.details.put(data.name, detail);
+  }
+}
+
+/*
+ * Learn from the method inputs to be able to suggest them in the future.
+ */
+function learnMethodInput(state, data) {
+  for (var i = 0; i < data.args.length; i++) {
+    var argName = data.signature[data.methodName].inArgs[i];
+    var input = {
+      argName: argName,
+      methodName: data.methodName,
+      signature: data.signature,
+      value: data.args[i]
+    };
+    debug('Update Input:', input);
+
+    smartService.record('learner-method-input', input);
+
+    // For debug, display what our prediction would be.
+    debug('PredictMI:', smartService.predict('learner-method-input', input));
+
+    // Save after making a successful parameterless RPC.
+    smartService.save('learner-method-input');
+  }
+}
+
+
+/*
+ * Learn to recommend this method to the user.
+ */
+function learnAutoRPC(state, data) {
+  // Log the successful RPC to the smart service.
+  var input = {
+    methodName: data.methodName,
+    signature: data.signature,
+    name: data.name,
+    reward: 1
+  };
+  smartService.record('learner-autorpc', input);
+
+  // For debug, display what our prediction would be.
+  debug('PredictA:', smartService.predict('learner-autorpc', input));
+
+  // Save after making a successful parameterless RPC.
+  smartService.save('learner-autorpc');
 }
\ No newline at end of file
diff --git a/src/components/browse/item-details/plugins/default.js b/src/components/browse/item-details/plugins/default.js
index 68d16e9..bc3f48f 100644
--- a/src/components/browse/item-details/plugins/default.js
+++ b/src/components/browse/item-details/plugins/default.js
@@ -1,20 +1,20 @@
 var h = require('mercury').h;
 
 module.exports = {
-  'shouldRender': shouldRender,
-  'render': render
+  'shouldFormat': shouldFormat,
+  'format': format
 };
 
 /*
- * By default, always render.
+ * By default, always format.
  */
-function shouldRender(input) {
+function shouldFormat(input) {
   return true;
 }
 
 /*
  * By default, the input is returned as prettified JSON.
  */
-function render(input) {
+function format(input) {
   return h('pre', JSON.stringify(input, null, 2));
 }
\ No newline at end of file
diff --git a/src/components/browse/item-details/plugins/histogram.js b/src/components/browse/item-details/plugins/histogram.js
index 85699ce..c04746a 100644
--- a/src/components/browse/item-details/plugins/histogram.js
+++ b/src/components/browse/item-details/plugins/histogram.js
@@ -2,24 +2,24 @@
 var h = require('mercury').h;
 
 module.exports = {
-  'shouldRender': shouldRender,
-  'render': render
+  'shouldFormat': shouldFormat,
+  'format': format
 };
 
 /*
- * Render if the appropriate histogram fields are present.
+ * Format if the appropriate histogram fields are present.
  * TODO(alexfandrianto): Negotiate a better way of identifying histogram data.
  */
-function shouldRender(input) {
+function shouldFormat(input) {
   return input.count !== undefined && input.sum !== undefined &&
     input.buckets !== undefined;
 }
 
 /*
- * The histogram is rendered with bars (a fork of ascii-histogram).
- * TODO(alexfandrianto): Consider using a prettier rendering package.
+ * The histogram is formatted with bars (a fork of ascii-histogram).
+ * TODO(alexfandrianto): Consider using a prettier formatting package.
  */
-function render(input) {
+function format(input) {
   var histData = {};
   input.buckets.forEach(function(obj) {
     histData[obj.lowBound] = obj.count;
diff --git a/src/services/smart-service-implementation.js b/src/services/smart-service-implementation.js
index edd74f6..cdf9094 100644
--- a/src/services/smart-service-implementation.js
+++ b/src/services/smart-service-implementation.js
@@ -7,14 +7,17 @@
 var debug = require('debug')('services:smart-service');
 var perceptron = require('../lib/learning/perceptron');
 var rank = require('../lib/learning/rank');
+var _ = require('lodash');
 
 var LEARNER_SHORTCUT = 1;
 var LEARNER_AUTORPC = 2;
+var LEARNER_METHOD_INPUT = 3;
 
 // Associate the learner types with the constructor
 var LEARNER_MAP = {};
 LEARNER_MAP[LEARNER_SHORTCUT] = shortcutLearner;
 LEARNER_MAP[LEARNER_AUTORPC] = autoRPCLearner;
+LEARNER_MAP[LEARNER_METHOD_INPUT] = methodInputLearner;
 
 // Associate the learner types with additional functions.
 // Note: update and predict are required.
@@ -29,11 +32,17 @@
   update: autoRPCLearnerUpdate,
   predict: autoRPCLearnerPredict
 };
+LEARNER_METHODS[LEARNER_METHOD_INPUT] = {
+  computeKey: methodInputLearnerComputeKey,
+  update: methodInputLearnerUpdate,
+  predict: methodInputLearnerPredict
+};
 
 // Export the implementation constants
 module.exports = {
   LEARNER_SHORTCUT: LEARNER_SHORTCUT,
   LEARNER_AUTORPC: LEARNER_AUTORPC,
+  LEARNER_METHOD_INPUT: LEARNER_METHOD_INPUT,
   LEARNER_MAP: LEARNER_MAP,
   LEARNER_METHODS: LEARNER_METHODS
 };
@@ -42,7 +51,7 @@
  * Create a shortcut learner that analyzes directory paths visited and predicts
  * the most useful shortcuts.
  * The expected attributes in params include:
- * k, the max # of shortcuts to return
+ * - k, the max # of shortcuts to return
  */
 function shortcutLearner(type, params) {
   this.directoryCount = {};
@@ -161,7 +170,7 @@
   features[input.methodName] = 1;
 
   // Same-named methods that share service signatures are likely similar.
-  features[input.methodName + '|' + JSON.stringify(input.signature)] = 1;
+  features[input.methodName + '|' + stringifySignature(input.signature)] = 1;
 
   // Services in the same namespace subtree may be queried similarly.
   var pathFeatures = pathFeatureExtractor(input.name);
@@ -200,6 +209,123 @@
 }
 
 /*
+ * Create a method input learner that suggests the most likely inputs to a
+ * given argument of a method.
+ * Params can optionally include:
+ * - minThreshold, the minimum score of a suggestable value
+ * - maxValues, the largest number of suggestable values that may be returned
+ * - penalty, a constant for the rate to penalize incorrect suggestions
+ * - reward, a constant for the rate to reward chosen values
+ */
+function methodInputLearner(type, params) {
+  this.type = type;
+  this.inputMap = {}; // map[string]map[string]number
+
+  // Override the default params with relevant fields from params.
+  this.params = {
+    penalty: 0.1,
+    reward: 0.4
+  };
+  _.assign(this.params, params);
+
+  addAttributes(this, LEARNER_METHODS[type]);
+}
+
+/*
+ * Given input data, compute the appropriate lookup key
+ * Input must have: argName, methodName, and signature.
+ */
+function methodInputLearnerComputeKey(input) {
+  var keyArr = [
+    stringifySignature(input.signature),
+    input.methodName,
+    input.argName
+  ];
+  return keyArr.join('|');
+}
+
+/*
+ * Given input data, boost the rank of the given value and
+ * penalize the other values.
+ * Input must have: argName, methodName, signature, and value
+ */
+function methodInputLearnerUpdate(input) {
+  var key = this.computeKey(input);
+  var predValues = this.predict(input);
+  var value = input.value;
+
+  // Setup the inputMap and values if not yet defined.
+  if (this.inputMap[key] === undefined) {
+    this.inputMap[key] = {};
+  }
+  var values = this.inputMap[key];
+  if (values[value] === undefined) {
+    values[value] = 0;
+  }
+
+  // Give a reward to the chosen value.
+  values[value] += this.params.reward * (1 - values[value]);
+
+  // Induce a penalty on failed predictions.
+  for (var i = 0; i < predValues.length; i++) {
+    var pred = predValues[i];
+    if (pred !== value) {
+      values[pred] += this.params.penalty * (0 - values[pred]);
+    }
+  }
+}
+
+/*
+ * Given input data, predict the most likely values for this method input.
+ * Input must have: argName, methodName, and signature.
+ */
+function methodInputLearnerPredict(input) {
+  var key = this.computeKey(input);
+  var values = this.inputMap[key];
+
+  // Immediately return nothing if there are no values to suggest.
+  if (values === undefined) {
+    return [];
+  }
+
+  // Convert the values to scored items for ranking.
+  var scoredItems = Object.getOwnPropertyNames(values).map(
+    function getScoredItem(value) {
+      return {
+        item: value,
+        score: values[value]
+      };
+    }
+  );
+
+  // Filter the scored items by minThreshold
+  if (this.params.minThreshold !== undefined) {
+    scoredItems = scoredItems.filter(function applyThreshold(scoredItem) {
+      return scoredItem.score >= this.minThreshold;
+    }, this);
+  }
+
+  // Rank the scored items and return the top values (limit to maxValues)
+  var maxValues = this.params.maxValues;
+  if (maxValues === undefined) {
+    maxValues = scoredItems.length;
+  }
+  return rank.getBestKItems(scoredItems, maxValues).map(function(goodItem) {
+    return goodItem.item;
+  });
+}
+
+/*
+ * Given an arbitrary method signature, compute a reasonable and consistent
+ * stringification for it.
+ */
+function stringifySignature(signature) {
+  var names = Object.getOwnPropertyNames(signature);
+  names.sort();
+  return JSON.stringify(names);
+}
+
+/*
  * Given a path string, this feature extractor assigns diminishing returns
  * credit to each ancestor along the path.
  */
diff --git a/src/veyron-config.js b/src/veyron-config.js
index f192ca7..dcd836a 100644
--- a/src/veyron-config.js
+++ b/src/veyron-config.js
@@ -1,7 +1,8 @@
 var logLevels = require('veyron').logLevels;
 var veyronConfig = {
   'logLevel': logLevels.INFO,
-  'wspr': 'http://localhost:8124'
+  'wspr': 'http://localhost:8124',
+  'authenticate': true
 };
 
 module.exports = veyronConfig;