Skip to content

Commit

Permalink
[breadboard, shared-ui, et al] Introduce GraphStore.graphs()
Browse files Browse the repository at this point in the history
- **First sketch of `GraphStore.graphs()`.**
- **Introduce `GraphStore.registerKit`.**
- **Move registering kits with GraphStore to runtime.**
- **Nicely dedupe the handlers.**
- **Move `deprecated` to a tag.**
- **Start using `GraphStore.graphs()`.**
- **Plumb `showExperimentalComponents` to `bb-component-selector`.**
- **docs(changeset): Introduce `GraphStore.graphs()`.**
- **Fix unit tests.**

Progress on #3965.
  • Loading branch information
dglazkov authored Dec 10, 2024
1 parent 1dc176d commit 4a898eb
Show file tree
Hide file tree
Showing 14 changed files with 312 additions and 74 deletions.
10 changes: 10 additions & 0 deletions .changeset/famous-cats-turn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@google-labs/breadboard": minor
"@breadboard-ai/visual-editor": patch
"@breadboard-ai/shared-ui": patch
"@breadboard-ai/manifest": patch
"@google-labs/breadboard-schema": patch
"@breadboard-ai/types": patch
---

Introduce `GraphStore.graphs()`.
108 changes: 106 additions & 2 deletions packages/breadboard/src/inspector/graph-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { GraphLoader, GraphLoaderContext } from "../loader/types.js";
import { MutableGraphImpl } from "./graph/mutable-graph.js";
import {
GraphStoreEntry,
GraphStoreArgs,
GraphStoreEventTarget,
InspectableGraph,
Expand All @@ -24,12 +25,13 @@ import {
MutableGraphStore,
} from "./types.js";
import { hash } from "../utils/hash.js";
import { Kit, NodeHandlerContext } from "../types.js";
import { Kit, NodeHandlerContext, NodeHandlerMetadata } from "../types.js";
import { Sandbox } from "@breadboard-ai/jsandbox";
import { createLoader } from "../loader/index.js";
import { SnapshotUpdater } from "../utils/snapshot-updater.js";
import { UpdateEvent } from "./graph/event.js";
import { collectCustomNodeTypes } from "./graph/kits.js";
import { collectCustomNodeTypes, createBuiltInKit } from "./graph/kits.js";
import { graphUrlLike } from "../utils/graph-url-like.js";

export { GraphStore, makeTerribleOptions, contextFromStore };

Expand Down Expand Up @@ -65,6 +67,8 @@ class GraphStore
readonly sandbox: Sandbox;
readonly loader: GraphLoader;

#legacyKits: GraphStoreEntry[];

#mainGraphIds: Map<string, MainGraphIdentifier> = new Map();
#mutables: Map<MainGraphIdentifier, SnapshotUpdater<MutableGraph>> =
new Map();
Expand All @@ -75,6 +79,93 @@ class GraphStore
this.kits = args.kits;
this.sandbox = args.sandbox;
this.loader = args.loader;
this.#legacyKits = this.#populateLegacyKits(args.kits);
}

graphs(): GraphStoreEntry[] {
const graphs = [...this.#mutables.entries()]
.flatMap(([mainGraphId, snapshot]) => {
const mutable = snapshot.current();
const descriptor = mutable.graph;
// TODO: Support exports and main module
const mainGraphMetadata = filterEmptyValues({
title: descriptor.title,
description: descriptor.description,
icon: descriptor.metadata?.icon,
url: descriptor.url,
tags: descriptor.metadata?.tags,
help: descriptor.metadata?.help,
id: mainGraphId,
});
return {
mainGraph: mutable.legacyKitMetadata || mainGraphMetadata,
...mainGraphMetadata,
};
})
.filter(Boolean) as GraphStoreEntry[];
return [...this.#legacyKits, ...graphs];
}

#populateLegacyKits(kits: Kit[]) {
kits = [...kits, createBuiltInKit()];
const all = kits.flatMap((kit) =>
Object.entries(kit.handlers).map(([type, handler]) => {
let metadata: NodeHandlerMetadata =
"metadata" in handler ? handler.metadata || {} : {};
const mainGraphTags = [...(kit.tags || [])];
if (metadata.deprecated) {
mainGraphTags.push("deprecated");
metadata = { ...metadata };
delete metadata.deprecated;
}
const tags = [...(metadata.tags || []), "component"];
return [
type,
{
url: type,
mainGraph: filterEmptyValues({
title: kit.title,
description: kit.description,
tags: mainGraphTags,
}),
...metadata,
tags,
},
] as [type: string, info: GraphStoreEntry];
})
);
return Object.values(
all.reduce(
(collated, [type, info]) => {
// Intentionally do the reverse of what goes on
// in `handlersFromKits`: last info wins,
// because here, we're collecting info, rather
// than handlers and the last info is the one
// that typically has the right stuff.
return { ...collated, [type]: info };
},
{} as Record<string, GraphStoreEntry>
)
);
}

registerKit(kit: Kit, dependences: MainGraphIdentifier[]): void {
Object.keys(kit.handlers).forEach((type) => {
if (graphUrlLike(type)) {
const mutable = this.addByURL(type, dependences, {});
mutable.legacyKitMetadata = filterEmptyValues({
url: kit.url,
title: kit.title,
description: kit.description,
tags: kit.tags,
id: mutable.id,
});
} else {
throw new Error(
`The type "${type}" is not Graph URL-like, unable to add this kit`
);
}
});
}

addByDescriptor(graph: GraphDescriptor): Result<MainGraphIdentifier> {
Expand Down Expand Up @@ -292,3 +383,16 @@ function emptyGraph(): GraphDescriptor {
nodes: [],
};
}

/**
* A utility function to filter out empty (null or undefined) values from
* an object.
*
* @param obj -- The object to filter.
* @returns -- The object with empty values removed.
*/
function filterEmptyValues<T extends Record<string, unknown>>(obj: T): T {
return Object.fromEntries(
Object.entries(obj).filter(([, value]) => !!value)
) as T;
}
54 changes: 51 additions & 3 deletions packages/breadboard/src/inspector/graph/kits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { toNodeHandlerMetadata } from "../../graph-based-node-handler.js";
import { getGraphHandlerFromStore } from "../../handler.js";
import {
GraphDescriptor,
Kit,
NodeDescriberResult,
NodeDescriptor,
NodeHandler,
Expand All @@ -30,9 +31,56 @@ import {
import { collectPortsForType, filterSidePorts } from "./ports.js";
import { describeInput, describeOutput } from "./schemas.js";

export { KitCache, collectCustomNodeTypes };
export { KitCache, collectCustomNodeTypes, createBuiltInKit };

const createBuiltInKit = (): InspectableKit => {
function unreachableCode() {
return function () {
throw new Error("This code should be never reached.");
};
}

function createBuiltInKit(): Kit {
return {
title: "Built-in Kit",
description: "A kit containing built-in Breadboard nodes",
url: "",
handlers: {
input: {
metadata: {
title: "Input",
description:
"The input node. Use it to request inputs for your board.",
help: {
url: "https://breadboard-ai.github.io/breadboard/docs/reference/kits/built-in/#the-input-node",
},
},
invoke: unreachableCode(),
},
output: {
metadata: {
title: "Output",
description:
"The output node. Use it to provide outputs from your board.",
help: {
url: "https://breadboard-ai.github.io/breadboard/docs/reference/kits/built-in/#the-output-node",
},
},
invoke: unreachableCode(),
},
comment: {
metadata: {
description:
"A comment node. Use this to put additional information on your board",
title: "Comment",
icon: "edit",
},
invoke: unreachableCode(),
},
},
};
}

const createBuiltInInspectableKit = (): InspectableKit => {
return {
descriptor: {
title: "Built-in Kit",
Expand Down Expand Up @@ -89,7 +137,7 @@ export const collectKits = (
): InspectableKit[] => {
const kits = mutable.store.kits;
return [
createBuiltInKit(),
createBuiltInInspectableKit(),
...createCustomTypesKit(graph.nodes, mutable),
...kits.map((kit) => {
const descriptor = {
Expand Down
3 changes: 3 additions & 0 deletions packages/breadboard/src/inspector/graph/mutable-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import {
GraphDescriptor,
GraphIdentifier,
KitDescriptor,
ModuleIdentifier,
} from "@breadboard-ai/types";
import { AffectedNode } from "../../editor/types.js";
Expand Down Expand Up @@ -45,6 +46,8 @@ class MutableGraphImpl implements MutableGraph {
readonly store: MutableGraphStore;
readonly id: MainGraphIdentifier;

legacyKitMetadata: KitDescriptor | null = null;

graph!: GraphDescriptor;
graphs!: InspectableGraphCache;
nodes!: InspectableNodeCache;
Expand Down
31 changes: 16 additions & 15 deletions packages/breadboard/src/inspector/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -695,21 +695,9 @@ export type InspectableGraphCache = {

export type MainGraphIdentifier = UUID;

export type GraphHandle = {
id: MainGraphIdentifier;
} & (
| {
type: "declarative";
/**
* The value is "" for the main graph.
*/
graphId: GraphIdentifier;
}
| {
type: "imperative";
moduleId: ModuleIdentifier;
}
);
export type GraphStoreEntry = NodeHandlerMetadata & {
mainGraph: NodeHandlerMetadata & { id: MainGraphIdentifier };
};

export type GraphStoreArgs = Required<InspectableGraphOptions>;

Expand All @@ -731,6 +719,18 @@ export type MutableGraphStore = TypedEventTargetType<GraphsStoreEventMap> & {
readonly sandbox: Sandbox;
readonly loader: GraphLoader;

graphs(): GraphStoreEntry[];

/**
* Registers a Kit with the GraphStore.
* Currently, only Kits that contain Graph URL-like types
* are support.
*
* @param kit - the kit to register
* @param dependences - known dependencies to this kit
*/
registerKit(kit: Kit, dependences: MainGraphIdentifier[]): void;

addByURL(
url: string,
dependencies: MainGraphIdentifier[],
Expand Down Expand Up @@ -791,6 +791,7 @@ export type InspectablePortCache = {
*/
export type MutableGraph = {
graph: GraphDescriptor;
legacyKitMetadata: KitDescriptor | null;
readonly id: MainGraphIdentifier;
readonly graphs: InspectableGraphCache;
readonly store: MutableGraphStore;
Expand Down
57 changes: 57 additions & 0 deletions packages/breadboard/tests/node/inspect/graph-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import { deepStrictEqual } from "node:assert";
import { describe, it } from "node:test";
import { makeTestGraphStore } from "../../helpers/_graph-store.js";
import { testKit } from "../test-kit.js";

describe("GraphStore.graphs", () => {
it("correctly lists legacy kits as graphs", () => {
const graphStore = makeTestGraphStore({
kits: [testKit],
});
deepStrictEqual(
graphStore.graphs().map((graph) => graph.url),
[
"invoke",
"map",
"promptTemplate",
"runJavascript",
"secrets",
"input",
"output",
"comment",
]
);
});

it("correctly lists added graphs", () => {
const graphStore = makeTestGraphStore({
kits: [testKit],
});
graphStore.addByDescriptor({
url: "https://example.com/foo",
title: "Foo",
nodes: [],
edges: [],
});
deepStrictEqual(
graphStore.graphs().map((graph) => graph.url),
[
"invoke",
"map",
"promptTemplate",
"runJavascript",
"secrets",
"input",
"output",
"comment",
"https://example.com/foo",
]
);
});
});
5 changes: 5 additions & 0 deletions packages/breadboard/tests/node/test-kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { InputValues, Kit, OutputValues } from "../../src/types.js";
// in tests/bgl/*.

export const testKit: Kit = {
title: "Test Kit",
url: import.meta.url,
handlers: {
invoke: {
Expand All @@ -38,6 +39,10 @@ export const testKit: Kit = {
},
},
map: {
metadata: {
title: "Map",
tags: ["experimental"],
},
invoke: async (inputs, context) => {
const { board, list = [] } = inputs;
if (!board) {
Expand Down
1 change: 1 addition & 0 deletions packages/manifest/bbm.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@
"description": "A tag that can be associated with a graph.\n- `published`: The graph is published (as opposed to a draft). It may be used in production and shared with others.\n- `tool`: The graph is intended to be a tool.\n- `experimental`: The graph is experimental and may not be stable.\n- `component`: The graph is intended to be a component.",
"enum": [
"component",
"deprecated",
"experimental",
"published",
"tool"
Expand Down
3 changes: 2 additions & 1 deletion packages/schema/breadboard.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,8 @@
"published",
"tool",
"experimental",
"component"
"component",
"deprecated"
],
"description": "A tag that can be associated with a graph.\n- `published`: The graph is published (as opposed to a draft). It may be used in production and shared with others.\n- `tool`: The graph is intended to be a tool.\n- `experimental`: The graph is experimental and may not be stable.\n- `component`: The graph is intended to be a component."
},
Expand Down
Loading

0 comments on commit 4a898eb

Please sign in to comment.