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

Python object macro support #9

Open
kirsle opened this issue Oct 19, 2016 · 2 comments
Open

Python object macro support #9

kirsle opened this issue Oct 19, 2016 · 2 comments

Comments

@kirsle
Copy link
Member

kirsle commented Oct 19, 2016

It'd be nice if rivescript-go were able to parse and run Python object macros for RiveScript bots, by using the Python C API.

There are two projects I found so far: sbinet/go-python and qur/gopy. They both only support Python 2 so far, but that will work for now.

I did some experimenting and came up with the following Go code that demonstrates the key pieces of functionality needed: dynamically parse a Python function, call the function giving it an array of string arguments, and retrieve the result of the function as a Go string.

package main

import (
    "fmt"
    "github.com/sbinet/go-python"
)

func init() {
    err := python.Initialize()
    if err != nil {
        panic(err.Error())
    }
}

func main() {
    // The source code of the python function we wanna be able to call.
    pycode := `
def test(rs, args):
    print "Test works"
    return "Forwards: {}\nBackwards: {}".format(
        " ".join(args),
        " ".join(args[::-1]),
    )
`

    // The []string to use as the 'args' param to `def test()`
    args := StringList_ToPython("Hello", "world")
    defer args.DecRef() // Always do this so Python can count references well.

    // To load the function you can simply eval the code in the global scope:
    python.PyRun_SimpleString(pycode)

    // Get the main module's dictionary so we can get a reference to our
    // function back out of it.
    main_module   := python.PyImport_AddModule("__main__")
    main_dict     := python.PyModule_GetDict(main_module)
    test_function := python.PyDict_GetItemString(main_dict, "test")

    // The tuple of (rs, args) arguments to pass to the function.
    // This tuple is the *args in Python lingo.
    test_args := python.PyTuple_New(2)
    defer test_args.DecRef()
    python.PyTuple_SetItem(test_args, 0, python.Py_None)
    python.PyTuple_SetItem(test_args, 1, args)

    // Call the actual Python function now. Functions return a *PyObject, and
    // we can cast it back to a string.
    returned := test_function.CallObject(test_args)
    result := python.PyString_AsString(returned)

    // Print the result of the function.
    fmt.Println("Result:", result)
}

// StringList_ToPython is a helper function that simply converts a Go []string
// into a Python List of the same length with the same contents.
func StringList_ToPython(items... string) *python.PyObject {
    list := python.PyList_New(len(items))

    for i, item := range items {
        python.PyList_SetItem(list, i, python.PyString_FromString(item))
    }

    return list
}

This code lets us:

  • Dynamically provide Python source for a function definition.
  • Prepare the Python *args tuple, converting Go strings into Python strings for the args argument (which is a list(str) type)
  • Call the function and gets its (string) result.

The other huge TODO is the rs parameter to the Python function: the above code sends in a NoneType, but IRL it would need to provide an object with at least a subset of the RiveScript API (most importantly, functions like rs.current_user() and rs.set_uservar() & friends). I imagine to expose the full RiveScript API to Python I'd need to write a whole wrapper class that translates all the arguments to/from Python types, but I'll probably just focus on the aforementioned useful functions to start out with.

@rmasci
Copy link

rmasci commented Jun 21, 2022

Wow. This is old but I overcame this issue five years ago -- sorry I didn't see this until now. My rivescript looks like this:

+ tell me what time it is
- LocalCommand "date"

+ run a custom script to get * from the database
- LocalCommand /var/tmp/bin/mycustomscript -u root -d Opetion1 -m <star1>

When the chatbot sees the first word of the response is a LocalCommand (You can change the name of this keyword in the settings file). It knows to exec everything after this. THis can be easily executed a better way than I did originally by using github.com/bitfield script:
<determine if firstword is 'LocalCommand' then run:

cmd := strings.Split(returnMessage, " ")[1:] // get everything after LocalCommand
returnMessage ,err:= script.Exec(cmd).String()

This allows my chatbot users to create commands in ANY language they prefer, or use standard Linux commands. I also have a version of this for 'RemoteCommand' that executes them on other servers using SSH.

@kirsle
Copy link
Member Author

kirsle commented Jun 21, 2022

@rmasci I've done similar before, like this example to run Perl object macros for the Python version of RiveScript: https://github.com/aichaos/rivescript-python/tree/master/eg/perl-objects

The various RiveScript versions have a SetHandler() function to register custom programming language hooks for non-native languages (up to the programmer to implement how those hooks work). The Python module also has a JavaScript example for bots that run out of a web browser environment (so the JS macros are run using embedded <script> tags in the user's own web browser): https://github.com/aichaos/rivescript-python/tree/master/eg/js-objects

Being that Go sits at a close level to C and can embed CPython (or Perl or other C-based languages) it may be possible to get a wide variety of languages "natively" supported without shell commands calling out to third party scripts to bridge the gap. Though on that latter idea, I was once playing with the idea of defining a 'standard' interface for all RiveScript libraries to be able to interact with any third party language (e.g., by a standard format for input/output similar to the CGI standard for web scripts) -- with the idea that hacks like that perl-objects example didn't need to be done in an ad-hoc basis by individual developers but that somebody could take a Perl wrapper (e.g.) and use it on any of the 5 RiveScript libraries with built-in support for the protocol.

In more recent years I have some further ideas that could be used:

  • Google's Starlark language is a Python-like syntax implemented in native Go: https://github.com/google/starlark-go It's not exactly Python but is familiar enough for users who would like to program object macros in Python.
  • Traefik has developed an interpreted version of Go: https://github.com/traefik/yaegi Currently RiveScript-go can't have > object * go in-line macros because Go can't evaluate itself at runtime and Yaegi seems to be at least a 99% compatible Go interpreted language which could support in-line Go object macros.
  • Goja is a better JavaScript interpreter for Go than otto is and supports a lot of ES6 syntax: https://github.com/dop251/goja RiveScript-Go currently uses otto for its JavaScript interpreter, but it's only old ES5 syntax and quirky at that (not all ES5 compatible code works with otto - try changing the type of a variable and you get a runtime exception, try initializing a variable as null and then assigning a type later, etc.).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants