https://rogersm.net/posts/developing-a-go-bot-embedding-ichiban-prolog/ Roger Sen Roger Sen exploring area * * * Home * All Posts * About me * Setting your mac serial number 23 Sep 2024 Developing a go bot embedding ichiban Prolog Ichiban Prolog is a #golang implementation of an ISO compatible Prolog. To learn how to embed it, I decided to add Prolog support to Hellabot, a simple irc bot. Hellabot design requires to program its triggers using a two step process as described in the code below: type Trigger struct { // Returns true if this trigger applies to the passed in message Condition func(*Bot, *Message) bool // The action to perform if Condition is true // return true if the message was 'consumed' Action func(*Bot, *Message) bool } Because the code to develop the trigger is in go, it requires to re-compile the bot every time a new trigger is created/updated. Because of this, embedding a language that can read the code externally is an attractive solution. In this case, I have selected Prolog due to being a flexible, high level language, extremely well documented and ideal to parse text using Prolog's DCGs. Our strategy to remove the recompilation dependency, we will update bot to read Prolog code from a file that can be edited any time by the user. In this post, the Prolog code will be loaded when Hellabot triggers the even on message reception. This will allow the user to change the bot's logic without the need to recompile and redeploy the bot. To make this job easier, we first simplify the trigger struct to have a single function that will execute the Prolog code. This new Exec() function will trigger the condition and execute the action in a single step. To make this possible we create a new struct to be used for Prolog triggers: type PrologTrigger struct { // Returns true if the Prolog code executes correctly Exec func(*hbot.Bot, *hbot.Message) bool } The first version of the code implements a extremely simple logic. PrologTrigger's Exec function will: 1. Instantiate a new ichiban Prolog interpreter. 2. Share the irc message to the interprter. 3. Load the external file with the irc message terms generated in # 2. 4. Execute the Prolog clause to execute the script. 5. Print the clause output to the irc client (using Hellabot API). Each step has it's own interesting challenges: 1. Instantiate a new ichiban Prolog interpreter This is probably the easiest step, because it just requires to instantiate ichiban Prolog runtime without any need for it to read/ write from any descriptor. var PrologTrigger = PrologTrigger{ Exec: func(irc *hbot.Bot, message *hbot.Message) bool { plRuntime := Prolog.New(nil, nil) [...] } } 2. Share the irc message to the interpreter For the Prolog code to have access to the *hbot.Message provided by Exec(), we need to make it available to the Prolog code. The easiest solution is to add the Hellabot message as Prolog terms that can be accessed by the Prolog code. This simplifies the code substantially, and even if it does not allow the code to access the Hellabot api directly, this is not a drawback, because the trigger is fired for each message and the message does not change during the code execution. For this we transform the different fields of the *hbot.Message to Prolog terms using a function called MessageToTerms() and store its results in a string to be loaded by the next step. message_command(""). message_content(""). [...] message_from(""). message_to(""). 3. Load the external file with the irc message terms generated in #2 To load the Prolog code, we decide for the Hellabot to load the code from a hardcoded file (bot.pl) stored in the same directory as the bot's binary using the function readPl. Once read we'll combine the message terms and the code to feed ichiban's runtime Exec() var PrologTrigger = PrologTrigger{ Exec: func(irc *hbot.Bot, message *hbot.Message) bool { [..] plCode := MessageToTerms(m) plCode += readPl("./bot.pl") if err := plRuntime.Exec(plCode); err != nil { irc.Logger.Crit("Prolog exec", "err", err) return false } [..] } } 4. Execute the Prolog clause to execute the script For this sample, we have an extremely simple Prolog code that is our bot.pl file bot(Output) :- message_command("PRIVMSG"), message_content("-Prolog"), Output = "This is the Prolog message". the bot term checks that the message command is "PRIVMSG" and the message content (the text received) is "-Prolog". If this is correct, Output will be unified to the string "This is the Prolog message". To start the code in bot.pl we exect the clause Bot(Message) using Hellabot Query() function. var PrologTrigger = PrologTrigger{ Exec: func(irc *hbot.Bot, message *hbot.Message) bool { [..] sols, err := plRuntime.Query("bot(Message).") if err != nil { irc.Logger.Crit("Prolog query", "err", err) return false } defer sols.Close() [..] } 5. Print the clause output to the irc client (using Hellabot API) Once we have queried the Prolog runtime we need to retrieve the message we want to send back to our client using the ichiban API. Because in Prolog we can return multiple solutions, ichiban designers decided to mimic sql go api, forcing us to loop through a set of solutions. In our code we will have a single solution "This is the Prolog message", but in case we had multiple ones the for sols.Next() { [...] } loop would ensure we capture all of them and return it to the client via the call irc.Reply(). var PrologTrigger = PrologTrigger{ Exec: func(irc *hbot.Bot, message *hbot.Message) bool { [..] for sols.Next() { var returns struct { Message string } if err := sols.Scan(&returns); err != nil { irc.Logger.Crit("Prolog scan", "err", err) return false } irc.Reply(m, returns.Message) } [..] // Additional error checks } } Implementing these five simple steps allow us to have a some code that covers our expectations. Alternatives and improvements The current solution is an easy way of integrating ichiban Prolog without having to develop code to enable Prolog to call go (see this code). There are some possibilities worth exploring to improve the code: 1. One would be to reduce the impact of setting up a new Prolog interpreter for every trigger: we can use a single interpreter for the whole program and serialise the access to it. Alternatively, using a pool of interpreters that are pre-initialised will surely reduce the Prolog.New(nil, nil) call. This will be left for a future post, because we need to describe the impact to the bot semantics. 2. Another option would be to move more code to the Prolog code, simplifying the go code and further reducing the need to recompile the bot when some changes are needed. Options to move more code into Prolog would allow us to develop something like this: :- set_Prolog_flag(double_quotes, atom). % message(?Command, ?Content, ?Raw, ?TimeStamp, ?To, ?From) % message retrieves the fields from Hellabot *hbot.Message % send_irc_message(+Text) % sends a Text to the irc connection. bot :- message("PRIVMSG", "-Prolog", _, _, _, _), send_irc_message("This is the Prolog message"). this would require to register a new predicate message/6 to access the message data and another predicate send_irc_message/1 to send data. This will be also left for a future post. Categories development go ichiban post prolog (c) 2020 Roger Sen