diff --git a/.changeset/famous-cats-turn.md b/.changeset/famous-cats-turn.md new file mode 100644 index 00000000000..ad72940459b --- /dev/null +++ b/.changeset/famous-cats-turn.md @@ -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()`. diff --git a/packages/breadboard/src/inspector/graph-store.ts b/packages/breadboard/src/inspector/graph-store.ts index d129d397b15..4d7e7d461f2 100644 --- a/packages/breadboard/src/inspector/graph-store.ts +++ b/packages/breadboard/src/inspector/graph-store.ts @@ -14,6 +14,7 @@ import { import { GraphLoader, GraphLoaderContext } from "../loader/types.js"; import { MutableGraphImpl } from "./graph/mutable-graph.js"; import { + GraphStoreEntry, GraphStoreArgs, GraphStoreEventTarget, InspectableGraph, @@ -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 }; @@ -65,6 +67,8 @@ class GraphStore readonly sandbox: Sandbox; readonly loader: GraphLoader; + #legacyKits: GraphStoreEntry[]; + #mainGraphIds: Map = new Map(); #mutables: Map> = new Map(); @@ -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 + ) + ); + } + + 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 { @@ -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>(obj: T): T { + return Object.fromEntries( + Object.entries(obj).filter(([, value]) => !!value) + ) as T; +} diff --git a/packages/breadboard/src/inspector/graph/kits.ts b/packages/breadboard/src/inspector/graph/kits.ts index a8a67baba1f..f67922ba2ca 100644 --- a/packages/breadboard/src/inspector/graph/kits.ts +++ b/packages/breadboard/src/inspector/graph/kits.ts @@ -8,6 +8,7 @@ import { toNodeHandlerMetadata } from "../../graph-based-node-handler.js"; import { getGraphHandlerFromStore } from "../../handler.js"; import { GraphDescriptor, + Kit, NodeDescriberResult, NodeDescriptor, NodeHandler, @@ -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", @@ -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 = { diff --git a/packages/breadboard/src/inspector/graph/mutable-graph.ts b/packages/breadboard/src/inspector/graph/mutable-graph.ts index e18b6971dd4..0c301cc7098 100644 --- a/packages/breadboard/src/inspector/graph/mutable-graph.ts +++ b/packages/breadboard/src/inspector/graph/mutable-graph.ts @@ -7,6 +7,7 @@ import { GraphDescriptor, GraphIdentifier, + KitDescriptor, ModuleIdentifier, } from "@breadboard-ai/types"; import { AffectedNode } from "../../editor/types.js"; @@ -45,6 +46,8 @@ class MutableGraphImpl implements MutableGraph { readonly store: MutableGraphStore; readonly id: MainGraphIdentifier; + legacyKitMetadata: KitDescriptor | null = null; + graph!: GraphDescriptor; graphs!: InspectableGraphCache; nodes!: InspectableNodeCache; diff --git a/packages/breadboard/src/inspector/types.ts b/packages/breadboard/src/inspector/types.ts index 4b78197eed6..eed6aa5e814 100644 --- a/packages/breadboard/src/inspector/types.ts +++ b/packages/breadboard/src/inspector/types.ts @@ -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; @@ -731,6 +719,18 @@ export type MutableGraphStore = TypedEventTargetType & { 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[], @@ -791,6 +791,7 @@ export type InspectablePortCache = { */ export type MutableGraph = { graph: GraphDescriptor; + legacyKitMetadata: KitDescriptor | null; readonly id: MainGraphIdentifier; readonly graphs: InspectableGraphCache; readonly store: MutableGraphStore; diff --git a/packages/breadboard/tests/node/inspect/graph-store.ts b/packages/breadboard/tests/node/inspect/graph-store.ts new file mode 100644 index 00000000000..d252112879c --- /dev/null +++ b/packages/breadboard/tests/node/inspect/graph-store.ts @@ -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", + ] + ); + }); +}); diff --git a/packages/breadboard/tests/node/test-kit.ts b/packages/breadboard/tests/node/test-kit.ts index bf3bcb20f77..bf04c9ca747 100644 --- a/packages/breadboard/tests/node/test-kit.ts +++ b/packages/breadboard/tests/node/test-kit.ts @@ -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: { @@ -38,6 +39,10 @@ export const testKit: Kit = { }, }, map: { + metadata: { + title: "Map", + tags: ["experimental"], + }, invoke: async (inputs, context) => { const { board, list = [] } = inputs; if (!board) { diff --git a/packages/manifest/bbm.schema.json b/packages/manifest/bbm.schema.json index 33710363d67..9ab15b16e0d 100644 --- a/packages/manifest/bbm.schema.json +++ b/packages/manifest/bbm.schema.json @@ -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" diff --git a/packages/schema/breadboard.schema.json b/packages/schema/breadboard.schema.json index a894197c408..d2bcf50f93d 100644 --- a/packages/schema/breadboard.schema.json +++ b/packages/schema/breadboard.schema.json @@ -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." }, diff --git a/packages/shared-ui/src/elements/component-selector/component-selector.ts b/packages/shared-ui/src/elements/component-selector/component-selector.ts index ce4cda7d212..a73239f3f26 100644 --- a/packages/shared-ui/src/elements/component-selector/component-selector.ts +++ b/packages/shared-ui/src/elements/component-selector/component-selector.ts @@ -8,6 +8,7 @@ import { LitElement, html, css, nothing } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { createRef, ref, Ref } from "lit/directives/ref.js"; import { + GraphStoreEntry, Kit, MainGraphIdentifier, MutableGraphStore, @@ -246,79 +247,72 @@ export class ComponentSelector extends LitElement { ) { const kitList = new Map< string, - { id: string; metadata: NodeHandlerMetadata }[] + { id: string; metadata: GraphStoreEntry }[] >(); - const inspectable = graphStore.inspect(mainGraphId, ""); - if (!inspectable) { - return kitList; - } - this.#graphURL = inspectable.raw().url || null; - const graphKits = inspectable?.kits(); - // This should likely be happening outside of this component. - // Weird to see the operations of graphStore in rendering. - // TODO: Refactor, move to runtime. - const boardServerKits = this.boardServerKits - ? graphStore.addKits(this.boardServerKits, [mainGraphId]) - : []; - const kits = [...graphKits, ...boardServerKits]; - kits.sort((kit1, kit2) => - (kit1.descriptor.title || "") > (kit2.descriptor.title || "") ? 1 : -1 - ); - - for (const kit of kits) { - if (!kit.descriptor.title) { + const graphs = graphStore.graphs(); + graphs.sort((graph1, graph2) => { + const title1 = graph1.mainGraph.title || ""; + const title2 = graph2.mainGraph.title || ""; + if (title1 > title2) { + return 1; + } + if (title1 < title2) { + return -1; + } + return (graph1.title || "") > (graph2.title || "") ? 1 : -1; + }); + + for (const graph of graphs) { + if (!graph.title) { continue; } - if (kit.descriptor.title === "Custom Types") { + const { mainGraph } = graph; + + if (!mainGraph.title) { continue; } - if (kit.descriptor.tags?.includes("deprecated")) { + if (mainGraph.id === mainGraphId) { + continue; + } + + if (mainGraph.title === "Custom Types") { + continue; + } + + if (mainGraph.tags?.includes("deprecated")) { continue; } if ( !this.showExperimentalComponents && - kit.descriptor.tags?.includes("experimental") + mainGraph.tags?.includes("experimental") ) { continue; } - const typeMetadata = kit.nodeTypes - .map((node) => { - const metadata = node.currentMetadata(); - if ( - !this.showExperimentalComponents && - metadata.tags?.includes("experimental") - ) { - return null; - } - return { id: node.type(), metadata }; - }) - .filter(Boolean) as { id: string; metadata: NodeHandlerMetadata }[]; - - const available = typeMetadata.filter( - ({ metadata }) => !metadata.deprecated - ); + if ( + !this.showExperimentalComponents && + graph.tags?.includes("experimental") + ) { + continue; + } - if (available.length === 0) { + if (!graph.tags?.includes("component")) { continue; } - if (kit.descriptor.title === "Built-in Kit") { - available.unshift({ - id: "comment", - metadata: { - description: - "A comment node. Use this to put additional information on your board", - title: "Comment", - icon: "edit", - }, - }); + if (graph.tags?.includes("deprecated")) { + continue; } - kitList.set(kit.descriptor.title, available); + let group = kitList.get(mainGraph.title); + if (!group) { + group = []; + kitList.set(mainGraph.title, group); + } + group.push({ id: graph.url!, metadata: graph }); } return kitList; } diff --git a/packages/shared-ui/src/elements/ui-controller/ui-controller.ts b/packages/shared-ui/src/elements/ui-controller/ui-controller.ts index cc4dbd67ffe..48920e5147e 100644 --- a/packages/shared-ui/src/elements/ui-controller/ui-controller.ts +++ b/packages/shared-ui/src/elements/ui-controller/ui-controller.ts @@ -467,6 +467,7 @@ export class UI extends LitElement { html`

Components

{ + server.ready().then(() => { + server.kits.forEach((kit) => { + graphStore.registerKit(kit, []); + }); + }); + }); + const boardServers = { servers, loader,