From 9ce7b24b6c1be291237afcbde5954e904e5789f8 Mon Sep 17 00:00:00 2001 From: David Luecke Date: Tue, 29 Sep 2015 22:26:32 -0700 Subject: [PATCH] Electron app working, adding binary and moving src folder --- .gitignore | 1 + .npmignore | 3 ++ bin/mysam | 27 ++++++++++++ config.json | 4 ++ lib/app.js | 17 ------- lib/index.js | 2 - lib/plugins.js | 50 --------------------- package.json | 23 +++++++--- readme.md | 28 ++++++++++-- src/app.js | 28 ++++++++++++ src/electron.js | 32 ++++++++++++++ {lib => src}/extractor.js | 0 src/loader.js | 73 +++++++++++++++++++++++++++++++ src/server.js | 5 +++ {lib => src}/services/classify.js | 21 +++++++-- {lib => src}/services/index.js | 0 {lib => src}/services/plugins.js | 0 test/extractor.test.js | 2 +- test/services/classify.test.js | 2 +- 19 files changed, 234 insertions(+), 84 deletions(-) create mode 100644 .npmignore create mode 100755 bin/mysam create mode 100644 config.json delete mode 100644 lib/app.js delete mode 100644 lib/index.js delete mode 100644 lib/plugins.js create mode 100644 src/app.js create mode 100644 src/electron.js rename {lib => src}/extractor.js (100%) create mode 100644 src/loader.js create mode 100644 src/server.js rename {lib => src}/services/classify.js (81%) rename {lib => src}/services/index.js (100%) rename {lib => src}/services/plugins.js (100%) diff --git a/.gitignore b/.gitignore index 19398c6..68e6bcb 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ node_modules .idea/ db-data/ +lib/ diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..2864de5 --- /dev/null +++ b/.npmignore @@ -0,0 +1,3 @@ +!lib/ +src/ +test/ \ No newline at end of file diff --git a/bin/mysam b/bin/mysam new file mode 100755 index 0000000..c252dcf --- /dev/null +++ b/bin/mysam @@ -0,0 +1,27 @@ +#!/usr/bin/env node + +var path = require('path'); +var program = require('commander'); +var root = path.join(__dirname, '..'); +var pkg = require(path.join(root, 'package.json')); +var electron = require('electron-prebuilt'); +var proc = require('child_process'); + +program.version(pkg.version) + .usage('[options]') + .description(pkg.description) + .option('-s, --server', 'Run server only') + .option('-d --develop', 'Run in development mode') + .parse(process.argv); + +process.env.NODE_ENV = program.develop ? 'development' : 'production'; + +if(program.develop) { + process.env.DEBUG = process.env.DEBUG ? process.env.DEBUG + ',mysam:*' : 'mysam:*'; +} + +if(program.server) { + require('../lib/server'); +} else { + proc.spawn(electron, [root], {stdio: 'inherit'}); +} diff --git a/config.json b/config.json new file mode 100644 index 0000000..3f7fbda --- /dev/null +++ b/config.json @@ -0,0 +1,4 @@ +{ + "port": 9090, + "frontend": "mysam-frontend" +} diff --git a/lib/app.js b/lib/app.js deleted file mode 100644 index 7b333d6..0000000 --- a/lib/app.js +++ /dev/null @@ -1,17 +0,0 @@ -import bodyParser from 'body-parser'; -import feathers from 'feathers'; -import hooks from 'feathers-hooks'; - -import services from './services/index'; -import plugins from './plugins'; - -const app = feathers() - .configure(feathers.rest()) - .configure(feathers.socketio()) - .configure(hooks()) - .use(bodyParser.json()) - .use(bodyParser.urlencoded({ extended: true })) - .configure(services) - .configure(plugins); - -export default app; diff --git a/lib/index.js b/lib/index.js deleted file mode 100644 index febf1b9..0000000 --- a/lib/index.js +++ /dev/null @@ -1,2 +0,0 @@ -require('babel/register'); -require('./app').listen(process.env.PORT || 3030); diff --git a/lib/plugins.js b/lib/plugins.js deleted file mode 100644 index 34b87f1..0000000 --- a/lib/plugins.js +++ /dev/null @@ -1,50 +0,0 @@ -import Q from 'q'; -import path from 'path'; -import npm from 'npm'; -import _ from 'lodash'; -import debug from 'debug'; -import feathers from 'feathers'; - -const log = debug('mysam:plugins'); - -export default function() { - const app = this; - const plugins = app.service('plugins'); - - Q.ninvoke(npm, 'load', { loaded: false, depth: 0 }) - .then(() => { - const dfd = Q.defer(); - - npm.commands.ls([], true, function (error, data) { - if(error) { - return dfd.reject(error); - } - - let configurers = _.map(data.dependencies, (dependency, name) => { - let config = dependency.mysam; - if(config) { - let pluginLoader = require(name); - - log(`Found plugin dependency ${dependency.name}`); - - if(typeof pluginLoader !== 'function') { - throw new Error(`Can not configure plugin ${dependency.name}`); - } else { - if(config.public) { - let publicPath = feathers.static(path.join(dependency.path, config.public)); - log(`Configuring ${dependency.name} at path ${publicPath}`); - app.use(`/${dependency.name}`, publicPath); - } - - return Q.ninvoke(plugins, 'create', dependency) - .then(() => app.configure(pluginLoader)); - } - } - }); - - dfd.resolve(Q.all(configurers)); - }); - - return dfd.promise; - }).fail(error => console.error(error.stack)); -} diff --git a/package.json b/package.json index ce9b3c4..82608db 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,32 @@ { "name": "mysam", "version": "0.0.1", - "description": "An open, web based \"intelligent\" agent that can listen to you and learn.", - "main": "lib/index", + "description": "An open, web based \"intelligent\" assistant that can listen to you and learn.", + "main": "lib/electron", + "bin": { + "testee": "./bin/testee" + }, "scripts": { - "start": "node lib/", + "start": "node lib/server", + "electron": "electron .", + "compile": "babel src --out-dir lib", + "compile:watch": "babel src --out-dir lib --watch", "test": "npm run jshint && npm run mocha", - "watch": "DEBUG=mysam:* supervisor --ignore node_modules/ --no-restart-on error lib/", - "jshint": "jshint lib/. test/. --config", + "jshint": "jshint src/. test/. --config", "mocha:watch": "mocha --compilers js:babel/register test/ --watch --recursive --growl --reporter Min", "mocha": "mocha --compilers js:babel/register test/ --recursive" }, + "repository": { + "type": "git", + "url": "git@github.com:daffl/mysam.git" + }, "author": "David Luecke ", "license": "MIT", "dependencies": { - "babel": "^5.4.7", "body-parser": "^1.13.0", + "commander": "^2.8.1", "debug": "^2.2.0", + "electron-prebuilt": "^0.33.3", "feathers": "^1.1.0-pre.0", "feathers-hooks": "^0.5.0", "feathers-memory": "^0.3.4", @@ -28,6 +38,7 @@ "q": "^1.4.1" }, "devDependencies": { + "babel": "^5.8.23", "jshint": "^2.8.0", "mocha": "^2.3.0", "supervisor": "^0.6.0" diff --git a/readme.md b/readme.md index 087dc4b..6a5f26a 100644 --- a/readme.md +++ b/readme.md @@ -1,7 +1,29 @@ -# Say hi to SAM +# Say hi SAM [ ![Codeship Status for daffl/mysam](https://codeship.com/projects/b26a3f10-3c66-0133-d19d-1276d5d0a1e7/status?branch=master)](https://codeship.com/projects/102258) -An open source, web based "intelligent agent" that can listen to you and learn. +Sam is an open-source, web-based "intelligent" assistant. It can listen to you, learn new actions and easily be extended with JavaScript plugins. Sam runs as an [Electron](http://electron.atom.io/) desktop application or in a modern browser. -> I'm still very alpha... +# Installation + +Install Sam globally via: + +> npm install mysam -g + +And then run either the desktop application with: + +> mysam + +Or the server (which is available at [localhost:9090](http://localhost:9090)) via: + +> mysam --server + +# Getting started + +# Plugins + +Plugins can be installed via [NPM](). To ask Sam about the weather run: + +> npm install mysam-weather -g + +Then restart the application. diff --git a/src/app.js b/src/app.js new file mode 100644 index 0000000..39ae91c --- /dev/null +++ b/src/app.js @@ -0,0 +1,28 @@ +import bodyParser from 'body-parser'; +import feathers from 'feathers'; +import hooks from 'feathers-hooks'; +import path from 'path'; + +import config from '../config.json'; +import services from './services/index'; +import loader from './loader'; + +const publicPath = path.join(__dirname, '..', 'node_modules', config.frontend); +const app = feathers() + .configure(feathers.rest()) + .configure(feathers.socketio()) + .configure(hooks()) + .use(bodyParser.json()) + .use(bodyParser.urlencoded({ extended: true })) + .use('/', function(req, res, next) { + if(req.url === '/' && process.env.NODE_ENV === 'production') { + return res.sendFile(path.join(publicPath, 'production.html')); + } + + next(); + }) + .use('/', feathers.static(publicPath)) + .configure(services) + .configure(loader); + +export default app; diff --git a/src/electron.js b/src/electron.js new file mode 100644 index 0000000..a189dc2 --- /dev/null +++ b/src/electron.js @@ -0,0 +1,32 @@ +import app from 'app'; +import BrowserWindow from 'browser-window'; +import crashReporter from 'crash-reporter'; + +import server from './app'; + +const config = require('../config.json'); + +let mainWindow = null; + +crashReporter.start(); +server.listen(process.env.PORT || config.port); + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); + } +}); + +app.on('ready', () => { + mainWindow = new BrowserWindow({ + width: 800, + height: 600 + }); + + mainWindow.loadUrl(`http://localhost:${config.port}`); + mainWindow.openDevTools(); + mainWindow.on('closed', () => { + mainWindow = null; + }); +}); + diff --git a/lib/extractor.js b/src/extractor.js similarity index 100% rename from lib/extractor.js rename to src/extractor.js diff --git a/src/loader.js b/src/loader.js new file mode 100644 index 0000000..f3f54f7 --- /dev/null +++ b/src/loader.js @@ -0,0 +1,73 @@ +import Q from 'q'; +import path from 'path'; +import npm from 'npm'; +import _ from 'lodash'; +import debug from 'debug'; +import feathers from 'feathers'; + +const log = debug('mysam:plugins'); + +export default function() { + const app = this; + const plugins = app.service('plugins'); + const loadPlugin = moduleName => { + if(!moduleName){ + return Q(null); + } + + let pkgPath = require.resolve(path.join(moduleName, 'package.json')); + let pkg = require(pkgPath); + let config = pkg.mysam; + + if(!config) { + return Q(null); + } + + log(`Found plugin ${pkg.name}`); + + let pluginLoader = require(moduleName); + + if(typeof pluginLoader !== 'function') { + throw new Error(`Can not configure plugin ${pkg.name}`); + } else { + } + if(config.public) { + let dirname = path.dirname(pkgPath); + let staticPath = path.join(dirname, config.public); + + log(`Setting up ${staticPath} at /${pkg.name}`); + + app.use(`/${pkg.name}`, feathers.static(staticPath)); + } + + return Q.ninvoke(plugins, 'create', pkg) + .then(() => { + try { + let pluginLoader = require(moduleName); + app.configure(pluginLoader); + } catch(e) { + log(`No server configuration module for ${pkg.name}.`); + } + }); + }; + + Q.ninvoke(npm, 'load', { loaded: false, global: true, depth: 0 }) + .then(() => { + const dfd = Q.defer(); + + npm.commands.ls([], true, function (error, data) { + if(error) { + return dfd.reject(error); + } + + let configurers = []; + + _.each(data.dependencies, dependency => + dependency && configurers.push(loadPlugin(dependency.path))); + + dfd.resolve(Q.all(configurers)); + }); + + return dfd.promise; + }).fail(error => console.error(error.stack)); +} diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..e7e929d --- /dev/null +++ b/src/server.js @@ -0,0 +1,5 @@ +import app from './app'; + +const config = require('../config.json'); + +app.listen(process.env.PORT || config.port); diff --git a/lib/services/classify.js b/src/services/classify.js similarity index 81% rename from lib/services/classify.js rename to src/services/classify.js index 4750e6e..2f69bc7 100644 --- a/lib/services/classify.js +++ b/src/services/classify.js @@ -3,6 +3,7 @@ import debug from 'debug'; import BrainJSClassifier from 'natural-brain'; import pos from 'pos'; import crypto from 'crypto'; +import _ from 'lodash'; import Extractor from '../extractor'; @@ -34,7 +35,10 @@ export default { create: [addPos, extract] }, create(data, params, callback) { + log('Classifying', data); + if(this.retrain) { + log('Retraining Neural network'); this.classifier.retrain(); this.retrain = false; } @@ -46,7 +50,7 @@ export default { Q.ninvoke(this.actions, 'get', action) .then(action => { - return Object.assign(action, { id, input, classifications }); + return _.extend(action, { id, input, classifications }); }) .then(result => { log(result); @@ -68,17 +72,26 @@ export default { // We want to classify every word BrainJSClassifier.disableStopWords(); - this.classifier = new BrainJSClassifier(); - this.app = app; + this.classifier = new BrainJSClassifier({ + iterations: 2000 + }); this.actions = app.service('actions'); this.actions.on('created', add); + //this.actions.on('removed', action => { + // + //}); Q.ninvoke(this.actions, 'find').then(list => { list.forEach(add); return list; }) - .then(list => list.length && this.classifier.train()) + .then(list => { + if(list.length) { + this.classifier.train(); + this.retrain = false; + } + }) .fail(error => log('ERROR: ', error.stack)); } }; diff --git a/lib/services/index.js b/src/services/index.js similarity index 100% rename from lib/services/index.js rename to src/services/index.js diff --git a/lib/services/plugins.js b/src/services/plugins.js similarity index 100% rename from lib/services/plugins.js rename to src/services/plugins.js diff --git a/test/extractor.test.js b/test/extractor.test.js index 61ced18..a65d7af 100644 --- a/test/extractor.test.js +++ b/test/extractor.test.js @@ -1,5 +1,5 @@ import assert from 'assert'; -import Extractor from '../lib/extractor'; +import Extractor from '../src/extractor'; describe('Sentence word extraction', () => { it('.match', function() { diff --git a/test/services/classify.test.js b/test/services/classify.test.js index 27df7e0..1c4c33b 100644 --- a/test/services/classify.test.js +++ b/test/services/classify.test.js @@ -1,6 +1,6 @@ import _ from 'lodash'; import assert from 'assert'; -import app from '../../lib/app'; +import app from '../../src/app'; const classify = app.service('classify');