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

Feature/subroutines with promises #78

Merged
merged 10 commits into from
Feb 11, 2016
47 changes: 47 additions & 0 deletions eg/reply-async/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Reply-async

This example demonstrates using replyAsync function. You should use **replyAsync** instead of **reply** if you have subroutines that return promises.

## Running the example

```bash
npm install && node weatherman.js
```

Refer to weatherman.rive for the list of supported commmands

## Using async subroutines

Whenever you have a subroutine that needs to call some sort of asynchronous function in order to return a value back to the script, you should use promises:

```javascript
var rs = new RiveScript();
rs.setSubroutine("asyncHelper", function(rs, args) {
// Rivescript comes bundled with RSVP.js which you can
// access through RiveScript.Promise alias
// but you are free to use your own Promise implementation
return new rs.Promise(function(resolve, reject) {
resolve('hello there');
});
})
```

Async reponses in Rive come in 2 flavors:

```javascript
// using promises
rs.replyAsync(username, message).then(function(reply) {
// good to go
}).catch(function(error){
// something went wrong
});

// or using callbacks
rs.replyAsync(username, message, this, function(error, reply) {
if (!error) {
// you can use reply here
} else {
// something went wrong, error has more info
}
});
```
10 changes: 10 additions & 0 deletions eg/reply-async/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "rive-weatherman",
"version": "1.0.0",
"description": "Your weather report",
"main": "weatherman.js",
"dependencies": {
"colors": "^1.1.2",
"request": "^2.69.0"
}
}
125 changes: 125 additions & 0 deletions eg/reply-async/weatherman.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Asynchronous Objects Example
// See the accompanying README.md for details.

// Run this demo: `node weatherman.js`

var readline = require("readline");
var request = require("request");
var colors = require('colors');

// This would just be require("rivescript") if not for running this
// example from within the RiveScript project.
var RiveScript = require("../../lib/rivescript");
var rs = new RiveScript();

var getWeather = function(location, callback) {
request.get({
url: "http://api.openweathermap.org/data/2.5/weather",
qs: {
q: location,
APPID: "6460241df9136925432064ac70416d05"
},
json: true
}, function(error, response) {
if (response.statusCode !== 200) {
callback.call(this, response.body);
} else {
callback.call(this, null, response.body);
}
});
};


rs.setSubroutine("getWeather", function (rs, args) {
return new rs.Promise(function(resolve, reject) {
getWeather(args.join(' '), function(error, data){
if(error) {
reject(error);
} else {
resolve(data.weather[0].description);
}
});
});
});

rs.setSubroutine("checkForRain", function(rs, args) {
return new rs.Promise(function(resolve, reject) {
getWeather(args.join(' '), function(error, data){
if(error) {
console.error('');
reject(error);
} else {
var rainStatus = data.rain ? 'yup :(' : 'nope';
resolve(rainStatus);
}
});
});
});

// Create a prototypical class for our own chatbot.
var AsyncBot = function(onReady) {
var self = this;

// Load the replies and process them.
rs.loadFile("weatherman.rive", function() {
rs.sortReplies();
onReady();
});

// This is a function for delivering the message to a user. Its actual
// implementation could vary; for example if you were writing an IRC chatbot
// this message could deliver a private message to a target username.
self.sendMessage = function(username, message) {
// This just logs it to the console like "[Bot] @username: message"
console.log(
["[Brick Tamland]", message].join(": ").underline.green
);
};

// This is a function for a user requesting a reply. It just proxies through
// to RiveScript.
self.getReply = function(username, message, callback) {
return rs.replyAsync(username, message, self).then(function(reply){
callback.call(this, null, reply);
}).catch(function(error) {
callback.call(this, error);
});
}
};

// Create and run the example bot.
var bot = new AsyncBot(function() {
// Drop into an interactive shell to get replies from the user.
// If this were something like an IRC bot, it would have a message
// handler from the server for when a user sends a private message
// to the bot's nick.
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});

rl.setPrompt("> ");
rl.prompt();
rl.on("line", function(cmd) {
// If this was an IRC bot, imagine "nick" came from the server as the
// sending user's IRC nickname.
nick = "Soandso";
console.log("[" + nick + "] " + cmd);

// Handle commands.
if (cmd === "/quit") {
process.exit(0);
} else {
reply = bot.getReply(nick, cmd, function(error, reply){
if (error) {
bot.sendMessage(nick, "Oops. The weather service is not cooperating!");
} else {
bot.sendMessage(nick, reply);
}
rl.prompt();
});
}
}).on("close", function() {
process.exit(0);
});
});
10 changes: 10 additions & 0 deletions eg/reply-async/weatherman.rive
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
! sub how's = how is
! sub is it going to = will it
! sub is it gonna = will it
! sub is it raining = will it rain

+ how is [the] weather in *
- <call>getWeather <star></call>

+ will it rain in *
- <call>checkForRain <star></call>
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,9 @@
"lib/lang/javascript.js"
],
"directories": {
"lib": "lib",
"doc": "docs"
"lib": "lib",
"doc": "docs"
},
"main": "lib/rivescript.js",
"repository": {
"type": "git",
"url": "https://github.com/aichaos/rivescript-js.git"
Expand Down Expand Up @@ -67,6 +66,8 @@
"scripts": {
"prepublish": "grunt dist",
"test": "grunt test"
},
"dependencies": {
"rsvp": "^3.1.0"
}

}
131 changes: 97 additions & 34 deletions src/brain.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
# Brain logic for RiveScript
utils = require("./utils")
inherit_utils = require("./inheritance")
RSVP = require("rsvp")

##
# Brain (RiveScript master)
Expand Down Expand Up @@ -36,7 +37,7 @@ class Brain
#
# Fetch a reply for the user.
##
reply: (user, msg, scope) ->
reply: (user, msg, scope, async) ->
@say "Asked to reply to [#{user}] #{msg}"

# Store the current user's ID.
Expand All @@ -60,6 +61,17 @@ class Brain
else
reply = @_getReply(user, msg, "normal", 0, scope)

reply = @processCallTags(reply, scope, async)

if not utils.isAPromise(reply)
@onAfterReply(msg, user, reply)
else
reply.then (result) =>
@onAfterReply(msg, user, result)

return reply

onAfterReply: (msg, user, reply) ->
# Save their reply history
@master._users[user].__history__.input.pop()
@master._users[user].__history__.input.unshift(msg)
Expand All @@ -69,7 +81,88 @@ class Brain
# Unset the current user ID.
@_currentUser = undefined

return reply
##
# string|Promise processCallTags (string reply, Object scope, bool async)
#
# Process <call> tags in the preprocessed reply string.
# If async is true, processCallTags can handle asynchronous subroutines
# and it returns a promise, other wise a string is returned
##
processCallTags: (reply, scope, async) ->
reply = reply.replace(/\{__call__\}/g, "<call>")
reply = reply.replace(/\{\/__call__\}/g, "</call>")
callRe = /<call>(.+?)<\/call>/ig

giveup = 0
matches = {}
promises = []

while true
giveup++
if giveup >= 50
@warn "Infinite loop looking for call tag!"
break

match = callRe.exec(reply)

if not match
break

text = utils.strip(match[1])
parts = text.split(/\s+/)
obj = parts.shift()
args = parts

matches[match[1]] =
text: text
obj: obj
args: args

# go through all the object calls and run functions
for k,data of matches
output = ""
if @master._objlangs[data.obj]
# We do. Do we have a handler for it?
lang = @master._objlangs[data.obj]
if @master._handlers[lang]
# We do.
output = @master._handlers[lang].call(@master, data.obj, data.args, scope)
else
output = "[ERR: No Object Handler]"
else
output = "[ERR: Object Not Found]"

# if we get a promise back and we are not in the async mode,
# leave an error message to suggest using an async version of rs
# otherwise, keep promises tucked into a list where we can check on
# them later
if utils.isAPromise(output)
if async
promises.push
promise: output
text: k
continue
else
output = "[ERR: Using async routine with reply: use replyAsync instead]"

reply = @._replaceCallTags(k, output, reply)

if not async
return reply
else
# wait for all the promises to be resolved and
# return a resulting promise with the final reply
return new RSVP.Promise (resolve, reject) =>
RSVP.all(p.promise for p in promises).then (results) =>
for i in [0...results.length]
reply = @_replaceCallTags(promises[i].text, results[i], reply)

resolve(reply)
.catch (reason) =>
reject(reason)

_replaceCallTags: (callSignature, callResult, reply) ->
return reply.replace(new RegExp("<call>" + utils.quotemeta(callSignature) + "</call>", "i"), callResult)

##
# string _getReply (string user, string msg, string context, int step, scope)
Expand Down Expand Up @@ -544,6 +637,8 @@ class Brain
# string[] botstars, int step, scope)
#
# Process tags in a reply element.
# XX: All the tags get processed here except for <call> tags that have
# a separate subroutine (refer to processCallTags for more info)
##
processTags: (user, msg, reply, st, bst, step, scope) ->
# Prepare the stars and botstars.
Expand Down Expand Up @@ -746,38 +841,6 @@ class Brain
reply = reply.replace(new RegExp("\\{@" + utils.quotemeta(target) + "\\}", "i"), subreply)
match = reply.match(/\{@(.+?)\}/)

# Object caller
reply = reply.replace(/\{__call__\}/g, "<call>")
reply = reply.replace(/\{\/__call__\}/g, "</call>")
match = reply.match(/<call>(.+?)<\/call>/i)
giveup = 0
while match
giveup++
if giveup >= 50
@warn "Infinite loop looking for call tag!"
break

text = utils.strip(match[1])
parts = text.split(/\s+/)
obj = parts.shift()
args = parts

# Do we know this object?
output = ""
if @master._objlangs[obj]
# We do. Do we have a handler for it?
lang = @master._objlangs[obj]
if @master._handlers[lang]
# We do.
output = @master._handlers[lang].call(@master, obj, args, scope)
else
output = "[ERR: No Object Handler]"
else
output = "[ERR: Object Not Found]"

reply = reply.replace(new RegExp("<call>" + utils.quotemeta(match[1]) + "</call>", "i"), output)
match = reply.match(/<call>(.+?)<\/call>/i)

return reply

##
Expand Down
Loading