Last month, I launched Eliza, a chatbot modeled after the classic MIT conversational program and written entirely in Ink. Though the project itself isn’t novel or interesting – it’s mostly a port of existing implementations of ELIZA – Eliza is built as an isomorphic Ink application, meaning a substantial part of the codebase can run both natively, on a server, and on the client (a Web browser).
See Eliza on GitHub Try Eliza →
For an app like Eliza, where the critical business logic (the code for generating a response to a user query in this case) is independent of a particular runtime platform, writing Ink programs that can run anywhere is useful. It means we can write one implementation of some logic and literally reuse it in many different clients. In fact, the Ink standard libraries (
In this post, I want to give a brief overview of the Eliza project and share the process of building a real-world isomorphic app in Ink.
Eliza, a chatbot written in Ink
The original ELIZA was one of the first “chat bots”. Invented in the MIT AI lab in the mid-60’s, it demonstrated how a simple algorithm based on matching patterns in language against a database could emulate reasonable human conversations, and became one of the first candidates for real “Turing tests”.
Eliza works by taking a line of input from the user and breaking it down into meaningful recognized pieces by running through a list of predefined rules called a “script”. Here’s a sample of the “doctor” script, which is one of the most popular scripts for Eliza. It’s the script that the Ink version also happens to use.
key: if 3 decomp: * if * reasmb: Do you think its likely that (2) ? reasmb: Do you wish that (2) ? reasmb: What do you know about (2) ? reasmb: Really, if (2) ? key: dreamed 4 decomp: * i dreamed * reasmb: Really, (2) ? reasmb: Have you ever fantasized (2) while you were awake ? reasmb: Have you ever dreamed (2) before ? reasmb: goto dream
In general, a script file works a little bit like regular expressions. Each rule matches against a specific pattern, like
if 3, and generates a list of possible responses by randomly selecting from the “reassembly” options outlined in the script, filled in with pieces of the user’s original question or prompt.
I ended up wrapping the conversational bot within a chat app-like interface, which you can try here.
Eliza’s algorithm is an interesting demonstration of the simplicity of most of our human conversations, because it often generates plausible responses (especially with more extensive predefined scripts), but there are also very clear limitations. For example, it can’t generate completely original responses that aren’t in a script, the way that modern language models easily can.
The Ink implementation of the Eliza algorithm is a modified port of a Python implementation at gezhiggins/eliza.py. Because semantics of Python and Ink are quite similar (they’re both dynamically typed interpreted programming languages with objects and maps at core), it was straightforward to port a Python implementation into Ink. The algorithm works by first parsing the script file into a structure optimized for lookup, and then taking in one line of input at a time to generate potential responses. After a short conversation, if the user goes off topic into something the algorithm doesn’t understand, there’s even a memory-like mechanism to bring up previous topics to get back on track.
Of course, the most interesting part about Eliza is not the algorithm itself, but the fact that it runs on both the server and the browser.
Making isomorphic Ink possible
Torus ❤️ Ink
Torus, like React, allows a program to define a user interface declaratively as a function from data to a tree of components. Both libraries have renderers that take the produced tree of components and compare it with the previously rendered tree to produce a list of DOM manipulations that they make to update the view. Torus’s renderer is much simpler in design that React, and it’s called
Torus.render. Every UI component in Torus internally depends on
Torus.render to take the virtual DOM produced and render any changes to the page.
To build Eliza, I borrowed this renderer layer from Torus by importing the Torus library onto the page, and wrote a small adapter library called
torus.js.ink that provides an idiomatic Ink API for the Torus renderer.
With the adapter, getting a basic web UI running as simple as the Ink program
` find an element into which we render the app ` root := bind(document, 'querySelector')('#root') ` define the app as a function of state => DOM tree ` App := state => ( update := Renderer(root).update ` we can call "update(vdom)" to re-render the app ` update( h('div', ['app'], [ str('Hello, World!') ]) ) )
Renderer Ink function wraps Torus’s
render function so that calling
Personally, this is by far the coolest part of Eliza. Torus and Ink are two of my biggest side projects, and to see them fit and work together like this through an Ink-based compiler is a holy grail moment.
I enjoyed building Eliza across native and Web, but the overall experience of building a Web app in Ink still has lots of shortcomings. Two of the most important missing pieces are both shortfalls of the September compiler itself: module loading and source maps.
Eliza was a cool project for me in many ways. I think it looks slick, and it works well as a chat interface. But my favorite part about Eliza is obviously that it marries two of my longest-running projects, Torus and Ink, into a good developer experience for me. And now that the floodgates to Ink on the web has officially opened, I’m excited to build many more projects on the Web with Ink going forward.
← Assembler in Ink, Part II: x86 assembly, instruction encoding, and debugging symbols