@breadboard-ai/build
Advanced tools
Comparing version
# Changelog | ||
## 0.5.0 | ||
### Minor Changes | ||
- 55a9647: Rename assertOutput to unsafeOutput | ||
- 1adb24c: Replace multiline field with format, which can be multiline or javascript | ||
- d9ac358: Convert secrets node to use @breadboard-ai/build. No functional difference, but the JSON schema should be slightly stricter. | ||
- 1e86a87: Allow object types to have additional properties. Additional properties are now disabled by default. | ||
### Patch Changes | ||
- 3f9507d: The breadboard type expression object({}) is now more strictly constrained to plain objects (rather than anything). | ||
- 1e86a87: Add enumeration type | ||
- 3f9507d: Simpler JSON schema serialization for array | ||
- 1adb24c: additionalProperties is now set on generated port JSON schemas | ||
- 1e86a87: Add support for default values on static inputs | ||
- fefd109: JSON schema for outputs no longer sets any required properties | ||
- c1dcb0a: Make serialization order more similar to existing one | ||
- 416aed2: Introduce `metadata` for `NodeHandler` entries, teaching node types in Kits to describe themselves. | ||
- f1883d1: Add an output function for customizing a board's output node layout | ||
- 1adb24c: Inbound edges, even if they don't have a value, now inform the auto generated input schema | ||
- d8cb0c9: Add unsafeCast function as an escape hatch for when an output doesn't match a type but you're really really sure it's ok | ||
- 34d9c6d: Generated JSON schemas are now more explicit and verbose | ||
- e6e0168: Allow describe functions to be async | ||
- 1adb24c: Fix some incorrect type errors from certain describe functions. | ||
- 3f9507d: The describe function can now return objects with new descriptions | ||
- 1adb24c: Automatically detected input ports are no longer required. Only static ones. | ||
- 3f9507d: Add multiline option to port config | ||
- c4ca6dc: Allow setting node IDs | ||
- 1adb24c: Allow specifying behaviors | ||
- cfbcdf2: Pass NodeHandlerContext to invoke functions | ||
- 1d9cb16: Inputs can now have examples | ||
- 49da151: Allow setting id on inputs, which can be used to customize the input node id, | ||
or to create multiple input nodes. If two input objects reference the same | ||
id, then they will both be placed into a BGL input node with that ID. If no | ||
id is specified, the usual "input-0" is used. | ||
- 3f9507d: Simpler JSON schema serialization for anyOf | ||
- dfd5ce2: Input ports can now be marked as optional. | ||
- cfc0f15: Add support for input titles | ||
- 00ccb9d: Add unsafeSchema function which allows returning raw arbitrary JSON schema from a describe function | ||
- 08eabf4: Title, description, and version are now included in BGL | ||
- 99fcffe: describe function now receives a NodeDescriberContext | ||
- d9ac358: Add ability to override default port title | ||
- Updated dependencies [cef20ca] | ||
- Updated dependencies [fbf7a83] | ||
- Updated dependencies [54baba8] | ||
- Updated dependencies [49c3aa1] | ||
- Updated dependencies [cdc23bb] | ||
- Updated dependencies [416aed2] | ||
- Updated dependencies [a1fcaea] | ||
- Updated dependencies [c3ed6a7] | ||
- Updated dependencies [3d48482] | ||
- Updated dependencies [f2eda0b] | ||
- Updated dependencies [626139b] | ||
- Updated dependencies [bd44e29] | ||
- Updated dependencies [43da00a] | ||
- Updated dependencies [c3587e1] | ||
- Updated dependencies [3f9507d] | ||
- @google-labs/breadboard@0.18.0 | ||
## 0.4.0 | ||
@@ -4,0 +64,0 @@ |
@@ -12,9 +12,13 @@ /** | ||
export { input } from "./internal/board/input.js"; | ||
export { output } from "./internal/board/output.js"; | ||
export { placeholder } from "./internal/board/placeholder.js"; | ||
export { serialize } from "./internal/board/serialize.js"; | ||
export { unsafeCast } from "./internal/board/unsafe-cast.js"; | ||
export type { SerializableBoard, } from "./internal/common/serializable.js"; | ||
export { defineNodeType } from "./internal/define/define.js"; | ||
export type { NodeFactoryFromDefinition } from "./internal/define/node-factory.js"; | ||
export { unsafeSchema } from "./internal/define/unsafe-schema.js"; | ||
export { anyOf } from "./internal/type-system/any-of.js"; | ||
export { array } from "./internal/type-system/array.js"; | ||
export { enumeration } from "./internal/type-system/enumeration.js"; | ||
export { object } from "./internal/type-system/object.js"; | ||
@@ -21,0 +25,0 @@ export { unsafeType } from "./internal/type-system/unsafe.js"; |
@@ -8,9 +8,13 @@ /** | ||
export { input } from "./internal/board/input.js"; | ||
export { output } from "./internal/board/output.js"; | ||
export { placeholder } from "./internal/board/placeholder.js"; | ||
export { serialize } from "./internal/board/serialize.js"; | ||
export { unsafeCast } from "./internal/board/unsafe-cast.js"; | ||
export { defineNodeType } from "./internal/define/define.js"; | ||
export { unsafeSchema } from "./internal/define/unsafe-schema.js"; | ||
export { anyOf } from "./internal/type-system/any-of.js"; | ||
export { array } from "./internal/type-system/array.js"; | ||
export { enumeration } from "./internal/type-system/enumeration.js"; | ||
export { object } from "./internal/type-system/object.js"; | ||
export { unsafeType } from "./internal/type-system/unsafe.js"; | ||
//# sourceMappingURL=index.js.map |
@@ -10,2 +10,3 @@ /** | ||
import type { GenericSpecialInput } from "./input.js"; | ||
import type { Output } from "./output.js"; | ||
/** | ||
@@ -42,6 +43,9 @@ * Define a new Breadboard board. | ||
export type BoardInputPorts = Record<string, InputPort<JsonSerializable> | GenericSpecialInput>; | ||
export type BoardOutputPorts = Record<string, OutputPortReference<JsonSerializable>>; | ||
export type BoardOutputPorts = Record<string, OutputPortReference<JsonSerializable> | Output<JsonSerializable>>; | ||
export type BoardDefinition<IPORTS extends BoardInputPorts, OPORTS extends BoardOutputPorts> = BoardInstantiateFunction<IPORTS, OPORTS> & { | ||
readonly inputs: IPORTS; | ||
readonly outputs: OPORTS; | ||
readonly title?: string; | ||
readonly description?: string; | ||
readonly version?: string; | ||
}; | ||
@@ -48,0 +52,0 @@ export type GenericBoardDefinition = BoardDefinition<any, any>; |
@@ -6,42 +6,79 @@ /** | ||
*/ | ||
import type { BroadenBasicType, Defined } from "../common/type-util.js"; | ||
import type { BreadboardType, ConvertBreadboardType, JsonSerializable } from "../type-system/type.js"; | ||
export declare function input(params?: InputParamsWithNeitherTypeNorDefault): Input<string>; | ||
export declare function input<T extends BreadboardType>(params: InputParamsWithTypeAndDefault<T>): InputWithDefault<ConvertBreadboardType<T>>; | ||
export declare function input<T extends InputParamsWithOnlyDefault<"string" | "number" | "boolean">>(params: T): InputWithDefault<BroadenBasicType<T["default"]>>; | ||
export declare function input<T extends InputParamsWithOnlyType<BreadboardType>>(params: T): Input<ConvertBreadboardType<T["type"]>>; | ||
export type GenericSpecialInput = Input<JsonSerializable> | InputWithDefault<JsonSerializable>; | ||
export declare function input(): Input<string>; | ||
export declare function input<T extends Record<string, unknown>>(params: T & { | ||
type: Defined; | ||
default: Defined; | ||
} & CheckParams<T>): InputWithDefault<T["type"] extends BreadboardType ? ConvertBreadboardType<T["type"]> : JsonSerializable>; | ||
export declare function input<T extends Record<string, unknown>>(params: T & { | ||
type: Defined; | ||
} & CheckParams<T>): Input<T["type"] extends BreadboardType ? ConvertBreadboardType<T["type"]> : JsonSerializable>; | ||
export declare function input<T extends Record<string, unknown>>(params: T & LooseParams & { | ||
default: Defined; | ||
type?: undefined; | ||
} & CheckParams<T>): InputWithDefault<T["default"] extends string | number | boolean ? BroadenBasicType<T["default"]> : JsonSerializable>; | ||
export declare function input(params: { | ||
$id?: string; | ||
description?: string; | ||
title?: string; | ||
type?: undefined; | ||
default?: undefined; | ||
examples?: undefined; | ||
}): Input<string>; | ||
export declare function input<T extends Record<string, unknown>>(params: T & { | ||
examples: Defined; | ||
} & CheckParams<T>): Input<T["examples"] extends string[] | number[] | boolean[] ? BroadenBasicType<T["examples"][number]> : JsonSerializable>; | ||
interface LooseParams { | ||
$id?: string; | ||
type?: BreadboardType; | ||
title?: string; | ||
description?: string; | ||
default?: JsonSerializable; | ||
examples?: JsonSerializable[]; | ||
} | ||
export interface Input<T extends JsonSerializable> { | ||
readonly __SpecialInputBrand: true; | ||
readonly __type: T; | ||
readonly id?: string; | ||
readonly type: BreadboardType; | ||
readonly title?: string; | ||
readonly description?: string; | ||
readonly default: undefined; | ||
readonly examples?: T[]; | ||
} | ||
export interface InputWithDefault<T extends JsonSerializable> { | ||
readonly __SpecialInputBrand: true; | ||
readonly id?: string; | ||
readonly title?: string; | ||
readonly type: BreadboardType; | ||
readonly description?: string; | ||
readonly default: T; | ||
readonly examples?: T[]; | ||
} | ||
interface InputParamsWithOnlyType<T extends BreadboardType> { | ||
type: T; | ||
export type GenericSpecialInput = Input<JsonSerializable> | InputWithDefault<JsonSerializable>; | ||
type CheckParams<T extends LooseParams> = (T["type"] extends Defined ? { | ||
$id?: string; | ||
type: BreadboardType; | ||
title?: string; | ||
default?: T["type"] extends BreadboardType ? ConvertBreadboardType<T["type"]> : JsonSerializable; | ||
examples?: Array<T["type"] extends BreadboardType ? ConvertBreadboardType<T["type"]> : JsonSerializable>; | ||
description?: string; | ||
default?: undefined; | ||
} | ||
interface InputParamsWithOnlyDefault<T extends "string" | "number" | "boolean"> { | ||
type?: undefined; | ||
} : T["default"] extends Defined ? { | ||
$id?: string; | ||
type?: never; | ||
title?: string; | ||
default: string | number | boolean; | ||
examples?: Array<T["default"]>; | ||
description?: string; | ||
default: ConvertBreadboardType<T>; | ||
} | ||
interface InputParamsWithTypeAndDefault<T extends BreadboardType> { | ||
type: T; | ||
} : T["examples"] extends Defined ? { | ||
$id?: string; | ||
type?: never; | ||
title?: string; | ||
default?: never; | ||
examples?: string[] | number[] | boolean[]; | ||
description?: string; | ||
default: ConvertBreadboardType<T>; | ||
} | ||
interface InputParamsWithNeitherTypeNorDefault { | ||
type?: undefined; | ||
description: string; | ||
default?: undefined; | ||
} | ||
type BroadenBasicType<T extends string | number | boolean> = T extends string ? string : T extends number ? number : T extends boolean ? boolean : never; | ||
} : never) & { | ||
[K in keyof T]: K extends "$id" | "type" | "title" | "default" | "examples" | "description" ? unknown : never; | ||
}; | ||
export {}; |
@@ -8,5 +8,2 @@ /** | ||
* Declare an input for a board. | ||
* | ||
* @param params | ||
* @returns | ||
*/ | ||
@@ -18,4 +15,5 @@ export function input(params) { | ||
} | ||
else if (params?.default !== undefined) { | ||
switch (typeof params.default) { | ||
else if (params?.default !== undefined || | ||
params?.examples?.[0] !== undefined) { | ||
switch (typeof (params.default ?? params.examples?.[0])) { | ||
case "string": { | ||
@@ -43,7 +41,10 @@ type = "string"; | ||
__SpecialInputBrand: true, | ||
id: params?.$id, | ||
type, | ||
title: params?.title, | ||
description: params?.description, | ||
default: params?.default, | ||
examples: params?.examples, | ||
}; | ||
} | ||
//# sourceMappingURL=input.js.map |
@@ -6,3 +6,3 @@ /** | ||
*/ | ||
import { OutputPortGetter } from "../common/port.js"; | ||
import { DefaultValue, OutputPortGetter } from "../common/port.js"; | ||
import { toJSONSchema } from "../type-system/type.js"; | ||
@@ -21,9 +21,7 @@ import { isPlaceholder } from "./placeholder.js"; | ||
// signature of the board. | ||
const mainInputNodeId = nextIdForType("input"); | ||
const mainOutputNodeId = nextIdForType("output"); | ||
const mainInputSchema = {}; | ||
const mainOutputSchema = {}; | ||
const outputNodes = new Map(); | ||
// Analyze inputs and remember some things about them that we'll need when we | ||
// traverse the outputs. | ||
const inputNames = new Map(); | ||
const inputNodes = new Map(); | ||
const inputObjectsToInputNodeInfo = new Map(); | ||
const unconnectedInputs = new Set(); | ||
@@ -34,12 +32,16 @@ const sortedBoardInputs = Object.entries(board.inputs).sort( | ||
for (const [mainInputName, input] of sortedBoardInputs) { | ||
if (inputNames.has(input)) { | ||
if (inputObjectsToInputNodeInfo.has(input)) { | ||
errors.push(`The same input was used as both ` + | ||
`${inputNames.get(input)} and ${mainInputName}.`); | ||
`${inputObjectsToInputNodeInfo.get(input).portName} and ${mainInputName}.`); | ||
} | ||
inputNames.set(input, mainInputName); | ||
const inputNodeId = isSpecialInput(input) && input.id ? input.id : "input-0"; | ||
inputObjectsToInputNodeInfo.set(input, { | ||
nodeId: inputNodeId, | ||
portName: mainInputName, | ||
}); | ||
unconnectedInputs.add(input); | ||
const schema = toJSONSchema(input.type); | ||
if (isSpecialInput(input)) { | ||
if (input.default !== undefined) { | ||
schema.default = input.default; | ||
if (input.title !== undefined) { | ||
schema.title = input.title; | ||
} | ||
@@ -49,4 +51,27 @@ if (input.description !== undefined) { | ||
} | ||
if (input.default !== undefined) { | ||
schema.default = input.default; | ||
} | ||
if (input.examples !== undefined && input.examples.length > 0) { | ||
schema.examples = input.examples; | ||
} | ||
} | ||
mainInputSchema[mainInputName] = schema; | ||
let inputNode = inputNodes.get(inputNodeId); | ||
if (inputNode === undefined) { | ||
inputNode = { | ||
id: inputNodeId, | ||
type: "input", | ||
configuration: { | ||
schema: { | ||
type: "object", | ||
properties: {}, | ||
required: [], | ||
// TODO(aomarks) Disallow extra properties? | ||
}, | ||
}, | ||
}; | ||
inputNodes.set(inputNodeId, inputNode); | ||
} | ||
inputNode.configuration.schema.properties[mainInputName] = schema; | ||
inputNode.configuration.schema.required.push(mainInputName); | ||
} | ||
@@ -57,10 +82,41 @@ // Recursively traverse the graph starting from outputs. | ||
([nameA], [nameB]) => nameA.localeCompare(nameB)); | ||
for (const [mainOutputName, output] of sortedBoardOutputs) { | ||
const actualOutput = output[OutputPortGetter]; | ||
mainOutputSchema[mainOutputName] = toJSONSchema(actualOutput.type); | ||
addEdge(visitNodeAndReturnItsId(actualOutput.node), actualOutput.name, mainOutputNodeId, mainOutputName); | ||
for (const [name, output] of sortedBoardOutputs) { | ||
const port = isSpecialOutput(output) | ||
? output.port[OutputPortGetter] | ||
: output[OutputPortGetter]; | ||
const outputNodeId = isSpecialOutput(output) | ||
? output.id ?? "output-0" | ||
: "output-0"; | ||
let outputNode = outputNodes.get(outputNodeId); | ||
if (outputNode === undefined) { | ||
outputNode = { | ||
id: outputNodeId, | ||
type: "output", | ||
configuration: { | ||
schema: { | ||
type: "object", | ||
properties: {}, | ||
required: [], | ||
// TODO(aomarks) Disallow extra properties? | ||
}, | ||
}, | ||
}; | ||
outputNodes.set(outputNodeId, outputNode); | ||
} | ||
const schema = toJSONSchema(port.type); | ||
if (isSpecialOutput(output)) { | ||
if (output.title !== undefined) { | ||
schema.title = output.title; | ||
} | ||
if (output.description !== undefined) { | ||
schema.description = output.description; | ||
} | ||
} | ||
outputNode.configuration.schema.properties[name] = schema; | ||
outputNode.configuration.schema.required.push(name); | ||
addEdge(visitNodeAndReturnItsId(port.node), port.name, outputNodeId, name); | ||
} | ||
if (unconnectedInputs.size > 0) { | ||
for (const input of unconnectedInputs.values()) { | ||
errors.push(`Board input "${inputNames.get(input)}" ` + | ||
errors.push(`Board input "${inputObjectsToInputNodeInfo.get(input).portName}" ` + | ||
`is not reachable from any of its outputs.`); | ||
@@ -74,29 +130,7 @@ } | ||
} | ||
const mainInputNode = { | ||
id: mainInputNodeId, | ||
type: "input", | ||
configuration: { | ||
schema: { | ||
type: "object", | ||
properties: mainInputSchema, | ||
required: Object.keys(mainInputSchema), | ||
// TODO(aomarks) Disallow extra properties | ||
}, | ||
}, | ||
}; | ||
const mainOutputNode = { | ||
id: mainOutputNodeId, | ||
type: "output", | ||
configuration: { | ||
schema: { | ||
type: "object", | ||
properties: mainOutputSchema, | ||
required: Object.keys(mainOutputSchema), | ||
}, | ||
}, | ||
}; | ||
// Sort the nodes and edges for deterministic BGL output. | ||
const sortedNodes = [...nodes.values()].sort((a, b) => a.id.localeCompare(b.id)); | ||
return { | ||
nodes: [mainInputNode, mainOutputNode, ...sortedNodes], | ||
...(board.title ? { title: board.title } : {}), | ||
...(board.description ? { description: board.description } : {}), | ||
...(board.version ? { version: board.version } : {}), | ||
// Sort the nodes and edges for deterministic BGL output. | ||
edges: edges.sort((a, b) => { | ||
@@ -106,8 +140,8 @@ if (a.from != b.from) { | ||
} | ||
if (a.to != b.to) { | ||
return a.to.localeCompare(b.to); | ||
} | ||
if (a.out != b.out) { | ||
return a.out.localeCompare(b.out); | ||
} | ||
if (a.to != b.to) { | ||
return a.to.localeCompare(b.to); | ||
} | ||
if (a.in != b.in) { | ||
@@ -118,2 +152,7 @@ return a.in.localeCompare(b.in); | ||
}), | ||
nodes: [ | ||
...[...inputNodes.values()].sort((a, b) => a.id.localeCompare(b.id)), | ||
...[...outputNodes.values()].sort((a, b) => a.id.localeCompare(b.id)), | ||
...[...nodes.values()].sort((a, b) => a.id.localeCompare(b.id)), | ||
], | ||
}; | ||
@@ -126,3 +165,3 @@ function visitNodeAndReturnItsId(node) { | ||
const { type } = node; | ||
const thisNodeId = nextIdForType(type); | ||
const thisNodeId = node.id ?? nextIdForType(type); | ||
const configuration = {}; | ||
@@ -144,5 +183,5 @@ // Note it's important we add the node to the nodes map before we next | ||
unconnectedInputs.delete(value); | ||
const mainInputPortName = inputNames.get(value); | ||
if (mainInputPortName !== undefined) { | ||
addEdge(mainInputNodeId, mainInputPortName, thisNodeId, portName); | ||
const inputNodeInfo = inputObjectsToInputNodeInfo.get(value); | ||
if (inputNodeInfo !== undefined) { | ||
addEdge(inputNodeInfo.nodeId, inputNodeInfo.portName, thisNodeId, portName); | ||
} | ||
@@ -168,2 +207,5 @@ else { | ||
} | ||
else if (value === DefaultValue) { | ||
// Omit the value. | ||
} | ||
else { | ||
@@ -184,3 +226,3 @@ configurationEntries.push([ | ||
function addEdge(from, fromPort, to, toPort) { | ||
edges.push({ from, out: fromPort, to, in: toPort }); | ||
edges.push({ from, to, out: fromPort, in: toPort }); | ||
} | ||
@@ -198,2 +240,7 @@ function nextIdForType(type) { | ||
} | ||
function isSpecialOutput(value) { | ||
return (typeof value === "object" && | ||
value !== null && | ||
"__SpecialOutputBrand" in value); | ||
} | ||
function isOutputPortReference(value) { | ||
@@ -200,0 +247,0 @@ return (typeof value === "object" && value !== null && OutputPortGetter in value); |
@@ -8,58 +8,7 @@ /** | ||
import type { Placeholder } from "../board/placeholder.js"; | ||
import type { PortConfig } from "../define/config.js"; | ||
import type { BreadboardType, ConvertBreadboardType, JsonSerializable } from "../type-system/type.js"; | ||
import type { SerializableInputPort, SerializableNode, SerializableOutputPort } from "./serializable.js"; | ||
export type PortConfig = StaticPortConfig | DynamicPortConfig; | ||
/** | ||
* Configuration parameters for a static Breadboard node port. A port is static | ||
* if it always exists for all instances of a node type. | ||
*/ | ||
interface StaticPortConfig { | ||
/** | ||
* The {@link BreadboardType} that values sent or received on this port will | ||
* be required to conform to. | ||
*/ | ||
type: BreadboardType; | ||
/** | ||
* An optional brief description of this port. Useful when introspecting and | ||
* debugging. | ||
*/ | ||
description?: string; | ||
/** | ||
* If true, this port is is the `primary` input or output port of the node it | ||
* belongs to. | ||
* | ||
* When a node definition has one primary input port, and/or one primary | ||
* output port, then instances of that node will themselves behave like that | ||
* primary input and/or output ports, depending on the context. Note that it | ||
* is an error for a node to have more than 1 primary input ports, or more | ||
* than 1 primary output ports. | ||
* | ||
* For example, an LLM node might have a primary input for `prompt`, and a | ||
* primary output for `completion`. This would mean that in API locations | ||
* where an input port is expected, instead of writing `llm.inputs.prompt`, | ||
* one could simply write `llm`, and the `prompt` port will be selected | ||
* automatically. Likewise for `completion`, where `llm` would be equivalent | ||
* to `llm.outputs.completion` where an output port is expected. | ||
* | ||
* Note this has no effect on Breadboard runtime behavior, it is purely a hint | ||
* to the JavaScript/TypeScript API to help make board construction more | ||
* concise. | ||
*/ | ||
primary?: boolean; | ||
} | ||
/** | ||
* Configuration parameters that apply to all dynamic Breadboard ports on a | ||
* node. | ||
* | ||
* A port is dynamic if its existence, name, type, or other metadata can be | ||
* different across different instances of a node type. | ||
*/ | ||
interface DynamicPortConfig extends StaticPortConfig { | ||
/** | ||
* The `primary` property should never be set on a dynamic port config, | ||
* because it is not possible for a dynamic port to be primary. | ||
*/ | ||
primary?: never; | ||
} | ||
export declare const OutputPortGetter = "__output"; | ||
export declare const DefaultValue: unique symbol; | ||
/** | ||
@@ -73,4 +22,4 @@ * A Breadboard node port which receives values. | ||
readonly node: SerializableNode; | ||
readonly value?: ValueOrOutputPort<T>; | ||
constructor(type: BreadboardType, name: string, node: SerializableNode, value: ValueOrOutputPort<T>); | ||
readonly value?: ValueOrOutputPort<T> | typeof DefaultValue; | ||
constructor(type: BreadboardType, name: string, node: SerializableNode, value: ValueOrOutputPort<T> | typeof DefaultValue); | ||
} | ||
@@ -77,0 +26,0 @@ /** |
@@ -9,2 +9,3 @@ /** | ||
export const OutputPortGetter = "__output"; | ||
export const DefaultValue = Symbol(); | ||
/** | ||
@@ -11,0 +12,0 @@ * A Breadboard node port which receives values. |
@@ -7,10 +7,15 @@ /** | ||
import type { GenericSpecialInput } from "../board/input.js"; | ||
import type { Output } from "../board/output.js"; | ||
import type { Placeholder } from "../board/placeholder.js"; | ||
import type { BreadboardType, JsonSerializable } from "../type-system/type.js"; | ||
import type { OutputPortGetter } from "./port.js"; | ||
import type { DefaultValue, OutputPortGetter } from "./port.js"; | ||
export interface SerializableBoard { | ||
inputs: Record<string, SerializableInputPort | GenericSpecialInput>; | ||
outputs: Record<string, SerializableOutputPortReference>; | ||
outputs: Record<string, SerializableOutputPortReference | Output<JsonSerializable>>; | ||
title?: string; | ||
description?: string; | ||
version?: string; | ||
} | ||
export interface SerializableNode { | ||
id?: string; | ||
type: string; | ||
@@ -23,3 +28,3 @@ inputs: Record<string, SerializableInputPort>; | ||
node: SerializableNode; | ||
value?: JsonSerializable | SerializableOutputPortReference | GenericSpecialInput | Placeholder<JsonSerializable>; | ||
value?: JsonSerializable | SerializableOutputPortReference | GenericSpecialInput | Placeholder<JsonSerializable> | typeof DefaultValue; | ||
} | ||
@@ -26,0 +31,0 @@ export interface SerializableOutputPort { |
@@ -40,2 +40,8 @@ /** | ||
} : never; | ||
/** | ||
* Some type, or a promise of it. | ||
*/ | ||
export type MaybePromise<T> = T | Promise<T>; | ||
export type Defined = {} | null; | ||
export type BroadenBasicType<T extends string | number | boolean> = T extends string ? string : T extends number ? number : T extends boolean ? boolean : never; | ||
export {}; |
@@ -6,3 +6,4 @@ /** | ||
*/ | ||
import type { BreadboardType } from "../type-system/type.js"; | ||
import type { BehaviorSchema } from "@google-labs/breadboard"; | ||
import type { BreadboardType, JsonSerializable } from "../type-system/type.js"; | ||
export type PortConfig = InputPortConfig | OutputPortConfig; | ||
@@ -12,17 +13,93 @@ export type PortConfigs = Record<string, PortConfig>; | ||
export type OutputPortConfig = StaticOutputPortConfig | DynamicOutputPortConfig; | ||
/** | ||
* Additional information about the format of the value. Primarily used to | ||
* determine how strings are displayed in the Breadboard Visual Editor. | ||
*/ | ||
export type Format = /* A string that is likely to contain multiple lines. */ "multiline" | /* A string that is JavaScript code. */ "javascript"; | ||
interface BaseConfig { | ||
/** | ||
* The {@link BreadboardType} that values sent or received on this port will | ||
* be required to conform to. | ||
*/ | ||
type: BreadboardType; | ||
/** | ||
* An optional title for the port. Defaults to the name of the port. | ||
*/ | ||
title?: string; | ||
/** | ||
* An optional brief description of this port. Useful when introspecting and | ||
* debugging. | ||
*/ | ||
description?: string; | ||
/** | ||
* Special format annotations. Primarily used as hints for the Breadboard | ||
* visual editor. | ||
*/ | ||
format?: Format; | ||
/** | ||
* Can be used to provide additional hints to the UI or to other parts of | ||
* the system about behavior of this particular input/output or input/output | ||
* port. | ||
*/ | ||
behavior?: BehaviorSchema[]; | ||
} | ||
export interface StaticInputPortConfig extends BaseConfig { | ||
interface StaticBase { | ||
/** | ||
* If true, this port is is the `primary` input or output port of the node it | ||
* belongs to. | ||
* | ||
* When a node definition has one primary input port, and/or one primary | ||
* output port, then instances of that node will themselves behave like that | ||
* primary input and/or output ports, depending on the context. Note that it | ||
* is an error for a node to have more than 1 primary input ports, or more | ||
* than 1 primary output ports. | ||
* | ||
* For example, an LLM node might have a primary input for `prompt`, and a | ||
* primary output for `completion`. This would mean that in API locations | ||
* where an input port is expected, instead of writing `llm.inputs.prompt`, | ||
* one could simply write `llm`, and the `prompt` port will be selected | ||
* automatically. Likewise for `completion`, where `llm` would be equivalent | ||
* to `llm.outputs.completion` where an output port is expected. | ||
* | ||
* Note this has no effect on Breadboard runtime behavior, it is purely a hint | ||
* to the JavaScript/TypeScript API to help make board construction more | ||
* concise. | ||
*/ | ||
primary?: true; | ||
} | ||
/** | ||
* Configuration for a static import port of a Breadboard node. | ||
*/ | ||
export interface StaticInputPortConfig extends BaseConfig, StaticBase { | ||
/** | ||
* A default value for this port. | ||
*/ | ||
default?: JsonSerializable; | ||
/** | ||
* If true, this port is not required and will be passed to `invoke` as | ||
* `undefined`. | ||
*/ | ||
optional?: true; | ||
} | ||
/** | ||
* Configuration for the dynamic import ports of a Breadboard node. | ||
*/ | ||
export interface DynamicInputPortConfig extends BaseConfig { | ||
} | ||
export interface StaticOutputPortConfig extends BaseConfig { | ||
primary?: true; | ||
/** | ||
* Configuration for a static output port of a Breadboard node. | ||
*/ | ||
export interface StaticOutputPortConfig extends BaseConfig, StaticBase { | ||
} | ||
/** | ||
* Configuration for the dynamic output ports of a Breadboard node. | ||
*/ | ||
export interface DynamicOutputPortConfig extends BaseConfig { | ||
/** | ||
* If true, for each dynamic input that an instance of this node type is | ||
* instantiated with, an output port with the same name will be automatically | ||
* created. | ||
*/ | ||
reflective?: true; | ||
} | ||
export {}; |
@@ -6,4 +6,6 @@ /** | ||
*/ | ||
import type { CountUnion, Expand } from "../common/type-util.js"; | ||
import type { NodeDescriberContext, NodeHandlerContext, NodeHandlerMetadata } from "@google-labs/breadboard"; | ||
import type { CountUnion, Expand, MaybePromise } from "../common/type-util.js"; | ||
import type { ConvertBreadboardType, JsonSerializable } from "../type-system/type.js"; | ||
import type { UnsafeSchema } from "./unsafe-schema.js"; | ||
import type { DynamicInputPortConfig, DynamicOutputPortConfig, InputPortConfig, OutputPortConfig, PortConfig, StaticInputPortConfig, StaticOutputPortConfig } from "./config.js"; | ||
@@ -104,3 +106,3 @@ import { type Definition } from "./definition.js"; | ||
*/ | ||
export declare function defineNodeType<I extends Record<string, InputPortConfig>, O extends Record<string, OutputPortConfig>, F extends LooseInvokeFn<I>, D extends LooseDescribeFn>(params: { | ||
export declare function defineNodeType<I extends Record<string, InputPortConfig>, O extends Record<string, OutputPortConfig>, F extends LooseInvokeFn<I>, D extends VeryLooseDescribeFn>(params: { | ||
name: string; | ||
@@ -111,2 +113,3 @@ inputs: I; | ||
describe?: D; | ||
metadata?: NodeHandlerMetadata; | ||
} & { | ||
@@ -116,5 +119,8 @@ inputs: StrictInputs<I>; | ||
invoke: StrictInvokeFn<I, O, F>; | ||
} & StrictDescribeFn<I, O>): Definition<Expand<GetStaticTypes<I>>, Expand<GetStaticTypes<O>>, GetDynamicTypes<I>, GetDynamicTypes<O>, GetReflective<O>, GetPrimary<I>, GetPrimary<O>>; | ||
} & StrictDescribeFn<I, O>): Definition<Expand<GetStaticTypes<I>>, Expand<GetStaticTypes<O>>, GetDynamicTypes<I>, GetDynamicTypes<O>, GetOptionalInputs<I> & keyof Expand<GetStaticTypes<I>>, GetReflective<O>, GetPrimary<I>, GetPrimary<O>>; | ||
type StrictInputs<I extends Record<string, InputPortConfig>> = { | ||
[K in keyof I]: K extends "*" ? StrictMatch<I[K], DynamicInputPortConfig> : StrictMatch<I[K], StaticInputPortConfig>; | ||
[K in keyof I]: K extends "$id" ? never : K extends "*" ? StrictMatch<I[K], DynamicInputPortConfig> : StrictMatch<I[K], GetDefault<I[K]> extends JsonSerializable ? StaticInputPortConfig & { | ||
default: Convert<I[K]>; | ||
optional: never; | ||
} : StaticInputPortConfig>; | ||
} & ForbidMultiplePrimaries<I>; | ||
@@ -124,2 +130,3 @@ type StrictOutputs<O extends Record<string, OutputPortConfig>> = { | ||
} & ForbidMultiplePrimaries<O>; | ||
type GetDefault<I extends PortConfig> = I extends StaticInputPortConfig ? I["default"] : undefined; | ||
/** | ||
@@ -140,8 +147,9 @@ * Check that ACTUAL is assignable to EXPECTED and that there are no excess | ||
}[keyof C]; | ||
type LooseInvokeFn<I extends Record<string, InputPortConfig>> = Expand<(staticParams: Expand<StaticInvokeParams<I>>, dynamicParams: Expand<DynamicInvokeParams<I>>) => { | ||
type LooseInvokeFn<I extends Record<string, InputPortConfig>> = Expand<(staticParams: Expand<StaticInvokeParams<I>>, dynamicParams: Expand<DynamicInvokeParams<I>>) => MaybePromise<{ | ||
[K: string]: JsonSerializable; | ||
} | Promise<{ | ||
[K: string]: JsonSerializable; | ||
}>>; | ||
type StrictInvokeFn<I extends Record<string, InputPortConfig>, O extends Record<string, OutputPortConfig>, F extends LooseInvokeFn<I>> = (staticInputs: Expand<StaticInvokeParams<I>>, dynamicInputs: Expand<DynamicInvokeParams<I>>) => StrictInvokeFnReturn<I, O, F> | Promise<StrictInvokeFnReturn<I, O, F>>; | ||
export type VeryLooseInvokeFn = (staticParams: Record<string, JsonSerializable>, dynamicParams: Record<string, JsonSerializable>, context: NodeHandlerContext) => { | ||
[K: string]: JsonSerializable | undefined; | ||
}; | ||
type StrictInvokeFn<I extends Record<string, InputPortConfig>, O extends Record<string, OutputPortConfig>, F extends LooseInvokeFn<I>> = (staticInputs: Expand<StaticInvokeParams<I>>, dynamicInputs: Expand<DynamicInvokeParams<I>>, context: NodeHandlerContext) => MaybePromise<StrictInvokeFnReturn<I, O, F>>; | ||
type StrictInvokeFnReturn<I extends Record<string, InputPortConfig>, O extends Record<string, OutputPortConfig>, F extends LooseInvokeFn<I>> = { | ||
@@ -153,3 +161,3 @@ [K in keyof Omit<O, "*">]: Convert<O[K]>; | ||
type StaticInvokeParams<I extends Record<string, InputPortConfig>> = { | ||
[K in keyof Omit<I, "*">]: Convert<I[K]>; | ||
[K in keyof Omit<I, "*">]: I[K] extends StaticInputPortConfig ? I[K]["optional"] extends true ? Convert<I[K]> | undefined : Convert<I[K]> : Convert<I[K]>; | ||
}; | ||
@@ -162,2 +170,5 @@ type DynamicInvokeParams<I extends Record<string, InputPortConfig>> = I["*"] extends DynamicInputPortConfig ? { | ||
}; | ||
type GetOptionalInputs<I extends Record<string, InputPortConfig>> = { | ||
[K in keyof I]: I[K] extends StaticInputPortConfig ? I[K]["optional"] extends true ? K : I[K]["default"] extends JsonSerializable ? K : never : never; | ||
}[keyof I]; | ||
type GetDynamicTypes<C extends Record<string, PortConfig>> = C["*"] extends PortConfig ? Convert<C["*"]> : undefined; | ||
@@ -169,23 +180,32 @@ type GetPrimary<C extends Record<string, PortConfig>> = { | ||
type Convert<C extends PortConfig> = ConvertBreadboardType<C["type"]>; | ||
type LooseDescribeFn = Function; | ||
export type LooseDescribeFn = (staticParams: Record<string, JsonSerializable>, dynamicParams: Record<string, JsonSerializable>, context?: NodeDescriberContext) => MaybePromise<{ | ||
inputs?: DynamicInputPorts; | ||
outputs?: DynamicInputPorts; | ||
}>; | ||
type VeryLooseDescribeFn = Function; | ||
export type DynamicInputPorts = string[] | { | ||
[K: string]: { | ||
description?: string; | ||
} | undefined; | ||
} | UnsafeSchema; | ||
type StrictDescribeFn<I extends Record<string, InputPortConfig>, O extends Record<string, OutputPortConfig>> = I["*"] extends DynamicInputPortConfig ? O["*"] extends DynamicOutputPortConfig ? O["*"]["reflective"] extends true ? { | ||
describe?: (staticInputs: Expand<StaticInvokeParams<I>>, dynamicInputs: Expand<DynamicInvokeParams<I>>) => { | ||
inputs: string[]; | ||
describe?: (staticInputs: Expand<StaticInvokeParams<I>>, dynamicInputs: Expand<DynamicInvokeParams<I>>, context?: NodeDescriberContext) => MaybePromise<{ | ||
inputs: DynamicInputPorts; | ||
outputs?: never; | ||
}; | ||
}>; | ||
} : { | ||
describe: (staticInputs: Expand<StaticInvokeParams<I>>, dynamicInputs: Expand<DynamicInvokeParams<I>>) => { | ||
inputs?: string[]; | ||
outputs: string[]; | ||
}; | ||
describe: (staticInputs: Expand<StaticInvokeParams<I>>, dynamicInputs: Expand<DynamicInvokeParams<I>>, context?: NodeDescriberContext) => MaybePromise<{ | ||
inputs?: DynamicInputPorts; | ||
outputs: DynamicInputPorts; | ||
}>; | ||
} : { | ||
describe?: (staticInputs: Expand<StaticInvokeParams<I>>, dynamicInputs: Expand<DynamicInvokeParams<I>>) => { | ||
inputs: string[]; | ||
describe?: (staticInputs: Expand<StaticInvokeParams<I>>, dynamicInputs: Expand<DynamicInvokeParams<I>>, context?: NodeDescriberContext) => MaybePromise<{ | ||
inputs: DynamicInputPorts; | ||
outputs?: never; | ||
}; | ||
}>; | ||
} : O["*"] extends DynamicOutputPortConfig ? { | ||
describe: (staticInputs: Expand<StaticInvokeParams<I>>, dynamicInputs: Expand<DynamicInvokeParams<I>>) => { | ||
describe: (staticInputs: Expand<StaticInvokeParams<I>>, dynamicInputs: Expand<DynamicInvokeParams<I>>, context?: NodeDescriberContext) => MaybePromise<{ | ||
inputs?: never; | ||
outputs: string[]; | ||
}; | ||
outputs: DynamicInputPorts; | ||
}>; | ||
} : { | ||
@@ -192,0 +212,0 @@ describe?: never; |
@@ -1,2 +0,1 @@ | ||
/* eslint-disable @typescript-eslint/ban-types */ | ||
/** | ||
@@ -118,2 +117,3 @@ * @license | ||
describe: impl.describe.bind(impl), | ||
metadata: params.metadata || {}, | ||
}); | ||
@@ -120,0 +120,0 @@ } |
@@ -6,3 +6,3 @@ /** | ||
*/ | ||
import type { InputValues, NodeDescriberResult, OutputValues } from "@google-labs/breadboard"; | ||
import type { InputValues, NodeDescriberContext, NodeDescriberResult, NodeHandlerContext, OutputValues, Schema } from "@google-labs/breadboard"; | ||
import type { Input, InputWithDefault } from "../board/input.js"; | ||
@@ -13,4 +13,5 @@ import type { Placeholder } from "../board/placeholder.js"; | ||
import type { Expand } from "../common/type-util.js"; | ||
import type { JsonSerializable } from "../type-system/type.js"; | ||
import { type JsonSerializable } from "../type-system/type.js"; | ||
import type { DynamicOutputPortConfig, PortConfig, PortConfigs } from "./config.js"; | ||
import type { LooseDescribeFn, VeryLooseInvokeFn } from "./define.js"; | ||
import { Instance } from "./instance.js"; | ||
@@ -21,4 +22,4 @@ export interface Definition<SI extends { | ||
[K: string]: JsonSerializable; | ||
}, DI extends JsonSerializable | undefined, DO extends JsonSerializable | undefined, R extends boolean, PI extends keyof SI | undefined, PO extends keyof SO | undefined> extends StrictNodeHandler { | ||
<A extends LooseInstantiateArgs>(args: A & StrictInstantiateArgs<SI, DI, A>): Instance<InstanceInputs<SI, DI, A>, InstanceOutputs<SI, SO, DO, R, A>, R extends true ? undefined : DO, PI, PO, R>; | ||
}, DI extends JsonSerializable | undefined, DO extends JsonSerializable | undefined, OI extends keyof SI, R extends boolean, PI extends keyof SI | undefined, PO extends keyof SO | undefined> extends StrictNodeHandler { | ||
<A extends LooseInstantiateArgs>(args: A & StrictInstantiateArgs<SI, OI, DI, A>): Instance<InstanceInputs<SI, DI, A>, InstanceOutputs<SI, SO, DO, R, A>, R extends true ? undefined : DO, PI, PO, R>; | ||
} | ||
@@ -29,13 +30,8 @@ export declare class DefinitionImpl<SI extends { | ||
[K: string]: JsonSerializable; | ||
}, DI extends JsonSerializable | undefined, DO extends JsonSerializable | undefined, R extends boolean, PI extends keyof SI | undefined, PO extends keyof SO | undefined> implements StrictNodeHandler { | ||
}, DI extends JsonSerializable | undefined, DO extends JsonSerializable | undefined, OI extends keyof SI, R extends boolean, PI extends keyof SI | undefined, PO extends keyof SO | undefined> implements StrictNodeHandler { | ||
#private; | ||
constructor(name: string, staticInputs: PortConfigs, staticOutputs: PortConfigs, dynamicInputs: PortConfig | undefined, dynamicOutputs: DynamicOutputPortConfig | undefined, primaryInput: string | undefined, primaryOutput: string | undefined, invoke: (staticParams: Record<string, JsonSerializable>, dynamicParams: Record<string, JsonSerializable>) => { | ||
[K: string]: JsonSerializable; | ||
}, describe?: (staticParams: Record<string, JsonSerializable>, dynamicParams: Record<string, JsonSerializable>) => { | ||
inputs?: string[]; | ||
outputs?: string[]; | ||
}); | ||
instantiate<A extends LooseInstantiateArgs>(args: A & StrictInstantiateArgs<SI, DI, A>): Instance<InstanceInputs<SI, DI, A>, InstanceOutputs<SI, SO, DO, R, A>, R extends true ? undefined : DO, PI, PO, R>; | ||
invoke(values: InputValues): Promise<OutputValues>; | ||
describe(values?: InputValues): Promise<NodeDescriberResult>; | ||
constructor(name: string, staticInputs: PortConfigs, staticOutputs: PortConfigs, dynamicInputs: PortConfig | undefined, dynamicOutputs: DynamicOutputPortConfig | undefined, primaryInput: string | undefined, primaryOutput: string | undefined, invoke: VeryLooseInvokeFn, describe?: LooseDescribeFn); | ||
instantiate<A extends LooseInstantiateArgs>(args: A & StrictInstantiateArgs<SI, OI, DI, A>): Instance<InstanceInputs<SI, DI, A>, InstanceOutputs<SI, SO, DO, R, A>, R extends true ? undefined : DO, PI, PO, R>; | ||
invoke(values: InputValues, context: NodeHandlerContext): Promise<OutputValues>; | ||
describe(values?: InputValues, inboundEdges?: Schema, _outboundEdges?: Schema, context?: NodeDescriberContext): Promise<NodeDescriberResult>; | ||
} | ||
@@ -45,6 +41,10 @@ type LooseInstantiateArgs = object; | ||
[K: string]: JsonSerializable; | ||
}, DI extends JsonSerializable | undefined, A extends LooseInstantiateArgs> = { | ||
[K in keyof SI]: SI[K] | OutputPortReference<SI[K]> | Input<SI[K]> | InputWithDefault<SI[K]> | Placeholder<SI[K]>; | ||
}, OI extends keyof SI, DI extends JsonSerializable | undefined, A extends LooseInstantiateArgs> = { | ||
$id?: string; | ||
} & { | ||
[K in keyof Omit<A, keyof SI>]: DI extends JsonSerializable ? DI | OutputPortReference<DI> | Input<DI> | InputWithDefault<DI> : never; | ||
[K in keyof Omit<SI, OI>]: InstantiateArg<SI[K]>; | ||
} & { | ||
[K in OI]?: InstantiateArg<SI[K]> | undefined; | ||
} & { | ||
[K in keyof Omit<A, keyof SI | "$id">]: DI extends JsonSerializable ? InstantiateArg<DI> : never; | ||
}; | ||
@@ -54,3 +54,3 @@ type InstanceInputs<SI extends { | ||
}, DI extends JsonSerializable | undefined, A extends LooseInstantiateArgs> = Expand<SI & { | ||
[K in keyof A]: K extends keyof SI ? SI[K] : DI; | ||
[K in keyof Omit<A, "$id">]: K extends keyof SI ? SI[K] : DI; | ||
}>; | ||
@@ -62,4 +62,5 @@ type InstanceOutputs<SI extends { | ||
}, DO extends JsonSerializable | undefined, R extends boolean, A extends LooseInstantiateArgs> = R extends true ? Expand<SO & { | ||
[K in Exclude<keyof A, keyof SI>]: DO; | ||
[K in Exclude<keyof A, keyof SI | "$id">]: DO; | ||
}> : SO; | ||
type InstantiateArg<T extends JsonSerializable> = T | OutputPortReference<T> | Input<T> | InputWithDefault<T> | Placeholder<T>; | ||
export {}; |
@@ -6,4 +6,6 @@ /** | ||
*/ | ||
import { toJSONSchema } from "../type-system/type.js"; | ||
import { Instance } from "./instance.js"; | ||
import { portConfigMapToJSONSchema } from "./json-schema.js"; | ||
import { isUnsafeSchema, unsafeSchemaAccessor, } from "./unsafe-schema.js"; | ||
export class DefinitionImpl { | ||
@@ -18,6 +20,8 @@ #name; | ||
#primaryOutput; | ||
// TODO(aomarks) Support promises | ||
#invoke; | ||
#describe; | ||
constructor(name, staticInputs, staticOutputs, dynamicInputs, dynamicOutputs, primaryInput, primaryOutput, invoke, describe) { | ||
if ("$id" in staticInputs) { | ||
throw new Error('"$id" cannot be used as an input port name because it is reserved'); | ||
} | ||
this.#name = name; | ||
@@ -43,15 +47,22 @@ this.#staticInputs = staticInputs; | ||
} | ||
invoke(values) { | ||
const { staticValues, dynamicValues } = this.#partitionRuntimeInputValues(values); | ||
return Promise.resolve(this.#invoke(staticValues, dynamicValues)); | ||
invoke(values, context) { | ||
const { staticValues, dynamicValues } = this.#applyDefaultsAndPartitionRuntimeInputValues(values); | ||
return Promise.resolve(this.#invoke(staticValues, dynamicValues, context)); | ||
} | ||
/** | ||
* Split the values between static and dynamic ports. We do this for type | ||
* safety, because in TypeScript it is unfortunately not possible to define an | ||
* object where the values of the unknown keys are of one type, and the known | ||
* keys are of an incompatible type. | ||
* Apply defaults, and split the values between static and dynamic ports. | ||
* | ||
* We split inputs values for type safety, because in TypeScript it is | ||
* unfortunately not possible to define an object where the values of the | ||
* unknown keys are of one type, and the known keys are of an incompatible | ||
* type. | ||
*/ | ||
#partitionRuntimeInputValues(values) { | ||
#applyDefaultsAndPartitionRuntimeInputValues(values) { | ||
const staticValues = {}; | ||
const dynamicValues = {}; | ||
for (const [name, config] of Object.entries(this.#staticInputs)) { | ||
if (config.default !== undefined) { | ||
staticValues[name] = config.default; | ||
} | ||
} | ||
for (const [name, value] of Object.entries(values)) { | ||
@@ -71,62 +82,115 @@ if (this.#staticInputs[name] !== undefined) { | ||
} | ||
async describe(values) { | ||
async describe(values, inboundEdges, _outboundEdges, context) { | ||
let user = undefined; | ||
if (this.#describe !== undefined) { | ||
if (values !== undefined) { | ||
const { staticValues, dynamicValues } = this.#partitionRuntimeInputValues(values); | ||
user = this.#describe(staticValues, dynamicValues); | ||
const { staticValues, dynamicValues } = this.#applyDefaultsAndPartitionRuntimeInputValues(values); | ||
user = await this.#describe(staticValues, dynamicValues, context); | ||
} | ||
else { | ||
user = this.#describe({}, {}); | ||
user = await this.#describe({}, {}, context); | ||
} | ||
} | ||
let inputSchema; | ||
if (this.#dynamicInputs === undefined) { | ||
if (isUnsafeSchema(user?.inputs)) { | ||
inputSchema = mergeStaticsAndUnsafeUserSchema(portConfigMapToJSONSchema(this.#staticInputs, []), user.inputs[unsafeSchemaAccessor]); | ||
} | ||
else if (this.#dynamicInputs === undefined) { | ||
// All inputs are static. | ||
inputSchema = portConfigMapToJSONSchema(this.#staticInputs); | ||
inputSchema = { | ||
...portConfigMapToJSONSchema(this.#staticInputs, []), | ||
additionalProperties: false, | ||
}; | ||
} | ||
else if (user?.inputs !== undefined) { | ||
// The definition author has provided the inputs. | ||
const d = this.#dynamicInputs; | ||
inputSchema = portConfigMapToJSONSchema({ | ||
...Object.fromEntries(user.inputs.map((name) => [name, d])), | ||
...this.#staticInputs, | ||
}); | ||
const { newStatic, newDynamic } = parseDynamicPorts(user.inputs, this.#dynamicInputs); | ||
inputSchema = portConfigMapToJSONSchema({ ...newStatic, ...this.#staticInputs }, | ||
// TODO(aomarks) The user should be able to indicate from their describe | ||
// function whether a specific input is optional/required. | ||
[]); | ||
if (newDynamic === undefined) { | ||
inputSchema.additionalProperties = false; | ||
} | ||
else if (newDynamic.type === "unknown") { | ||
inputSchema.additionalProperties = true; | ||
} | ||
else { | ||
inputSchema.additionalProperties = toJSONSchema(newDynamic.type); | ||
} | ||
} | ||
else if (values !== undefined) { | ||
else { | ||
// No definition author inputs, assume all actual inputs are valid. | ||
const d = this.#dynamicInputs; | ||
inputSchema = portConfigMapToJSONSchema({ | ||
...Object.fromEntries(Object.keys(values).map((name) => [name, d])), | ||
...this.#staticInputs, | ||
}); | ||
const actualInputNames = [ | ||
...new Set([ | ||
...Object.keys(values ?? {}), | ||
...Object.keys(inboundEdges?.properties ?? {}), | ||
]), | ||
].sort(); | ||
const actualDynamicInputNames = actualInputNames.filter( | ||
// Only include the actual input names that aren't in our static inputs. | ||
(name) => this.#staticInputs[name] === undefined); | ||
const dynamicInputConfig = this.#dynamicInputs; | ||
inputSchema = { | ||
...portConfigMapToJSONSchema({ | ||
...Object.fromEntries(actualInputNames.map((name) => [name, dynamicInputConfig])), | ||
...this.#staticInputs, | ||
}, actualDynamicInputNames), | ||
additionalProperties: this.#dynamicInputs.type === "unknown" | ||
? true | ||
: toJSONSchema(this.#dynamicInputs.type), | ||
}; | ||
} | ||
else { | ||
// No definition author inputs or values. | ||
inputSchema = portConfigMapToJSONSchema(this.#staticInputs); | ||
let outputSchema; | ||
if (isUnsafeSchema(user?.outputs)) { | ||
outputSchema = mergeStaticsAndUnsafeUserSchema(portConfigMapToJSONSchema(this.#staticOutputs, true), user.outputs[unsafeSchemaAccessor]); | ||
} | ||
let outputSchema; | ||
if (this.#dynamicOutputs === undefined) { | ||
else if (this.#dynamicOutputs === undefined) { | ||
// All outputs are static. | ||
outputSchema = portConfigMapToJSONSchema(this.#staticOutputs); | ||
outputSchema = { | ||
...portConfigMapToJSONSchema(this.#staticOutputs, | ||
// TODO(aomarks) The Breadboard visual editor interprets JSON schema | ||
// "required" on an output as "the user must wire this to something" | ||
// (shows up as a red port). That might be not quite right, it seems | ||
// like "required" here should describe the expectations of the node | ||
// implementation's return object, not the way the user choses to use | ||
// the output. | ||
true), | ||
additionalProperties: false, | ||
}; | ||
} | ||
else if (this.#reflective) { | ||
// We're reflective, so our outputs are determined by our dynamic inputs. | ||
const dynamicInputNames = Object.keys(inputSchema.properties).filter((name) => this.#staticInputs[name] === undefined); | ||
const dynamicInputNames = Object.keys(inputSchema.properties ?? {}).filter((name) => this.#staticInputs[name] === undefined); | ||
const d = this.#dynamicOutputs; | ||
outputSchema = portConfigMapToJSONSchema({ | ||
...Object.fromEntries(dynamicInputNames.map((name) => [name, d])), | ||
...this.#staticOutputs, | ||
}); | ||
outputSchema = { | ||
...portConfigMapToJSONSchema({ | ||
...Object.fromEntries(dynamicInputNames.map((name) => [name, d])), | ||
...this.#staticOutputs, | ||
}, true), | ||
additionalProperties: false, | ||
}; | ||
} | ||
else if (user?.outputs !== undefined) { | ||
// The definition author has provided the outputs. | ||
const d = this.#dynamicOutputs; | ||
const { newStatic, newDynamic } = parseDynamicPorts(user.outputs, this.#dynamicOutputs); | ||
outputSchema = portConfigMapToJSONSchema({ | ||
...Object.fromEntries(user.outputs.map((name) => [name, d])), | ||
...newStatic, | ||
...this.#staticOutputs, | ||
}); | ||
}, true); | ||
if (newDynamic === undefined) { | ||
outputSchema.additionalProperties = false; | ||
} | ||
else if (newDynamic.type === "unknown") { | ||
outputSchema.additionalProperties = true; | ||
} | ||
else { | ||
outputSchema.additionalProperties = toJSONSchema(newDynamic.type); | ||
} | ||
} | ||
else { | ||
outputSchema = portConfigMapToJSONSchema(this.#staticOutputs); | ||
outputSchema = { | ||
...portConfigMapToJSONSchema(this.#staticOutputs, true), | ||
additionalProperties: toJSONSchema(this.#dynamicOutputs.type), | ||
}; | ||
} | ||
@@ -139,2 +203,30 @@ return { | ||
} | ||
function parseDynamicPorts(ports, base) { | ||
ports = Array.isArray(ports) | ||
? Object.fromEntries(ports.map((name) => [name, {}])) | ||
: ports; | ||
const newStatic = Object.fromEntries(Object.entries(ports) | ||
.filter( | ||
/** See {@link DynamicInputPorts} for why undefined is possible here. */ | ||
([name, config]) => config !== undefined && name !== "*") | ||
.map(([name, config]) => [name, { ...base, ...config }])); | ||
const newDynamic = ports["*"] !== undefined ? { ...base, ...ports["*"] } : undefined; | ||
return { newStatic, newDynamic }; | ||
} | ||
function mergeStaticsAndUnsafeUserSchema(statics, unsafe) { | ||
const merged = { ...statics, ...unsafe }; | ||
if (statics.properties || unsafe.properties) { | ||
merged.properties = { ...statics.properties, ...unsafe.properties }; | ||
} | ||
// The JSON Schema types say that required can be boolean, but there is | ||
// no evidence for that in the JSON Schema documentation. | ||
const aRequired = statics.required; | ||
const bRequired = unsafe.required; | ||
if (aRequired || bRequired) { | ||
merged.required = [ | ||
...new Set([...(aRequired ?? []), ...(bRequired ?? [])]), | ||
]; | ||
} | ||
return merged; | ||
} | ||
//# sourceMappingURL=definition.js.map |
@@ -16,2 +16,3 @@ /** | ||
#private; | ||
readonly id?: string; | ||
readonly type: string; | ||
@@ -33,4 +34,6 @@ readonly inputs: { | ||
[K: string]: JsonSerializable | OutputPortReference<JsonSerializable>; | ||
} & { | ||
$id?: string; | ||
}); | ||
assertOutput: DO extends JsonSerializable ? R extends false ? <N extends string>(name: N extends keyof O ? never : N) => OutputPort<DO> : never : never; | ||
unsafeOutput: DO extends JsonSerializable ? R extends false ? <N extends string>(name: N extends keyof O ? never : N) => OutputPort<DO> : never : never; | ||
} |
@@ -7,4 +7,5 @@ /** | ||
/* eslint-disable @typescript-eslint/ban-types */ | ||
import { InputPort, OutputPort, OutputPortGetter, } from "../common/port.js"; | ||
import { DefaultValue, InputPort, OutputPort, OutputPortGetter, } from "../common/port.js"; | ||
export class Instance { | ||
id; | ||
type; | ||
@@ -25,2 +26,7 @@ inputs; | ||
this.#reflective = reflective; | ||
this.id = args["$id"]; | ||
if (this.id !== undefined) { | ||
args = { ...args }; | ||
delete args["$id"]; | ||
} | ||
{ | ||
@@ -39,5 +45,5 @@ const { ports, primary } = this.#processInputs(staticInputs, args); | ||
#assertedOutputs = new Map(); | ||
assertOutput = ((name) => { | ||
unsafeOutput = ((name) => { | ||
if (this.#dynamicOutputType === undefined) { | ||
throw new Error(`assertOutput was called unnecessarily on a BreadboardNode. ` + | ||
throw new Error(`unsafeOutput was called unnecessarily on a BreadboardNode. ` + | ||
`Type "${this.type}" has entirely static outputs. ` + | ||
@@ -47,3 +53,3 @@ `Use "<node>.outputs.${name}" instead.`); | ||
if (this.#reflective) { | ||
throw new Error(`assertOutput was called unnecessarily on a BreadboardNode. ` + | ||
throw new Error(`unsafeOutput was called unnecessarily on a BreadboardNode. ` + | ||
`Type "${this.type}" is reflective. ` + | ||
@@ -53,3 +59,3 @@ `Use "<node>.outputs.${name}" instead.`); | ||
if (this.outputs[name] !== undefined) { | ||
throw new Error(`assertOutput was called unnecessarily on a BreadboardNode. ` + | ||
throw new Error(`unsafeOutput was called unnecessarily on a BreadboardNode. ` + | ||
`Type "${this.type}" already has a static port called "${name}". ` + | ||
@@ -72,6 +78,8 @@ `Use "<node>.outputs.${name}" instead.`); | ||
const arg = args[name]; | ||
if (arg === undefined) { | ||
if (arg === undefined && | ||
config.optional !== true && | ||
config.default === undefined) { | ||
throw new Error(`Argument ${name} is required`); | ||
} | ||
const port = new InputPort(config.type, name, this, arg); | ||
const port = new InputPort(config.type, name, this, arg ?? DefaultValue); | ||
ports[name] = port; | ||
@@ -78,0 +86,0 @@ if (config.primary) { |
@@ -8,6 +8,6 @@ /** | ||
import type { PortConfigMap } from "../common/port.js"; | ||
export declare function portConfigMapToJSONSchema(config: PortConfigMap): JSONSchema4 & { | ||
properties: { | ||
export declare function portConfigMapToJSONSchema(config: PortConfigMap, forceOptional: string[] | /* all */ true): JSONSchema4 & { | ||
properties?: { | ||
[k: string]: JSONSchema4; | ||
}; | ||
}; |
@@ -7,18 +7,34 @@ /** | ||
import { toJSONSchema } from "../type-system/type.js"; | ||
export function portConfigMapToJSONSchema(config) { | ||
export function portConfigMapToJSONSchema(config, forceOptional) { | ||
const sortedPropertyEntries = Object.entries(config).sort(([nameA], [nameB]) => nameA.localeCompare(nameB)); | ||
const forceOptionalSet = new Set(forceOptional === true ? Object.keys(config) : forceOptional); | ||
return { | ||
type: "object", | ||
properties: Object.fromEntries(Object.entries(config) | ||
.sort(([nameA], [nameB]) => nameA.localeCompare(nameB)) | ||
.map(([name, { description, type }]) => { | ||
const schema = toJSONSchema(type); | ||
schema.title = name; | ||
if (description !== undefined) { | ||
properties: Object.fromEntries(sortedPropertyEntries.map(([name, { title, description, type, behavior, ...config }]) => { | ||
const schema = { title: title ?? name }; | ||
if (description) { | ||
schema.description = description; | ||
} | ||
Object.assign(schema, toJSONSchema(type)); | ||
const defaultValue = config.default; | ||
if (defaultValue !== undefined) { | ||
schema.default = defaultValue; | ||
} | ||
if (config.format !== undefined) { | ||
schema.format = config.format; | ||
} | ||
if (behavior !== undefined && behavior.length > 0) { | ||
schema.behavior = behavior; | ||
} | ||
return [name, schema]; | ||
})), | ||
required: Object.keys(config).sort(), | ||
required: sortedPropertyEntries | ||
.filter(([name, config]) => { | ||
const isOptional = config.optional === true; | ||
const hasDefault = config.default !== undefined; | ||
return !isOptional && !hasDefault && !forceOptionalSet.has(name); | ||
}) | ||
.map(([name]) => name), | ||
}; | ||
} | ||
//# sourceMappingURL=json-schema.js.map |
@@ -6,3 +6,3 @@ /** | ||
*/ | ||
import type { NewNodeFactory } from "@google-labs/breadboard"; | ||
import type { NewNodeFactory, NewNodeValue } from "@google-labs/breadboard"; | ||
import type { Definition } from "./definition.js"; | ||
@@ -16,6 +16,8 @@ import type { JsonSerializable } from "../type-system/type.js"; | ||
*/ | ||
export type NodeFactoryFromDefinition<D extends Definition<any, any, any, any, any, any, any>> = D extends Definition<infer SI, infer SO, infer DI, infer DO, any, any, any> ? NewNodeFactory<Expand<SI & (DI extends JsonSerializable ? { | ||
[K: string]: DI; | ||
} : object)>, Expand<SO & (DO extends JsonSerializable ? { | ||
[K: string]: DO; | ||
} : object)>> : never; | ||
export type NodeFactoryFromDefinition<D extends Definition<any, any, any, any, any, any, any, any>> = D extends Definition<infer SI, infer SO, infer DI, infer DO, infer OI, any, any, any> ? NewNodeFactory<Expand<Omit<SI, OI> & { | ||
[K in OI]?: SI[K]; | ||
} & (DI extends JsonSerializable ? { | ||
[K: string]: NewNodeValue; | ||
} : {})>, Expand<SO & (DO extends JsonSerializable ? { | ||
[K: string]: NewNodeValue; | ||
} : {})>> : never; |
@@ -14,8 +14,10 @@ /** | ||
export function anyOf(...members) { | ||
const types = members.map(toJSONSchema); | ||
const allTypesAreBasic = types.every((member) => typeof member.type === "string" && Object.keys(member).length === 1); | ||
return { | ||
jsonSchema: { | ||
anyOf: members.map(toJSONSchema), | ||
}, | ||
jsonSchema: allTypesAreBasic | ||
? { type: types.map((member) => member.type) } | ||
: { anyOf: types }, | ||
}; | ||
} | ||
//# sourceMappingURL=any-of.js.map |
@@ -6,10 +6,11 @@ /** | ||
*/ | ||
import { type AdvancedBreadboardType, type BreadboardType, type ConvertBreadboardType } from "./type.js"; | ||
/** | ||
* Make a Breadboard type for an object. | ||
* | ||
* @param properties A map from property name to Breadboard Type. | ||
*/ | ||
export declare function object<T extends Record<string, BreadboardType>>(properties: T): AdvancedBreadboardType<{ | ||
import type { Expand } from "../common/type-util.js"; | ||
import { type AdvancedBreadboardType, type BreadboardType, type ConvertBreadboardType, type JsonSerializable } from "./type.js"; | ||
export declare function object<T extends Record<string, BreadboardType>>(properties: T): AdvancedBreadboardType<keyof T extends never ? object & JsonSerializable : { | ||
[P in keyof T]: ConvertBreadboardType<T[P]>; | ||
}>; | ||
export declare function object<T extends Record<string, BreadboardType>, A extends BreadboardType>(properties: T, additional: A): AdvancedBreadboardType<Expand<{ | ||
[x: string]: ConvertBreadboardType<A>; | ||
} & { | ||
[P in keyof T]: ConvertBreadboardType<T[P]>; | ||
}>>; |
@@ -10,16 +10,27 @@ /** | ||
* | ||
* @param properties A map from property name to Breadboard Type. | ||
* @param properties Object mapping from property name to Breadboard Type. | ||
* @param additional A Breadboard Type that is allowed for additional properties | ||
* (meaning ones not listed in {@link properties}). If ommitted, no additional | ||
* properties are allowed. | ||
*/ | ||
export function object(properties) { | ||
return { | ||
jsonSchema: { | ||
type: "object", | ||
properties: Object.fromEntries(Object.entries(properties).map(([name, type]) => [ | ||
name, | ||
toJSONSchema(type), | ||
])), | ||
required: Object.keys(properties), | ||
}, | ||
export function object(properties, additional) { | ||
const jsonSchema = { | ||
type: "object", | ||
properties: Object.fromEntries(Object.entries(properties).map(([name, type]) => [ | ||
name, | ||
toJSONSchema(type), | ||
])), | ||
}; | ||
jsonSchema.required = Object.keys(properties); | ||
if (additional === undefined) { | ||
jsonSchema.additionalProperties = false; | ||
} | ||
else if (additional === "unknown") { | ||
jsonSchema.additionalProperties = true; | ||
} | ||
else { | ||
jsonSchema.additionalProperties = toJSONSchema(additional); | ||
} | ||
return { jsonSchema }; | ||
} | ||
//# sourceMappingURL=object.js.map |
@@ -11,3 +11,6 @@ /** | ||
if (typeof type === "object" && "jsonSchema" in type) { | ||
return type.jsonSchema; | ||
// Make a copy because it's not uncommon for callers to mutate this object, | ||
// (e.g. adding a description to a port schema), and 2 ports might share an | ||
// advanced breadboard type instance (e.g. dynamic ports). | ||
return structuredClone(type.jsonSchema); | ||
} | ||
@@ -22,5 +25,11 @@ switch (type) { | ||
case "unknown": { | ||
// {} is our equivalent to TypeScript's `unknown` in JSON Schema, since it | ||
// enforces no type constraints at all. | ||
return {}; | ||
// All possible JSON schema data types. | ||
// | ||
// We could return {} here, but returning all possible types is a bit more | ||
// explicit. Also, there is some other Breadboard code which assumes when | ||
// there is no type, that the type is string (this is a bug, since no type | ||
// actually means anything). | ||
return { | ||
type: ["array", "boolean", "null", "number", "object", "string"], | ||
}; | ||
} | ||
@@ -27,0 +36,0 @@ default: { |
{ | ||
"name": "@breadboard-ai/build", | ||
"version": "0.4.0", | ||
"version": "0.5.0", | ||
"description": "JavaScript library for building boards and defining new node types for the Breadboard AI prototyping library", | ||
@@ -48,2 +48,3 @@ "license": "Apache-2.0", | ||
"test:only": "wireit", | ||
"coverage": "wireit", | ||
"lint": "wireit", | ||
@@ -91,2 +92,12 @@ "test-and-lint": "wireit", | ||
}, | ||
"coverage": { | ||
"command": "node --test --enable-source-maps --experimental-test-coverage --test-reporter lcov --test-reporter-destination=lcov.info dist/test/*_test.js", | ||
"dependencies": [ | ||
"build:tsc" | ||
], | ||
"files": [], | ||
"output": [ | ||
"lcov.info" | ||
] | ||
}, | ||
"lint": { | ||
@@ -109,3 +120,3 @@ "command": "eslint src/ --ext .ts", | ||
"dependencies": { | ||
"@google-labs/breadboard": "^0.17.0", | ||
"@google-labs/breadboard": "^0.18.0", | ||
"@types/json-schema": "^7.0.15" | ||
@@ -112,0 +123,0 @@ }, |
@@ -60,2 +60,14 @@ # @breadboard-ai/build | ||
- `title`: (Optional) A concise title for this input. Defaults to the name of | ||
the port. | ||
- `default`: (Optional) A default value for this input. | ||
- `format`: (Optional) Additional information about the format of the value. | ||
Primarily used to determine how strings are displayed in the Breadboard Visual | ||
Editor. Valid values: | ||
- `multiline`: A string that is likely to contain multiple lines. | ||
- `javascript`: A string that is JavaScript code. | ||
- `primary`: (Optional) Enables a syntactic sugar feature for an output port to | ||
@@ -144,4 +156,6 @@ make wiring nodes more concise. When a node has a `primary` output port, then | ||
`invoke`), and should return an object containing either or both of `inputs` and | ||
`outputs`, each an array of strings, the names for the input and/or output ports | ||
that should be dynamically opened. | ||
`outputs`, which can either be an array of strings or an object. When an array | ||
of strings, the strings are the names of the ports to open. When an object, the | ||
keys are the names of the ports to open, and the values are an object matching | ||
`{description: string}`. | ||
@@ -166,7 +180,7 @@ For example, in `templater` above, the `describe` function parses the static | ||
### `assertOutput` | ||
### `unsafeOutput` | ||
When a node has dynamic outputs, but is not `reflective`, it is not possible at | ||
compile time for Breadboard to know what the valid output ports of a node are. | ||
In this case, use the `assertOutput` method to get an output port with a given | ||
In this case, use the `unsafeOutput` method to get an output port with a given | ||
name. Note that there is no guarantee this port will exist at runtime, so a | ||
@@ -177,3 +191,3 @@ runtime error could occur. | ||
way to use fully static or reflective nodes whenever possible to avoid the use | ||
of `assertOutput`). | ||
of `unsafeOutput`). | ||
@@ -203,5 +217,5 @@ ```ts | ||
// *actually* be valid at runtime. | ||
const foo = lengths.assertOutput("foo"); | ||
const bar = lengths.assertOutput("bar"); | ||
const baz = lengths.assertOutput("baz"); // Oops! | ||
const foo = lengths.unsafeOutput("foo"); | ||
const bar = lengths.unsafeOutput("bar"); | ||
const baz = lengths.unsafeOutput("baz"); // Oops! | ||
``` | ||
@@ -351,2 +365,3 @@ | ||
- `"null"` | ||
- `"unknown"` | ||
@@ -358,4 +373,6 @@ ### Utility types | ||
- `object({ prop1: <type1>, prop2: <type2>, ... })`: A function which generates a | ||
JSON Schema `object` and its corresponding TypeScript `{...}` type. | ||
- `object({ prop1: <type1>, prop2: <type2>, ... }, [<additional>])`: A function | ||
which generates a JSON Schema `object` and its corresponding TypeScript | ||
`{...}` type. If the optional second argument is set, then the object will | ||
also allow additional properties of the given type. | ||
@@ -365,2 +382,5 @@ - `anyOf(<type1>, <type2>, ...)`: A function which generates a JSON Schema | ||
- `enumeration(<type1>, <type2>, ...)`: A function which generates a JSON Schema | ||
`enum` and its corresponding TypeScript union (`type1 | type2`). | ||
### Unsafe type escape hatch | ||
@@ -367,0 +387,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
235950
29.46%75
19.05%2294
23.87%409
5.14%+ Added
- Removed