Skip to content

Commit

Permalink
Convert secrets to @breadboard-ai/build (breadboard-ai#1462)
Browse files Browse the repository at this point in the history
Also adds the ability to override `title` when defining ports.
  • Loading branch information
aomarks authored Apr 19, 2024
1 parent 532598b commit d9ac358
Show file tree
Hide file tree
Showing 9 changed files with 127 additions and 65 deletions.
5 changes: 5 additions & 0 deletions .changeset/little-clocks-poke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@breadboard-ai/build": minor
---

Convert secrets node to use @breadboard-ai/build. No functional difference, but the JSON schema should be slightly stricter.
5 changes: 5 additions & 0 deletions .changeset/witty-seahorses-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@breadboard-ai/build": patch
---

Add ability to override default port title
5 changes: 4 additions & 1 deletion packages/build/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ the following fields:
displayed in the Breadboard visual editor and in other places where
introspection/debugging is performed.

- `default`: An optional default value for this input.
- `title`: (Optional) A concise title for this input. Defaults to the name of
the port.

- `default`: (Optional) A default value for this input.

- `primary`: (Optional) Enables a syntactic sugar feature for an output port to
make wiring nodes more concise. When a node has a `primary` output port, then
Expand Down
5 changes: 5 additions & 0 deletions packages/build/src/internal/common/port.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ interface StaticPortConfig {
*/
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.
Expand Down
1 change: 1 addition & 0 deletions packages/build/src/internal/define/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type OutputPortConfig = StaticOutputPortConfig | DynamicOutputPortConfig;

interface BaseConfig {
type: BreadboardType;
title?: string;
description?: string;
multiline?: true;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/build/src/internal/define/json-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function portConfigMapToJSONSchema(
sortedEntries.map(([name, config]) => {
const { description, type, multiline } = config;
const schema: JSONSchema4 = {
title: name,
title: config.title ?? name,
};
if (description) {
schema.description = description;
Expand Down
47 changes: 47 additions & 0 deletions packages/build/src/test/define_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1027,6 +1027,53 @@ test("defaults", async () => {
assert.deepEqual(await d.describe({ si1: "bar", si2: 123 }), expectedSchema);
});

test("override title", async () => {
const d = defineNodeType({
name: "foo",
inputs: {
si1: { type: "string", title: "custom1" },
si2: { type: "number" },
},
outputs: {
so1: { type: "boolean" },
so2: { type: "null", title: "custom2" },
},
invoke: () => ({
so1: true,
so2: null,
}),
});
assert.deepEqual(await d.describe(), {
inputSchema: {
properties: {
si1: {
title: "custom1",
type: "string",
},
si2: {
title: "si2",
type: "number",
},
},
required: ["si1", "si2"],
type: "object",
},
outputSchema: {
properties: {
so1: {
title: "so1",
type: "boolean",
},
so2: {
title: "custom2",
type: "null",
},
},
type: "object",
},
});
});

test("error: missing name", () => {
assert.throws(
() =>
Expand Down
86 changes: 29 additions & 57 deletions packages/core-kit/src/nodes/secrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

/**
* A kind of input node that provides secret values, such as API keys.
* Currently, it simply reads them from environment.
*/

import type {
InputValues,
NodeDescriberFunction,
NodeHandler,
OutputValues,
} from "@google-labs/breadboard";
import { array, defineNodeType } from "@breadboard-ai/build";

type Environment = "node" | "browser" | "worker";

Expand All @@ -34,7 +24,7 @@ export type SecretWorkerResponse = {
data: string;
};

const getEnvironmentValue = async (key: string) => {
const getEnvironmentValue = (key: string) => {
const env = environment();
if (env === "node") {
return process.env[key];
Expand All @@ -59,7 +49,7 @@ export const requireNonEmpty = (key: string, value?: string | null) => {
};

const getKeys = (
inputs: InputValues = { keys: [] },
inputs: { keys: string[] } = { keys: [] },
safe: boolean
): string[] => {
const { keys } = inputs as SecretInputs;
Expand All @@ -79,50 +69,32 @@ const getKeys = (
return keys;
};

export const secretsDescriber: NodeDescriberFunction = async (
inputs?: InputValues
) => {
const keys = getKeys(inputs, true);
const properties = keys
? Object.fromEntries(
keys.map((key) => [
key,
{
title: key,
},
])
)
: {};
return {
inputSchema: {
properties: {
keys: {
title: "secrets",
description: "The array of secrets to retrieve from the node.",
type: "array",
items: {
type: "string",
},
},
},
/**
* A kind of input node that provides secret values, such as API keys.
* Currently, it simply reads them from environment.
*/
export default defineNodeType({
name: "secrets",
inputs: {
keys: {
title: "secrets",
description: "The array of secrets to retrieve from the node.",
type: array("string"),
},
outputSchema: {
properties,
},
outputs: {
"*": {
type: "string",
},
};
};

export default {
describe: secretsDescriber,
invoke: async (inputs: InputValues) => {
const keys = getKeys(inputs, false);
return Object.fromEntries(
await Promise.all(
keys.map(async (key) => [
key,
requireNonEmpty(key, await getEnvironmentValue(key)),
])
)
) as OutputValues;
},
} satisfies NodeHandler;
describe: (inputs) => ({
outputs: inputs.keys,
}),
invoke: (inputs) =>
Object.fromEntries(
getKeys(inputs, false).map((key) => [
key,
requireNonEmpty(key, getEnvironmentValue(key)),
])
),
});
36 changes: 30 additions & 6 deletions packages/core-kit/tests/secrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,26 @@

import test from "ava";

import { secretsDescriber } from "../src/nodes/secrets.js";
import secrets from "../src/nodes/secrets.js";

test("describer correctly responds to no inputs", async (t) => {
t.like(await secretsDescriber(), {
t.deepEqual(await secrets.describe(), {
inputSchema: {
type: "object",
properties: {
keys: {
title: "secrets",
description: "The array of secrets to retrieve from the node.",
type: "array",
items: {
type: "string",
},
},
},
required: ["keys"],
},
outputSchema: {
type: "object",
properties: {},
},
});
Expand All @@ -27,33 +35,49 @@ test("describer correctly responds to inputs", async (t) => {
const inputs = {
keys: ["SECRET1", "SECRET2"],
};
t.like(await secretsDescriber(inputs), {
t.deepEqual(await secrets.describe(inputs), {
inputSchema: {
type: "object",
properties: {
keys: {
title: "secrets",
description: "The array of secrets to retrieve from the node.",
type: "array",
items: {
type: "string",
},
},
},
required: ["keys"],
},
outputSchema: {
type: "object",
properties: {
SECRET1: { title: "SECRET1" },
SECRET2: { title: "SECRET2" },
SECRET1: { title: "SECRET1", type: "string" },
SECRET2: { title: "SECRET2", type: "string" },
},
},
});
});

test("describer correctly responds to unknown inputs", async (t) => {
t.like(await secretsDescriber(), {
t.deepEqual(await secrets.describe(), {
inputSchema: {
type: "object",
properties: {
keys: {
title: "secrets",
description: "The array of secrets to retrieve from the node.",
type: "array",
items: {
type: "string",
},
},
},
required: ["keys"],
},
outputSchema: {
type: "object",
properties: {},
},
});
Expand Down

0 comments on commit d9ac358

Please sign in to comment.