As I continue to play around with IBM Bluemix, this week I spent some time playing with the Personality Insights service. This service uses IBM Watson to analyze textual input and try to determine personality aspects of the author. It focuses on three areas of analysis:
- Determining what the needs of the author are. These needs are narrowed to twelve main areas and rated on a 0-100 percentile scale. The needs are: Excitement, Harmony, Curiosity, Ideal, Closeness, Self-expression, Liberty, Love, Practicality, Stability, Challenge, and Structure.
- Determining what the values of the author are. These are things Watson believe will be important to the author. As with needs, values are focused on a set of core items: Self-transcendence / Helping others, Conservation / Tradition, Hedonism / Taking pleasure in life, Self-enhancement / Achieving success, and Open to change / Excitement.
- Finally, the PI service reports on the "Big Five" - this is a personality model that tries to describe how a person interacts with the world.
As you can imagine, this is pretty deep stuff, and frankly, my wife who is working on her sociology degree would probably have a better understanding of the results. You can check out the full docs as well as look at the API docs for more information.
For my demo, I decided to try something interesting. The PI service works best when it has at least 3500 words. A typical blog post may include five hundred or so words, and with a typical RSS feed being ten items, I decided to build an application that would analyze an RSS feed and try to determine the personality of the author. I called it the Blog Personality Scan. I'll link to the demo in a moment, but let's look at the code first.
First, we'll look at the app.js file for the Node app. It is pretty trivial as there are only two views - the home page and the API to send an RSS url.
/*jshint node:false */
/* global console,require */
var express = require('express');
var hbs = require('hbs');
var url = require('url');
hbs.registerHelper('raw-helper', function(options) {
return options.fn();
});
var rssReader = require('./rssreader.js');
var insightsAPI = require('./insights.js');
// setup middleware
var app = express();
app.use(app.router);
app.use(express.errorHandler());
app.use(express.static(__dirname + '/public')); //setup static public directory
app.set('view engine', 'html');
app.engine('html', hbs.__express);
app.set('views', __dirname + '/views'); //optional since express defaults to CWD/views
// render index page
app.get('/', function(req, res){
res.render('index');
});
app.get('/parse', function(req, res) {
var url_parts = url.parse(req.url, true);
var query = url_parts.query;
if(!query.rss) {
res.json({error:"Invalid data sent."});
return;
}
rssReader.parse(query.rss, function(err,content) {
if(err) {
res.json(err);
} else {
console.log('bak with content, len is '+content.length);
insightsAPI.parse(query.rss, query.rss, content, function(data) {
console.log('back from IAPI');
//console.log(JSON.stringify(data));
res.json(data);
});
}
});
});
// There are many useful environment variables available in process.env.
// VCAP_APPLICATION contains useful information about a deployed application.
var appInfo = JSON.parse(process.env.VCAP_APPLICATION || "{}");
// TODO: Get application information and use it in your app.
// VCAP_SERVICES contains all the credentials of services bound to
// this application. For details of its content, please refer to
// the document or sample of each service.
if(process.env.VCAP_SERVICES) {
var services = JSON.parse(process.env.VCAP_SERVICES || "{}");
console.log(services);
var apiUrl = services.personality_insights[0].credentials.url;
var apiUsername = services.personality_insights[0].credentials.username;
var apiPassword = services.personality_insights[0].credentials.password;
} else {
var credentials = require('./credentials.json');
var apiUrl = credentials.apiUrl;
var apiUsername = credentials.apiUsername;
var apiPassword = credentials.apiPassword;
}
insightsAPI.setAuth(apiUrl, apiUsername, apiPassword);
// The IP address of the Cloud Foundry DEA (Droplet Execution Agent) that hosts this application:
var host = (process.env.VCAP_APP_HOST || 'localhost');
// The port on the DEA for communication with the application:
var port = (process.env.VCAP_APP_PORT || 3000);
// Start server
app.listen(port, host);
console.log('App started on port ' + port);
Not terribly exciting and I wouldn't share it normally, but I specifically wanted to call out the bits that look at process.env.VCAP_SERVICES
. This is how my app picks up the API credentials when running in the Bluemix environment.
RSS reading is taken care of by the feedparser NPM package. This is the same one I used for ColdFusionBloggers.org. I'll skip that code as it isn't that exciting.
The real fun part comes in the code used to interact with the PI service:
var https = require('https');
var querystring = require('querystring');
var url = require('url');
var apiUsername;
var apiPassword;
var apiUrl;
var apiHost;
var apiPath;
function setAuth(apiurl, u, p) {
apiUrl = apiurl;
apiUsername=u;
apiPassword=p;
var parts = url.parse(apiUrl);
apiHost = parts.host;
apiPath = parts.pathname;
}
function sendInsights(user,source,input,cb) {
//cb(fake);return;
var data = {"contentItems":[]};
var item = {};
item.userid = user;
item.sourceid = source;
this.id = this.userid + '_'+this.sourceid;
item.contenttype = "text/plain";
//todo - remove html from input. the service does it, but we can do it ourselves
item.language = "en";
item.content = input;
data.contentItems.push(item);
var postData = JSON.stringify(data);
var options = {
host: apiHost,
port: 443,
path: apiPath + "/v2/profile",
headers: {
'Authorization': 'Basic ' + new Buffer(apiUsername + ':' + apiPassword).toString('base64'),
'Content-Type':'application/json',
'Content-Length': Buffer.byteLength(postData)
},
method:"post"
};
console.log(options);
var req = https.request(options, function(resp) {
var body = "";
resp.on("data", function(chunk) {
body += chunk;
});
resp.on("end", function() {
//console.log("done");console.log(body);
cb(JSON.parse(body));
});
});
req.write(postData);
req.end();
};
var InsightAPI = {
setAuth:setAuth,
parse:sendInsights
};
module.exports = InsightAPI;
Ok, perhaps "exciting" is a bit much. Honestly, it is just a HTTP hit and a JSON response. Simple - but that's kind of the point. A good service should be rather simple to use.
The rest was just presenting the results. The folks at Bluemix created a cool demo with charts and stuff, but I decided to keep it simple and just render the values - sorted. I used Handlebars to make it a bit nicer, which ended up being a bit confusing to me. It never occurred to me to consider what would happen when I used a Handlebars template for the client side in a view that was being run by a Node.js app using Handlebars on the client as well. As you can guess, it didn't work well at first. If you look back at that first code listing you'll see a helper called raw-helper. I needed to add this so I could use Handlebar's syntax in my view and have the server ignore it. This is how it looks in index.html:
<script id="reportTemplate" type="text/x-handlebars-template">
{{{{raw-helper}}}}
<div class="row">
<div class="col-md-4">
<h2>Values</h2>
{{#each values}}
<div class="row">
<div class="col-md-6"><strong>{{name}}</strong></div>
<div class="col-md-6">{{perc percentage}}</div>
</div>
{{/each}}
</div>
<div class="col-md-4">
<h2>Needs</h2>
{{#each needs}}
<div class="row">
<div class="col-md-6"><strong>{{name}}</strong></div>
<div class="col-md-6">{{perc percentage}}</div>
</div>
{{/each}}
</div>
<div class="col-md-4">
<h2>The Big 5</h2>
{{#each big5}}
<div class="row">
<div class="col-md-6"><strong>{{name}}</strong></div>
<div class="col-md-6">{{perc percentage}}</div>
</div>
{{#each children}}
<div class="row">
<div class="col-md-offset12 col-md-5 text-muted">{{name}}</div>
<div class="col-md-6 text-muted">{{perc percentage}}</div>
</div>
{{/each}}
{{/each}}
</div>
</div>
{{{{/raw-helper}}}}
</script>
Once I got this working, I was mostly OK, but then I did stupid crap like adding a helper in the Node.js app.js when I really needed it in the client-side app.js. I probably shouldn't have named those files the same. So what do the results look like? I'm going to link to the demo of course, but here are some examples. First, my own blog:
Next, Gruber of Daring Fireball:
And finally, Sarah Palin's blog:
Want to try it yourself? Check out the demo here: http://bloginsights.mybluemix.net/. You can see the entire source code for the project here: https://github.com/cfjedimaster/bloginsights.