Please note:this document is a work in progress.

Graph Inspector API

The Inspector API provides a way to inspect a graph to make sense of it. Because a serialized graph representation (also known as the BGL document) is basically just JSON containing arrays of nodes and edges, a the actual semantics of the graph need to be added separately. This is what the Inspector API does. Think of it as the DOM API for the graph.

Note

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

Graph

The entry point for the Inspector API is the inspect method. It expects a GraphDescriptor as its first argument (the GraphDescriptor is the TypeScript type representing the BGL document):

import { inspect } from "@google-labs/breadboard";

// Returns an instance of `InspectableGraph`.
const graph = inspect(bgl);

Once we have an instance of InspectableGraph, we can use it to query the graph:

// Get a node by id.
// Returns an instance of `InspectableNode`.
const node = graph.nodeById("input-1");

// Get all nodes of type.
// Returns an array of `InspectableNode`.
const outputs = graph.nodesByType("output");

// Get all nodes in the graph/
// Returns an array of `InspectableNode`.
const all = graph.nodes();

// Get all entry nodes for the graph.
// Entry nodes are those that don't have incoming edges.
// Returns an array of 'InspectableNode`.
const entries = graph.entries();

Nodes

The result of querying the graph is typically an instance of InspectableNode or an array of them. The InspectableNode enables examining a node in the graph:

// Get a list of incoming edges for this node.
// The incoming edges are those that are directed toward the node.
// (also known as "heads" for graph math folks)
// Returns an array of `InspectableEdge`;
const incoming = node.incoming();

// Get a list of outgoing edges for this node.
// The outgoing edges are those that originate from the node.
// (also known as "tails" for graph math folks)
// Returns an array of `InspectableEdge`;
const outgoing = node.outgoing();

// See if the node is a default entry point (no incoming edges or
// labeled as "default").
// Returns true or false.
const isEntry = node.isEntry();

// See if the node is a "describe" entry point.
// Returns true or false.
const isDescribeEntry = node.isEntry("describe");

// See if the node is an exit node (no outgoing edges)
// Returns true or false.
const isExit = node.isExit();

// Get a node title (or node id if node title wasn't specified
// in `NodeMetadata`).
// Returns string
const title = node.title();

Note

The InspectableNode instances are stable across the lifetime of a particular node within the graph. Any method or property that returns an InspectableNode in this API will return the same instance for the node of a given id.

Edges

In addition to InspectableNode, the API may return InspectableEdge, which represents the edge in a graph. For example, the InspectableGraph.edges method returns a list of all edges within a graph:

// Returns an array of `InspectableEdge`.
const edges = graph.edges();

The InspectableEdges provides access to two instances of InspectableNode that the underlying edge is connecting as well as the names of the ports:

// The outgoing node of the edge (aka the "tail")
// Returns an instance of `InspectableNode`.
const from = edge.from;

// The name of the port of the outgoing edge.
// Returns string.
const out = edge.out;

// The incoming node of the edge (aka the "head")
// Returns an instance of `InspectableNode`.
const to = edge.from;

// The name of the port of the incoming edge.
// Returns string.
const inPort = edge.in;

Note

The InspectableEdge instances are stable across the lifetime of a particular edge within the graph. Any method or property within this API will return the same instance of an InspectableEdge for a given edge. This means that, for example, we can use InspectableEdge instances as keys in Map.

Kits

At runtime, graphs invoke the nodes during traversal. The actual functions that are being invoked are stored in kits (collections of nodes). We can optionally supply kits to the inspector so that we can examine their contents. The second, optional InspectableGraphOptions argument to inspect has a member kits that gives us a way to specify the kits for the graph:

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

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

Once the kits are supplied, we can inspect them using the kits method, which returns a list of inspectable kits:

// Returns an array of `InspectableKit`.
const kits = graph.kits();

Each item in the list of kits has properties to inspect the kit it represents, such as the data structure that contains the kit metadata (title, version, url, description) and the list of node types that the kit contains:

for (const kit of kits) {
  const { title } = kit.descriptor;
  // Prints the kit title.
  console.log(`Kit: ${title}`);
  for (const nodeType of kit.nodeTypes) {
    // Do something with node types...
  }
}

The nodeTypes of the InspectableKit contains a list of items each representing a node type contained within a kit. An item has two methods: one to get the type of the node, and the other is an asynchronous method to query the ports that will be available on the node of this type when it has no edges.

// Returns string.
const type = nodeType.type();
// Async, returns `Promise<InspectablePorts>`.
const ports = await nodeType.ports();

For a discussion on ports and how to use them, see the section below.

Ports

Each node has a set of ports that it expects as inputs and a set of ports it expect as outputs.

The Inspector API provides a way to examine the expected ports of any node within a graph with the InspectableNode.ports method. Calling this method will give two lists of ports for a node:

// Async, returns Promise<InspectableNodePorts>.
const ports = await node.ports();

This method also takes an optional InputValues argument that can be useful for some types of nodes that change their input/output port configuration based on the inputs.

// Given this argument, the `promptTemplate` node will parse the template,
// see that it needs a `name` value to correctly fill in the template,
// and change its shape to expect `name` as an additional input port
const promptTemplatePorts = await promptTemplate.ports({
  template: "Hello {{name}}!",
});

For convenience, the method will supply a node's configured values as inputs by default. This means that if the template in example above is part of node's configuration, we don't have supply it again as argument.

The resulting of InspectableNodePorts provides access to two members, inputs and outputs. Both are the instances of InspectablePortList:

const ports = await node.ports();

// Returns `InspectablePortList`
const inputs = ports.inputs;

// Returns `InspectablePortList`
const outputs = ports.outputs;

The InspectablePortList instance has gives us access to an array of InspectablePort via the ports property, representing the input or output ports of the node as well as the fixed property.

// Returns an array of `InspectablePort`.
const inputPorts = inputs.ports;

// Returns `true` or `false`.
const areInputsFixed = inputs.fixed;

The fixed property returns true if the list of ports is fixed and false if the node expects a dynamic number of ports.

For example, the value will be true for the json.validateJson input ports, since it has two fixed input ports: json and schema.

const validateJsonPorts = await validateJson.ports();
// Prints `true`.
console.log(validateJsonPorts.inputs.fixed);

Conversely, the core.invoke node will have dynamic number of ports, because it passes its inputs to the invoked graph as arguments.

const invokePorts = await invoke.ports();
// Prints `false`.
console.log(invokePorts.inputs.fixed);

The InspectablePort instance gives us a sense of the state of the port of a node within a graph. The ports method computes this state based on the incoming and outgoing edges for the node, what the node expects as its inputs and outputs, as well as its configured values.

const inputPorts = (await node.ports()).inputs.ports;
const firstInputPort = inputPorts[0];

We can get the name of the port:

// Returns string.
const name = firstInputPort.name;

For input ports, we can see if the port's value was specified in node's configuration (true) or if it is specified by the incoming edge (false). The value is always false for the output ports.

// Returns `true` if the port was specified in the node's configuration
const configured = firstInputPort.configured;

We can get the JSON schema of the port:

// Returns `Schema`.
const schema = firstInputPort.schema;

If we want to check whether a given port can connect to another port, we can use the type property:

// Returns an `InspectablePortType` instance.
const type = outputPort.type;
// Returns true if `outputPort` can connect to the `inputPort`.
const canConnect = type.canConnect(firstInputPort.type);

The canConnect method will examine the schema of both ports and return true when the schemas are compatible and false when they are not.

We can check if this is the "star port".

// Returns boolean
const star = firstInputPort.star;

Every node will have a single star port as part of its input and output port lists. The star port is a port that is only used to connect the "star edge": the one that is represented by the * port name.

Note

The star edge is special in that it communicates that all output port values of one node will be supplied as input port values of another node. Since it is not always possible to know what those values are without actually running the graph, using star edges means that we might not be able to determine whether or not the input ports are specified as expected.

We can also get the edges that are connected to this port:

// Returns `InspectableEdge[]`.
const edges = firstInputPort.edges;

This can be useful when there are more than one edges incoming or outgoing for this port.

Most importantly, we can get the status of the port:

// Returns a `PortStatus` instance.
const status = firstInputPort.status;

The port status can be one of the following values:

Note

If the kits option isn't supplied, the ports method will presume that the node does not have any expectations for its inputs or outputs. All ports will have the PortStatus.Connected state.

Subgraphs

Nodes that invoke graphs

Some nodes may represent entire subgraphs. For instance, core.invoke node takes a board as its argument, and invokes that graph, passing its own inputs to this subgraph and returning its results as own outputs.

Tip

Make sure that when calling inspect, the BGL document argument has the url property set to a valid URL that represents the current location of this graph. It will enable nodes that do loading as part of describing themselves (such as core.invoke) to correctly resolve any relative paths that might be given as their inputs.

This value will be automatically set when loading a BGL file using the GraphLoader.load method.

It is the responsibility of the respective nodes to provide an accurate description of their input and output ports.

For instance, when core.invoke is asked to describe itself -- and provided it has all the necessary information, and the BGL document has a valid url property, -- it will show the invoked graph's inputs and outputs as its own ports.

Embedded subgraphs

Similar in spirit, but different in quality are embedded subgraphs. Every BGL document may have zero or more subgraphs embedded into it. These subgraphs are miniature BGL documents in themselves.

Each embedded subgraph has an identifier that is unique within this BGL document. This identifier is can be used to address the subgraph in a URL with a fragment identifier (commonly known as 'hash'). For example, if the BGL file at http://example.com/foo.bgl.json has an embedded subgraph with the ide of bar, this subgraph's URL is http://example.com/foo.bgl.json#bar.

To find out whether or not a given BGL document has such graphs, use the graphs property:

// Returns an object with keys as subgraph identifiers
// and values as `InspectableGraph` instances
const subgraphs = graph.graphs();
// it can be null.
if (subgraphs !== null) {
  // Returns an `InspectableGraph` instance
  const foo = subgraphs["foo"];
}