Please note:this document is a work in progress.

Editor API

The Editor API provides a way to edit a graph. It is designed to work in conjunction with the Inspector API and helps ensure that the graph edits retain their structural integrity.

Note

The full list of types of Editor API can be found in /packages/breadboard/src/editor/types.ts

Creating an editor

Calling the edit method creates an instance of an EditableGraph. This method expects a GraphDescriptor as its first argument:

import { edit } from "google-labs/breadboard";

// Returns an instance of `EditableGraph`.
const graph = edit(bgl);

The editor API provides one method for applying edits to the graph: edit. This method takes three members (two required):

// Adds a node with id = "foo" and type = "type".
// Returns `Promise<EditResult>`.
const result = await graph.edit(
  [{ type: "addnode", node: { id: "foo", type: "type" } }],
  `Create Node "foo"`
);
if (!result.success) {
  console.warn("Adding node failed with this error", result.error);
}

The string label plays an important role. It groups the edit operations for the purpose of collecting history. See more about how to use it in the Graph history management (undo/redo) section.

When dryRun is set to true, the method will not perform the actual edit, but report the result as if the edit as applied. This is useful if we want to check whether an edit would be valid without actually making an edit.

// Does not actually add node with id = "foo" and type = "type",
// just checks to see if such a node could be added.
// Returns `Promise<EditResult>`.
const result = await graph.edit(
  [{ type: "addnode", node: { id: "foo", type: "type" } }],
  "Adding Node (dry run)",
  true
);
if (!result.success) {
  console.warn("Adding node will fail with this error", result.error);
} else {
  console.log("Yay, we can add this node, proceed forth");
}

Multiple changes to the graph are performed as one atomic unit when specified in the same method:

// Adds a node with id = "foo" and type = "type" ...
// .. and a node with id = "bar" and type = "type" as one atomic operation.
// Returns `Promise<EditResult>`.
const result = await graph.edit([
  { type: "addnode", node: { id: "foo", type: "type" } },
  { type: "addnode", node: { id: "bar", type: "type" } },
]);
if (!result.success) {
  console.warn("Adding node failed with this error", result.error);
}

Kits

To ensure that edit method does not jeopardize the integrity of the graph, we need to supply the editor a list of kits. Kits are collections of functions that are invoked during running the graph. We can provide kits as kits property on the second, optional EditableGraphOptions argument of the edit method:

import Core from "@google-labs/core-kit";
import JSONKit from "@google-labs/json-kit";

const graph = edit(bgl, { kits: [asRuntime(Core), asRunTime(JSONKit)] });

Editing a graph

There are the six edit operations that we can perform on the graph:

Starting a new graph

If we want to start a brand-new graph, we can use the handy blank method, provided by the Editor API:

import { blank } from "google-labs/breadboard";

// Returns a new `GraphDescriptor` instance
// of a pre-built blank graph.
const myNewGraph = blank();

The newly-created graph will have a pre-filled title and description, a version of 0.0.1 and two connected nodes: the input node connected to the output node with one wire. The wire will go from text port to text port of the respective nodes.

Blank graph diagram

Graph versioning

To help us keep track of the edits, the EditableGraph has a version() method, which returns the current version of the graph:

// Returns a number.
const current = graph.version();

By default, a new EditableGraph instance starts with version 0 and increments it for each change.

To supply a different starting version, use the version option when creating a new EditableGraph instance:

import Core from "@google-labs/core-kit";
import JSONKit from "@google-labs/json-kit";

// Let's start with version 1000.
const version = 1000;
const graph = edit(bgl, {
  kits: [asRuntime(Core), asRunTime(JSONKit)],
  version,
});

Graph history management (undo/redo)

In addition to simple versioning, the Editor API tracks history of the graph to enable undo/redo capability. The history() method on an EditableGraph instance provides a few handy helpers for that:

// Returns an `EditHistory` instance.
const history = graph.history();
if (history.canUndo()) {
  history.undo();
}
history.redo();

// Prints out a list of history entries with a ">" marker next
// to the current history entry.
const labels = history.entries().map((entry) => entry.label);
console.group("History:");
labels.forEach((label, index) => {
  const current = index === history.index() ? ">" : " ";
  console.log(`${index}:${current} ${label}`);
});
console.groupEnd();

The string label that was supplied for an edit operation allows the user of the API to group multiple edit operations into a single history entry.

Each edit call that has the same label as the previous edit call will be grouped with that previous call: no new history entry will be created for it.

When the edit call has a label that's different from the previous call, a new history entry will be created.

// Creates a new history entry.
const result = await graph.edit(
  [{ type: "addnode", node: { id: "foo", type: "type" } }],
  `Create Node "foo"`
);
// Different label, creates another history entry.
const result = await graph.edit(
  [{ type: "changemetadata", id: "foo", metadata: { title: "F" } }],
  `Editing metadata for node "foo"`
);
// Label is the same as the previous call, no new entry created.
const result = await graph.edit(
  [{ type: "changemetadata", id: "foo", metadata: { title: "Fo" } }],
  `Editing metadata for node "foo"`
);
// Label is the same as the previous call, no new entry created.
const result = await graph.edit(
  [{ type: "changemetadata", id: "foo", metadata: { title: "Foo" } }],
  `Editing metadata for node "foo"`
);
// Different label, creates another history entry.
const result = await graph.edit(
  [{ type: "addnode", node: { id: "bar", type: "type" } }],
  `Create Node "bar"`
);

Editing subgraphs

Since every graph may have embedded subgraphs in it, we can use the Editor API to access and edit these subgraphs as well. Every subgraph has an identifier that is unique among all subgraphs within their graph. The API uses this id to add, remove, replace subgraphs and manages the EditableGraph instances for subgraphs.

// Returns an `EditableGraph` instance or `null` if not found.
const subgraph = graph.getGraph("foo");
if (subgraph) {
  // Edit the subgraph
  // ...
}

// Attempts to add a new subgraph and returns `EditResult`.
// Returns null if a subgraph with this id already exists,
// and an `EditableGraph` instance otherwise.
const newSubgraph = graph.addGraph("bar", blank());
if (!newSubgraph) {
  console.log("A graph with id 'bar' already exists.");
}

// Attempts to remove the subgraph and returns `EditResult`.
// Will fail if a subgraph with this id does not exist.
const result = graph.removeGraph("bar");
if (result.success) {
  console.log("Yay, removed subgraph 'bar'.");
} else {
  console.log("The subgraph 'bar' does not exist".)
}

// Attempts to replace a subgraph and returns `EditResult`.
// Returns null if a subgraph with this id does not exist,
// and an `EditableGraph` instance of the new subgraph otherwise.
const replaced = graph.replaceGraph("foo", blank());
if (!replaced) {
  console.log("A graph with id 'foo' does not exist.")
}

To find out if a particular EditableGraph instance is an embedded subgraph, use the parent() method:

// If subgraph, returns `EditableGraph` instance of the parent graph.
const parentGraph = subgraph.parent();
if (parentGraph) {
  console.log("A subgraph!");
} else {
  console.log("Not a subgraph");
}

Because they are part of a larger graph, subgraphs do not have their own versions and attempting to call the version() method on a subgraph will throw an error.

Subgraphs may not contain other subgraphs, so in the same fashion as version(), the getGraph, addGraph, replaceGraph, and removeGraph will throw when called on a subgraph.

Accessing the graph

To access the underlying GraphDescriptor instance, use the raw() method on the EditableGraph instance.

const graph = edit(bgl, { kits });
await graph.addNode({ id: "foo", type: "bar" });
// Returns the `GraphDescriptor` instance.
const newBgl = graph.raw();

The raw() method will correctly serialize all of graph's subgraph and reflect their edits.

Inspecting the graph

Because the graph constantly changes, it can be tedious to keep track of the latest changes and keep creating new instances of InspectableGraph. To help with that, there's an inspect method on the EditableGraph:

// Guaranteed to be inspecting the latest graph.
// Returns `InspectableGraph`.
const inspectableGraph = graph.inspect();

In term of lifecycle, the InspectableGraph changes more frequently than the EditableGraph. So, hang on to the EditableGraph instance and use it to create InspectableGraph instances. It will cache them for you, only creating a new inspector when the graph changes.

Listening to changes

The EditableGraph instance also the addEventListener method, which works pretty much like any EventTarget -- we can subscribe to listen to events that are dispatched by this instance. Currently the following events are supported: