Hybrid Engine - Rete.js

Hybrid Engine

For a brief overview of the concept of combining Dataflow and Control flow, we recommend reading the Hybrid article

Hybrid engineChatbotPluginDataflowControl flow

Install dependencies

bash
npm i rete rete-engine

Prepare nodes

In order to work properly, all node classes must implement execute method for Control flow and data method for Dataflow.

The Start class is designed to pass control and has a default data method that returns an empty object.

ts
const socket = new ClassicPreset.Socket("socket");

class Start extends ClassicPreset.Node {
  constructor() {
    super("Start");
    this.addOutput("exec", new ClassicPreset.Output(socket, "Exec"));
  }

  execute(_: never, forward: (output: "exec") => void) {
    forward("exec");
  }

  data() {
    return {};
  }
}

Along with receiving control, the Log class can also request data from the incoming nodes through the message port using the fetchInputs method of the DataflowEngine instance.

ts
class Log extends ClassicPreset.Node {
  constructor() {
    super("Log");

    this.addInput("exec", new ClassicPreset.Input(socket, "Exec", true));
    this.addInput("message", new ClassicPreset.Input(socket, "Text"));
    this.addOutput("exec", new ClassicPreset.Output(socket, "Exec"));
  }

  async execute(input: "exec", forward: (output: "exec") => void) {
    const inputs = (await dataflow.fetchInputs(this.id)) as {
      message: string[];
    };

    console.log((inputs.message && inputs.message[0]) || "");

    forward("exec");
  }

  data() {
    return {};
  }
}

The TextNode class is responsible only for providing data and cannot receive or pass control.

ts
class TextNode extends ClassicPreset.Node {
  constructor(private text: string) {
    super("Text");
    this.addOutput("value", new ClassicPreset.Output(socket, "Number"));
  }

  execute() {}

  data(): { value: string } {
    return {
      value: this.text
    };
  }
}

class Connection<A extends NodeProps, B extends NodeProps> extends ClassicPreset.Connection<A, B> {}

type NodeProps = Start | Log | TextNode;
type ConnProps = Connection<Start, Log> | Connection<TextNode, Log>;
type Schemes = GetSchemes<NodeProps, ConnProps>;

Connect

ts
import { NodeEditor } from "rete";
import { DataflowEngine, ControlFlowEngine } from "rete-engine";

const editor = new NodeEditor<Schemes>();
const dataflow = new DataflowEngine<Schemes>(({ inputs, outputs }) => {
  return {
    inputs: () => Object.keys(inputs).filter((name) => name !== "exec"),
    outputs: () => Object.keys(outputs).filter((name) => name !== "exec")
  };
});
const controlflow = new ControlFlowEngine<Schemes>(() => {
  return {
    inputs: () => ["exec"],
    outputs: () => ["exec"]
  };
});

editor.use(dataflow);
editor.use(controlflow);

Add nodes and connections

ts
const start = new Start();
const text1 = new TextNode("log");
const log1 = new Log();

const con1 = new Connection(start, "exec", log1, "exec");
const con2 = new Connection(text1, "value", log1, "message");

await editor.addNode(start);
await editor.addNode(text1);
await editor.addNode(log1);

await editor.addConnection(con1);
await editor.addConnection(con2);

Execution

The node start serves as the starting point for the graph execution.

ts
engine.execute(start.id);

As a result, the start node passes control to the log1 node, which fetches data from the text1 node using the fetchInputs method.

Check out the complete result on the Hybrid engine example page. Additionally, you can explore another more sophisticated Chatbot example employing this approach.