 Hey there, so I'm Josh Adams from ElixirCypx and Icetope11. I'm Robbie Clements, and I've watched ElixirCypx, and I'm from Center Source in Nashville, Tennessee. And we're going to talk today about building a visual programming language. In this case, it's for building chatbots. In a more general sense, it's about building a business rules engine. So we chose to do it in the domain of chatbots because it's an easier domain to get your head around, and it has not a lot of complexity. Giving talks is always a little bit stressful, we found it useful to do breathing exercises. So we wanted to start out by sharing some that we found helpful and I promise this is not a soft talk, we don't do that, there's lots of code, just bear with us. So just go ahead and just clear your mind and try to repeat the exercise you see on the slides. So first, just breathe in and out really deeply for a bit, and then try to breathe just a bit more rapidly. And then finally, just exhale as hard as you possibly can. And so now you've achieved Zen, and you're ready for a talk. Okay, dope. So the structure of our talk closely resembles the structure of our project. So the concept is being able to change runtime behavior of your system through a visual programming language. Generally these are called business rules engines or production systems. The AST and its representation, this is a description of the types of programs we support building with this particular language. The compiler which turns an AST into a function, there's the front end which is an angular JS based browser application that lets you review and modify rules and store them in the back end. There's the API which is the component that allows storing and modifying the rules and it interfaces with the system that ultimately these rules are being run against. And then there's the demo where you decide to ask for our autographs. Okay, so to start things off, we'll just discuss this at a high level concept. At a high level it's just a business rules engine. So why do people use a business rules engine? Well, typically the idea is that your application is one relatively static concept and there's some configuration that can be filled with to make it behave differently. This configuration is the business logic. This way the application doesn't need to be changed or redeployed just to support tweaks to the business logic. And this helps support the core idea that things that change at different paces, they should be separate things. So that business logic, it should cover a bunch of things like the percentage of sales tax to charge based on the location of a purchase. Additional questions necessary to gather more information to make better decisions later on in an application. And suggestions of classification of a given domain in any based on some of its attributes. So an example of this one is we're doing a project where our client needs to categorize doctor visits and support ICD-10 medical coding based on the information they know about the doctor visit. So sometimes the person that's writing the rule knows that in this case, if we had like these other two questions answered, we would know enough information to make a suggestion. So they can write a rule that says, okay, if this is the case, then return a couple of questions we need asked. In other cases, they might know everything but there's two or three suggestions that they can make. And so they might want to build a rule that says, okay, in this case, I know to code it this way. And over time, business analysts can categorize more and more visits correctly. So without changing the application, the application gets better, right? So what is a visual programming language? So here's a screenshot of Scratch. This is a program built in Scratch. Scratch is easily the most popular visual programming language. I'm sure most people think of it when someone says this. Kids use this to make games without knowing how to program, except for it really is knowing how to program, right? This is a good way to learn it. Here's an example of a rules engine built in exactly the style that we're working on that's a bit further along. So it's not quite as pretty as Scratch yet and you can't really see it so easily on this screen with that washout. But this is the spiritual successor to the Angular app that we're showing in this talk and sort of the conceptual successor to the electric piece. So one category of rules engines involves non-programmers changing the runtime behavior of the system. Well, we built a custom visual programming language that allows you to build out the chatbots easily and modify their behavior at runtime without knowing how to program. So it's also impossible to construct a program without syntax errors in our program, which is a nice additional feature. So to support this, we define a custom AST that represents the rules where rules are really just tiny programs. They either return something or they don't. Okay, so we've mentioned the word AST a couple of times already and when I've talked about this with people, sometimes they ask me, what is that? So like all good programmers, you might go to Google to find out. So Google's great. Let's just click on that first result here. Yep. All right, so AST stands for Aspartate Amenote Transference, which is, right. So I think this is wrong. So we'll try again. Maybe it's the second result. Okay, so it's an abstract syntax tree. So this makes more sense, right? So here we have a tree that represents a program. You can't see my pretty line so well. That represents the structure of a program in an orderly fashion. So this might be the result of parsing a text-based language, right? In our case, since we're a visual programming language, you're just actually directly manipulating the AST of the program when you're building your rules. So here's an example that says, if the variable input contains the string table flip, then evaluate a response expression with the input awesome guy flipping a table out of joy. Anyway, so to actually run these rules, we go through a few transformation steps, which isn't that surprising since that's all that programming really is, right? Transforming inputs and outputs. So in our case, our input at the beginning is some JSON that is a slightly more verbose version of our idealized AST, and then we transform that into our AST. Then we transform that AST into Elixir's AST, your quoted forms. And finally, we compile that into functions that are ultimately run in processes by a further part of the system. So here's the end goal. So editing a program in the browser in a tactile way. So this AST is just a thing that checks for that rule we talked about earlier. This is a very tiny programming language, but the core idea is solid and can be expanded upon. So let's talk about how it works at runtime. At runtime, we have a process that represents a collection of rules, and it sent a message every time a document is evaluated against the rule set, and it returns all the responses that its rules returned. So during runtime, those rules can be changed, and when that happens, we just swap out the process of state that's holding the list of rules for that chatbot. All right, so now that you understand how this works holistically, let's get into the details. We're gonna look at the AST and its representation. So this is really comprised of two parts. Our language is idealized AST and the JSON representation. So we have to have both parts because we're choosing to transmit the AST by JSON. So if we use something like BERT or Transit or protocol buffers to transfer the AST, we could conceivably avoid the second part, the second representation, but we didn't. So, okay, so here are the tight specs for our AST. Essentially, an expression is a two tuple where the first element is the type of an expression, and the second element is the list of arguments. And these are the expression types that we support. So we have an if statement or an if-else statement, and it works just like you'd think. The first argument is just used as a conditional, and the second is the then branch. So then the third, if there is one, is the else branch. Then we have a var and it can fetch a variable out of the execution context. We put the message, the rules are run against into the input variable. So far, this is the only valid variable that we have right now. And then we have the contains. It just returns a boolean that says whether or not the first argument contains the second argument. And then the response just builds a response truck, which is defined in our code. Right now, this is all that our language defines. It would be really easy to add more general or special features as we wanted to. But as we mentioned, our AST, it can't be represented natively in JSON. Among other things, there are no atoms or tuples in JSON. And consequently, we define a representation of it that is JSON compatible. So here is how our AST comes from the front end in the JSON representation. And then we have a function called convert that takes in a string representing the JSON and outputs a two-tuple representing our AST. And here you can see the input and the output to the function for an example. And here's the implementation. So it all fits on one slide. Basically, we pattern match against different potential inputs. And then we return our idealized AST representation of the input. Okay, so now we have our AST. So the next thing is to transform it into valid elixir-quoted forms. So ultimately, you might consider compiling this into modules if you actually have a system that needs to go fast. It's a lot faster than interpreting quoted forms. You might expect. But for our use cases in this demo, this is plenty fast, so we're not doing that. The transform is a module that takes in our ideal AST and outputs an elixir AST. So we'll look at an example. And it's not the easiest thing to read there, but that's our table flip quoted form, our AST on the left. And then on the right, there's the elixir-quoted form that would represent sort of the program that that AST represents. And we have a test that just verifies that that works. Anyway, so here's the implementation. Very similar to what you saw before, except for this time we're using like unquote unquote. So we're just pattern matching on the expression type, and we're using quote and unquote to build out our quoted forms. This is obviously recursive, so for each argument in each expression, we generate elixir for it until we get to the leaf nodes and don't have to do it anymore. And then here's the implementation for var, response, true, false, binaries, and contains. So basically this is two slides to show everything that that consists of. It's not very complicated code. Okay, so now we have our elixir-quoted forms and we want to evaluate those against our input string. So the compiler is a tiny, tiny module that just returns a function that takes an input and returns the rules output given that input. So it receives our AST as its input and then it returns a function that evaluates the quoted forms that the transformer generates for the AST and it binds the input variable into its context so that we can get that var out. So we'll see what that looks like. And again, this is super tiny. So on the top left, we just have a test that takes that JSON table flip AST, compiles it down, and then asserts that we have a function that operates like we wanted it to. And then on the right, you can see the actual implementation which is literally just generate the quoted forms, call eval quoted on them, and then return the response. So that's the core of the pure functional part of the system. Next, we have a chatbot gen server. So this is an abstraction around a list of rules. In our current implementation, we aren't threading any state through this so it's not technically necessary for us to have a gen server here. But this would be the place that if you were building a system that built up state as rules were evaluated, this would be the place you would do it. So a chatbot just consists of a name and a list of rules and they run in their own processes. So they handle evaluating a given input against their list of rules and collecting the results to return them. And here's the type for a gen server state. As I mentioned, it's just a name and a list of rules. A rule can be one of two things. It's either a module name that has an apply function which takes a message and returns a response or it's just a function that takes a message and returns a response. And this was just basically for some prototyping I was doing. It could be just one or the other. I had some fun with this type spec because I'm pretty sure this is the first time I did a type spec that specged an anonymous function based on its input and output types. Anyway, and this is the public API for evaluating message. So this also includes the gen service handle call for that case. There's something kind of clever happening here and I wanna point it out. I don't know how clever it is but I enjoyed it the first time I sort of came up with doing it this way. Anyway, so this is the first case where like protocols made me super happy. There are other ways that we could do this thing but this makes me happy. So you can see here that we're calling the apply function for each of the chatbot rules and we're doing that inside of a for comprehension. So applying the rule is done differently depending on whether it was a module or a function but that's not really important. But the into key says that for our for comprehension we're gonna take the response of the for comprehension and put it into the chatbot DSL response. And so we do that with the collectible protocol. And so I just wanted to show the implementation of that for response. So here's what the response structs type looks like. It has a from string and we're using this to fill it with the chatbot's name and then there's a list of messages. And so this is super easy. So let's look at the implementation of the collectible protocol. So first things first at the bottom of the actual structs file I defined the collectible implementation and here collectible all it needs is an into function to be defined. And so I just delegate it to the response module like the module this is for. I've enjoyed doing it that way. I played with the like putting the implementation in the def-imple here. You may not prefer just doing a delegate but I enjoy it. Then let's look at the implementation of the into function because that's the important part. So it looks like a big function but that's just because there's lots of comments. So we'll walk through the bits that matter. So here and is that readable? I think it is but I'm not back there. So basically in the event that we return a nil in the inside of the for comprehension then we just basically don't do anything with it. We don't collect it into the response. This acts like a filter, not a filter of what you're comprehending across but a filter on responses. And this is just nice because this is the place that the nils go away. So in Ruby if I were doing something like this I might like have a big array and it might have some nils in it and I might call compact on it and then later on I might have someplace else that hadn't called compact on it so I might just litter compact throughout my code because that's how you solve the problem. But this is the only place this can happen so I never will have to worry about nils past this point. Anyway so I don't know this makes me happy a little bit. It's a tiny filter but it's fun to do it here. Anyway enough of that. So if we're collecting a message we put it into the list of messages. So this is sort of the field in the struct. This is where in some more interesting rules engines you might collect different types of return values into different fields in your struct so maybe you're building a more comprehensive response type and so maybe depending on the return value you put it into different positions in the structure that you're returning. Anyway in the medical coding engine for instance we might return additional questions those would go somewhere. We might return guidances they might go somewhere else. We might return ICD-10 codes. So here they're all just messages so we just collect them in the one spot. Anyway so you also could imagine that maybe we have since this is a chat bot thing maybe we wanna implement something that has some state so like maybe you're incrementing a counter based on giving kudos to a person in the room and so that would just be a response type and then the server would handle doing something with that response type and that would just be in a different field I would assume. Anyway so this is the next bit of the collectible protocol. This is basically we're done so here we're just returning the source now that we've collected stuff into it. You might use this for like doing some cleanup before you actually returned. I haven't found a case where I needed to do that but it seems fair like if you're collecting across something that has some open port or something. And then you also have to handle the case with the collectible halts. I also haven't had to deal with this ever but if you're gonna show it for completeness sake so you can actually implement this. Anyway so that's that. All right so now we have the ability to turn our AST into functions that do whatever we want. And now we need to make it easy to build out various ASTs and ultimately store them in a persistent store. Right now we're just gonna talk about the front end for a minute. So we built our front end in Angular. It communicates with our Elixir API and it consists of two parts. The first part just allows you to visually manage your AST elements and then the second part allows you to create multiple chatbots and each chatbot having a list of rules that it adheres to that consist of our AST. And so this is where it all begins right here. This is our AST builder HTML view. One of the cool parts about building this in Angular if you guys have never used it before is directives. These are just markers that you can place on a DOM element and they tell Angular's HTML compiler to attach a specified behavior to that DOM element. So you can see on line three we're just calling our AST element directive that's defined in our AST builder controller. And here is where that is defined. In the controller we have this directive called AST element and it has a template specified on line 47 and it will render that template if it's ever called. And it also has a create block function on line 50 that expects a type that sets our scopes AST based on the type that it receives. So this is how you can turn a placeholder element into an expression of a given type. For instance, if it's past the string if it sets our AST to contain the string if with the empty arguments. And then if it's past the string contains it sets the AST with the type contains and arguments that specify a type of var that should be an input and a type of string that contains a static string as its arguments. And then lastly, if it's past the string response it just sets AST with a type of response that has arguments of a static response string. So also our AST element directive has a function that checks our AST and if it's empty then this is gonna return true and if it does then our AST element template gets rendered. This is specified in the directive that I just showed you. So everything inside of the NGF on line two will be rendered. Angular's NGF directive it just removes or recreates a portion of the DOM tree based on an expression. So if you notice we're calling that function AST as empty. So since our NGF returns true we render a dropdown menu with a button that calls our create block function. And that passes the expression you choose based on the selected value from the menu. So next on line 13 you can see we're calling another function named AST as if. So back in our directive we have that very function it checks our AST again and it's just checking it this time for the string if. And if it returns true our directive is called and it renders its template that's defined on line 35. And then here's that template. So if you'll notice on line four it's calling the AST element directive that we initially called to start this process. So this is where the nesting of the AST elements begin but we had an issue with Angular because Angular doesn't allow directives to recursively call themselves. So we had to add a library called Angular recursion I think and that had the functionality that we needed for this to work. So back in our AST element template on line 16 we're calling the function AST contains and our AST is contains. And if it returns true it renders its template. So by now you probably notice that we're just using the directives to pattern match based on what type of expression that this AST node represents. Same thing with our AST contains function. If it returns true this is the directive that will actually be called. The difference between this one is it's actually like setting the AST and the scope based on what input that the user inputs and just enters that into our AST. And just for good measure our AST contains template that's rendered. All right so I'm going to go back to our AST element template. We move on to line 16. We're calling the AST as response. And if it returns true then this directive is called. You see what's going on here. And here's its response function just checking the AST for the string response. And then this is its directive that gets called. And then this one just like the other one sets the AST based on what input that the user enters. And then finally our AST response template. So I want to go back here one more time just to reiterate on what's happening. This is our AST element template. We're using the directives names like AST, empty, AST if, AST contains and AST response to pattern match based on the NGF before each directive. So it will render its template based on the type of expression that the AST node represents. And the AST if directive just calls the original AST element to keep nesting and doing our pattern matching process. So the second part of the front end is pretty simple. It's just the crud interface that allows us to create our chatbots and interacts with the Elixir API. It's just basic crud implementation. But I think it's important just because it's the segue into the next section of our talk. You can just see we're making calls to the API to manage chatbots and then the same thing here just to create rules for the chatbots. So after talking about all these calls to the API let's have Josh talk about the API. So that's the front end. It interacts with the API. Our main rules engine that we talked about initially is a separate OTP application. And our API is just a small Phoenix application that interacts with our rules engine application. This is our database oriented representation of a chatbot. It's basically the same as you saw in the OTP app. As you can see, it's just a name and a list of rules. Every time a new one is added or the details change we restart the gen server associated with a chatbot. Here's the model we use to store rules in the database. They have an AST field where they can store the AST for the rule and they belong to a given chatbot, so nothing too fancy. Here are our routes. This is all very basic Phoenix and Ecto stuff. Just some nested routes in an API pipeline. So we have a chatbot's resource managed by a chatbot controller. Underneath it is nested a rules resource managed by a rules controller, rule controller. Anyway, I'm pretty happy with the integration test for the API. I used a method I learned from a blog post by Dan Swain. And basically this is just a support library that can interact with our API using an HTTP client during our tests and then writing our integration test becomes very easy. So this thing encodes our request on the way out so we give it a map or a struct it turns into JSON. It gets decoded on the way in and that way we can easily specify the endpoint and anything we want to post to it. Here I'm just testing the status code of responses. So this is making sure that we can create chatbots in our API and it's very, very easy to write this test, right? We also make sure that when we try to make a chatbot with an empty name it's rejected. Similarly for the rules endpoints. So in our setup I use the API to create a chatbot. I fetch its ID out of the response and then we post the rules endpoint to create new rules on that chatbot. And we make sure we can't create an empty rule and that we can update a rule. And so our conceptual model says that we're gonna be replying the chats but our rules engine doesn't need to know anything about how that happens, right? We're just responding with messages from our rules but they don't need to do anything at that point. So consequently we have the XMPP bits living in the API project. And so Hedwig is an XMPP client that Sonny Skragan wrote and we're using it to connect to an XMPP server when we start our API up. And all the messages that get returned from rules get relayed to this handler module and this is sort of how you can build bots with Hedwig. And so when we see a message we turn it into the message struct that our rule evaluator knows how to deal with. Then we use a function on the chatbot from the OTP app called ScatterGather. We send it out serially to each running chatbot gen server. And then we loop through all the responses that came back and we call handleResponse on them. So the way that ScatterGather works is the chatbots in their net for the gen server they register themselves with PG2 into a group named chatbots and then ScatterGather just gets the list of PIDs and asks them serially to evaluate the messages. HandleResponse just sends the responses message back to the XMPP server and it prefixes it with the name of the chatbot that the response is from. And this is primarily because I only actually have one chatbot connected, otherwise you would just use its nick, right? And in the API's model for the chatbot we define how to start the chatbot DSL's gen servers for the chatbots. So we define this ensure started function. It takes a chatbot model. If one's running, if a gen server for that model is running it stops it, it starts a new one and then it registers with a name so that later we can find it to kill it again. We could just replace it state but here we're actually killing it. And so anyway, we call this function from the controllers whenever a chatbot or whenever it's rules are updated. And anyway, so now we have a demo and I need to, let me see, I'll actually run it up there rather than mirror my screen if I can figure out how to, no I don't. Bull honky. Oh, here we go, there we go. Oh, come on, really? I believe in magic. Okay. All right, fine. I will do this. You can't beat me. I don't think you can beat me. Hold on a second, that's what I needed. Okay, here we go. So here, if I can find my mouse, maybe I should have mirrored the screen. I know, it's huge. Okay, so here's a list of our chatbots. We create these two when we start the app so like there, it's seeds basically. And so you can go and look and this guy has the table flip rule. So if we look at his AST, where is it? He doesn't have any rules. This guy has rules. I don't think the other guy have a rule. Let me just back out here. Okay, there we go. So here's his rule. It's really hard to read it. So this is basically responsive design in action. Okay, here's my input. So if the input can then stable flip, then he responds with this guy, right? Let me go ahead with that. Sorry. Okay, so that's that rule and let's see it in action, right? So I've got here I have a Jabber client that is connected to this thing. And so I can say things. First off, I've got one that sort of runs all the time. He's just an uppercaser. So that gets up cased. If I can say table flip, right? And that happens. So that's good. And then we've got another guy who's really macabre. So anyway, but let's actually go and add a rule. So we're gonna add a new chatbot for this. Oh, hold on. I have to type in here. Okay. So this is the cop. So we have a new chatbot down here called cop. Gonna make a new rule. And we're gonna say, if the input contains something, then we're gonna respond with something. And I know this is a very basic sort of language, but it is very flexible. We've got a much more complicated thing. So let me come in here and find my presenter notes, which are totally not showing up. That's fantastic. In a moment, this is super important that I get this right. Because he, bear with me. Computers, how do they work? Okay, I believe in miracles. Okay, so we're making this rule. So we're saying, all right, if the input contains, now one thing that's worth pointing out, because somebody's gonna wonder this immediately, the bots don't respond to each other. So I'm not gonna get it. We thought about doing that, but then it just infinitely loop possibly. It'd be pretty boring anyway. So if somebody flips a table in this room, the cops' responsibility is to fix it. Oh, that's wrong, hold on a second, gotta get rid of that extra stuff. Yeah, so that's his responsibility. So we'll save it, and we haven't restarted anything, right? We come over here and let's go ahead and flip a table. Yeah, all right. So that's a live demo of editing rules. Obviously this is just chatbots, but yeah, it's honestly super flexible. So let us go back to presenting here. Okay though, so does anybody have any questions about this? Finally, again, I'm Josh from I-11.com and from ElixirCypx, and there's my GitHub. I'm Robbie from Center of Sources. I think that's it, guys. Okay, thank you.