aichaos / rivescript-js

A RiveScript interpreter for JavaScript. RiveScript is a scripting language for chatterbots.
https://www.rivescript.com/
MIT License
377 stars 145 forks source link

How can I use 'object' to call a javascript function that's in another file? #258

Open AndrewYatzkan opened 6 years ago

AndrewYatzkan commented 6 years ago

Hello! I'm trying to use a joke API to have the bot send a joke when the user asks for one, but I'm running into a problem. This is the code I use to get the joke, then set the joke as a variable for the bot.

function getJoke() {
const fetch = require("node-fetch");
const url =
  "http://api.icndb.com/jokes/random";
fetch(url)
  .then(response => {
    response.json().then(json => {
            var joke = `${json.value.joke}`
            talkback.setVariable('joke', joke)
    });
  })
  .catch(error => {
    console.log(error);
  });
}
function getJoke() {
const fetch = require("node-fetch");
const url =
  "http://api.icndb.com/jokes/random";
fetch(url)
  .then(response => {
    response.json().then(json => {
            var joke = `${json.value.joke}`
            talkback.setVariable('joke', joke)
    });
  })
  .catch(error => {
    console.log(error);
  });
}

I'm doing this in an external javascript file and not as a rivescript object because I need to use talkback.setVariable('joke', joke) to set the variable, and talkback isn't defined in the rivescript file.

I've tried many methods but I haven't found out how I can use the same function, or variable, across both the files. I've spent a long time on this, but I'm assuming it's a simple tweak that will make it work. Thanks!

kirsle commented 6 years ago

Does require work inside the >object?

AndrewYatzkan commented 6 years ago
> object makeJoke javascript

const main = require('main.js');

console.log(main.test);

< object

just returns [ERR: Error when executing JavaScript object: Cannot find module 'main.js']

kirsle commented 6 years ago

Ah - that sounds familiar so I went searching and found issue #47 where I looked into this before.

RiveScript loads the >object by using eval(), so the object runs from the context of the rivescript/lang/javascript package, and import paths had to be relative to there... like doing require "../../local_module"

There might be an elegant workaround by messing with Node's import paths for where it searches for modules, but it would take more experimenting. You can pass a scope in to the reply function (like this) and an object can access local variables on that same this as a way to make some dependencies available to it as a possible workaround.

AndrewYatzkan commented 6 years ago

I'm probably missing something, but in order to use the reply function you must use bot.reply('local-user', 'message'), but bot isn't defined in the rive file

kirsle commented 6 years ago

Object macros are allowed to use Promises if you invoke the bot's reply with the replyAsync() function instead of reply(). Here's an example. You use the resolve() function of the promise to return reply text to be used in place of the <call> tag in the response.

Source code of an object macro (whether the body of a setSubroutine() or defined in RiveScript in an > object):

return new Promise(function(resolve, reject) {
    // do something async
    setTimeout(function() {
        resolve("here is the reply!");
    }, 1000);
    // reject("an error reply instead on error");
});

With this RiveScript code:

+ test
- The answer is: <call>objectname</call>

the bot should eventually reply with "The answer is: here is the reply!"

The application running the bot would have to use replyAsync() for it to support the promise; using reply() would have the <call> tag just substitute an error message (it would still execute the code, but it has no way to wait for the resolve() answer, so the <call> tag gets the error message as a placeholder instead).

var bot = new RiveScript();
bot.loadFile("test.rive");

// says something like "The answer is: [ERR: can't run async object: use replyAsync instead]"
var reply = bot.reply("user", "test");

// works as intended
bot.replyAsync("user", "test").then(function(reply) {
    console.log("Bot>", reply);
});
AndrewYatzkan commented 6 years ago

Thanks for the help, and I'm sorry for taking your time, but if you can't tell I'm pretty new at this, and I'm sort of lost. I tried changing reply to replyAsync in return e.message.channel.sendMessage(talkback.replyAsync("local-user", triggr)), but it just returns [object Object]. I can send you the code if that would help, but if that takes too much time I completely understand

kirsle commented 6 years ago

replyAsync() returns an object of type Promise, which isn't directly useful as a return value. You just have to invert your function calls.

talkback.replyAsync("local-user", triggr).then(function(reply) {
    e.message.channel.sendMessage(reply);
});

Async in JavaScript allows the interpreter to do multiple things at the same time, by letting one task defer itself and come back with an answer "in the future." The reply() function is synchronous, which means that while reply() is busy searching the bot for a reply, your JavaScript app isn't able to handle any other tasks at the same time, for example it can't answer other people while it's busy answering one person. The other incoming messages have to wait their turn. With replyAsync() it allows the JavaScript engine some time to do other tasks while it waits for a reply. When the reply is ready, the .then() handler can be called and you can send that reply to the user who requested it.

A lot of programming languages are synchronous by default (examples: Python, Perl, C/C++), if you have a function that has to do work that takes 5 seconds before coming up with an answer, your entire program "blocks" on that function for the whole 5 seconds and it can't do anything else during that time. Languages can work around it by using threads or multiple processes, but JavaScript is single-threaded and can't do true multitasking. Instead it has its async model where a function can voluntarily give control back to the interpreter to do other things while it waits. This is most often done with I/O bound tasks, like waiting to read from a file on disk, or waiting for a network response to come back, things that can take a long time from a CPU perspective and which would just waste the program's time if it needed to wait around for it.

AndrewYatzkan commented 6 years ago

Now the bot runs as it originally did but with replyAsync instead of reply, but

+ test
- <call>makeJoke</call>
> object makeJoke javascript

return new Promise(function(resolve, reject) {
    // do something async
    setTimeout(function() {
        resolve("here is the reply!");
    }, 1000);
    // reject("an error reply instead on error");
});
< object

just returns [object Promise]

kirsle commented 6 years ago

It might need to use rs.Promise instead of Promise... when it was first implemented Promises weren't widely supported so rs.Promise was a polyfill, but, the code might be expecting the polyfill. :cry:

AndrewYatzkan commented 6 years ago

That did the trick, but now how can I use a variable from an external javascript file in the resolve()?

hisRoyalty commented 2 years ago

@AndrewYatzkan was this solved?

kirsle commented 2 years ago

@hisRoyalty @AndrewYatzkan

A couple ideas: if trying to import a module with require() from inside a > object declaration in RiveScript, the import path may need to be modified because the macro was evaluated from the context of the rivescript/lang/javascript module so it needs more "../../" path components in front to get back to your app's directory. This is mainly only an issue when using the > object syntax in your RiveScript file.

Alternatively you can declare the object macro from your JavaScript app using setSubroutine() and then normal scoping rules apply and it can access any variables in scope:

const RiveScript = require("rivescript"),
    fetch = require("node-fetch"); // <- module you want to use

let bot = new RiveScript();

bot.setSubroutine("makeJoke", function(rs, args) {
  const url =
  "http://api.icndb.com/jokes/random";
  fetch(url) // <-- use it here, it's in scope
  .then(response => {
    response.json().then(json => {
            var joke = `${json.value.joke}`
            talkback.setVariable('joke', joke)
    });
  })
  .catch(error => {
    console.log(error);
  });
  }
});

As a third option, the scope parameter to reply() allows you to pass an object that you want "this" to refer to from the context of your object macro. You can use this to smuggle variables in or allow the >object syntax to access properties and functions belonging to your program. The scope example shows this off: https://github.com/aichaos/rivescript-js/tree/master/eg/scope

I think something along these lines might work if you get clever with scope:

// In your bot program
let scope = {
    "fetch": require("node-fetch")
}

// Pass that scope in when you call reply()
let reply = await bot.reply(user, message, scope);
// In your .rive sources
> object getJoke javascript
    let fetch = this.fetch;
    return fetch('some url').then(res => res.json()) // or w/e
< object