Conversational Graphs

Note: This is an archived post I recovered from my old blog.

This design is a theoretical solution for modeling complex conversational flows as directional graphs. Conversational graphs will be expressed in JSON using JSON Graph Format spec as a reference.

As a rule of thumb, conversational graph declarations should be serializable to JSON, portable, and contain absolutely no code. This reduces platform and language dependency and facilitates extendability.

Highly configurable

In theory, directed graphs can be arranged to satisfy virtually any conversational flow. Even parallel processes such as performing asynchronous requests while other flows are happening may be possible.

Even if in practice not all configurations were possible, graphs will be able to describe most conversational flows. These flows will not be limited to trees and can be arranged in a natural, non-linear fashion.

Graphs as modular components

Each graph can be connected to other graphs via abstract nodes (entry_node and exit_node) that can lead to other flows. This facilitates portability, modularity, and composability.

Graphs will be arranged as atomic, specialized flows that will be part of a greater graph that connects them together through their entry and exit nodes.

The following graph could be a possible declaration for a simplified, modular flow that specializes in getting a user's email:

image

Note that not all graphs will have only one edge to exit. Any edge can flow to/from the exit/entry node; the flow could be adapted to exit on an I don't have an email response, for example. Additional metadata could be passed through the exit edge to serve the entry node and edge of the following flow.

Definitions

Graphs

A graph describes the relations between nodes through edges. In the context of this model, all graphs are directional and describe a conversational flow.

Example of a simplified graph JSON object:

{
  "graph": {
    "id": "example_graph",
    "label": "An example graph.",
    "nodes": [
      {
        "id": "node_a",
        "type": "example_node",
        "label": "Example of a simple node A"
      },
      {
        "id": "node_b",
        "type": "example_node",
        "label": "Example of a simple node B"
      }
    ],
    "edges": [
      {
        "type": "entry_edge",
        "source": "entry_node",
        "target": "node_a",
        "relation": "flow"
      },
      {
        "type": "example_edge",
        "source": "node_a",
        "target": "node_b",
        "relation": "flow"
      },
      {
        "type": "exit_edge",
        "source": "node_b",
        "target": "exit_node",
        "relation": "flow"
      }
    ]
  }
}

Nodes

A node (or vertex) represents a discrete, atomic, single-purpose piece of a flow, fundamentally an action.

Example of a simplified node JSON object:

{
  "id": "ask_email",
  "type": "ask",
  "label": "What is your email address?"
}

The JSON object above describes an ask action. Note that the node object itself does not describe the next or previous steps. That's what edges are for.

Nodes as actions

For example, nodes might be treated as discrete actions; conversational actions that can include but may not be limited to:

  • ask – ask a question expecting a response that is sent to the next node.

  • say – send a message, expecting no response.

  • store – store a message, status response may be sent to next node.

Naturally, custom actions might be added to the model. Such as actions specialized on performing asynchronous tasks like http requests, for example.

Abstract nodes

In order to achieve modularity in conversational graphs, specialized flows should make no assumptions on the rest of the flow. However, while this might be true, graphs aren't designed to solve the modularity problem.

The proposed solution: abstract nodes.

Abstract nodes are nodes that serve as placeholders for the preceding or following nodes in a higher-order graph (a graph of graphs, so to speak).

There are two types of abstract nodes:

  • entry_node – the first node of any flow. An abstract reference (a placeholder) that represents the last node of the previous graph.

  • exit_node – an abstract reference to a node that follows the last node in a graph.

Edges

An edge (or link) represents a directional control and/or data flow. Each edge must have a source node and a destination node and only one of them may be an abstract node. Edges represent a data or control flow from a source node to a target node; every edge is directed, the whole graph is directed.

Example of a simplified edge JSON object:

{
  "type": "action",
  "source": "valid_email",
  "target": "store_email",
  "relation": "flow"
}

Edges for flow control

Edges might serve as a way to control which nodes to execute. An edge of type control might use assertion metadata to determine whether to move to a node or not.

Example of an edge as a flow control mechanism:

{
  "type": "control",
  "source": "ask_email",
  "target": "valid_email",
  "relation": "flow",
  "metadata": {
    "assertion": {
      "==": [{ "var": "response.is_email" }, true]
    }
  }
}

The edge above directs the flow from ask_email to valid_email only if response.is_email is true.

In this example JsonLogic is used as an assertion mechanism, but other mechanisms (such as an NLP text-classification layer) could be employed to control the flow of a conversation.

Entry and exit edges

Entry and exit edges are in charge of connecting an abstract node to a regular node and transferring data to and from a graph.

Example: Example JSON Conversational Graph

If we wanted the following flow to happen:

Bot: What is your email address?
You: My email is wrong-email.com
Bot: The email you provided is invalid.
Bot: What is your email address?
You: My email is bob@gmail.com
Bot: Great! I will store your email now.

We would declare the flow in a JSON as follows:

{
  "graph": {
    "directed": true,
    "label": "Ask for email flow.",
    "nodes": [
      {
        "id": "ask_email",
        "type": "ask",
        "label": "What is your email address?"
      },
      {
        "id": "invalid_email",
        "type": "say",
        "label": "The email you provided is invalid."
      },
      {
        "id": "valid_email",
        "type": "say",
        "label": "Great! I will store your email now."
      },
      {
        "id": "store_email",
        "type": "store",
        "label": "* saves email in db *",
        "metadata": {
          "take": "response.text",
          "save_as": "user.email"
        }
      }
    ],
    "edges": [
      {
        "type": "entry_edge",
        "source": "entry_node",
        "target": "ask_email",
        "relation": "flow"
      },
      {
        "type": "control",
        "source": "ask_email",
        "target": "valid_email",
        "relation": "flow",
        "metadata": {
          "assertion": {
            "==": [{ "var": "response.is_email" }, true]
          }
        }
      },
      {
        "type": "control",
        "source": "ask_email",
        "target": "invalid_email",
        "relation": "flow",
        "metadata": {
          "assertion": {
            "==": [{ "var": "response.is_email" }, false]
          }
        }
      },
      {
        "type": "action",
        "source": "valid_email",
        "target": "store_email",
        "relation": "flow"
      },
      {
        "type": "action",
        "source": "invalid_email",
        "target": "ask_email",
        "relation": "flow"
      },
      {
        "type": "exit_edge",
        "source": "store_email",
        "target": "exit_node",
        "relation": "flow"
      }
    ]
  }
}

This flow could be conceptually visualized as follows:

image